From 2c552cd716f9abbe90ec4b22e51d0cde155c2bf9 Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 15 Jun 2026 20:23:56 +0900 Subject: [PATCH 001/618] refactor: align workflow trigger dispatch --- .changeset/workflow-trigger-dispatch.md | 5 + AGENTS.md | 2 +- example/workflows/order-processing.ts | 6 +- example/workflows/sample.ts | 6 +- .../create-sdk/templates/workflow/README.md | 2 +- .../src/workflow/order-fulfillment.test.ts | 19 +-- .../src/workflow/order-fulfillment.ts | 8 +- packages/sdk/docs/services/workflow.md | 26 ++-- packages/sdk/docs/testing.md | 24 ++-- .../workflows/processOrder.ts | 10 +- .../services/workflow/ast-transformer.test.ts | 46 +++---- .../services/workflow/source-transformer.ts | 2 +- .../services/workflow/trigger-transformer.ts | 2 +- .../configure/services/workflow/job.test.ts | 49 +++---- .../src/configure/services/workflow/job.ts | 25 ++-- .../configure/services/workflow/registry.ts | 70 +++------- .../services/workflow/test-env-key.ts | 4 +- .../configure/services/workflow/workflow.ts | 13 +- packages/sdk/src/vitest/index.ts | 1 + packages/sdk/src/vitest/mock.test.ts | 83 +++++++++--- packages/sdk/src/vitest/mock.ts | 27 ++-- packages/sdk/src/vitest/workflow-local.ts | 128 ++++++++++++++++++ packages/sdk/src/vitest/workflow-runtime.ts | 10 +- 23 files changed, 343 insertions(+), 225 deletions(-) create mode 100644 .changeset/workflow-trigger-dispatch.md create mode 100644 packages/sdk/src/vitest/workflow-local.ts diff --git a/.changeset/workflow-trigger-dispatch.md b/.changeset/workflow-trigger-dispatch.md new file mode 100644 index 000000000..76984874a --- /dev/null +++ b/.changeset/workflow-trigger-dispatch.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Align workflow job `.trigger()` with the platform runtime. Job triggers now delegate to `tailor.workflow.triggerJobFunction()` instead of running job bodies locally, and `trigger()` returns the job result directly instead of a Promise wrapper. Use `mockWorkflow()` to mock trigger results in tests, or `runWorkflowLocally()` for full-chain local workflow tests. diff --git a/AGENTS.md b/AGENTS.md index 606b9f5df..557f4b27d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,7 +83,7 @@ Key files: - `createWorkflow()` result **must** be default exported - All jobs **must** be named exports (including mainJob and triggered jobs) - Job names must be unique across the entire project -- `.trigger()` returns a `Promise>` — typically `await` it to read the value +- Job `.trigger()` returns `Awaited` directly; read the value synchronously unless the job output itself is promise-like - `defineWaitPoints(define => ({ key: define() }))` creates typed wait/resolve points - Wait/resolve methods runtime-delegate to `tailor.workflow.wait/resolve` on the platform; acquire the mock with `using wf = mockWorkflow()` from `@tailor-platform/sdk/vitest` (with the `tailor-runtime` environment) and use `wf.setWaitHandler` / `wf.setResolveHandler` to mock in tests — see [testing.md](packages/sdk/docs/testing.md#jobs-that-wait-on-approval) - Use `wps.key.wait()` for namespaced access, or `export const { key } = defineWaitPoints(...)` for destructured 2-level access diff --git a/example/workflows/order-processing.ts b/example/workflows/order-processing.ts index da0d512b2..375f16099 100644 --- a/example/workflows/order-processing.ts +++ b/example/workflows/order-processing.ts @@ -6,12 +6,12 @@ import { sendNotification } from "./jobs/send-notification"; export const processOrder = createWorkflowJob({ name: "process-order", - body: async (input: { orderId: string; customerId: string }, { env }) => { + body: (input: { orderId: string; customerId: string }, { env }) => { // Log env for demonstration console.log("Environment:", env); // Fetch customer information using trigger - const customer = await fetchCustomer.trigger({ + const customer = fetchCustomer.trigger({ customerId: input.customerId, }); @@ -20,7 +20,7 @@ export const processOrder = createWorkflowJob({ } // Send notification to customer using trigger - const notification = await sendNotification.trigger({ + const notification = sendNotification.trigger({ message: `Your order ${input.orderId} is being processed`, recipient: customer.email, }); diff --git a/example/workflows/sample.ts b/example/workflows/sample.ts index 49956d92e..8033ecc02 100644 --- a/example/workflows/sample.ts +++ b/example/workflows/sample.ts @@ -21,10 +21,10 @@ export const check_inventory = createWorkflowJob({ export const validate_order = createWorkflowJob({ name: "validate-order", - body: async (input: { orderId: string }) => { + body: (input: { orderId: string }) => { console.log("Order ID:", input.orderId); - const inventoryResult = await check_inventory.trigger(); - const paymentResult = await process_payment.trigger(); + const inventoryResult = check_inventory.trigger(); + const paymentResult = process_payment.trigger(); return { inventoryResult, paymentResult }; }, }); diff --git a/packages/create-sdk/templates/workflow/README.md b/packages/create-sdk/templates/workflow/README.md index 8476d85fc..7fdf55a06 100644 --- a/packages/create-sdk/templates/workflow/README.md +++ b/packages/create-sdk/templates/workflow/README.md @@ -7,7 +7,7 @@ Demonstrates workflow patterns with job chaining, trigger testing, and dependenc - Workflow with multiple jobs (`createWorkflow`, `createWorkflowJob`) - Job chaining via `.trigger()` - Database operations in workflow jobs (DI pattern) -- Integration testing with `workflow.mainJob.trigger()` +- Integration testing with `runWorkflowLocally()` ## Getting Started diff --git a/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts b/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts index db1a1bc4b..75f2e3a6f 100644 --- a/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts +++ b/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts @@ -1,3 +1,4 @@ +import { runWorkflowLocally } from "@tailor-platform/sdk/vitest"; import { describe, expect, test, vi } from "vitest"; import workflow, { fulfillOrder, @@ -43,16 +44,16 @@ describe("order fulfillment workflow", () => { describe("orchestration tests with mocked triggers", () => { test("fulfillOrder chains all jobs", async () => { - using _validateSpy = vi.spyOn(validateOrder, "trigger").mockResolvedValue({ + using _validateSpy = vi.spyOn(validateOrder, "trigger").mockReturnValue({ valid: true, orderId: "order-1", }); - using _paymentSpy = vi.spyOn(processPayment, "trigger").mockResolvedValue({ + using _paymentSpy = vi.spyOn(processPayment, "trigger").mockReturnValue({ transactionId: "txn-order-1", amount: 100, status: "completed" as const, }); - using _confirmSpy = vi.spyOn(sendConfirmation, "trigger").mockResolvedValue({ + using _confirmSpy = vi.spyOn(sendConfirmation, "trigger").mockReturnValue({ orderId: "order-1", transactionId: "txn-order-1", confirmed: true, @@ -81,16 +82,16 @@ describe("order fulfillment workflow", () => { }); test("workflow.mainJob.body() chains all jobs", async () => { - using _validateSpy = vi.spyOn(validateOrder, "trigger").mockResolvedValue({ + using _validateSpy = vi.spyOn(validateOrder, "trigger").mockReturnValue({ valid: true, orderId: "order-2", }); - using _paymentSpy = vi.spyOn(processPayment, "trigger").mockResolvedValue({ + using _paymentSpy = vi.spyOn(processPayment, "trigger").mockReturnValue({ transactionId: "txn-order-2", amount: 200, status: "completed" as const, }); - using _confirmSpy = vi.spyOn(sendConfirmation, "trigger").mockResolvedValue({ + using _confirmSpy = vi.spyOn(sendConfirmation, "trigger").mockReturnValue({ orderId: "order-2", transactionId: "txn-order-2", confirmed: true, @@ -107,9 +108,9 @@ describe("order fulfillment workflow", () => { }); }); - describe("integration tests with .trigger()", () => { - test("workflow.mainJob.trigger() executes all jobs", async () => { - const result = await workflow.mainJob.trigger({ + describe("integration tests with runWorkflowLocally()", () => { + test("runWorkflowLocally() executes all jobs", async () => { + const result = await runWorkflowLocally(workflow, { orderId: "order-3", amount: 300, }); diff --git a/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.ts b/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.ts index 163406811..5e6de9b58 100644 --- a/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.ts +++ b/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.ts @@ -34,8 +34,8 @@ export const sendConfirmation = createWorkflowJob({ export const fulfillOrder = createWorkflowJob({ name: "fulfill-order", - body: async (input: { orderId: string; amount: number }) => { - const validation = await validateOrder.trigger({ + body: (input: { orderId: string; amount: number }) => { + const validation = validateOrder.trigger({ orderId: input.orderId, amount: input.amount, }); @@ -44,12 +44,12 @@ export const fulfillOrder = createWorkflowJob({ throw new Error("Order validation failed"); } - const payment = await processPayment.trigger({ + const payment = processPayment.trigger({ orderId: input.orderId, amount: input.amount, }); - const confirmation = await sendConfirmation.trigger({ + const confirmation = sendConfirmation.trigger({ orderId: input.orderId, transactionId: payment.transactionId, }); diff --git a/packages/sdk/docs/services/workflow.md b/packages/sdk/docs/services/workflow.md index 022c963d4..33a923483 100644 --- a/packages/sdk/docs/services/workflow.md +++ b/packages/sdk/docs/services/workflow.md @@ -105,11 +105,11 @@ import { sendNotification } from "./jobs/send-notification"; export const mainJob = createWorkflowJob({ name: "main-job", - body: async (input: { customerId: string }) => { - const customer = await fetchCustomer.trigger({ + body: (input: { customerId: string }) => { + const customer = fetchCustomer.trigger({ customerId: input.customerId, }); - const notification = await sendNotification.trigger({ + const notification = sendNotification.trigger({ message: "Order processed", recipient: customer.email, }); @@ -130,31 +130,31 @@ Using `.trigger()` inside a loop works correctly, as long as the loop is determi // ✅ OK: deterministic loop — same calls in the same order on every execution const regions = ["us", "eu", "ap"]; for (const region of regions) { - const result = await fetchData.trigger({ region }); + const result = fetchData.trigger({ region }); results.push(result); } ``` ```typescript // ❌ Bad: non-deterministic — argument changes between executions -await processJob.trigger({ timestamp: Date.now() }); +processJob.trigger({ timestamp: Date.now() }); // ✅ OK: call Date.now() in separated job -const timestamp = await timestampJob.trigger(); -await processJob.trigger({ timestamp }); +const timestamp = timestampJob.trigger(); +processJob.trigger({ timestamp }); ``` ```typescript // ❌ Bad: non-deterministic — external data may change between executions const items = await fetch("https://api.example.com/items").then((r) => r.json()); for (const item of items) { - await processItem.trigger({ id: item.id }); + processItem.trigger({ id: item.id }); } // ✅ OK: call fetch("https://api.example.com/items").then((r) => r.json()); in separated job -const items = await fetchItemsJob.trigger(); +const items = fetchItemsJob.trigger(); for (const item of items) { - await processItem.trigger({ id: item.id }); + processItem.trigger({ id: item.id }); } ``` @@ -178,15 +178,15 @@ import { sendNotification } from "./jobs/send-notification"; // Jobs must be named exports export const processOrder = createWorkflowJob({ name: "process-order", - body: async (input: { customerId: string }, { env, invoker }) => { + body: (input: { customerId: string }, { env, invoker }) => { // `env` contains values from `tailor.config.ts` -> `env`. // `invoker` is the principal running this job, overridden by `authInvoker` // when set; `null` for anonymous calls. // Trigger other jobs by calling .trigger() on the job object. - const customer = await fetchCustomer.trigger({ + const customer = fetchCustomer.trigger({ customerId: input.customerId, }); - await sendNotification.trigger({ + sendNotification.trigger({ message: "Order processed", recipient: customer.email, }); diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index b3e8ebbe0..a6e9d64c8 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -12,7 +12,9 @@ Lean on unit tests for the day-to-day feedback loop — they run fast and exerci Unit-test entrypoints exposed by the SDK: - `resolver.body({ input, user, env })` — invoke a resolver -- `workflowJob.body(input, { env })` / `workflowJob.trigger(input)` — invoke or chain a workflow job +- `workflowJob.body(input, { env })` — invoke a workflow job body directly +- `workflowJob.trigger(input)` — chain a workflow job through the workflow runtime +- `runWorkflowLocally(workflow, args)` — run a workflow chain locally with real job bodies - `executor.operation.body(args)` — invoke a function-kind executor Helpers under `@tailor-platform/sdk/test`: @@ -23,6 +25,7 @@ Platform API mocks under `@tailor-platform/sdk/vitest` (for use with the [`tailo - `mockTailordb` — TailorDB query stubs and call recording - `mockWorkflow` — `tailor.workflow` job / wait / resolve mocks +- `runWorkflowLocally` — local full-chain workflow runner - `mockSecretmanager`, `mockAuthconnection`, `mockIdp`, `mockFile`, `mockIconv` — 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. @@ -126,7 +129,7 @@ test("content-based mock", async () => { ### Workflow Mock -`.trigger()` runs the real job bodies locally out of the box (see [Running a full workflow locally](#running-a-full-workflow-locally)). Acquire `mockWorkflow()` when you want to override responses with `setJobHandler` / `enqueueResult` or assert on `triggeredJobs`: +Workflow job `.trigger()` calls delegate to `tailor.workflow.triggerJobFunction`. Acquire `mockWorkflow()` when you want to provide trigger responses with `setJobHandler` / `enqueueResult` or assert on `triggeredJobs`. If no response is configured, the mock throws so missing job mocks fail loudly: ```typescript import { mockWorkflow } from "@tailor-platform/sdk/vitest"; @@ -645,16 +648,16 @@ import { fulfillOrder, processPayment, sendConfirmation, validateOrder } from ". describe("fulfillOrder", () => { test("chains validate → pay → confirm", async () => { - using _validateSpy = vi.spyOn(validateOrder, "trigger").mockResolvedValue({ + using _validateSpy = vi.spyOn(validateOrder, "trigger").mockReturnValue({ valid: true, orderId: "order-1", }); - using _paymentSpy = vi.spyOn(processPayment, "trigger").mockResolvedValue({ + using _paymentSpy = vi.spyOn(processPayment, "trigger").mockReturnValue({ transactionId: "txn-order-1", amount: 100, status: "completed", }); - using _confirmSpy = vi.spyOn(sendConfirmation, "trigger").mockResolvedValue({ + using _confirmSpy = vi.spyOn(sendConfirmation, "trigger").mockReturnValue({ orderId: "order-1", transactionId: "txn-order-1", confirmed: true, @@ -708,22 +711,25 @@ describe("processWithApproval", () => { #### Running a full workflow locally -To exercise the full chain with real job bodies, just call `workflow.mainJob.trigger()` — no `mockWorkflow()` needed. Dependent jobs run their real `.body()` functions, and trigger args/results cross the same JSON boundary as the platform, so a non-serializable payload fails the test exactly as it would in production: +To exercise the full chain with real job bodies, call `runWorkflowLocally(workflow, args)`. Dependent jobs run their real `.body()` functions, and trigger args/results cross the same JSON boundary as the platform, so a non-serializable payload fails the test exactly as it would in production: ```typescript +import { runWorkflowLocally } from "@tailor-platform/sdk/vitest"; import { describe, expect, test } from "vitest"; import workflow from "./order-fulfillment"; describe("order-fulfillment workflow", () => { - test("mainJob.trigger() executes all jobs", async () => { - const result = await workflow.mainJob.trigger({ orderId: "order-3", amount: 300 }); + test("runWorkflowLocally() executes all jobs", async () => { + const result = await runWorkflowLocally(workflow, { orderId: "order-3", amount: 300 }); expect(result).toMatchObject({ confirmed: true, paymentStatus: "completed" }); }); }); ``` -Acquire `mockWorkflow()` only when you need to override a dependent job with `wf.setJobHandler(...)` / `wf.enqueueResult(...)` (the rest still run their real bodies), control the env via `wf.setEnv(...)`, or assert on `wf.triggeredJobs`. +Pass `{ env }` as the third argument when job bodies need configuration values during the local run. + +This helper is still a local runner. Use E2E tests when you need to verify deployed workflow scheduling, suspension, or replay behavior. **Use when:** you want to verify orchestration end to end without the cost of a real deployment. diff --git a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/workflows/processOrder.ts b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/workflows/processOrder.ts index 7bd682c82..69b24d169 100644 --- a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/workflows/processOrder.ts +++ b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/workflows/processOrder.ts @@ -3,14 +3,14 @@ import { format } from "date-fns"; export const fetchDetails = createWorkflowJob({ name: "fetch-details", - body: async (input: { orderId: string }) => { + body: (input: { orderId: string }) => { return { orderId: input.orderId, status: "found" }; }, }); export const notifyUser = createWorkflowJob({ name: "notify-user", - body: async (_input: { message: string; recipient: string }) => { + body: (_input: { message: string; recipient: string }) => { const timestamp = format(new Date(), "yyyy-MM-dd HH:mm:ss"); return { sent: true, timestamp }; }, @@ -18,15 +18,15 @@ export const notifyUser = createWorkflowJob({ export const processOrder = createWorkflowJob({ name: "process-order", - body: async (input: { orderId: string; userEmail: string }) => { - const details = await fetchDetails.trigger({ orderId: input.orderId }); + body: (input: { orderId: string; userEmail: string }) => { + const details = fetchDetails.trigger({ orderId: input.orderId }); // trigger return may be undefined when the upstream job produces no value // oxlint-disable-next-line typescript/no-unnecessary-condition if (!details) { throw new Error(`Order ${input.orderId} not found`); } - const notification = await notifyUser.trigger({ + const notification = notifyUser.trigger({ message: `Order ${input.orderId} processed`, recipient: input.userEmail, }); diff --git a/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts b/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts index 847b8aa52..d9afbd3a7 100644 --- a/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts +++ b/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts @@ -464,7 +464,7 @@ const mainJob = createWorkflowJob({ ); expect(result).toContain( - '(async () => tailor.workflow.triggerJobFunction("fetch-data", { id: input.id }))()', + 'tailor.workflow.triggerJobFunction("fetch-data", { id: input.id })', ); // fetchData declaration is removed (const fetchData = ...) expect(result).not.toContain("const fetchData"); @@ -509,9 +509,7 @@ const mainJob = createWorkflowJob({ // mainJob body is preserved expect(result).toContain('result: "main"'); // trigger is transformed (job name appears in triggerJobFunction call) - expect(result).toContain( - '(async () => tailor.workflow.triggerJobFunction("heavy-job", undefined))()', - ); + expect(result).toContain('tailor.workflow.triggerJobFunction("heavy-job", undefined)'); }); test("removes declarations of multiple other jobs", () => { @@ -558,12 +556,8 @@ const mainJob = createWorkflowJob({ // heavy code is removed (part of job1/job2 body) expect(result).not.toContain("heavy code"); // triggers are transformed (job names appear in triggerJobFunction calls) - expect(result).toContain( - '(async () => tailor.workflow.triggerJobFunction("job-one", undefined))()', - ); - expect(result).toContain( - '(async () => tailor.workflow.triggerJobFunction("job-two", undefined))()', - ); + expect(result).toContain('tailor.workflow.triggerJobFunction("job-one", undefined)'); + expect(result).toContain('tailor.workflow.triggerJobFunction("job-two", undefined)'); }); test("does not modify jobs without trigger calls", () => { @@ -1005,8 +999,8 @@ async function processOrder(orderId: string) { }); }); - describe("async IIFE wrapping for job triggers", () => { - test("wraps job.trigger() in an async IIFE and preserves await", () => { + describe("direct job trigger transformation", () => { + test("replaces job.trigger() with triggerJobFunction() and preserves await", () => { const source = ` const customer = await fetchCustomer.trigger({ customerId: "123" }); console.log(customer); @@ -1017,11 +1011,11 @@ console.log(customer); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain( - 'const customer = await (async () => tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" }))()', + 'const customer = await tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" })', ); }); - test("wraps multiple job.trigger() calls in an async IIFE", () => { + test("replaces multiple job.trigger() calls directly", () => { const source = ` const customer = await fetchCustomer.trigger({ customerId: "123" }); const notification = await sendNotification.trigger({ message: "Hello" }); @@ -1034,10 +1028,8 @@ const notification = await sendNotification.trigger({ message: "Hello" }); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); - expect(result).toContain('(async () => tailor.workflow.triggerJobFunction("fetch-customer"'); - expect(result).toContain( - '(async () => tailor.workflow.triggerJobFunction("send-notification"', - ); + expect(result).toContain('tailor.workflow.triggerJobFunction("fetch-customer"'); + expect(result).toContain('tailor.workflow.triggerJobFunction("send-notification"'); }); test("does not wrap workflow.trigger() calls (already async)", () => { @@ -1053,7 +1045,7 @@ const executionId = await orderWorkflow.trigger({ orderId: "123" }, { authInvoke expect(result).not.toContain("(async () => tailor.workflow.triggerWorkflow"); }); - test("wraps job.trigger() without await so it still returns a Promise", () => { + test("replaces job.trigger() without await with the raw platform result", () => { const source = ` const customerPromise = fetchCustomer.trigger({ customerId: "123" }); `; @@ -1063,12 +1055,12 @@ const customerPromise = fetchCustomer.trigger({ customerId: "123" }); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain( - 'const customerPromise = (async () => tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" }))()', + 'const customerPromise = tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" })', ); expect(result).not.toMatch(/\bawait\b/); }); - test("wraps job.trigger() inside Promise.all array elements", () => { + test("replaces job.trigger() inside Promise.all array elements", () => { const source = ` const [customer, notification] = await Promise.all([ fetchCustomer.trigger({ customerId: "123" }), @@ -1084,15 +1076,15 @@ const [customer, notification] = await Promise.all([ const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain( - '(async () => tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" }))()', + 'tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" })', ); expect(result).toContain( - '(async () => tailor.workflow.triggerJobFunction("send-notification", { message: "Hello" }))()', + 'tailor.workflow.triggerJobFunction("send-notification", { message: "Hello" })', ); expect(result).toContain("await Promise.all(["); }); - test("wraps job.trigger() before .then() chains", () => { + test("replaces job.trigger() before .then() chains without preserving Promise wrapping", () => { const source = ` fetchCustomer.trigger({ customerId: "123" }).then((customer) => { console.log(customer); @@ -1104,11 +1096,11 @@ fetchCustomer.trigger({ customerId: "123" }).then((customer) => { const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain( - '(async () => tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" }))().then(', + 'tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" }).then(', ); }); - test("wraps job.trigger() nested inside an unknown .trigger() argument", () => { + test("replaces job.trigger() nested inside an unknown .trigger() argument", () => { const source = ` unknown.trigger(fetchCustomer.trigger({ customerId: "123" })); `; @@ -1118,7 +1110,7 @@ unknown.trigger(fetchCustomer.trigger({ customerId: "123" })); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain( - '(async () => tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" }))()', + 'tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" })', ); expect(result).toContain("unknown.trigger("); }); diff --git a/packages/sdk/src/cli/services/workflow/source-transformer.ts b/packages/sdk/src/cli/services/workflow/source-transformer.ts index acd69316d..87311cbf2 100644 --- a/packages/sdk/src/cli/services/workflow/source-transformer.ts +++ b/packages/sdk/src/cli/services/workflow/source-transformer.ts @@ -221,7 +221,7 @@ export function transformWorkflowSource( const jobName = jobNameMap.get(call.identifierName); if (jobName) { - const transformedCall = `(async () => tailor.workflow.triggerJobFunction("${jobName}", ${call.argsText || "undefined"}))()`; + const transformedCall = `tailor.workflow.triggerJobFunction("${jobName}", ${call.argsText || "undefined"})`; replacements.push({ start: call.callRange.start, end: call.callRange.end, diff --git a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts b/packages/sdk/src/cli/services/workflow/trigger-transformer.ts index 5c233b153..ccb84bb5f 100644 --- a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts +++ b/packages/sdk/src/cli/services/workflow/trigger-transformer.ts @@ -460,7 +460,7 @@ export function transformFunctionTriggers( } else if (call.kind === "job") { const jobName = jobNameMap.get(call.identifierName); if (jobName) { - const transformedCall = `(async () => tailor.workflow.triggerJobFunction("${jobName}", ${call.argsText || "undefined"}))()`; + const transformedCall = `tailor.workflow.triggerJobFunction("${jobName}", ${call.argsText || "undefined"})`; replacements.push({ start: call.callRange.start, diff --git a/packages/sdk/src/configure/services/workflow/job.test.ts b/packages/sdk/src/configure/services/workflow/job.test.ts index 14032279e..dd4b8a011 100644 --- a/packages/sdk/src/configure/services/workflow/job.test.ts +++ b/packages/sdk/src/configure/services/workflow/job.test.ts @@ -10,7 +10,7 @@ describe("WorkflowJob type inference", () => { name: "test", body: () => ({ status: "ok" as const, count: 42 }), }); - type Output = Awaited>; + type Output = ReturnType; expectTypeOf().toEqualTypeOf<{ status: "ok"; count: number }>(); }); @@ -45,7 +45,7 @@ describe("WorkflowJob type inference", () => { name: "test", body: (): UserOutput => ({ id: "123", created: true }), }); - type Output = Awaited>; + type Output = ReturnType; expectTypeOf().toEqualTypeOf(); }); @@ -229,7 +229,7 @@ describe("WorkflowJob type constraints", () => { name: "test", body: () => ({ result: "ok", count: 42, active: true as boolean }), }); - expectTypeOf(job.trigger).returns.resolves.toEqualTypeOf<{ + expectTypeOf(job.trigger).returns.toEqualTypeOf<{ result: string; count: number; active: boolean; @@ -246,7 +246,7 @@ describe("WorkflowJob type constraints", () => { }, }), }); - expectTypeOf(job.trigger).returns.resolves.toEqualTypeOf<{ + expectTypeOf(job.trigger).returns.toEqualTypeOf<{ data: { id: string; tags: string[]; @@ -259,7 +259,7 @@ describe("WorkflowJob type constraints", () => { name: "test", body: () => undefined, }); - expectTypeOf(job.trigger).returns.resolves.toEqualTypeOf(); + expectTypeOf(job.trigger).returns.toEqualTypeOf(); }); test("returns T | undefined for T | undefined output", () => { @@ -269,7 +269,7 @@ describe("WorkflowJob type constraints", () => { return Math.random() > 0.5 ? { value: 1 } : undefined; }, }); - expectTypeOf(job.trigger).returns.resolves.toEqualTypeOf<{ value: number } | undefined>(); + expectTypeOf(job.trigger).returns.toEqualTypeOf<{ value: number } | undefined>(); }); }); @@ -279,7 +279,7 @@ describe("WorkflowJob type constraints", () => { name: "test", body: () => ({ result: "ok" }), }); - const _trigger: () => Promise<{ result: string }> = job.trigger; + const _trigger: () => { result: string } = job.trigger; expectTypeOf(_trigger).toBeFunction(); }); @@ -288,7 +288,7 @@ describe("WorkflowJob type constraints", () => { name: "test", body: (input: { id: string }) => ({ result: input.id }), }); - const _trigger: (input: { id: string }) => Promise<{ result: string }> = job.trigger; + const _trigger: (input: { id: string }) => { result: string } = job.trigger; expectTypeOf(_trigger).toBeFunction(); }); }); @@ -305,7 +305,7 @@ describe("WorkflowJob type constraints", () => { test("trigger return preserves Output as-is", () => { type Job = WorkflowJob<"test", undefined, { id: string; result: string }>; - expectTypeOf>().resolves.toEqualTypeOf<{ + expectTypeOf>().toEqualTypeOf<{ id: string; result: string; }>(); @@ -380,41 +380,24 @@ describe("WorkflowJob type constraints", () => { }); // Plain `node` environment (no `tailor-runtime`, no `mockWorkflow()`), so -// `.trigger()` exercises the no-shim fallback. -describe("trigger fallback without tailor.workflow", () => { - test("runs the registered job body locally", async () => { +// `.trigger()` should not execute job bodies locally. +describe("trigger without tailor.workflow", () => { + test("job trigger throws instead of running the registered body", () => { const double = createWorkflowJob({ name: "fallback-double", body: (input: { n: number }) => ({ doubled: input.n * 2 }), }); - expect(await double.trigger({ n: 21 })).toEqual({ doubled: 42 }); + expect(() => double.trigger({ n: 21 })).toThrow(/tailor\.workflow is not available/); }); - test("runs the whole chain via workflow.mainJob.trigger()", async () => { - const inner = createWorkflowJob({ - name: "fallback-inner", - body: (input: { n: number }) => ({ n: input.n + 1 }), - }); + test("workflow trigger throws instead of running the main job", () => { const main = createWorkflowJob({ name: "fallback-main", - body: async (input: { n: number }) => { - const a = await inner.trigger({ n: input.n }); - const b = await inner.trigger({ n: a.n }); - return { total: b.n }; - }, + body: (input: { n: number }) => ({ total: input.n + 1 }), }); const workflow = createWorkflow({ name: "fallback-wf", mainJob: main }); - expect(await workflow.mainJob.trigger({ n: 0 })).toEqual({ total: 2 }); - }); - - test("enforces the JSON boundary on the fallback path", async () => { - const bad = createWorkflowJob({ - name: "fallback-bad", - body: () => ({ when: new Date() }) as never, - }); - - await expect(bad.trigger()).rejects.toThrow(/Date instance/); + expect(() => workflow.trigger({ n: 0 })).toThrow(/tailor\.workflow is not available/); }); }); diff --git a/packages/sdk/src/configure/services/workflow/job.ts b/packages/sdk/src/configure/services/workflow/job.ts index 33e94327c..08bf48874 100644 --- a/packages/sdk/src/configure/services/workflow/job.ts +++ b/packages/sdk/src/configure/services/workflow/job.ts @@ -37,23 +37,20 @@ type JobBody = [null] extends [I] * Type constraints: * - Input: Must be JsonValue-compatible (plain objects/arrays; no class instances or functions) or undefined. * - Output: Must be JsonValue-compatible (plain objects/arrays; no class instances or functions), undefined, or void. - * - Trigger returns `Awaited` as-is (no Jsonify transformation). + * - Trigger returns `Awaited` as-is (no Promise or Jsonify transformation). */ export interface WorkflowJob { name: Name; /** - * Trigger this job with the given input. Returns a Promise that resolves - * to the job's output value. + * Trigger this job with the given input and return the job's output value. * @example * body: async (input) => { - * const a = await jobA.trigger({ id: input.id }); - * const b = await jobB.trigger({ id: input.id }); + * const a = jobA.trigger({ id: input.id }); + * const b = jobB.trigger({ id: input.id }); * return { a, b }; * } */ - trigger: [Input] extends [undefined] - ? () => Promise> - : (input: Input) => Promise>; + trigger: [Input] extends [undefined] ? () => Awaited : (input: Input) => Awaited; body: (input: Input, context: WorkflowJobContext) => Output | Promise; } @@ -90,9 +87,9 @@ interface CreateWorkflowJobConfig { * // Orchestrator job that fans out to other jobs. * export const orchestrate = createWorkflowJob({ * name: "orchestrate", - * body: async (input: { orderId: string }) => { - * const inventory = await checkInventory.trigger({ orderId: input.orderId }); - * const payment = await processPayment.trigger({ orderId: input.orderId }); + * body: (input: { orderId: string }) => { + * const inventory = checkInventory.trigger({ orderId: input.orderId }); + * const payment = processPayment.trigger({ orderId: input.orderId }); * return { inventory, payment }; * }, * }); @@ -102,18 +99,18 @@ export function createWorkflowJob> { const body = config.body as (input: I, context: WorkflowJobContext) => O | Promise; - // Test-only registry/trigger shim; the platform bundle sets the flag so it is DCE'd. + // Test-only local runner registry; the platform bundle sets the flag so it is DCE'd. if (!process.env.TAILOR_PLATFORM_BUNDLE) { registerJob(config.name, body as RegisteredJobBody); } const trigger = process.env.TAILOR_PLATFORM_BUNDLE - ? async () => { + ? () => { throw new Error( "This workflow job's .trigger() is rewritten at build time and is unavailable in the bundle", ); } - : async (args?: unknown) => (await dispatchTriggerJob(config.name, args)) as Awaited; + : (args?: unknown) => dispatchTriggerJob(config.name, args) as Awaited; return brandValue( { name: config.name, trigger, body } as WorkflowJob>, diff --git a/packages/sdk/src/configure/services/workflow/registry.ts b/packages/sdk/src/configure/services/workflow/registry.ts index 8b0905441..2780b3ea9 100644 --- a/packages/sdk/src/configure/services/workflow/registry.ts +++ b/packages/sdk/src/configure/services/workflow/registry.ts @@ -13,12 +13,7 @@ export type RegisteredJobBody = ( context: { env: TailorEnv; invoker?: TailorInvoker }, ) => unknown | Promise; -export interface RegisteredWorkflow { - mainJobName: string; -} - const JOB_REGISTRY_KEY: unique symbol = Symbol.for("tailor-platform/sdk:job-registry"); -const WORKFLOW_REGISTRY_KEY: unique symbol = Symbol.for("tailor-platform/sdk:workflow-registry"); type PlatformWorkflow = { triggerWorkflow: (name: string, args?: unknown, options?: unknown) => Promise; @@ -27,7 +22,6 @@ type PlatformWorkflow = { type GlobalWithRegistry = typeof globalThis & { [JOB_REGISTRY_KEY]?: Map; - [WORKFLOW_REGISTRY_KEY]?: Map; tailor?: { workflow?: PlatformWorkflow }; }; @@ -41,20 +35,10 @@ function jobs(): Map { return map; } -function workflows(): Map { - const g = globalThis as GlobalWithRegistry; - let map = g[WORKFLOW_REGISTRY_KEY]; - if (!map) { - map = new Map(); - g[WORKFLOW_REGISTRY_KEY] = map; - } - return map; -} - /** * Register a job body keyed by job name. Called as a side effect by - * `createWorkflowJob` so the vitest mock can execute the body when - * `globalThis.tailor.workflow.triggerJobFunction(name, args)` is invoked. + * `createWorkflowJob` so `runWorkflowLocally()` can execute dependent job + * bodies when `globalThis.tailor.workflow.triggerJobFunction(name, args)` is invoked. * * In production builds the bundler rewrites `.trigger()` calls so this registry * is never read; the gated write is dropped as dead code. @@ -74,30 +58,22 @@ export function getRegisteredJob(name: string): RegisteredJobBody | undefined { return jobs().get(name); } -/** - * Register a workflow's main job name so the mock can run the workflow locally. - * @param name - Workflow name - * @param mainJobName - Name of the workflow's main job - */ -export function registerWorkflow(name: string, mainJobName: string): void { - workflows().set(name, { mainJobName }); -} - -/** - * Look up a registered workflow by name. - * @param name - Workflow name - * @returns The registered workflow, or undefined - */ -export function getRegisteredWorkflow(name: string): RegisteredWorkflow | undefined { - return workflows().get(name); -} - function currentPlatformWorkflow(): PlatformWorkflow | undefined { // globalThis may not have the tailor property at runtime // oxlint-disable-next-line typescript/no-unnecessary-condition return (globalThis as GlobalWithRegistry).tailor?.workflow; } +function requirePlatformWorkflow(): PlatformWorkflow { + const workflow = currentPlatformWorkflow(); + if (!workflow) { + throw new Error( + "tailor.workflow is not available. Run tests in the `tailor-runtime` Vitest environment and use mockWorkflow(), or use runWorkflowLocally() from @tailor-platform/sdk/vitest for local workflow execution.", + ); + } + return workflow; +} + // A valid placeholder UUID, so callers that validate the execution id behave the // same locally as against the platform. export const TRIGGER_DEFAULT = "00000000-0000-4000-8000-000000000000"; @@ -106,25 +82,18 @@ function serializeReturn(out: unknown): unknown { return out instanceof Promise ? out.then((v) => platformSerialize(v)) : platformSerialize(out); } -// Runs the registered body across the platform JSON boundary. Shared by the -// `tailor-runtime` default runner and the no-shim `.trigger()` fallback below. +// Runs the registered body across the platform JSON boundary for +// `runWorkflowLocally()`. export function runRegisteredJob(name: string, args?: unknown): unknown { const body = getRegisteredJob(name); const out = body ? body(platformSerialize(args), buildJobContext()) : null; return serializeReturn(out); } -export async function runRegisteredWorkflow(name: string, args?: unknown): Promise { - const workflow = getRegisteredWorkflow(name); - if (workflow) await runRegisteredJob(workflow.mainJobName, args); - return TRIGGER_DEFAULT; -} - -// `.trigger()` routes through the installed `tailor.workflow` shim, falling back -// to running the registered body/workflow locally when none is installed. +// `.trigger()` routes through the installed `tailor.workflow` shim. Local body +// execution is intentionally available only through `runWorkflowLocally()`. export function dispatchTriggerJob(name: string, args?: unknown): unknown { - const workflow = currentPlatformWorkflow(); - return workflow ? workflow.triggerJobFunction(name, args) : runRegisteredJob(name, args); + return requirePlatformWorkflow().triggerJobFunction(name, args); } export function dispatchTriggerWorkflow( @@ -132,8 +101,5 @@ export function dispatchTriggerWorkflow( args?: unknown, options?: unknown, ): Promise { - const workflow = currentPlatformWorkflow(); - return workflow - ? workflow.triggerWorkflow(name, args, options) - : runRegisteredWorkflow(name, args); + return requirePlatformWorkflow().triggerWorkflow(name, args, options); } diff --git a/packages/sdk/src/configure/services/workflow/test-env-key.ts b/packages/sdk/src/configure/services/workflow/test-env-key.ts index a7db0eba9..28dc2a9a6 100644 --- a/packages/sdk/src/configure/services/workflow/test-env-key.ts +++ b/packages/sdk/src/configure/services/workflow/test-env-key.ts @@ -1,7 +1,7 @@ /** * Typed accessors for the test-time globalThis slot used to pass `env` from * `mockWorkflow().setEnv()` (in `@tailor-platform/sdk/vitest`) to - * `createWorkflowJob().trigger()` bodies. The slot key is private to this + * `runWorkflowLocally()` job bodies. The slot key is private to this * module; callers go through the get/set/clear functions below so both sides * share the same access path. * @@ -41,7 +41,7 @@ export function clearWorkflowTestEnv(): void { } /** - * Env-var fallback read by `.trigger()` when `mockWorkflow().setEnv()` is unset. + * Env-var fallback read by `runWorkflowLocally()` when `mockWorkflow().setEnv()` is unset. * @deprecated Use `mockWorkflow().setEnv()` from `@tailor-platform/sdk/vitest`. * @internal */ diff --git a/packages/sdk/src/configure/services/workflow/workflow.ts b/packages/sdk/src/configure/services/workflow/workflow.ts index bb54512f9..2d4bc68b1 100644 --- a/packages/sdk/src/configure/services/workflow/workflow.ts +++ b/packages/sdk/src/configure/services/workflow/workflow.ts @@ -1,6 +1,6 @@ /* oxlint-disable typescript/no-explicit-any */ import { brandValue } from "@/utils/brand"; -import { dispatchTriggerWorkflow, registerWorkflow } from "./registry"; +import { dispatchTriggerWorkflow } from "./registry"; import type { AuthInvoker } from "../auth"; import type { WorkflowJob } from "./job"; import type { MachineUserName } from "@/configure/types/machine-user"; @@ -48,8 +48,8 @@ interface WorkflowDefinition> { * export const fetchData = createWorkflowJob({ name: "fetch-data", body: async (input: { id: string }) => ({ id: input.id }) }); * export const processData = createWorkflowJob({ * name: "process-data", - * body: async (input: { id: string }) => { - * const data = await fetchData.trigger({ id: input.id }); + * body: (input: { id: string }) => { + * const data = fetchData.trigger({ id: input.id }); * return { data }; * }, * }); @@ -63,11 +63,6 @@ interface WorkflowDefinition> { export function createWorkflow>( config: WorkflowDefinition, ): Workflow { - // Test-only registry/trigger shim; the platform bundle sets the flag so it is DCE'd. - if (!process.env.TAILOR_PLATFORM_BUNDLE) { - registerWorkflow(config.name, config.mainJob.name); - } - return brandValue( { ...config, @@ -77,7 +72,7 @@ export function createWorkflow>( "workflow.trigger() is rewritten at build time and unavailable in the bundle", ); } - : async (args, options) => await dispatchTriggerWorkflow(config.name, args, options), + : (args, options) => dispatchTriggerWorkflow(config.name, args, options), } as Workflow, "workflow", ); diff --git a/packages/sdk/src/vitest/index.ts b/packages/sdk/src/vitest/index.ts index c26cd0b4c..4ee832f93 100644 --- a/packages/sdk/src/vitest/index.ts +++ b/packages/sdk/src/vitest/index.ts @@ -69,4 +69,5 @@ export { mockIconv, } from "./mock"; +export { runWorkflowLocally, type RunWorkflowLocallyOptions } from "./workflow-local"; export { createKyselyMock, type KyselyMock, type ExecutedQuery } from "./mock-kysely"; diff --git a/packages/sdk/src/vitest/mock.test.ts b/packages/sdk/src/vitest/mock.test.ts index 3aa783fb0..f690c230e 100644 --- a/packages/sdk/src/vitest/mock.test.ts +++ b/packages/sdk/src/vitest/mock.test.ts @@ -14,6 +14,7 @@ import { cleanupMocks, RUNTIME_FLAG_KEY, } from "./mock"; +import { runWorkflowLocally } from "./workflow-local"; describe("mock", () => { beforeEach(() => { @@ -116,10 +117,10 @@ describe("mock", () => { vi.unstubAllEnvs(); }); - test("records triggered jobs", () => { + test("records triggered jobs even when no handler is configured", () => { using wf = mockWorkflow(); const trigger = (globalThis as any).tailor.workflow.triggerJobFunction; - trigger("my-job", { key: "value" }); + expect(() => trigger("my-job", { key: "value" })).toThrow(/No workflow job mock/); expect(wf.triggeredJobs).toEqual([{ jobName: "my-job", args: { key: "value" } }]); }); @@ -159,37 +160,55 @@ describe("mock", () => { test("reset clears all state", () => { using wf = mockWorkflow(); const trigger = (globalThis as any).tailor.workflow.triggerJobFunction; - trigger("job", {}); + expect(() => trigger("job", {})).toThrow(/No workflow job mock/); wf.reset(); expect(wf.triggeredJobs).toHaveLength(0); }); - test("setEnv exposes env to job bodies via .trigger()", async () => { + test("setEnv exposes env to job bodies via runWorkflowLocally()", async () => { using wf = mockWorkflow(); const captureEnv = createWorkflowJob({ name: "capture-env", body: (_input: undefined, ctx) => ctx.env, }); + const workflow = createWorkflow({ name: "capture-env-workflow", mainJob: captureEnv }); wf.setEnv({ STAGE: "test", REGION: "asia" }); - const env = await captureEnv.trigger(); + const env = await runWorkflowLocally(workflow); expect(env).toEqual({ STAGE: "test", REGION: "asia" }); }); + test("runWorkflowLocally env option overrides and restores the mock env", async () => { + using wf = mockWorkflow(); + const captureEnv = createWorkflowJob({ + name: "capture-env-option", + body: (_input: undefined, ctx) => ctx.env, + }); + const workflow = createWorkflow({ name: "capture-env-option-workflow", mainJob: captureEnv }); + + wf.setEnv({ STAGE: "from-mock" }); + + expect( + await runWorkflowLocally(workflow, undefined, { env: { STAGE: "from-option" } }), + ).toEqual({ STAGE: "from-option" }); + expect(await runWorkflowLocally(workflow)).toEqual({ STAGE: "from-mock" }); + }); + test("reset clears env back to {}", async () => { using wf = mockWorkflow(); const captureEnv = createWorkflowJob({ name: "capture-env-reset", body: (_input: undefined, ctx) => ctx.env, }); + const workflow = createWorkflow({ name: "capture-env-reset-workflow", mainJob: captureEnv }); wf.setEnv({ STAGE: "test" }); wf.reset(); - expect(await captureEnv.trigger()).toEqual({}); + expect(await runWorkflowLocally(workflow)).toEqual({}); }); describe("backward-compat: deprecated WORKFLOW_TEST_ENV_KEY env-var", () => { @@ -199,11 +218,15 @@ describe("mock", () => { name: "capture-env-compat-priority", body: (_input: undefined, ctx) => ctx.env, }); + const workflow = createWorkflow({ + name: "capture-env-compat-priority-workflow", + mainJob: captureEnv, + }); vi.stubEnv(WORKFLOW_TEST_ENV_KEY, JSON.stringify({ STAGE: "fallback" })); wf.setEnv({ STAGE: "from-setenv" }); - expect(await captureEnv.trigger()).toEqual({ STAGE: "from-setenv" }); + expect(await runWorkflowLocally(workflow)).toEqual({ STAGE: "from-setenv" }); }); test("env-var is used when setEnv has not been called", async () => { @@ -211,10 +234,14 @@ describe("mock", () => { name: "capture-env-compat-fallback", body: (_input: undefined, ctx) => ctx.env, }); + const workflow = createWorkflow({ + name: "capture-env-compat-fallback-workflow", + mainJob: captureEnv, + }); vi.stubEnv(WORKFLOW_TEST_ENV_KEY, JSON.stringify({ STAGE: "from-env-var" })); - expect(await captureEnv.trigger()).toEqual({ STAGE: "from-env-var" }); + expect(await runWorkflowLocally(workflow)).toEqual({ STAGE: "from-env-var" }); }); test("throws when the env-var is valid JSON but not an object", async () => { @@ -223,25 +250,46 @@ describe("mock", () => { name: "capture-env-compat-nonobject", body: (_input: undefined, ctx) => ctx.env, }); + const workflow = createWorkflow({ + name: "capture-env-compat-nonobject-workflow", + mainJob: captureEnv, + }); vi.stubEnv(WORKFLOW_TEST_ENV_KEY, "42"); - await expect(captureEnv.trigger()).rejects.toThrow(/must be a JSON object/); + await expect(runWorkflowLocally(workflow)).rejects.toThrow(/must be a JSON object/); }); }); }); describe("default workflow runtime (no mockWorkflow needed)", () => { - test("a job's .trigger() runs its real body", async () => { + test("a job's .trigger() requires a configured workflow mock", () => { const double = createWorkflowJob({ name: "default-runtime-double", body: (input: { n: number }) => ({ doubled: input.n * 2 }), }); - expect(await double.trigger({ n: 21 })).toEqual({ doubled: 42 }); + expect(() => double.trigger({ n: 21 })).toThrow(/No workflow job mock/); + }); + + test("workflow.trigger() returns an execution id without running the main job", async () => { + let bodyRan = false; + const main = createWorkflowJob({ + name: "default-runtime-main-trigger", + body: () => { + bodyRan = true; + return { ok: true }; + }, + }); + const workflow = createWorkflow({ name: "default-runtime-trigger-wf", mainJob: main }); + + await expect(workflow.trigger(undefined)).resolves.toBe( + "00000000-0000-4000-8000-000000000000", + ); + expect(bodyRan).toBe(false); }); - test("workflow.mainJob.trigger() runs the whole chain", async () => { + test("runWorkflowLocally() runs the whole chain", async () => { const inner = createWorkflowJob({ name: "default-runtime-inner", body: (input: { n: number }) => ({ n: input.n + 1 }), @@ -249,23 +297,24 @@ describe("mock", () => { const main = createWorkflowJob({ name: "default-runtime-main", body: async (input: { n: number }) => { - const a = await inner.trigger({ n: input.n }); - const b = await inner.trigger({ n: a.n }); + const a = inner.trigger({ n: input.n }); + const b = inner.trigger({ n: a.n }); return { total: b.n }; }, }); const workflow = createWorkflow({ name: "default-runtime-wf", mainJob: main }); - expect(await workflow.mainJob.trigger({ n: 0 })).toEqual({ total: 2 }); + expect(await runWorkflowLocally(workflow, { n: 0 })).toEqual({ total: 2 }); }); - test("the JSON boundary rejects a non-serializable trigger result", async () => { + test("runWorkflowLocally() rejects a non-serializable trigger result", async () => { const bad = createWorkflowJob({ name: "default-runtime-bad", body: () => ({ when: new Date() }) as never, }); + const workflow = createWorkflow({ name: "default-runtime-bad-wf", mainJob: bad }); - await expect(bad.trigger()).rejects.toThrow(/Date instance/); + await expect(runWorkflowLocally(workflow)).rejects.toThrow(/Date instance/); }); }); diff --git a/packages/sdk/src/vitest/mock.ts b/packages/sdk/src/vitest/mock.ts index d7103ab48..96cb1436b 100644 --- a/packages/sdk/src/vitest/mock.ts +++ b/packages/sdk/src/vitest/mock.ts @@ -23,15 +23,10 @@ */ import { type Mock, vi } from "vitest"; -import { - getRegisteredJob, - getRegisteredWorkflow, - TRIGGER_DEFAULT, -} from "@/configure/services/workflow/registry"; +import { TRIGGER_DEFAULT } from "@/configure/services/workflow/registry"; import { assertDefined } from "@/utils/assert"; import { platformSerialize } from "@/utils/test/platform-serialize"; import { - buildJobContext, clearWorkflowTestEnv, writeWorkflowTestEnv, } from "../configure/services/workflow/test-env-key"; @@ -337,25 +332,21 @@ export function mockWorkflow() { const root = tailorRoot(); const prev = root.workflow; - // Default impls (also restored by reset): run the registered body by name so a - // `.trigger()` with no handler/result executes the real job locally. - const defaultTriggerJob = (jobName: string, args?: unknown): unknown => { - const body = getRegisteredJob(jobName); - return body ? body(args, buildJobContext()) : null; + const defaultTriggerJob = (jobName: string, _args?: unknown): unknown => { + throw new Error( + `No workflow job mock for "${jobName}". Call mockWorkflow().setJobHandler(...) or enqueueResult(...), or use runWorkflowLocally() for local workflow execution.`, + ); }; const defaultTriggerWorkflow = async ( - workflowName: string, - args?: unknown, + _workflowName: string, + _args?: unknown, _options?: TriggerWorkflowOptions, ): Promise => { - const wf = getRegisteredWorkflow(workflowName); - if (wf) await installedTriggerJobFunction(wf.mainJobName, args); return TRIGGER_DEFAULT; }; // Inner vi.fns hold the overridable behavior + call recording; the installed - // shims below cross the platform JSON boundary (serialize args + results) once - // so every path (default body, setJobHandler, enqueueResult) is covered. + // shims below cross the platform JSON boundary (serialize args + results) once. const triggerJobFunction = vi.fn(defaultTriggerJob); const triggerWorkflow = vi.fn(defaultTriggerWorkflow); const wait = vi.fn((_key: string, _payload?: unknown): unknown => null); @@ -465,7 +456,7 @@ export function mockWorkflow() { }) as SetWaitHandler, /** - * Set the `env` passed to job bodies invoked via `createWorkflowJob().trigger()`. + * Set the `env` passed to job bodies invoked via `runWorkflowLocally()`. * Cleared on dispose / reset. * @param env - Env passed to job bodies. */ diff --git a/packages/sdk/src/vitest/workflow-local.ts b/packages/sdk/src/vitest/workflow-local.ts new file mode 100644 index 000000000..6c15ef085 --- /dev/null +++ b/packages/sdk/src/vitest/workflow-local.ts @@ -0,0 +1,128 @@ +/* oxlint-disable typescript/no-explicit-any */ +import { TRIGGER_DEFAULT, runRegisteredJob } from "../configure/services/workflow/registry"; +import { + clearWorkflowTestEnv, + readWorkflowTestEnv, + writeWorkflowTestEnv, +} from "../configure/services/workflow/test-env-key"; +import type { Workflow, WorkflowJob } from "../configure/services/workflow"; +import type { TailorWorkflowAPI } from "../runtime/workflow"; +import type { TailorEnv } from "../types/env"; + +type AnyWorkflowJob = WorkflowJob; +type AnyWorkflow = Workflow; + +type WorkflowInput = + W["mainJob"] extends WorkflowJob ? I : never; +type WorkflowOutput = + W["mainJob"] extends WorkflowJob ? Awaited : never; + +type GlobalWithTailor = { + tailor?: { + workflow?: TailorWorkflowAPI; + }; +}; + +export interface RunWorkflowLocallyOptions { + /** Env passed to workflow job bodies during this local run. */ + env?: TailorEnv; +} + +/** + * Run a workflow's main job and dependent job triggers locally with real job bodies. + * + * Use this for local full-chain workflow tests. Regular `.trigger()` calls + * delegate to the platform workflow runtime and should be mocked with + * `mockWorkflow()` when you are not intentionally running the local chain. + * @param workflow - Workflow definition to run + * @returns The main job output + */ +export function runWorkflowLocally>>( + workflow: W, +): Promise>; +/** + * Run a no-input workflow locally with optional runner settings. + * @param workflow - Workflow definition to run + * @param args - Must be `undefined` for no-input workflows + * @param options - Local runner options + * @returns The main job output + */ +export function runWorkflowLocally>>( + workflow: W, + args: undefined, + options?: RunWorkflowLocallyOptions, +): Promise>; +/** + * Run a workflow locally with real job bodies. + * @param workflow - Workflow definition to run + * @param args - Arguments passed to the workflow's main job + * @param options - Local runner options + * @returns The main job output + */ +export function runWorkflowLocally( + workflow: W, + args: WorkflowInput, + options?: RunWorkflowLocallyOptions, +): Promise>; +export async function runWorkflowLocally( + workflow: W, + args?: WorkflowInput, + options?: RunWorkflowLocallyOptions, +): Promise> { + const root = globalThis as unknown as GlobalWithTailor; + const previousTailor = root.tailor; + const previousEnv = readWorkflowTestEnv(); + const hasPreviousEnv = previousEnv !== undefined; + + if (options?.env !== undefined) { + writeWorkflowTestEnv({ ...options.env }); + } + + root.tailor = { + ...previousTailor, + workflow: createLocalWorkflowRuntime(previousTailor?.workflow), + }; + + try { + return (await runRegisteredJob(workflow.mainJob.name, args)) as WorkflowOutput; + } finally { + if (previousTailor) { + root.tailor = previousTailor; + } else { + delete root.tailor; + } + + if (options?.env !== undefined) { + if (hasPreviousEnv) { + writeWorkflowTestEnv(previousEnv); + } else { + clearWorkflowTestEnv(); + } + } + } +} + +function createLocalWorkflowRuntime(previous?: TailorWorkflowAPI): TailorWorkflowAPI { + return { + triggerJobFunction: (name, args) => runRegisteredJob(name, args), + triggerWorkflow: (name, args, options) => + previous ? previous.triggerWorkflow(name, args, options) : Promise.resolve(TRIGGER_DEFAULT), + wait: (key, payload) => { + if (previous) { + return previous.wait(key, payload); + } + throw new Error( + `No wait handler for "${key}". Acquire mockWorkflow() and call setWaitHandler(...).`, + ); + }, + resolve: async (executionId, key, callback) => { + if (previous) { + await previous.resolve(executionId, key, callback); + return; + } + throw new Error( + "No resolve handler. Acquire mockWorkflow() and call setResolveHandler(...).", + ); + }, + }; +} diff --git a/packages/sdk/src/vitest/workflow-runtime.ts b/packages/sdk/src/vitest/workflow-runtime.ts index 1ef202715..17c5786e3 100644 --- a/packages/sdk/src/vitest/workflow-runtime.ts +++ b/packages/sdk/src/vitest/workflow-runtime.ts @@ -1,7 +1,7 @@ // Default `tailor.workflow` runner installed by the `tailor-runtime` environment. // Must stay free of `vitest` (`vi`): it loads via `./globals` in the environment // realm where `vi` is unavailable, hence relative imports only (no `@/` alias). -import { runRegisteredJob, runRegisteredWorkflow } from "../configure/services/workflow/registry"; +import { TRIGGER_DEFAULT } from "../configure/services/workflow/registry"; export interface DefaultWorkflowRuntime { triggerJobFunction: (name: string, args?: unknown) => unknown; @@ -16,8 +16,12 @@ export interface DefaultWorkflowRuntime { export function createDefaultWorkflowRuntime(): DefaultWorkflowRuntime { return { - triggerJobFunction: (name, args) => runRegisteredJob(name, args), - triggerWorkflow: (name, args) => runRegisteredWorkflow(name, args), + triggerJobFunction: (name) => { + throw new Error( + `No workflow job mock for "${name}". Acquire mockWorkflow() and call setJobHandler(...) or enqueueResult(...), or use runWorkflowLocally() for local workflow execution.`, + ); + }, + triggerWorkflow: async () => TRIGGER_DEFAULT, wait: (key: string): unknown => { throw new Error( `No wait handler for "${key}". Acquire mockWorkflow() and call setWaitHandler(...).`, From 95f587a8979d08846ea290f18017c4e86ae731da Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 15 Jun 2026 20:45:51 +0900 Subject: [PATCH 002/618] docs: clarify workflow local runner comments --- packages/sdk/src/configure/services/workflow/job.ts | 2 +- packages/sdk/src/configure/services/workflow/test-env-key.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/configure/services/workflow/job.ts b/packages/sdk/src/configure/services/workflow/job.ts index 08bf48874..4fe8ec334 100644 --- a/packages/sdk/src/configure/services/workflow/job.ts +++ b/packages/sdk/src/configure/services/workflow/job.ts @@ -72,7 +72,7 @@ interface CreateWorkflowJobConfig { * class instances exposing methods are rejected via the property walk. * @param config - Job configuration with name and body function. * @param config.name - Unique job name across the project. - * @param config.body - Async function that processes the job input. + * @param config.body - Function that processes the job input. * @returns A WorkflowJob that can be triggered from other jobs. * @example * // Simple job with async body: diff --git a/packages/sdk/src/configure/services/workflow/test-env-key.ts b/packages/sdk/src/configure/services/workflow/test-env-key.ts index 28dc2a9a6..09927b5f5 100644 --- a/packages/sdk/src/configure/services/workflow/test-env-key.ts +++ b/packages/sdk/src/configure/services/workflow/test-env-key.ts @@ -25,7 +25,7 @@ export function readWorkflowTestEnv(): TailorEnv | undefined { /** * Write the test-time env slot. - * @param env - Env value to expose to `.trigger()` bodies. + * @param env - Env value to expose to `runWorkflowLocally()` job bodies. * @internal */ export function writeWorkflowTestEnv(env: TailorEnv): void { From 477577f8cbd190244295d0e787b4488249e7434f Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 15 Jun 2026 20:58:15 +0900 Subject: [PATCH 003/618] fix: replay async local workflow triggers --- .../configure/services/workflow/registry.ts | 14 --- packages/sdk/src/vitest/mock.test.ts | 2 +- packages/sdk/src/vitest/workflow-local.ts | 107 +++++++++++++++++- 3 files changed, 103 insertions(+), 20 deletions(-) diff --git a/packages/sdk/src/configure/services/workflow/registry.ts b/packages/sdk/src/configure/services/workflow/registry.ts index 2780b3ea9..e90caaadf 100644 --- a/packages/sdk/src/configure/services/workflow/registry.ts +++ b/packages/sdk/src/configure/services/workflow/registry.ts @@ -1,5 +1,3 @@ -import { platformSerialize } from "@/utils/test/platform-serialize"; -import { buildJobContext } from "./test-env-key"; import type { TailorEnv } from "@/types/env"; import type { TailorInvoker } from "@/types/user"; @@ -78,18 +76,6 @@ function requirePlatformWorkflow(): PlatformWorkflow { // same locally as against the platform. export const TRIGGER_DEFAULT = "00000000-0000-4000-8000-000000000000"; -function serializeReturn(out: unknown): unknown { - return out instanceof Promise ? out.then((v) => platformSerialize(v)) : platformSerialize(out); -} - -// Runs the registered body across the platform JSON boundary for -// `runWorkflowLocally()`. -export function runRegisteredJob(name: string, args?: unknown): unknown { - const body = getRegisteredJob(name); - const out = body ? body(platformSerialize(args), buildJobContext()) : null; - return serializeReturn(out); -} - // `.trigger()` routes through the installed `tailor.workflow` shim. Local body // execution is intentionally available only through `runWorkflowLocally()`. export function dispatchTriggerJob(name: string, args?: unknown): unknown { diff --git a/packages/sdk/src/vitest/mock.test.ts b/packages/sdk/src/vitest/mock.test.ts index f690c230e..f9b056d8a 100644 --- a/packages/sdk/src/vitest/mock.test.ts +++ b/packages/sdk/src/vitest/mock.test.ts @@ -292,7 +292,7 @@ describe("mock", () => { test("runWorkflowLocally() runs the whole chain", async () => { const inner = createWorkflowJob({ name: "default-runtime-inner", - body: (input: { n: number }) => ({ n: input.n + 1 }), + body: async (input: { n: number }) => ({ n: input.n + 1 }), }); const main = createWorkflowJob({ name: "default-runtime-main", diff --git a/packages/sdk/src/vitest/workflow-local.ts b/packages/sdk/src/vitest/workflow-local.ts index 6c15ef085..fc7c821e7 100644 --- a/packages/sdk/src/vitest/workflow-local.ts +++ b/packages/sdk/src/vitest/workflow-local.ts @@ -1,10 +1,12 @@ /* oxlint-disable typescript/no-explicit-any */ -import { TRIGGER_DEFAULT, runRegisteredJob } from "../configure/services/workflow/registry"; +import { TRIGGER_DEFAULT, getRegisteredJob } from "../configure/services/workflow/registry"; import { + buildJobContext, clearWorkflowTestEnv, readWorkflowTestEnv, writeWorkflowTestEnv, } from "../configure/services/workflow/test-env-key"; +import { platformSerialize } from "../utils/test/platform-serialize"; import type { Workflow, WorkflowJob } from "../configure/services/workflow"; import type { TailorWorkflowAPI } from "../runtime/workflow"; import type { TailorEnv } from "../types/env"; @@ -23,6 +25,24 @@ type GlobalWithTailor = { }; }; +interface TriggerRecord { + jobName: string; + args: unknown; + result: unknown; +} + +interface LocalExecution { + records: TriggerRecord[]; + cursor: number; +} + +class PendingTrigger { + constructor( + readonly jobName: string, + readonly args: unknown, + ) {} +} + export interface RunWorkflowLocallyOptions { /** Env passed to workflow job bodies during this local run. */ env?: TailorEnv; @@ -73,6 +93,7 @@ export async function runWorkflowLocally( const previousTailor = root.tailor; const previousEnv = readWorkflowTestEnv(); const hasPreviousEnv = previousEnv !== undefined; + const runner = createLocalJobRunner(); if (options?.env !== undefined) { writeWorkflowTestEnv({ ...options.env }); @@ -80,11 +101,11 @@ export async function runWorkflowLocally( root.tailor = { ...previousTailor, - workflow: createLocalWorkflowRuntime(previousTailor?.workflow), + workflow: createLocalWorkflowRuntime(previousTailor?.workflow, runner.triggerJobFunction), }; try { - return (await runRegisteredJob(workflow.mainJob.name, args)) as WorkflowOutput; + return (await runner.runJob(workflow.mainJob.name, args)) as WorkflowOutput; } finally { if (previousTailor) { root.tailor = previousTailor; @@ -102,9 +123,85 @@ export async function runWorkflowLocally( } } -function createLocalWorkflowRuntime(previous?: TailorWorkflowAPI): TailorWorkflowAPI { +function createLocalJobRunner(): { + runJob: (name: string, args?: unknown) => Promise; + triggerJobFunction: (name: string, args?: unknown) => unknown; +} { + let activeExecution: LocalExecution | undefined; + + const triggerJobFunction = (jobName: string, args?: unknown): unknown => { + if (!activeExecution) { + throw new Error( + `Cannot trigger workflow job "${jobName}" outside runWorkflowLocally() job execution.`, + ); + } + + const serializedArgs = platformSerialize(args); + const index = activeExecution.cursor; + activeExecution.cursor += 1; + + const cached = activeExecution.records[index]; + if (cached) { + assertSameTrigger(cached, jobName, serializedArgs); + return cached.result; + } + + throw new PendingTrigger(jobName, serializedArgs); + }; + + const runJob = async (name: string, args?: unknown): Promise => { + const body = getRegisteredJob(name); + if (!body) { + return null; + } + + const records: TriggerRecord[] = []; + + for (;;) { + const execution: LocalExecution = { records, cursor: 0 }; + const previousExecution = activeExecution; + activeExecution = execution; + + try { + const out = await body(platformSerialize(args), buildJobContext()); + if (execution.cursor !== records.length) { + throw new Error( + `Workflow job trigger sequence changed while replaying "${name}". Expected ${records.length} trigger(s), but replay reached ${execution.cursor}.`, + ); + } + return platformSerialize(out); + } catch (cause) { + if (cause instanceof PendingTrigger) { + const result = await runJob(cause.jobName, cause.args); + records.push({ jobName: cause.jobName, args: cause.args, result }); + continue; + } + throw cause; + } finally { + activeExecution = previousExecution; + } + } + }; + + return { runJob, triggerJobFunction }; +} + +function assertSameTrigger(record: TriggerRecord, jobName: string, args: unknown): void { + if (record.jobName === jobName && JSON.stringify(record.args) === JSON.stringify(args)) { + return; + } + + throw new Error( + `Workflow job trigger sequence changed while replaying. Expected ${record.jobName}(${JSON.stringify(record.args)}), but got ${jobName}(${JSON.stringify(args)}).`, + ); +} + +function createLocalWorkflowRuntime( + previous: TailorWorkflowAPI | undefined, + triggerJobFunction: (name: string, args?: unknown) => unknown, +): TailorWorkflowAPI { return { - triggerJobFunction: (name, args) => runRegisteredJob(name, args), + triggerJobFunction, triggerWorkflow: (name, args, options) => previous ? previous.triggerWorkflow(name, args, options) : Promise.resolve(TRIGGER_DEFAULT), wait: (key, payload) => { From e5ff7340cdbbbd55d563f2040947c3abc00f74e2 Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 15 Jun 2026 21:09:42 +0900 Subject: [PATCH 004/618] fix: harden local workflow replay --- packages/sdk/src/vitest/mock.test.ts | 49 +++++++++++++++++++++++ packages/sdk/src/vitest/workflow-local.ts | 26 +++++++++--- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/vitest/mock.test.ts b/packages/sdk/src/vitest/mock.test.ts index f9b056d8a..0508e5e9e 100644 --- a/packages/sdk/src/vitest/mock.test.ts +++ b/packages/sdk/src/vitest/mock.test.ts @@ -307,6 +307,55 @@ describe("mock", () => { expect(await runWorkflowLocally(workflow, { n: 0 })).toEqual({ total: 2 }); }); + test("runWorkflowLocally() clones cached trigger results on replay", async () => { + const mutable = createWorkflowJob({ + name: "default-runtime-mutable", + body: () => ({ items: [] as string[] }), + }); + const step = createWorkflowJob({ + name: "default-runtime-step", + body: () => ({ ok: true }), + }); + const main = createWorkflowJob({ + name: "default-runtime-mutation-main", + body: () => { + const result = mutable.trigger(); + result.items.push("x"); + step.trigger(); + return result; + }, + }); + const workflow = createWorkflow({ + name: "default-runtime-mutation-wf", + mainJob: main, + }); + + expect(await runWorkflowLocally(workflow)).toEqual({ items: ["x"] }); + }); + + test("runWorkflowLocally() hides replay signals from user catch blocks", async () => { + const inner = createWorkflowJob({ + name: "default-runtime-caught-inner", + body: async () => ({ ok: true }), + }); + const main = createWorkflowJob({ + name: "default-runtime-caught-main", + body: () => { + try { + return inner.trigger(); + } catch { + return { ok: false }; + } + }, + }); + const workflow = createWorkflow({ + name: "default-runtime-caught-wf", + mainJob: main, + }); + + expect(await runWorkflowLocally(workflow)).toEqual({ ok: true }); + }); + test("runWorkflowLocally() rejects a non-serializable trigger result", async () => { const bad = createWorkflowJob({ name: "default-runtime-bad", diff --git a/packages/sdk/src/vitest/workflow-local.ts b/packages/sdk/src/vitest/workflow-local.ts index fc7c821e7..0af24ac39 100644 --- a/packages/sdk/src/vitest/workflow-local.ts +++ b/packages/sdk/src/vitest/workflow-local.ts @@ -34,6 +34,7 @@ interface TriggerRecord { interface LocalExecution { records: TriggerRecord[]; cursor: number; + pending?: PendingTrigger; } class PendingTrigger { @@ -135,6 +136,9 @@ function createLocalJobRunner(): { `Cannot trigger workflow job "${jobName}" outside runWorkflowLocally() job execution.`, ); } + if (activeExecution.pending) { + throw activeExecution.pending; + } const serializedArgs = platformSerialize(args); const index = activeExecution.cursor; @@ -143,10 +147,12 @@ function createLocalJobRunner(): { const cached = activeExecution.records[index]; if (cached) { assertSameTrigger(cached, jobName, serializedArgs); - return cached.result; + return platformSerialize(cached.result); } - throw new PendingTrigger(jobName, serializedArgs); + const pending = new PendingTrigger(jobName, serializedArgs); + activeExecution.pending = pending; + throw pending; }; const runJob = async (name: string, args?: unknown): Promise => { @@ -164,6 +170,15 @@ function createLocalJobRunner(): { try { const out = await body(platformSerialize(args), buildJobContext()); + if (execution.pending) { + const result = await runJob(execution.pending.jobName, execution.pending.args); + records.push({ + jobName: execution.pending.jobName, + args: execution.pending.args, + result, + }); + continue; + } if (execution.cursor !== records.length) { throw new Error( `Workflow job trigger sequence changed while replaying "${name}". Expected ${records.length} trigger(s), but replay reached ${execution.cursor}.`, @@ -171,9 +186,10 @@ function createLocalJobRunner(): { } return platformSerialize(out); } catch (cause) { - if (cause instanceof PendingTrigger) { - const result = await runJob(cause.jobName, cause.args); - records.push({ jobName: cause.jobName, args: cause.args, result }); + const pending = cause instanceof PendingTrigger ? cause : execution.pending; + if (pending) { + const result = await runJob(pending.jobName, pending.args); + records.push({ jobName: pending.jobName, args: pending.args, result }); continue; } throw cause; From 323a7715f154f77a2491095e927b37d8c8117374 Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 15 Jun 2026 21:21:49 +0900 Subject: [PATCH 005/618] fix: replay failed local workflow triggers --- packages/sdk/src/vitest/mock.test.ts | 32 +++++++++++++++ packages/sdk/src/vitest/workflow-local.ts | 49 ++++++++++++++++++----- 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/packages/sdk/src/vitest/mock.test.ts b/packages/sdk/src/vitest/mock.test.ts index 0508e5e9e..9e189dd40 100644 --- a/packages/sdk/src/vitest/mock.test.ts +++ b/packages/sdk/src/vitest/mock.test.ts @@ -356,6 +356,38 @@ describe("mock", () => { expect(await runWorkflowLocally(workflow)).toEqual({ ok: true }); }); + test("runWorkflowLocally() replays failed trigger errors into user catch blocks once", async () => { + let attempts = 0; + const inner = createWorkflowJob({ + name: "default-runtime-failing-inner", + body: async () => { + attempts += 1; + throw new Error("inner failed"); + }, + }); + const main = createWorkflowJob({ + name: "default-runtime-failing-main", + body: () => { + try { + inner.trigger(); + return { handled: false, message: "" }; + } catch (cause) { + return { handled: true, message: (cause as Error).message }; + } + }, + }); + const workflow = createWorkflow({ + name: "default-runtime-failing-wf", + mainJob: main, + }); + + expect(await runWorkflowLocally(workflow)).toEqual({ + handled: true, + message: "inner failed", + }); + expect(attempts).toBe(1); + }); + test("runWorkflowLocally() rejects a non-serializable trigger result", async () => { const bad = createWorkflowJob({ name: "default-runtime-bad", diff --git a/packages/sdk/src/vitest/workflow-local.ts b/packages/sdk/src/vitest/workflow-local.ts index 0af24ac39..28c7ebcc2 100644 --- a/packages/sdk/src/vitest/workflow-local.ts +++ b/packages/sdk/src/vitest/workflow-local.ts @@ -25,11 +25,19 @@ type GlobalWithTailor = { }; }; -interface TriggerRecord { +type TriggerRecord = { jobName: string; args: unknown; - result: unknown; -} +} & ( + | { + status: "fulfilled"; + result: unknown; + } + | { + status: "rejected"; + error: unknown; + } +); interface LocalExecution { records: TriggerRecord[]; @@ -147,6 +155,9 @@ function createLocalJobRunner(): { const cached = activeExecution.records[index]; if (cached) { assertSameTrigger(cached, jobName, serializedArgs); + if (cached.status === "rejected") { + throw cached.error; + } return platformSerialize(cached.result); } @@ -171,12 +182,7 @@ function createLocalJobRunner(): { try { const out = await body(platformSerialize(args), buildJobContext()); if (execution.pending) { - const result = await runJob(execution.pending.jobName, execution.pending.args); - records.push({ - jobName: execution.pending.jobName, - args: execution.pending.args, - result, - }); + await settlePendingTrigger(records, execution.pending, runJob); continue; } if (execution.cursor !== records.length) { @@ -188,8 +194,7 @@ function createLocalJobRunner(): { } catch (cause) { const pending = cause instanceof PendingTrigger ? cause : execution.pending; if (pending) { - const result = await runJob(pending.jobName, pending.args); - records.push({ jobName: pending.jobName, args: pending.args, result }); + await settlePendingTrigger(records, pending, runJob); continue; } throw cause; @@ -202,6 +207,28 @@ function createLocalJobRunner(): { return { runJob, triggerJobFunction }; } +async function settlePendingTrigger( + records: TriggerRecord[], + pending: PendingTrigger, + runJob: (name: string, args?: unknown) => Promise, +): Promise { + try { + records.push({ + jobName: pending.jobName, + args: pending.args, + status: "fulfilled", + result: await runJob(pending.jobName, pending.args), + }); + } catch (error) { + records.push({ + jobName: pending.jobName, + args: pending.args, + status: "rejected", + error, + }); + } +} + function assertSameTrigger(record: TriggerRecord, jobName: string, args: unknown): void { if (record.jobName === jobName && JSON.stringify(record.args) === JSON.stringify(args)) { return; From 9fa6c3342b1ed1dba3f6075c136f36161ccdf3a6 Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 15 Jun 2026 21:44:34 +0900 Subject: [PATCH 006/618] fix: preserve workflow trigger promise semantics --- .../sdk/src/configure/services/workflow/job.test.ts | 4 ++-- .../sdk/src/configure/services/workflow/workflow.ts | 2 +- packages/sdk/src/vitest/mock.test.ts | 12 ++++++++++++ packages/sdk/src/vitest/workflow-runtime.ts | 6 +++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/sdk/src/configure/services/workflow/job.test.ts b/packages/sdk/src/configure/services/workflow/job.test.ts index dd4b8a011..888d9065c 100644 --- a/packages/sdk/src/configure/services/workflow/job.test.ts +++ b/packages/sdk/src/configure/services/workflow/job.test.ts @@ -391,13 +391,13 @@ describe("trigger without tailor.workflow", () => { expect(() => double.trigger({ n: 21 })).toThrow(/tailor\.workflow is not available/); }); - test("workflow trigger throws instead of running the main job", () => { + test("workflow trigger rejects instead of running the main job", async () => { const main = createWorkflowJob({ name: "fallback-main", body: (input: { n: number }) => ({ total: input.n + 1 }), }); const workflow = createWorkflow({ name: "fallback-wf", mainJob: main }); - expect(() => workflow.trigger({ n: 0 })).toThrow(/tailor\.workflow is not available/); + await expect(workflow.trigger({ n: 0 })).rejects.toThrow(/tailor\.workflow is not available/); }); }); diff --git a/packages/sdk/src/configure/services/workflow/workflow.ts b/packages/sdk/src/configure/services/workflow/workflow.ts index 2d4bc68b1..bf8f9dc3c 100644 --- a/packages/sdk/src/configure/services/workflow/workflow.ts +++ b/packages/sdk/src/configure/services/workflow/workflow.ts @@ -72,7 +72,7 @@ export function createWorkflow>( "workflow.trigger() is rewritten at build time and unavailable in the bundle", ); } - : (args, options) => dispatchTriggerWorkflow(config.name, args, options), + : async (args, options) => await dispatchTriggerWorkflow(config.name, args, options), } as Workflow, "workflow", ); diff --git a/packages/sdk/src/vitest/mock.test.ts b/packages/sdk/src/vitest/mock.test.ts index 9e189dd40..c8ecfb2a9 100644 --- a/packages/sdk/src/vitest/mock.test.ts +++ b/packages/sdk/src/vitest/mock.test.ts @@ -289,6 +289,18 @@ describe("mock", () => { expect(bodyRan).toBe(false); }); + test("workflow.trigger() validates args in the default runtime", async () => { + const main = createWorkflowJob({ + name: "default-runtime-trigger-args", + body: (input: { when: string }) => ({ when: input.when }), + }); + const workflow = createWorkflow({ name: "default-runtime-trigger-args-wf", mainJob: main }); + + await expect(workflow.trigger({ when: new Date() } as never)).rejects.toThrow( + /Date instance/, + ); + }); + test("runWorkflowLocally() runs the whole chain", async () => { const inner = createWorkflowJob({ name: "default-runtime-inner", diff --git a/packages/sdk/src/vitest/workflow-runtime.ts b/packages/sdk/src/vitest/workflow-runtime.ts index 17c5786e3..d8f44714e 100644 --- a/packages/sdk/src/vitest/workflow-runtime.ts +++ b/packages/sdk/src/vitest/workflow-runtime.ts @@ -2,6 +2,7 @@ // Must stay free of `vitest` (`vi`): it loads via `./globals` in the environment // realm where `vi` is unavailable, hence relative imports only (no `@/` alias). import { TRIGGER_DEFAULT } from "../configure/services/workflow/registry"; +import { platformSerialize } from "../utils/test/platform-serialize"; export interface DefaultWorkflowRuntime { triggerJobFunction: (name: string, args?: unknown) => unknown; @@ -21,7 +22,10 @@ export function createDefaultWorkflowRuntime(): DefaultWorkflowRuntime { `No workflow job mock for "${name}". Acquire mockWorkflow() and call setJobHandler(...) or enqueueResult(...), or use runWorkflowLocally() for local workflow execution.`, ); }, - triggerWorkflow: async () => TRIGGER_DEFAULT, + triggerWorkflow: async (_name, args) => { + platformSerialize(args); + return TRIGGER_DEFAULT; + }, wait: (key: string): unknown => { throw new Error( `No wait handler for "${key}". Acquire mockWorkflow() and call setWaitHandler(...).`, From c2310d8c2e7a55aaede108273ff0035d9f0516b2 Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 15 Jun 2026 21:58:55 +0900 Subject: [PATCH 007/618] fix: validate nested local workflow triggers --- packages/sdk/src/vitest/mock.test.ts | 24 +++++++++++++++++++++++ packages/sdk/src/vitest/workflow-local.ts | 9 +++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/vitest/mock.test.ts b/packages/sdk/src/vitest/mock.test.ts index c8ecfb2a9..0a680c4f3 100644 --- a/packages/sdk/src/vitest/mock.test.ts +++ b/packages/sdk/src/vitest/mock.test.ts @@ -400,6 +400,30 @@ describe("mock", () => { expect(attempts).toBe(1); }); + test("runWorkflowLocally() validates nested workflow trigger args", async () => { + const innerMain = createWorkflowJob({ + name: "default-runtime-nested-workflow-inner", + body: (_input: { when: string }) => ({ ok: true }), + }); + const innerWorkflow = createWorkflow({ + name: "default-runtime-nested-workflow", + mainJob: innerMain, + }); + const main = createWorkflowJob({ + name: "default-runtime-nested-workflow-main", + body: async () => { + await innerWorkflow.trigger({ when: new Date() } as never); + return { ok: true }; + }, + }); + const workflow = createWorkflow({ + name: "default-runtime-nested-workflow-wf", + mainJob: main, + }); + + await expect(runWorkflowLocally(workflow)).rejects.toThrow(/Date instance/); + }); + test("runWorkflowLocally() rejects a non-serializable trigger result", async () => { const bad = createWorkflowJob({ name: "default-runtime-bad", diff --git a/packages/sdk/src/vitest/workflow-local.ts b/packages/sdk/src/vitest/workflow-local.ts index 28c7ebcc2..468a39005 100644 --- a/packages/sdk/src/vitest/workflow-local.ts +++ b/packages/sdk/src/vitest/workflow-local.ts @@ -245,8 +245,13 @@ function createLocalWorkflowRuntime( ): TailorWorkflowAPI { return { triggerJobFunction, - triggerWorkflow: (name, args, options) => - previous ? previous.triggerWorkflow(name, args, options) : Promise.resolve(TRIGGER_DEFAULT), + triggerWorkflow: async (name, args, options) => { + if (previous) { + return await previous.triggerWorkflow(name, args, options); + } + platformSerialize(args); + return TRIGGER_DEFAULT; + }, wait: (key, payload) => { if (previous) { return previous.wait(key, payload); From 5fe58506f9338000ff623b431d961ba448d518f3 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 03:09:19 +0900 Subject: [PATCH 008/618] feat(sdk): add TailorPrincipal and deprecate per-context principal types Add `TailorPrincipal`, a unified type for the caller, actor, and invoker of a function execution. Deprecate `TailorUser`, `TailorActor`, `TailorActorType`, `TailorInvoker`, and the `unauthenticatedTailorUser` constant in favor of it; they are unified into `TailorPrincipal` in the next major version. --- .changeset/tailor-principal-type.md | 5 ++++ packages/sdk/src/configure/index.ts | 1 + packages/sdk/src/types/actor.ts | 14 +++++++++-- packages/sdk/src/types/user.ts | 36 ++++++++++++++++++++++++++--- 4 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 .changeset/tailor-principal-type.md diff --git a/.changeset/tailor-principal-type.md b/.changeset/tailor-principal-type.md new file mode 100644 index 000000000..edfcebff4 --- /dev/null +++ b/.changeset/tailor-principal-type.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": minor +--- + +Add `TailorPrincipal`, a unified type for the caller, actor, and invoker of a function execution. `TailorUser`, `TailorActor`, `TailorInvoker`, and the `unauthenticatedTailorUser` constant are now deprecated and will be unified into `TailorPrincipal` (with absence represented as `null`) in the next major version. diff --git a/packages/sdk/src/configure/index.ts b/packages/sdk/src/configure/index.ts index fa5dd0e1f..a5b7f695d 100644 --- a/packages/sdk/src/configure/index.ts +++ b/packages/sdk/src/configure/index.ts @@ -17,6 +17,7 @@ export namespace t { export { type TailorField } from "@/configure/types/type"; export { + type TailorPrincipal, type TailorUser, type TailorInvoker, unauthenticatedTailorUser, diff --git a/packages/sdk/src/types/actor.ts b/packages/sdk/src/types/actor.ts index 262a2bebd..ed17eb5d9 100644 --- a/packages/sdk/src/types/actor.ts +++ b/packages/sdk/src/types/actor.ts @@ -1,9 +1,19 @@ import type { InferredAttributeList, InferredAttributeMap } from "./user"; -/** User type enum values from the Tailor Platform server. */ +/** + * User type enum values from the Tailor Platform server. + * + * @deprecated `TailorPrincipal` represents the type as `"user"` or + * `"machine_user"`. This enum is removed in the next major version. + */ export type TailorActorType = "USER_TYPE_USER" | "USER_TYPE_MACHINE_USER" | "USER_TYPE_UNSPECIFIED"; -/** Represents an actor in event triggers. */ +/** + * Represents an actor in event triggers. + * + * @deprecated Use `TailorPrincipal` instead. `TailorActor` is unified into + * `TailorPrincipal` in the next major version. + */ export type TailorActor = { /** The ID of the workspace the user belongs to. */ workspaceId: string; diff --git a/packages/sdk/src/types/user.ts b/packages/sdk/src/types/user.ts index 6033d3932..ac2ccfd35 100644 --- a/packages/sdk/src/types/user.ts +++ b/packages/sdk/src/types/user.ts @@ -14,7 +14,31 @@ export type InferredAttributeList = AttributeList["__tuple"] extends [] ? string[] : AttributeList["__tuple"]; -/** Represents a user in the Tailor platform. */ +/** + * Represents the principal associated with a function execution. + * + * Unifies the caller, actor, and invoker shapes into a single type: a stable + * `id`, a `type` of `"user"` or `"machine_user"`, and non-null attributes. + */ +export type TailorPrincipal = { + /** The ID of the principal. */ + id: string; + /** The type of the principal. */ + type: "user" | "machine_user"; + /** The ID of the workspace the principal belongs to. */ + workspaceId: string; + /** A map of the principal's attributes. */ + attributes: InferredAttributeMap; + /** A list of the principal's attribute IDs. */ + attributeList: InferredAttributeList; +}; + +/** + * Represents a user in the Tailor platform. + * + * @deprecated Use {@link TailorPrincipal} instead. `TailorUser` is unified into + * `TailorPrincipal` in the next major version. + */ export type TailorUser = { /** * The ID of the user. @@ -40,7 +64,12 @@ export type TailorUser = { attributeList: InferredAttributeList; }; -/** Represents an unauthenticated user in the Tailor platform. */ +/** + * Represents an unauthenticated user in the Tailor platform. + * + * @deprecated Represent an absent principal as `null` instead. This constant is + * removed in the next major version. + */ export const unauthenticatedTailorUser: TailorUser = { id: "00000000-0000-0000-0000-000000000000", type: "", @@ -59,7 +88,8 @@ export const unauthenticatedTailorUser: TailorUser = { * * `null` for anonymous requests. * - * TODO(v2): unify with `TailorUser` — same underlying principal shape. + * @deprecated Use {@link TailorPrincipal} (as `TailorPrincipal | null`) instead. + * `TailorInvoker` is unified into `TailorPrincipal` in the next major version. */ export type TailorInvoker = { /** The ID of the invoker (user ID or machine user ID). */ From 49774322ec618f34148820c91c01937bd68fa0b6 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 03:16:29 +0900 Subject: [PATCH 009/618] fix(sdk): deprecate test-entry unauthenticatedTailorUser and clarify actor migration Deprecate the `unauthenticatedTailorUser` constant exported from `@tailor-platform/sdk/test`, which users actually import, alongside the configure-entry one. Note that the event executor `actor` value changes its `userId`/`userType` fields to `id`/`type` when unified into `TailorPrincipal`. --- .changeset/tailor-principal-type.md | 7 ++++++- packages/sdk/src/types/actor.ts | 3 ++- packages/sdk/src/utils/test/index.ts | 7 ++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.changeset/tailor-principal-type.md b/.changeset/tailor-principal-type.md index edfcebff4..26a8c9c97 100644 --- a/.changeset/tailor-principal-type.md +++ b/.changeset/tailor-principal-type.md @@ -2,4 +2,9 @@ "@tailor-platform/sdk": minor --- -Add `TailorPrincipal`, a unified type for the caller, actor, and invoker of a function execution. `TailorUser`, `TailorActor`, `TailorInvoker`, and the `unauthenticatedTailorUser` constant are now deprecated and will be unified into `TailorPrincipal` (with absence represented as `null`) in the next major version. +Add `TailorPrincipal`, a unified type for the caller, actor, and invoker of a function execution. + +The principal types are deprecated in favor of it and unified into `TailorPrincipal` (with absence represented as `null`) in the next major version: + +- `TailorUser`, `TailorInvoker`, and the `unauthenticatedTailorUser` constant (including the one from `@tailor-platform/sdk/test`). +- The event executor `actor` value, whose `userId`/`userType` fields become `id`/`type` with `"user"`/`"machine_user"` values. diff --git a/packages/sdk/src/types/actor.ts b/packages/sdk/src/types/actor.ts index ed17eb5d9..1597d00de 100644 --- a/packages/sdk/src/types/actor.ts +++ b/packages/sdk/src/types/actor.ts @@ -12,7 +12,8 @@ export type TailorActorType = "USER_TYPE_USER" | "USER_TYPE_MACHINE_USER" | "USE * Represents an actor in event triggers. * * @deprecated Use `TailorPrincipal` instead. `TailorActor` is unified into - * `TailorPrincipal` in the next major version. + * `TailorPrincipal` in the next major version, where `userId`/`userType` become + * `id`/`type` with `"user"`/`"machine_user"` values. */ export type TailorActor = { /** The ID of the workspace the user belongs to. */ diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index 23667ca3a..353d0631a 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -13,7 +13,12 @@ export { createImportMain, } from "./mock"; -/** Represents an unauthenticated user in the Tailor platform. */ +/** + * Represents an unauthenticated user in the Tailor platform. + * + * @deprecated Represent an absent principal as `null` instead. This constant is + * removed in the next major version. + */ export const unauthenticatedTailorUser = { id: "00000000-0000-0000-0000-000000000000", type: "", From f49c6d1b5a856969cb4e04ae7d3a87ed34aa020f Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 04:51:16 +0900 Subject: [PATCH 010/618] feat(runtime)!: remove runtime globals compatibility --- .../remove-runtime-globals-compatibility.md | 8 +++++ example/tests/bundled_execution.test.ts | 1 + packages/sdk-codemod/src/registry.ts | 2 +- packages/sdk/docs/runtime.md | 6 ++-- packages/sdk/src/runtime/globals.test.ts | 11 ++++++ packages/sdk/src/runtime/globals.ts | 34 ------------------- packages/sdk/tsdown.config.ts | 31 +---------------- 7 files changed, 25 insertions(+), 68 deletions(-) create mode 100644 .changeset/remove-runtime-globals-compatibility.md diff --git a/.changeset/remove-runtime-globals-compatibility.md b/.changeset/remove-runtime-globals-compatibility.md new file mode 100644 index 000000000..decabff09 --- /dev/null +++ b/.changeset/remove-runtime-globals-compatibility.md @@ -0,0 +1,8 @@ +--- +"@tailor-platform/sdk": major +"@tailor-platform/sdk-codemod": patch +--- + +Remove the v1 runtime globals compatibility layer. Importing from `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` declarations; opt into globals with `@tailor-platform/sdk/runtime/globals` or use the typed wrappers from `@tailor-platform/sdk/runtime`. + +The capital-cased `Tailordb.*` namespace is removed. Run `pnpm dlx @tailor-platform/sdk-codemod v2/tailordb-namespace` before upgrading if your project still references `Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, or `typeof Tailordb.Client`. diff --git a/example/tests/bundled_execution.test.ts b/example/tests/bundled_execution.test.ts index 846ebaf3e..160e7fd0f 100644 --- a/example/tests/bundled_execution.test.ts +++ b/example/tests/bundled_execution.test.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +import "@tailor-platform/sdk/runtime/globals"; import { mockTailordb, mockWorkflow } from "@tailor-platform/sdk/vitest"; import { format as formatDate } from "date-fns"; import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 886a22ab9..af8151aeb 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -81,7 +81,7 @@ const allCodemods: CodemodPackage[] = [ 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`.", + "Rewrite references to the removed capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the lowercase `tailordb.*` namespace exposed by `@tailor-platform/sdk/runtime/globals`.", since: "1.0.0", until: "2.0.0", scriptPath: "v2/tailordb-namespace/scripts/transform.js", diff --git a/packages/sdk/docs/runtime.md b/packages/sdk/docs/runtime.md index 5f1bfeb70..4858d3df1 100644 --- a/packages/sdk/docs/runtime.md +++ b/packages/sdk/docs/runtime.md @@ -50,9 +50,7 @@ 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. -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: +Importing from `@tailor-platform/sdk` does not activate the ambient `tailor.*` / `tailordb.*` declarations. If you want to opt into the globals, add a single side-effect import anywhere in your project: ```ts import "@tailor-platform/sdk/runtime/globals"; @@ -68,6 +66,8 @@ Or register the entry in `tsconfig.json`: } ``` +The globals entry exposes the lowercase `tailordb.*` namespace only. Use `pnpm dlx @tailor-platform/sdk-codemod v2/tailordb-namespace` before upgrading if your project still references the removed capital-cased `Tailordb.*` namespace from `@tailor-platform/function-types`. + ## Namespaces 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. diff --git a/packages/sdk/src/runtime/globals.test.ts b/packages/sdk/src/runtime/globals.test.ts index 5acb283c8..ea8d39dab 100644 --- a/packages/sdk/src/runtime/globals.test.ts +++ b/packages/sdk/src/runtime/globals.test.ts @@ -8,6 +8,11 @@ */ import "@/runtime/globals"; import { describe, expectTypeOf, test } from "vitest"; +import type { TailordbCommandType } from "@/runtime"; + +// @ts-expect-error Tailordb was removed in v2; use lowercase tailordb.*. +const legacyTailordbQueryResult = null as unknown as Tailordb.QueryResult<{ id: string }>; +void legacyTailordbQueryResult; describe("@tailor-platform/sdk/runtime/globals activates ambient globals", () => { test("tailor.iconv.convert is declared as a function", () => { @@ -34,6 +39,12 @@ describe("@tailor-platform/sdk/runtime/globals activates ambient globals", () => expectTypeOf().toBeFunction(); }); + test("tailordb namespace exposes query helper types", () => { + expectTypeOf>().not.toBeAny(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().not.toBeAny(); + }); + test("TailorDBFileError is declared as a global class", () => { expectTypeOf().not.toBeAny(); }); diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index 03e94a939..60497d344 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -61,40 +61,6 @@ 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 iconv { type Iconv = IconvInstance; diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index e713eab8c..c4448f393 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -1,33 +1,9 @@ -import { cpSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { cpSync, rmSync } 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. 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 = /^\/\/\/ \r?\n/; - 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); - 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(root); -} - function copyErdViewerAssets(outDir: string): void { const source = path.resolve("src/cli/commands/tailordb/erd/viewer-assets"); const target = path.resolve(outDir, "cli/erd-viewer-assets"); @@ -98,16 +74,11 @@ export default defineConfig({ js: ".mjs", dts: ".d.mts", }), - // Remove in v2.0. - banner: { - dts: '/// ', - }, // peer dependencies: prevent bundling, resolve at runtime deps: { neverBundle: ["vite", "vitest"] }, sourcemap: true, plugins, onSuccess: (config) => { - stripBannerExceptConfigureEntry(config.outDir); copyErdViewerAssets(config.outDir); }, }); From 84325f8602a5631b7c323c997b1425235509920e Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 04:59:40 +0900 Subject: [PATCH 011/618] feat(cli)!: remove legacy command aliases --- .changeset/remove-v2-cli-aliases.md | 5 +++++ packages/sdk/docs/cli/application.md | 2 -- packages/sdk/docs/cli/crashreport.md | 2 -- packages/sdk/src/cli/commands/crashreport/index.ts | 1 - packages/sdk/src/cli/commands/deploy/index.test.ts | 8 -------- packages/sdk/src/cli/commands/deploy/index.ts | 1 - packages/sdk/src/cli/commands/login.ts | 1 - packages/sdk/src/cli/commands/workflow/start.ts | 1 - packages/sdk/src/cli/query/index.ts | 1 - 9 files changed, 5 insertions(+), 17 deletions(-) create mode 100644 .changeset/remove-v2-cli-aliases.md delete mode 100644 packages/sdk/src/cli/commands/deploy/index.test.ts diff --git a/.changeset/remove-v2-cli-aliases.md b/.changeset/remove-v2-cli-aliases.md new file mode 100644 index 000000000..65b9a03e6 --- /dev/null +++ b/.changeset/remove-v2-cli-aliases.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Remove deprecated CLI aliases for the v2 command surface. Use `tailor-sdk deploy` instead of `tailor-sdk apply`, `tailor-sdk crashreport` instead of `tailor-sdk crash-report`, and the hyphenated `--machine-user` option instead of the hidden `--machineuser` alias. diff --git a/packages/sdk/docs/cli/application.md b/packages/sdk/docs/cli/application.md index 2582418a7..e7ff3f3f3 100644 --- a/packages/sdk/docs/cli/application.md +++ b/packages/sdk/docs/cli/application.md @@ -99,8 +99,6 @@ See [Global Options](../cli-reference.md#global-options) for options available t Deploy your application by applying the Tailor configuration. -**Aliases:** `apply` - diff --git a/packages/sdk/docs/cli/crashreport.md b/packages/sdk/docs/cli/crashreport.md index 1f6ae5408..475b6b2f1 100644 --- a/packages/sdk/docs/cli/crashreport.md +++ b/packages/sdk/docs/cli/crashreport.md @@ -12,8 +12,6 @@ Commands for managing crash reports. Manage crash reports. -**Aliases:** `crash-report` - diff --git a/packages/sdk/src/cli/commands/crashreport/index.ts b/packages/sdk/src/cli/commands/crashreport/index.ts index 2a405e78e..dc68be178 100644 --- a/packages/sdk/src/cli/commands/crashreport/index.ts +++ b/packages/sdk/src/cli/commands/crashreport/index.ts @@ -4,7 +4,6 @@ import { sendCommand } from "./send"; export const crashReportCommand = defineCommand({ name: "crashreport", - aliases: ["crash-report"], description: "Manage crash reports.", subCommands: { list: listCommand, diff --git a/packages/sdk/src/cli/commands/deploy/index.test.ts b/packages/sdk/src/cli/commands/deploy/index.test.ts deleted file mode 100644 index 4cd9b1b6f..000000000 --- a/packages/sdk/src/cli/commands/deploy/index.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { deployCommand } from "@/cli/commands/deploy"; - -describe("deployCommand", () => { - test("exposes 'apply' as an alias", () => { - expect(deployCommand.aliases).toContain("apply"); - }); -}); diff --git a/packages/sdk/src/cli/commands/deploy/index.ts b/packages/sdk/src/cli/commands/deploy/index.ts index 4ccc1e66d..54d5ce601 100644 --- a/packages/sdk/src/cli/commands/deploy/index.ts +++ b/packages/sdk/src/cli/commands/deploy/index.ts @@ -7,7 +7,6 @@ import { assertWritable } from "@/cli/shared/readonly-guard"; export const deployCommand = defineAppCommand({ name: "deploy", - aliases: ["apply"], description: "Deploy your application by applying the Tailor configuration.", args: z .object({ diff --git a/packages/sdk/src/cli/commands/login.ts b/packages/sdk/src/cli/commands/login.ts index 8e1171549..464cc3711 100644 --- a/packages/sdk/src/cli/commands/login.ts +++ b/packages/sdk/src/cli/commands/login.ts @@ -133,7 +133,6 @@ export const loginCommand = defineAppCommand({ z .object({ "machine-user": arg(z.literal(true), { - hiddenAlias: "machineuser", description: "Login as a platform machine user.", required: true, }), diff --git a/packages/sdk/src/cli/commands/workflow/start.ts b/packages/sdk/src/cli/commands/workflow/start.ts index 2cb47f2a8..86521344e 100644 --- a/packages/sdk/src/cli/commands/workflow/start.ts +++ b/packages/sdk/src/cli/commands/workflow/start.ts @@ -350,7 +350,6 @@ export const startCommand = defineAppCommand({ ...nameArgs, "machine-user": arg(z.string(), { alias: "m", - hiddenAlias: "machineuser", description: "Machine user name", env: "TAILOR_PLATFORM_MACHINE_USER_NAME", }), diff --git a/packages/sdk/src/cli/query/index.ts b/packages/sdk/src/cli/query/index.ts index 9b15a0de0..26010cf6f 100644 --- a/packages/sdk/src/cli/query/index.ts +++ b/packages/sdk/src/cli/query/index.ts @@ -754,7 +754,6 @@ export const queryCommand = defineAppCommand({ }), "machine-user": arg(z.string(), { alias: "m", - hiddenAlias: "machineuser", description: "Machine user name for query execution", env: "TAILOR_PLATFORM_MACHINE_USER_NAME", }), From 2c3d7add213b171df2959b8a14e8dc2e3c3a7ec7 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 05:00:06 +0900 Subject: [PATCH 012/618] feat(cli)!: remove tailor-sdk-skills shim --- .changeset/remove-tailor-sdk-skills-shim.md | 5 +++++ packages/sdk/package.json | 3 +-- packages/sdk/src/cli/skills.ts | 21 --------------------- packages/sdk/tsdown.config.ts | 1 - 4 files changed, 6 insertions(+), 24 deletions(-) create mode 100644 .changeset/remove-tailor-sdk-skills-shim.md delete mode 100644 packages/sdk/src/cli/skills.ts diff --git a/.changeset/remove-tailor-sdk-skills-shim.md b/.changeset/remove-tailor-sdk-skills-shim.md new file mode 100644 index 000000000..1038a3dfc --- /dev/null +++ b/.changeset/remove-tailor-sdk-skills-shim.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Remove the deprecated `tailor-sdk-skills` binary shim. Use `tailor-sdk skills install` to install the bundled Tailor SDK agent skill. diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 6b6cba1fc..736c12bfd 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -9,8 +9,7 @@ "directory": "packages/sdk" }, "bin": { - "tailor-sdk": "./dist/cli/index.mjs", - "tailor-sdk-skills": "./dist/cli/skills.mjs" + "tailor-sdk": "./dist/cli/index.mjs" }, "files": [ "CHANGELOG.md", diff --git a/packages/sdk/src/cli/skills.ts b/packages/sdk/src/cli/skills.ts deleted file mode 100644 index bda77fc16..000000000 --- a/packages/sdk/src/cli/skills.ts +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env node - -import { spawn } from "node:child_process"; -import { fileURLToPath } from "node:url"; -import { dirname, resolve } from "pathe"; -import { logger } from "./shared/logger"; - -logger.warn( - "`tailor-sdk-skills` is deprecated and will be removed in v2. Use `tailor-sdk skills install` instead.", -); - -const here = dirname(fileURLToPath(import.meta.url)); -const mainCli = resolve(here, "index.mjs"); - -const child = spawn(process.execPath, [mainCli, "skills", "install", ...process.argv.slice(2)], { - stdio: "inherit", -}); - -child.on("exit", (code) => { - process.exit(code ?? 1); -}); diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index e713eab8c..608dabd29 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -64,7 +64,6 @@ export default defineConfig({ "src/configure/index.ts", "src/cli/index.ts", "src/cli/lib.ts", - "src/cli/skills.ts", "src/utils/test/index.ts", "src/kysely/index.ts", "src/plugin/index.ts", From f0895f4231578e54229004d3aa5bac6bd24361e3 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 05:11:30 +0900 Subject: [PATCH 013/618] feat(config)!: remove defineGenerators support --- .changeset/remove-define-generators.md | 5 + example/tests/scripts/generate_files.ts | 4 +- .../tests/tailor.config.generators-compat.ts | 13 - packages/create-sdk/src/context.ts | 2 +- packages/sdk/docs/configuration.md | 2 - packages/sdk/docs/generator/builtin.md | 257 ------------------ packages/sdk/docs/generator/custom.md | 147 ---------- packages/sdk/docs/generator/index.md | 66 ----- packages/sdk/docs/plugin/custom.md | 4 +- packages/sdk/docs/plugin/index.md | 2 +- packages/sdk/docs/services/resolver.md | 2 +- .../tailor.config.generators-compat.ts | 14 - packages/sdk/src/cli/shared/config-loader.ts | 52 +--- packages/sdk/src/configure/config.ts | 12 - packages/sdk/src/configure/index.ts | 2 +- .../sdk/src/parser/generator-config/schema.ts | 58 +--- packages/sdk/src/plugin/builtin/registry.ts | 14 - packages/sdk/src/plugin/compat.test.ts | 74 ----- 18 files changed, 17 insertions(+), 713 deletions(-) create mode 100644 .changeset/remove-define-generators.md delete mode 100644 example/tests/tailor.config.generators-compat.ts delete mode 100644 packages/sdk/docs/generator/builtin.md delete mode 100644 packages/sdk/docs/generator/custom.md delete mode 100644 packages/sdk/docs/generator/index.md delete mode 100644 packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailor.config.generators-compat.ts delete mode 100644 packages/sdk/src/plugin/builtin/registry.ts delete mode 100644 packages/sdk/src/plugin/compat.test.ts diff --git a/.changeset/remove-define-generators.md b/.changeset/remove-define-generators.md new file mode 100644 index 000000000..bf2676d7f --- /dev/null +++ b/.changeset/remove-define-generators.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Remove `defineGenerators()` and legacy `generators` config support. Use `definePlugins()` with the built-in plugin packages for code generation. diff --git a/example/tests/scripts/generate_files.ts b/example/tests/scripts/generate_files.ts index 2a4b5afd9..33e4f5cf7 100644 --- a/example/tests/scripts/generate_files.ts +++ b/example/tests/scripts/generate_files.ts @@ -92,14 +92,12 @@ async function listGeneratedFiles(dirPath: string, depth = 0, maxDepth = 3): Pro } } -const generatorsCompatDir = "tests/fixtures/generators"; const pluginsCompatDir = "tests/fixtures/plugins"; export async function generateCompatFiles(): Promise { - for (const dir of [generatorsCompatDir, pluginsCompatDir]) { + for (const dir of [pluginsCompatDir]) { if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true }); } - await generate({ configPath: "./tests/tailor.config.generators-compat.ts" }); await generate({ configPath: "./tests/tailor.config.plugins-compat.ts" }); // Also run deploy --buildOnly for plugins-compat (used by bundled_execution tests) diff --git a/example/tests/tailor.config.generators-compat.ts b/example/tests/tailor.config.generators-compat.ts deleted file mode 100644 index 36a32aec5..000000000 --- a/example/tests/tailor.config.generators-compat.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineGenerators } from "@tailor-platform/sdk"; -import config, { auth } from "../tailor.config"; -export default config; -export { auth }; -export const generators = defineGenerators( - ["@tailor-platform/kysely-type", { distPath: "./tests/fixtures/generators/db.ts" }], - ["@tailor-platform/enum-constants", { distPath: "./tests/fixtures/generators/enums.ts" }], - ["@tailor-platform/file-utils", { distPath: "./tests/fixtures/generators/files.ts" }], - [ - "@tailor-platform/seed", - { distPath: "./tests/fixtures/generators/seed", machineUserName: "manager-machine-user" }, - ], -); diff --git a/packages/create-sdk/src/context.ts b/packages/create-sdk/src/context.ts index 339d99a27..4ebf5a4fb 100644 --- a/packages/create-sdk/src/context.ts +++ b/packages/create-sdk/src/context.ts @@ -31,7 +31,7 @@ const templateHints: Record = { workflow: "Workflow patterns with job chaining and testing", executor: "Executor trigger types (record, resolver, schedule, webhook)", "static-web-site": "Static website with auth and IdP integration", - generators: "Built-in generators: kysely, enums, files, seed", + generators: "Built-in generation plugins: kysely, enums, files, seed", }; const validateName = (name: string | undefined) => { diff --git a/packages/sdk/docs/configuration.md b/packages/sdk/docs/configuration.md index d6b2bceae..e8e1fbc55 100644 --- a/packages/sdk/docs/configuration.md +++ b/packages/sdk/docs/configuration.md @@ -294,5 +294,3 @@ export const plugins = definePlugins( enumConstantsPlugin({ distPath: "./generated/enums.ts" }), ); ``` - -See [Generators](./generator/index.md) for legacy `defineGenerators()` documentation. diff --git a/packages/sdk/docs/generator/builtin.md b/packages/sdk/docs/generator/builtin.md deleted file mode 100644 index d961934df..000000000 --- a/packages/sdk/docs/generator/builtin.md +++ /dev/null @@ -1,257 +0,0 @@ -# Builtin Generators - -The SDK includes four builtin generators for common code generation tasks. - -## @tailor-platform/kysely-type - -Generates Kysely type definitions and the `getDB()` function for type-safe database access. - -### Configuration - -```typescript -["@tailor-platform/kysely-type", { distPath: "./generated/tailordb.ts" }]; -``` - -| Option | Type | Description | -| ---------- | -------- | --------------------------- | -| `distPath` | `string` | Output file path (required) | - -### Output - -Generates a TypeScript file containing: - -- Type definitions for all TailorDB types -- `getDB(namespace)` function to create Kysely instances -- Utility types for `Timestamp`, `Serial`, and `ObjectColumnType` (used for nested object fields so insert and select types stay correct) - -### Usage - -```typescript -import { getDB } from "./generated/tailordb"; - -// In resolvers -body: async (context) => { - const db = getDB("tailordb"); - const users = await db - .selectFrom("User") - .selectAll() - .where("email", "=", context.input.email) - .execute(); - return { users }; -}; - -// In executors -body: async ({ newRecord }) => { - const db = getDB("tailordb"); - await db.insertInto("AuditLog").values({ userId: newRecord.id, action: "created" }).execute(); -}; - -// In workflow jobs -body: async (input, { env }) => { - const db = getDB("tailordb"); - return await db - .selectFrom("Order") - .selectAll() - .where("id", "=", input.orderId) - .executeTakeFirst(); -}; -``` - -### Raw SQL - -For queries that the Kysely query builder can't express, use the `sql` tag re-exported from `@tailor-platform/sdk/kysely`. Plain value substitutions (`${...}`) are sent as bound parameters, so user-supplied values are parameterized safely. SQL fragments produced by Kysely helpers (for example `sql.raw(...)`, identifiers, refs) are inlined into the generated SQL string by design — do not pass untrusted input through those. - -```typescript -import { sql } from "@tailor-platform/sdk/kysely"; -import { getDB } from "./generated/tailordb"; - -createResolver({ - name: "supplierCountByState", - operation: "query", - input: { country: t.string() }, - output: t.object({ - rows: t.array(t.object({ state: t.string(), count: t.int() })), - }), - body: async ({ input }) => { - const db = getDB("tailordb"); - const { rows } = await sql<{ state: string; count: number }>` - SELECT state, COUNT(*) AS count - FROM "Supplier" - WHERE country = ${input.country} - GROUP BY state - `.execute(db); - return { rows }; - }, -}); -``` - -The same `sql` tag works inside `db.transaction().execute(async (trx) => ...)` by passing `trx` to `.execute()`: - -```typescript -await db.transaction().execute(async (trx) => { - await sql`UPDATE "Supplier" SET state = ${state} WHERE id = ${id}`.execute(trx); -}); -``` - -## @tailor-platform/enum-constants - -Extracts enum constants from TailorDB type definitions. - -### Configuration - -```typescript -["@tailor-platform/enum-constants", { distPath: "./generated/enums.ts" }]; -``` - -| Option | Type | Description | -| ---------- | -------- | --------------------------- | -| `distPath` | `string` | Output file path (required) | - -### Output - -Generates TypeScript constants for all enum fields: - -```typescript -// Generated output -export const OrderStatus = { - PENDING: "PENDING", - PROCESSING: "PROCESSING", - COMPLETED: "COMPLETED", - CANCELLED: "CANCELLED", -} as const; - -export type OrderStatus = (typeof OrderStatus)[keyof typeof OrderStatus]; -``` - -### Usage - -```typescript -import { OrderStatus } from "./generated/enums"; - -// Type-safe enum usage -const status: OrderStatus = OrderStatus.PENDING; - -// In queries -const orders = await db - .selectFrom("Order") - .selectAll() - .where("status", "=", OrderStatus.COMPLETED) - .execute(); -``` - -## @tailor-platform/file-utils - -Generates utility functions for handling file-type fields in TailorDB. - -### Configuration - -```typescript -["@tailor-platform/file-utils", { distPath: "./generated/files.ts" }]; -``` - -| Option | Type | Description | -| ---------- | -------- | --------------------------- | -| `distPath` | `string` | Output file path (required) | - -### Output - -Generates TypeScript interfaces and utilities for types with file fields: - -```typescript -// Generated output -export interface UserFileFields { - avatar: string; - documents: string; -} - -export function getUserFileFields(): (keyof UserFileFields)[] { - return ["avatar", "documents"]; -} -``` - -## @tailor-platform/seed - -Generates seed data configuration files for database initialization. - -### Configuration - -```typescript -// Basic configuration -["@tailor-platform/seed", { distPath: "./seed" }]; - -// With default machine user -["@tailor-platform/seed", { distPath: "./seed", machineUserName: "admin" }]; -``` - -| Option | Type | Description | -| -------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `distPath` | `string` | Output directory path (required) | -| `machineUserName` | `string` | Default machine user name (can be overridden at runtime) | -| `disableIdpUserSync` | `{ userToIdp?: boolean; idpToUser?: boolean }` | Skip emitting individual `_User <-> userProfile` foreign keys. Both directions are emitted by default. See [IdP user synchronization](#idp-user-synchronization). | - -### IdP user synchronization - -When `auth.userProfile` is configured, the seed plugin treats the userProfile -type (e.g. `User`) and the IdP-managed `_User` table as a pair. By default it -emits foreign keys in both directions so that `validate` rejects any row in -either table that does not have a matching counterpart: - -| Direction | Foreign key | Catches | -| ----------- | ---------------------------------------------- | ---------------------------------------------- | -| `idpToUser` | `_User.name` → `.` | Creating an IdP credential with no profile row | -| `userToIdp` | `.` → `_User.name` | Creating a profile row with no IdP credential | - -Neither direction is enforced by the runtime. In production it is normal for -one side to exist without the other — for example a userProfile row exists -the moment a user is invited, but the corresponding `_User` row appears only -after the user finishes signing up. To seed such states, set the relevant -direction in `disableIdpUserSync` to `true`: - -```typescript -// Allow seeding invited-but-not-registered userProfile rows. -// Still rejects _User rows without a matching userProfile row. -["@tailor-platform/seed", { distPath: "./seed", disableIdpUserSync: { userToIdp: true } }]; - -// Allow seeding _User rows whose userProfile does not exist yet. -// Still rejects userProfile rows without a matching _User row. -["@tailor-platform/seed", { distPath: "./seed", disableIdpUserSync: { idpToUser: true } }]; -``` - -Omitted directions default to `false` (FK emitted). - -### Output - -Generates a seed directory structure: - -``` -seed/ -├── data/ -│ ├── User.jsonl # Seed data files (JSONL format) -│ ├── User.schema.ts # lines-db schema definitions -│ └── Product.jsonl -└── exec.mjs # Executable script -``` - -### Usage - -Run the generated executable script: - -```bash -# With machine user from config -node seed/exec.mjs - -# Specify machine user at runtime (required if not configured, or to override) -node seed/exec.mjs --machine-user admin - -# Short form -node seed/exec.mjs -m admin - -# With other options -node seed/exec.mjs -m admin --truncate --yes -``` - -The `--machine-user` option is required at runtime if `machineUserName` is not configured in the generator options. - -The generated files are compatible with gql-ingest for bulk data import. - -The `exec.mjs` is fully regenerated on every `sdk generate` and starts with an `@generated` header — do not hand-edit it. Its `--truncate` path reuses the `tailordb truncate` command, so namespaces declared with `{ external: true }` are skipped automatically and a shell app cannot wipe a sibling app's data via `seed:reset`. diff --git a/packages/sdk/docs/generator/custom.md b/packages/sdk/docs/generator/custom.md deleted file mode 100644 index 5272636d3..000000000 --- a/packages/sdk/docs/generator/custom.md +++ /dev/null @@ -1,147 +0,0 @@ -# Custom Generators (Deprecated) - -> **Deprecated**: Use `definePlugins()` with generation-time hooks (`onTypeLoaded`, `generate`, etc.) instead. See [Custom Plugins](../plugin/custom.md) for the recommended approach. - -Create your own generators by implementing the `CodeGenerator` interface. - -## CodeGenerator Interface - -```typescript -interface CodeGenerator { - id: string; - description: string; - - // Process individual items - processType(args: { - type: TailorDBType; - namespace: string; - source: { filePath: string; exportName: string }; - }): T | Promise; - - processResolver(args: { resolver: Resolver; namespace: string }): R | Promise; - - processExecutor(executor: Executor): E | Promise; - - // Aggregate per namespace (optional) - processTailorDBNamespace?(args: { - namespace: string; - types: Record; - }): Ts | Promise; - - processResolverNamespace?(args: { - namespace: string; - resolvers: Record; - }): Rs | Promise; - - // Final aggregation - aggregate(args: { - input: GeneratorInput; - executorInputs: E[]; - baseDir: string; - }): GeneratorResult | Promise; -} -``` - -## GeneratorResult - -Generators return a `GeneratorResult` containing files to create: - -```typescript -interface GeneratorResult { - files: Array<{ - path: string; // Relative path from project root - content: string; // File content - skipIfExists?: boolean; // Skip if file already exists (default: false) - executable?: boolean; // Make file executable (default: false) - }>; - errors?: string[]; -} -``` - -## Example: Simple Type List Generator - -```typescript -import type { CodeGenerator, GeneratorResult } from "@tailor-platform/sdk"; - -const typeListGenerator: CodeGenerator = { - id: "type-list", - description: "Generates a list of all TailorDB type names", - - processType({ type }) { - return type.name; - }, - - processResolver() { - return null; - }, - - processExecutor() { - return null; - }, - - processTailorDBNamespace({ types }) { - return Object.values(types); - }, - - aggregate({ input }) { - const allTypes = input.tailordb.flatMap((ns) => ns.types); - const content = `// Generated type list\nexport const types = ${JSON.stringify(allTypes, null, 2)} as const;\n`; - - return { - files: [{ path: "generated/types.ts", content }], - }; - }, -}; -``` - -## Using Custom Generators - -Pass the generator object directly to `defineGenerators()`: - -```typescript -import { defineGenerators } from "@tailor-platform/sdk"; -import { typeListGenerator } from "./generators/type-list"; - -export const generators = defineGenerators( - ["@tailor-platform/kysely-type", { distPath: "./generated/tailordb.ts" }], - typeListGenerator, // Custom generator -); -``` - -## Available Input Data - -### TailorDBType - -Contains full type information including: - -- `name`: Type name -- `fields`: Field definitions with types, validation, and descriptions -- `relations`: Relationship definitions -- `indexes`: Index configurations -- `permission`: Permission rules - -### Resolver - -Contains resolver configuration: - -- `name`: Resolver name -- `operation`: Query or mutation -- `input`: Input schema -- `output`: Output schema - -### Executor - -Contains executor configuration: - -- `name`: Executor name -- `trigger`: Trigger configuration -- `operation`: Execution target - -### GeneratorAuthInput - -Contains authentication configuration when available: - -- `name`: Auth service name -- `userProfile`: User profile type information -- `machineUsers`: Machine user definitions -- `oauth2Clients`: OAuth2 client configurations diff --git a/packages/sdk/docs/generator/index.md b/packages/sdk/docs/generator/index.md deleted file mode 100644 index 25882e41b..000000000 --- a/packages/sdk/docs/generator/index.md +++ /dev/null @@ -1,66 +0,0 @@ -# Generators - -Generators analyze your TailorDB types, Resolvers, and Executors to automatically generate TypeScript code. - -## Overview - -When you run `tailor-sdk generate`, the SDK: - -1. Loads all TailorDB types, Resolvers, and Executors from your configuration -2. Passes each definition to the configured generators -3. Aggregates the results and writes output files - -This enables generators to create derived code based on your application's schema. For example, the `@tailor-platform/kysely-type` generator produces type-safe database access code from your TailorDB definitions. - -## Configuration - -Define generators in `tailor.config.ts` using `defineGenerators()`: - -```typescript -import { defineConfig, defineGenerators } from "@tailor-platform/sdk"; - -export const generators = defineGenerators( - ["@tailor-platform/kysely-type", { distPath: "./generated/tailordb.ts" }], - ["@tailor-platform/enum-constants", { distPath: "./generated/enums.ts" }], -); - -export default defineConfig({ - name: "my-app", - // ... -}); -``` - -**Important**: The `generators` export must be a named export (not default). - -## CLI Commands - -### Generate Files - -```bash -tailor-sdk generate -``` - -Generates all configured output files. - -### Watch Mode - -```bash -tailor-sdk generate --watch -``` - -Watches for file changes and regenerates automatically. - -## Environment Variables - -### `TAILOR_PLATFORM_SDK_DTS_PATH` - -Customize the output path of the generated `tailor.d.ts` type definition file. By default, `tailor.d.ts` is written next to `tailor.config.ts`. - -```bash -TAILOR_PLATFORM_SDK_DTS_PATH=src/types/tailor.d.ts tailor-sdk generate -``` - -## Generator Types - -- [Builtin Generators](./builtin.md) - Ready-to-use generators included with the SDK -- [Custom Generators](./custom.md) - Create your own generators (Preview) diff --git a/packages/sdk/docs/plugin/custom.md b/packages/sdk/docs/plugin/custom.md index 3b50de80f..33e07c7cd 100644 --- a/packages/sdk/docs/plugin/custom.md +++ b/packages/sdk/docs/plugin/custom.md @@ -18,7 +18,7 @@ const myPlugin: Plugin = { export default myPlugin; // Required: must be default export ``` -This is required so that generators can use plugin-generated TailorDB types via `getGeneratedType()`. +This is required so that other plugins and generation-time hooks can use plugin-generated TailorDB types via `getGeneratedType()`. ## Plugin Interface @@ -276,7 +276,7 @@ import type { ## getGeneratedType Helper -The SDK provides an async `getGeneratedType()` helper function to retrieve plugin-generated TailorDB types. This enables generators and other tools to work with types generated by plugins. +The SDK provides an async `getGeneratedType()` helper function to retrieve plugin-generated TailorDB types. This enables plugins and other tools to work with types generated by plugins. ```typescript import { join } from "node:path"; diff --git a/packages/sdk/docs/plugin/index.md b/packages/sdk/docs/plugin/index.md index e265c5ac9..c646700bf 100644 --- a/packages/sdk/docs/plugin/index.md +++ b/packages/sdk/docs/plugin/index.md @@ -149,7 +149,7 @@ These hooks produce TailorDB types, resolvers, and executors that become part of | `onResolverReady` | TailorDB types, Resolvers, Auth | Write output files | | `onExecutorReady` | TailorDB types, Resolvers, Executors, Auth | Write output files | -These hooks receive all finalized data and produce output files (TypeScript code, etc.). They replace the previous standalone `defineGenerators()` approach. No `importPath` required. +These hooks receive all finalized data and produce output files (TypeScript code, etc.). No `importPath` required. A plugin can implement hooks from either or both phases. diff --git a/packages/sdk/docs/services/resolver.md b/packages/sdk/docs/services/resolver.md index cdc6ac61f..6e5694b3a 100644 --- a/packages/sdk/docs/services/resolver.md +++ b/packages/sdk/docs/services/resolver.md @@ -240,7 +240,7 @@ Define actual resolver logic in the `body` function. Function arguments include: ### Using Kysely for Database Access -If you're generating Kysely types with a generator, you can use `getDB` to execute typed queries: +If you're generating Kysely types with `kyselyTypePlugin`, you can use `getDB` to execute typed queries: ```typescript import { getDB } from "../generated/tailordb"; diff --git a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailor.config.generators-compat.ts b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailor.config.generators-compat.ts deleted file mode 100644 index fcf2c1a1b..000000000 --- a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailor.config.generators-compat.ts +++ /dev/null @@ -1,14 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -- test fixture runs as standalone config, needs node:path -import * as path from "node:path"; -import * as url from "node:url"; -import { defineGenerators } from "@tailor-platform/sdk"; -import config from "./tailor.config"; - -const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); -const outDir = path.join(__dirname, "generators-compat-out"); - -export default config; -export const generators = defineGenerators( - ["@tailor-platform/kysely-type", { distPath: path.join(outDir, "db.ts") }], - ["@tailor-platform/enum-constants", { distPath: path.join(outDir, "enums.ts") }], -); diff --git a/packages/sdk/src/cli/shared/config-loader.ts b/packages/sdk/src/cli/shared/config-loader.ts index c1ca7c789..534e32aed 100644 --- a/packages/sdk/src/cli/shared/config-loader.ts +++ b/packages/sdk/src/cli/shared/config-loader.ts @@ -2,14 +2,12 @@ import * as fs from "node:fs"; import { pathToFileURL } from "node:url"; import * as path from "pathe"; import { AppConfigSchema } from "@/parser/app-config/schema"; -import { CodeGeneratorSchema, BaseGeneratorConfigSchema } from "@/parser/generator-config/schema"; import { PluginConfigSchema } from "@/parser/plugin-config"; -import { builtinPlugins } from "@/plugin/builtin/registry"; import { loadConfigPath } from "./context"; import { installCliTailordbStub } from "./mock"; +import type { AnyCodeGenerator } from "@/cli/commands/generate/types"; import type { AppConfig } from "@/types/app-config"; import type { Plugin } from "@/types/plugin"; -import type { z } from "zod"; /** * Loaded configuration with resolved path @@ -21,13 +19,10 @@ export interface LoadConfigOptions { importNonce?: string; } -// Generator schema for custom CodeGenerator objects (builtin generators are handled as plugins) -const GeneratorConfigSchema = CodeGeneratorSchema.brand("CodeGenerator"); - -export type Generator = z.output; +export type Generator = AnyCodeGenerator; /** - * Load Tailor configuration file and associated generators and plugins. + * Load Tailor configuration file and associated plugins. * @param configPath - Optional explicit config path * @param options - Optional module import behavior. * @returns Loaded config, generators, plugins, and config path @@ -66,50 +61,11 @@ export async function loadConfig( throw new Error(`Invalid Tailor config in ${resolvedPath}:\n${issues}`); } - // Collect all generator exports (generators, generators2, etc.) - const allGenerators: Generator[] = []; // Collect all plugin exports (plugins, plugins2, etc.) const allPlugins: Plugin[] = []; for (const value of Object.values(configModule)) { if (Array.isArray(value)) { - // Try to parse as generators (converting builtin tuples to plugins) - const generatorParsed = value.reduce( - (acc, item) => { - if (!acc.success) return acc; - - // Check if this is a builtin generator tuple that should be converted to a plugin - const baseResult = BaseGeneratorConfigSchema.safeParse(item); - if (baseResult.success && Array.isArray(baseResult.data)) { - const [id, options] = baseResult.data as [string, Record]; - const pluginFactory = builtinPlugins.get(id); - if (pluginFactory) { - acc.convertedPlugins.push(pluginFactory(options)); - return acc; - } - } - - // Try to parse as a custom CodeGenerator object - const result = GeneratorConfigSchema.safeParse(item); - if (result.success) { - acc.items.push(result.data); - } else { - acc.success = false; - } - return acc; - }, - { success: true, items: [] as Generator[], convertedPlugins: [] as Plugin[] }, - ); - if ( - generatorParsed.success && - (generatorParsed.items.length > 0 || generatorParsed.convertedPlugins.length > 0) - ) { - allGenerators.push(...generatorParsed.items); - allPlugins.push(...generatorParsed.convertedPlugins); - continue; - } - - // Try to parse as plugins const pluginParsed = value.reduce( (acc, item) => { if (!acc.success) return acc; @@ -132,7 +88,7 @@ export async function loadConfig( return { config: { ...configModule.default, path: resolvedPath } as LoadedConfig, - generators: allGenerators, + generators: [], plugins: allPlugins, }; } diff --git a/packages/sdk/src/configure/config.ts b/packages/sdk/src/configure/config.ts index e9f59598a..fe1a13341 100644 --- a/packages/sdk/src/configure/config.ts +++ b/packages/sdk/src/configure/config.ts @@ -1,5 +1,4 @@ import type { AppConfig } from "@/types/app-config"; -import type { GeneratorConfig } from "@/types/generator-config"; import type { Plugin } from "@/types/plugin"; /** @@ -17,17 +16,6 @@ export function defineConfig< return config; } -/** - * Define generators to be used with the Tailor SDK. - * @deprecated Use definePlugins() with generation hooks (onTypeLoaded, generate, etc.) instead. - * @param configs - Generator configurations - * @returns Generator configurations as given - */ -/* @__NO_SIDE_EFFECTS__ */ -export function defineGenerators(...configs: GeneratorConfig[]) { - return configs; -} - /** * Define plugins to be used with the Tailor SDK. * Plugins can generate additional types, resolvers, and executors diff --git a/packages/sdk/src/configure/index.ts b/packages/sdk/src/configure/index.ts index fa5dd0e1f..6d2cd672f 100644 --- a/packages/sdk/src/configure/index.ts +++ b/packages/sdk/src/configure/index.ts @@ -29,7 +29,7 @@ export { type IdpNameRegistry, type IdpName } from "@/configure/types/idp-name"; export * from "@/configure/services"; -export { defineConfig, defineGenerators, definePlugins } from "@/configure/config"; +export { defineConfig, definePlugins } from "@/configure/config"; // Plugin types for custom plugin development export type { diff --git a/packages/sdk/src/parser/generator-config/schema.ts b/packages/sdk/src/parser/generator-config/schema.ts index 936d0f346..14c4d1591 100644 --- a/packages/sdk/src/parser/generator-config/schema.ts +++ b/packages/sdk/src/parser/generator-config/schema.ts @@ -1,57 +1 @@ -import { z } from "zod"; - -// Dependency kind enum for generators -const DependencyKindSchema = z.enum(["tailordb", "resolver", "executor"]); -export type DependencyKind = z.infer; - -// Literal-based schemas for each generator -const KyselyTypeConfigSchema = z.tuple([ - z.literal("@tailor-platform/kysely-type"), - z.object({ distPath: z.string() }), -]); - -const SeedConfigSchema = z.tuple([ - z.literal("@tailor-platform/seed"), - z.object({ - distPath: z.string(), - machineUserName: z.string().optional(), - disableIdpUserSync: z - .object({ - userToIdp: z.boolean().optional(), - idpToUser: z.boolean().optional(), - }) - .optional(), - }), -]); - -const EnumConstantsConfigSchema = z.tuple([ - z.literal("@tailor-platform/enum-constants"), - z.object({ distPath: z.string() }), -]); - -const FileUtilsConfigSchema = z.tuple([ - z.literal("@tailor-platform/file-utils"), - z.object({ distPath: z.string() }), -]); - -// Custom generator schema with dependencies -export const CodeGeneratorSchema = z.object({ - id: z.string(), - description: z.string(), - dependencies: z.array(DependencyKindSchema), - processType: z.function().optional(), - processResolver: z.function().optional(), - processExecutor: z.function().optional(), - processTailorDBNamespace: z.function().optional(), - processResolverNamespace: z.function().optional(), - aggregate: z.function({ output: z.any() }), -}); - -// Base schema for generator config (before transformation to actual Generator instances) -export const BaseGeneratorConfigSchema = z.union([ - KyselyTypeConfigSchema, - SeedConfigSchema, - EnumConstantsConfigSchema, - FileUtilsConfigSchema, - CodeGeneratorSchema, -]); +export type DependencyKind = "tailordb" | "resolver" | "executor"; diff --git a/packages/sdk/src/plugin/builtin/registry.ts b/packages/sdk/src/plugin/builtin/registry.ts deleted file mode 100644 index 4d702a548..000000000 --- a/packages/sdk/src/plugin/builtin/registry.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { enumConstantsPlugin, EnumConstantsGeneratorID } from "./enum-constants"; -import { fileUtilsPlugin, FileUtilsGeneratorID } from "./file-utils"; -import { kyselyTypePlugin, KyselyGeneratorID } from "./kysely-type"; -import { seedPlugin, SeedGeneratorID } from "./seed"; -import type { Plugin } from "@/types/plugin"; - -// Map of builtin generator IDs to plugin factory functions -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- builtin plugins accept heterogeneous options -export const builtinPlugins = new Map Plugin>([ - [KyselyGeneratorID, (options) => kyselyTypePlugin(options)], - [SeedGeneratorID, (options) => seedPlugin(options)], - [EnumConstantsGeneratorID, (options) => enumConstantsPlugin(options)], - [FileUtilsGeneratorID, (options) => fileUtilsPlugin(options)], -]); diff --git a/packages/sdk/src/plugin/compat.test.ts b/packages/sdk/src/plugin/compat.test.ts deleted file mode 100644 index 9bd98a632..000000000 --- a/packages/sdk/src/plugin/compat.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { randomUUID } from "node:crypto"; -import * as fs from "node:fs"; -import * as path from "node:path"; -import { beforeAll, describe, expect, test } from "vitest"; -import { generate } from "@/cli/commands/generate/service"; - -describe("defineGenerators and definePlugins produce identical output", () => { - const fixtureDir = path.resolve(__dirname, "../cli/commands/deploy/__test_fixtures__"); - const generatorsDir = path.join(fixtureDir, "generators-compat-out"); - const pluginsDir = path.join(fixtureDir, "plugins-compat-out"); - - const collectFiles = (rootDir: string): string[] => { - const files: string[] = []; - const traverse = (currentDir: string) => { - const entries = fs.readdirSync(currentDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.name === ".DS_Store") continue; - const fullPath = path.join(currentDir, entry.name); - if (entry.isDirectory()) { - traverse(fullPath); - } else { - files.push(path.relative(rootDir, fullPath).split(path.sep).join("/")); - } - } - }; - traverse(rootDir); - return files.toSorted(); - }; - - beforeAll(async () => { - process.env.TAILOR_PLATFORM_WORKSPACE_ID ??= randomUUID(); - - for (const dir of [generatorsDir, pluginsDir]) { - if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true }); - } - - await generate({ - configPath: path.join(fixtureDir, "tailor.config.generators-compat.ts"), - }); - await generate({ - configPath: path.join(fixtureDir, "tailor.config.plugins-compat.ts"), - }); - }, 120000); - - test("plugin output includes all generated files from defineGenerators", () => { - const generatorFiles = collectFiles(generatorsDir); - const pluginFiles = collectFiles(pluginsDir); - expect(generatorFiles.length).toBeGreaterThan(0); - for (const file of generatorFiles) { - expect(pluginFiles, `File ${file} missing from plugins output`).toContain(file); - } - }); - - test("all files have identical content", () => { - const files = collectFiles(generatorsDir); - expect(files.length).toBeGreaterThan(0); - - const normalizeConfigPath = (content: string) => - content.replace( - /tailor\.config\.(generators|plugins)-compat\.ts/g, - "tailor.config.compat.ts", - ); - - for (const file of files) { - const generatorContent = normalizeConfigPath( - fs.readFileSync(path.join(generatorsDir, file), "utf-8"), - ); - const pluginContent = normalizeConfigPath( - fs.readFileSync(path.join(pluginsDir, file), "utf-8"), - ); - expect(pluginContent, `Content mismatch in ${file}`).toBe(generatorContent); - } - }); -}); From 7604ad5fdf27b791e8f1db880faed156cd6554d5 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 05:17:40 +0900 Subject: [PATCH 014/618] feat(cli)!: require direct function test-run args --- .../remove-function-test-run-input-wrapper.md | 5 ++++ packages/sdk/e2e/function-test-run.test.ts | 19 ++++++++----- .../sdk/src/cli/commands/function/test-run.ts | 28 ++++--------------- 3 files changed, 22 insertions(+), 30 deletions(-) create mode 100644 .changeset/remove-function-test-run-input-wrapper.md diff --git a/.changeset/remove-function-test-run-input-wrapper.md b/.changeset/remove-function-test-run-input-wrapper.md new file mode 100644 index 000000000..5341ba525 --- /dev/null +++ b/.changeset/remove-function-test-run-input-wrapper.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Remove support for wrapping `tailor-sdk function test-run --arg` resolver input in an `input` object. Pass resolver input fields directly as the `--arg` JSON value. diff --git a/packages/sdk/e2e/function-test-run.test.ts b/packages/sdk/e2e/function-test-run.test.ts index 2ebd66785..374b81eb6 100644 --- a/packages/sdk/e2e/function-test-run.test.ts +++ b/packages/sdk/e2e/function-test-run.test.ts @@ -26,7 +26,7 @@ import { AuthInvokerSchema, type AuthInvoker } from "@tailor-proto/tailor/v1/aut import { describe, test, expect, beforeAll } from "vitest"; import { bundleForTestRun, type ResolvedMachineUser } from "../src/cli/commands/function/bundle"; import { detectFunctionType } from "../src/cli/commands/function/detect"; -import { resolveResolverArg } from "../src/cli/commands/function/test-run"; +import { validateResolverArg } from "../src/cli/commands/function/test-run"; import { initOperatorClient, type OperatorClient } from "../src/cli/shared/client"; import { loadAccessToken } from "../src/cli/shared/context"; import { executeScript, type ScriptExecutionResult } from "../src/cli/shared/script-executor"; @@ -92,7 +92,12 @@ async function runTestRun( if (!detected.hasInput) { resolvedArg = undefined; } else if (detected.inputSchema) { - resolvedArg = resolveResolverArg(resolvedArg, detected.inputSchema, machineUser, workspaceId); + resolvedArg = validateResolverArg( + resolvedArg, + detected.inputSchema, + machineUser, + workspaceId, + ); } } @@ -180,7 +185,7 @@ describe.sequential("E2E: function test-run", () => { describe("resolver", () => { test("runs add resolver with input arguments", async () => { const result = await runTestRun("resolvers/add.ts", { - arg: '{"input":{"a":1,"b":2}}', + arg: '{"a":1,"b":2}', }); expect(result.success).toBe(true); @@ -213,7 +218,7 @@ describe.sequential("E2E: function test-run", () => { test("injects environment variables into resolver", async () => { const result = await runTestRun("resolvers/env.ts", { - arg: '{"input":{"multiplier":3}}', + arg: '{"multiplier":3}', }); expect(result.success).toBe(true); @@ -226,7 +231,7 @@ describe.sequential("E2E: function test-run", () => { test("supports getDB in resolver (stepChain)", async () => { const result = await runTestRun("resolvers/stepChain.ts", { - arg: '{"input":{"user":{"name":{"first":"John","last":"Doe"}}}}', + arg: '{"user":{"name":{"first":"John","last":"Doe"}}}', }); expect(result.success).toBe(true); @@ -239,7 +244,7 @@ describe.sequential("E2E: function test-run", () => { test("inserts nested object with Date and verifies round-trip", async () => { const result = await runTestRun("resolvers/insertNestedProfileWithDate.ts", { - arg: '{"input":{"name":"Test User","email":"test@example.com"}}', + arg: '{"name":"Test User","email":"test@example.com"}', }); expect(result.success).toBe(true); @@ -254,7 +259,7 @@ describe.sequential("E2E: function test-run", () => { test("reports validation errors for invalid input", async () => { const result = await runTestRun("resolvers/add.ts", { - arg: '{"input":{"a":100,"b":2}}', + arg: '{"a":100,"b":2}', }); expect(result.success).toBe(false); diff --git a/packages/sdk/src/cli/commands/function/test-run.ts b/packages/sdk/src/cli/commands/function/test-run.ts index 0fda018e9..0e0a96028 100644 --- a/packages/sdk/src/cli/commands/function/test-run.ts +++ b/packages/sdk/src/cli/commands/function/test-run.ts @@ -136,7 +136,7 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file ); args.arg = undefined; } else if (detected.inputSchema) { - args.arg = resolveResolverArg(args.arg, detected.inputSchema, machineUser, workspaceId); + args.arg = validateResolverArg(args.arg, detected.inputSchema, machineUser, workspaceId); } } @@ -317,16 +317,14 @@ async function resolveMachineUser( } /** - * Resolve resolver arg format: detect and unwrap deprecated {"input":{...}} wrapper. - * Tries new format (arg = input fields) first via schema parse. - * If that fails and arg looks like old format, tries unwrapping. + * Validate resolver arg format against the detected input schema. * @param argStr - JSON string of the arg * @param inputSchema - Pre-built schema object from detect (has .parse()) * @param machineUser - Resolved machine user info * @param workspaceId - Workspace ID - * @returns Resolved JSON string (unwrapped if old format) + * @returns The original JSON string */ -export function resolveResolverArg( +export function validateResolverArg( argStr: string, inputSchema: NonNullable, machineUser: ResolvedMachineUser, @@ -346,22 +344,6 @@ export function resolveResolverArg( return argStr; } - // New format failed — check if old format works - if ( - Object.keys(parsed).length === 1 && - parsed.input != null && - typeof parsed.input === "object" && - !Array.isArray(parsed.input) - ) { - const oldResult = inputSchema.parse({ value: parsed.input, data: parsed.input, user }); - if (!oldResult.issues) { - logger.warn( - '[DEPRECATED] Wrapping args with "input" key (e.g. {"input":{...}}) is deprecated. Pass input fields directly (e.g. {"a":1}). The "input" wrapper will be removed in v2.', - ); - return JSON.stringify(parsed.input); - } - } - - // Both failed — pass as-is, let server report the validation error + // Pass as-is and let the server report the validation error. return argStr; } From 07cc256b9f1d695694d67438e1b0cb6df096ece8 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 05:27:13 +0900 Subject: [PATCH 015/618] feat(auth)!: remove auth invoker helper --- .changeset/remove-auth-invoker-helper.md | 5 ++ .../src/executor/checkInventory.ts | 3 +- .../templates/workflow/e2e/workflow.test.ts | 3 +- packages/sdk/docs/services/auth.md | 2 - packages/sdk/docs/services/executor.md | 2 - packages/sdk/docs/services/resolver.md | 2 - packages/sdk/docs/services/workflow.md | 2 - packages/sdk/docs/testing.md | 3 +- .../cli/commands/deploy/auth-invoker.test.ts | 2 +- .../src/cli/commands/deploy/auth-invoker.ts | 6 +- .../commands/workflow/start.api-types.test.ts | 52 ++++------- .../src/cli/commands/workflow/start.test.ts | 32 ++++++- .../sdk/src/cli/commands/workflow/start.ts | 41 +++++++-- .../services/workflow/ast-transformer.test.ts | 12 +-- .../services/workflow/trigger-transformer.ts | 13 ++- .../src/configure/services/auth/index.test.ts | 89 +------------------ .../sdk/src/configure/services/auth/index.ts | 21 +---- .../services/executor/executor.test.ts | 2 +- .../configure/services/executor/executor.ts | 3 +- .../configure/services/executor/operation.ts | 7 +- .../services/resolver/resolver.test.ts | 7 +- .../configure/services/resolver/resolver.ts | 5 +- .../configure/services/workflow/workflow.ts | 3 +- .../sdk/src/configure/types/machine-user.ts | 16 +--- packages/sdk/src/types/auth.ts | 29 +++--- 25 files changed, 131 insertions(+), 231 deletions(-) create mode 100644 .changeset/remove-auth-invoker-helper.md diff --git a/.changeset/remove-auth-invoker-helper.md b/.changeset/remove-auth-invoker-helper.md new file mode 100644 index 000000000..5c94a22e7 --- /dev/null +++ b/.changeset/remove-auth-invoker-helper.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Remove the deprecated `auth.invoker("")` helper. Pass machine user names directly as `authInvoker` strings in resolver, executor, and workflow APIs. diff --git a/packages/create-sdk/templates/inventory-management/src/executor/checkInventory.ts b/packages/create-sdk/templates/inventory-management/src/executor/checkInventory.ts index 93bc9d1cf..8106daa3c 100644 --- a/packages/create-sdk/templates/inventory-management/src/executor/checkInventory.ts +++ b/packages/create-sdk/templates/inventory-management/src/executor/checkInventory.ts @@ -1,6 +1,5 @@ import { createExecutor, recordUpdatedTrigger } from "@tailor-platform/sdk"; import { inventory } from "../db/inventory"; -import config from "../../tailor.config"; import { getDB } from "../generated/kysely-tailordb"; export default createExecutor({ @@ -22,6 +21,6 @@ export default createExecutor({ }) .execute(); }, - invoker: config.auth.invoker("manager"), + authInvoker: "manager", }, }); diff --git a/packages/create-sdk/templates/workflow/e2e/workflow.test.ts b/packages/create-sdk/templates/workflow/e2e/workflow.test.ts index 2bc2adc36..e60009b17 100644 --- a/packages/create-sdk/templates/workflow/e2e/workflow.test.ts +++ b/packages/create-sdk/templates/workflow/e2e/workflow.test.ts @@ -1,7 +1,6 @@ import { randomUUID } from "node:crypto"; import { describe, expect, test } from "vitest"; import { startWorkflow } from "@tailor-platform/sdk/cli"; -import config from "../tailor.config"; import userProfileSyncWorkflow from "../src/workflow/sync-profile"; describe("workflow", () => { @@ -11,7 +10,7 @@ describe("workflow", () => { const { executionId, wait } = await startWorkflow({ workflow: userProfileSyncWorkflow, - authInvoker: config.auth.invoker("admin"), + authInvoker: "admin", arg: { name: "workflow-test-user", email: testEmail, diff --git a/packages/sdk/docs/services/auth.md b/packages/sdk/docs/services/auth.md index 1db203bd5..f40737bb7 100644 --- a/packages/sdk/docs/services/auth.md +++ b/packages/sdk/docs/services/auth.md @@ -310,8 +310,6 @@ export default createResolver({ Type narrowing is provided by the generated `tailor.d.ts` (the `MachineUserNameRegistry` interface). Run `tailor-sdk generate` (or `apply`) after defining new machine users to refresh it. -> **Deprecated:** The `auth.invoker("")` helper is still available for backward compatibility. Prefer the string form — it does not require importing `auth` from `tailor.config.ts` into runtime files, avoiding bundling config-layer (Node-only) dependencies. - ## OAuth 2.0 Clients Configure OAuth 2.0 clients for third-party applications: diff --git a/packages/sdk/docs/services/executor.md b/packages/sdk/docs/services/executor.md index 11ef602bf..4e784761e 100644 --- a/packages/sdk/docs/services/executor.md +++ b/packages/sdk/docs/services/executor.md @@ -347,8 +347,6 @@ export default createExecutor({ }); ``` -> **Deprecated:** `auth.invoker("batch-processor")` still works, but is deprecated. Prefer the string form to avoid importing config-layer modules into runtime files. - ## Event Payloads Each trigger type provides specific context data in the callback functions. diff --git a/packages/sdk/docs/services/resolver.md b/packages/sdk/docs/services/resolver.md index cdc6ac61f..4d9df21d7 100644 --- a/packages/sdk/docs/services/resolver.md +++ b/packages/sdk/docs/services/resolver.md @@ -371,6 +371,4 @@ export default createResolver({ The machine user name is looked up in the auth service configured on your app (`machineUsers` in `defineAuth`). The namespace is resolved automatically — no need to import `auth` from `tailor.config.ts` in resolver files. -> **Deprecated:** `auth.invoker("batch-processor")` still works, but is deprecated. Importing `auth` into runtime files pulls config-layer (Node-only) dependencies into the bundle. - **Note:** `authInvoker` controls the permissions for database operations and other platform actions. The `user` object passed to `body` still reflects the original caller, while `invoker` reflects the principal actually running the body. diff --git a/packages/sdk/docs/services/workflow.md b/packages/sdk/docs/services/workflow.md index 022c963d4..564dc8ada 100644 --- a/packages/sdk/docs/services/workflow.md +++ b/packages/sdk/docs/services/workflow.md @@ -383,8 +383,6 @@ export default createResolver({ }); ``` -> **Deprecated:** `auth.invoker("manager-machine-user")` still works but is deprecated. Using the string form avoids importing `auth` into runtime code. - See the full working example in the repository: [example/resolvers/triggerWorkflow.ts](https://github.com/tailor-platform/sdk/blob/main/example/resolvers/triggerWorkflow.ts). ## File Organization diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index b3e8ebbe0..f4cba7133 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -823,14 +823,13 @@ Use `startWorkflow` from the CLI helpers. It starts the workflow on the deployed import { randomUUID } from "node:crypto"; import { startWorkflow } from "@tailor-platform/sdk/cli"; import { describe, expect, test } from "vitest"; -import config from "../tailor.config"; import userProfileSync from "../src/workflow/sync-profile"; describe("user-profile-sync workflow", () => { test("executes end to end", { timeout: 180_000 }, async () => { const { executionId, wait } = await startWorkflow({ workflow: userProfileSync, - authInvoker: config.auth.invoker("admin"), + authInvoker: "admin", arg: { name: "workflow-test", email: `wf-${randomUUID()}@example.com`, diff --git a/packages/sdk/src/cli/commands/deploy/auth-invoker.test.ts b/packages/sdk/src/cli/commands/deploy/auth-invoker.test.ts index b79b5d4e9..65ec1116e 100644 --- a/packages/sdk/src/cli/commands/deploy/auth-invoker.test.ts +++ b/packages/sdk/src/cli/commands/deploy/auth-invoker.test.ts @@ -24,7 +24,7 @@ describe("normalizeAuthInvoker", () => { test("throws when string authInvoker is given without an authNamespace", () => { expect(() => normalizeAuthInvoker("kiosk", undefined, 'Resolver "foo"')).toThrow( - /Resolver "foo".*no Auth service is configured/, + /Resolver "foo".*Configure an Auth service before using authInvoker/, ); }); }); diff --git a/packages/sdk/src/cli/commands/deploy/auth-invoker.ts b/packages/sdk/src/cli/commands/deploy/auth-invoker.ts index 160ab21c7..16c980caf 100644 --- a/packages/sdk/src/cli/commands/deploy/auth-invoker.ts +++ b/packages/sdk/src/cli/commands/deploy/auth-invoker.ts @@ -6,8 +6,8 @@ import type { AuthInvoker } from "@/types/auth.generated"; * Accepts either: * - `undefined` — returns undefined * - a plain string (machine user name) — expands to `{ namespace, machineUserName }` using `authNamespace` - * - an object `{ namespace, machineUserName }` — returned as-is - * @param authInvoker - String machine user name or object form + * - an internal object `{ namespace, machineUserName }` — returned as-is + * @param authInvoker - String machine user name or internal object form * @param authNamespace - Auth service namespace (required when authInvoker is a string) * @param context - Contextual label used in error messages (e.g. `resolver "foo"`) * @returns Object form of auth invoker, or undefined @@ -22,7 +22,7 @@ export function normalizeAuthInvoker( if (!authNamespace) { throw new Error( `${context} uses a string authInvoker ("${authInvoker}"), but no Auth service is configured. ` + - `Configure an Auth service or use the object form { namespace, machineUserName }.`, + `Configure an Auth service before using authInvoker.`, ); } return { namespace: authNamespace, machineUserName: authInvoker }; diff --git a/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts b/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts index 0e01e5ce2..b361b6bf6 100644 --- a/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts +++ b/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts @@ -1,25 +1,8 @@ // oxlint-disable vitest/expect-expect -- Type-only assertions are checked by TypeScript. import { describe, test, expectTypeOf } from "vitest"; -import { defineAuth } from "@/configure/services/auth"; -import { db } from "@/configure/services/tailordb"; import { createWorkflow, createWorkflowJob } from "@/configure/services/workflow"; import { type StartWorkflowOptions, type StartWorkflowTypedOptions } from "./start"; -const userType = db.type("User", { - email: db.string().unique(), -}); - -const auth = defineAuth("main-auth", { - userProfile: { - type: userType, - usernameField: "email", - }, - machineUsers: { - admin: {}, - worker: {}, - }, -}); - const calculationJob = createWorkflowJob({ name: "calculation", body: (input: { a: number; b: number }) => ({ sum: input.a + input.b }), @@ -102,13 +85,13 @@ describe("startWorkflow API types", () => { test("infers arg type from workflow", () => { acceptsCalculationWorkflowOptions({ workflow: calculationWorkflow, - authInvoker: auth.invoker("admin"), + authInvoker: "admin", arg: { a: 1, b: 2 }, }); acceptsCalculationWorkflowOptions({ workflow: calculationWorkflow, - authInvoker: auth.invoker("admin"), + authInvoker: "admin", // @ts-expect-error - arg shape must match workflow input arg: { x: 1, y: 2 }, }); @@ -121,44 +104,41 @@ describe("startWorkflow API types", () => { // @ts-expect-error - arg is required for workflows with input acceptsCalculationWorkflowOptions({ workflow: calculationWorkflow, - authInvoker: auth.invoker("admin"), + authInvoker: "admin", }); }); test("does not allow arg for workflows without input", () => { acceptsNoInputWorkflowOptions({ workflow: noInputWorkflow, - authInvoker: auth.invoker("admin"), + authInvoker: "admin", }); acceptsNoInputWorkflowOptions({ workflow: noInputWorkflow, - authInvoker: auth.invoker("admin"), + authInvoker: "admin", // @ts-expect-error - no-input workflow must not receive arg arg: { any: "value" }, }); }); - test("keeps machine user names type-safe via auth.invoker", () => { + test("accepts machine user names as strings", () => { acceptsCalculationWorkflowOptions({ workflow: calculationWorkflow, - authInvoker: auth.invoker("worker"), + authInvoker: "worker", arg: { a: 1, b: 2 }, }); - - // @ts-expect-error - invalid machine user name - auth.invoker("invalid-machine-user"); }); test("keeps default generic usable when StartWorkflowTypedOptions generic is omitted", () => { acceptsDefaultWorkflowOptions({ workflow: noInputWorkflow, - authInvoker: auth.invoker("admin"), + authInvoker: "admin", }); acceptsDefaultWorkflowOptions({ workflow: calculationWorkflow, - authInvoker: auth.invoker("admin"), + authInvoker: "admin", arg: { a: 1, b: 2 }, }); }); @@ -166,26 +146,26 @@ describe("startWorkflow API types", () => { test("supports union workflow types without collapsing arg type", () => { acceptsUnionWorkflowOptions({ workflow: calculationWorkflow, - authInvoker: auth.invoker("admin"), + authInvoker: "admin", arg: { a: 1, b: 2 }, }); acceptsUnionWorkflowOptions({ workflow: noInputWorkflow, - authInvoker: auth.invoker("admin"), + authInvoker: "admin", }); }); test("supports union workflow input types without collapsing arg type", () => { acceptsUnionInputWorkflowOptions({ workflow: calculationWorkflow, - authInvoker: auth.invoker("admin"), + authInvoker: "admin", arg: { a: 1, b: 2 }, }); acceptsUnionInputWorkflowOptions({ workflow: textWorkflow, - authInvoker: auth.invoker("admin"), + authInvoker: "admin", arg: { message: "hello" }, }); @@ -198,13 +178,13 @@ describe("startWorkflow API types", () => { test("supports plain workflow unions without collapsing arg type", () => { acceptsPlainUnionWorkflowOptions({ workflow: plainWorkflowA, - authInvoker: auth.invoker("admin"), + authInvoker: "admin", arg: { foo: 1 }, }); acceptsPlainUnionWorkflowOptions({ workflow: plainWorkflowB, - authInvoker: auth.invoker("admin"), + authInvoker: "admin", arg: { bar: "x" }, }); @@ -228,7 +208,7 @@ describe("startWorkflow API types", () => { acceptsDeprecatedOptions({ // @ts-expect-error - deprecated options must keep legacy name/machineUser shape workflow: calculationWorkflow, - authInvoker: auth.invoker("admin"), + authInvoker: "admin", arg: { a: 1, b: 2 }, }); }); diff --git a/packages/sdk/src/cli/commands/workflow/start.test.ts b/packages/sdk/src/cli/commands/workflow/start.test.ts index 2d207c533..8bb2bfd0c 100644 --- a/packages/sdk/src/cli/commands/workflow/start.test.ts +++ b/packages/sdk/src/cli/commands/workflow/start.test.ts @@ -71,10 +71,7 @@ describe("startWorkflow runtime overload", () => { body: () => undefined, }, }, - authInvoker: { - namespace: "typed-ns", - machineUserName: "typed-user", - }, + authInvoker: "typed-user", } as never); expect(loadConfig).toHaveBeenCalledTimes(1); @@ -92,6 +89,33 @@ describe("startWorkflow runtime overload", () => { ); }); + test("typed shape resolves auth namespace from config and sends proto authInvoker", async () => { + await startWorkflow({ + workflow: { + name: "typed-workflow", + mainJob: { + body: () => undefined, + }, + }, + authInvoker: "typed-user", + }); + + expect(loadConfig).toHaveBeenCalledTimes(1); + expect(getApplicationMock).toHaveBeenCalledWith({ + workspaceId: "workspace-1", + applicationName: "my-app", + }); + expect(testStartWorkflowMock).toHaveBeenCalledWith( + expect.objectContaining({ + workflowId: "id:typed-workflow", + authInvoker: expect.objectContaining({ + namespace: "auth-ns", + machineUserName: "typed-user", + }), + }), + ); + }); + test("start command with jsonMode emits only parseable JSON to stdout", async () => { using stdout = captureStdout(); using stderr = captureStderr(); diff --git a/packages/sdk/src/cli/commands/workflow/start.ts b/packages/sdk/src/cli/commands/workflow/start.ts index 2cb47f2a8..b6e876b4d 100644 --- a/packages/sdk/src/cli/commands/workflow/start.ts +++ b/packages/sdk/src/cli/commands/workflow/start.ts @@ -18,6 +18,7 @@ import { nameArgs, waitArgs } from "./args"; import { getWorkflowExecution, printExecutionWithLogs } from "./executions"; import { resolveWorkflow } from "./get"; import { type WorkflowExecutionInfo, toWorkflowExecutionInfo } from "./transform"; +import type { MachineUserName } from "@/types/auth"; import type { WorkflowExecution } from "@tailor-proto/tailor/v1/workflow_resource_pb"; import type { Jsonifiable } from "type-fest"; @@ -64,9 +65,10 @@ export interface StartWorkflowOptions { type StartWorkflowTypedBaseOptions = { workflow: W; - authInvoker: AuthInvoker; + authInvoker: MachineUserName; workspaceId?: string; profile?: string; + configPath?: string; interval?: number; }; @@ -269,6 +271,22 @@ async function startWorkflowCore( } } +async function resolveApplicationAuthNamespace(options: { + client: Awaited>; + workspaceId: string; + configPath?: string; +}): Promise { + const { config } = await loadConfig(options.configPath); + const { application } = await options.client.getApplication({ + workspaceId: options.workspaceId, + applicationName: config.name, + }); + if (!application?.authNamespace) { + throw new Error(`Application ${config.name} does not have an auth configuration.`); + } + return application.authNamespace; +} + async function startWorkflowByName( options: StartWorkflowOptions, ): Promise { @@ -281,21 +299,18 @@ async function startWorkflowByName( profile: options.profile, }); - const { config } = await loadConfig(options.configPath); - const { application } = await client.getApplication({ + const authNamespace = await resolveApplicationAuthNamespace({ + client, workspaceId, - applicationName: config.name, + configPath: options.configPath, }); - if (!application?.authNamespace) { - throw new Error(`Application ${config.name} does not have an auth configuration.`); - } return await startWorkflowCore({ client, workspaceId, workflowName: options.name, authInvoker: { - namespace: application.authNamespace, + namespace: authNamespace, machineUserName: options.machineUser, }, arg: options.arg, @@ -330,12 +345,20 @@ export async function startWorkflow( workspaceId: options.workspaceId, profile: options.profile, }); + const authNamespace = await resolveApplicationAuthNamespace({ + client, + workspaceId, + configPath: options.configPath, + }); return await startWorkflowCore({ client, workspaceId, workflowName: options.workflow.name, - authInvoker: options.authInvoker, + authInvoker: { + namespace: authNamespace, + machineUserName: options.authInvoker, + }, arg: options.arg, interval: options.interval, }); diff --git a/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts b/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts index 847b8aa52..f47ea4011 100644 --- a/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts +++ b/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts @@ -766,7 +766,7 @@ describe("AST Transformer - transformFunctionTriggers", () => { const source = ` const workflowRunId = await orderWorkflow.trigger( { orderId: "123", customerId: "456" }, - { authInvoker: auth.invoker("admin") } + { authInvoker: "admin" } ); `; const workflowNameMap = new Map([["orderWorkflow", "order-processing"]]); @@ -776,12 +776,12 @@ const workflowRunId = await orderWorkflow.trigger( expect(result).toContain('tailor.workflow.triggerWorkflow("order-processing"'); expect(result).toContain('{ orderId: "123", customerId: "456" }'); - expect(result).toContain('{ authInvoker: auth.invoker("admin") }'); + expect(result).toContain('{ authInvoker: "admin" }'); }); test("transforms workflow.trigger() with shorthand authInvoker", () => { const source = ` -const authInvoker = auth.invoker("admin"); +const authInvoker = "admin"; const result = await myWorkflow.trigger({ id: 1 }, { authInvoker }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); @@ -813,7 +813,7 @@ const result = await myWorkflow.trigger({ id: 1 }, { authInvoker: "kiosk" }); expect(result).toContain('{ authInvoker: __tailor_normalizeAuthInvoker("kiosk") }'); // Helper injected at the top of the file with the namespace baked in expect(result).toContain( - 'const __tailor_normalizeAuthInvoker = (v) => typeof v === "string" ? { namespace: "my-auth", machineUserName: v } : v;', + 'const __tailor_normalizeAuthInvoker = (machineUserName) => ({ namespace: "my-auth", machineUserName });', ); }); @@ -942,7 +942,7 @@ const event = button.trigger("click"); test("only transforms trigger calls for known workflows and jobs", () => { const source = ` // Known workflow - should be transformed -const wfResult = await orderWorkflow.trigger({ id: 1 }, { authInvoker: auth.invoker("admin") }); +const wfResult = await orderWorkflow.trigger({ id: 1 }, { authInvoker: "admin" }); // Known job - should be transformed const jobResult = await fetchData.trigger({ id: 2 }); @@ -989,7 +989,7 @@ async function processOrder(orderId: string) { // Then trigger a workflow for processing const workflowRunId = await orderWorkflow.trigger( { orderId, data }, - { authInvoker: auth.invoker("system") } + { authInvoker: "system" } ); return { data, workflowRunId }; diff --git a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts b/packages/sdk/src/cli/services/workflow/trigger-transformer.ts index 5c233b153..5b9f2c2ed 100644 --- a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts +++ b/packages/sdk/src/cli/services/workflow/trigger-transformer.ts @@ -41,14 +41,14 @@ const NORMALIZER_IDENTIFIER = "__tailor_normalizeAuthInvoker"; /** * Build the source text of the injected normalizer helper. * - * Accepts either a plain string (machine user name) or the object form - * `{ namespace, machineUserName }`, and always returns the object form. + * Accepts a plain string machine user name and returns the object form + * expected by the platform RPC. * The auth namespace is baked in at bundle time. * @param authNamespace - Auth service namespace to embed * @returns Source line defining the helper */ function buildNormalizerHelperSource(authNamespace: string): string { - return `const ${NORMALIZER_IDENTIFIER} = (v) => typeof v === "string" ? { namespace: ${JSON.stringify(authNamespace)}, machineUserName: v } : v;\n`; + return `const ${NORMALIZER_IDENTIFIER} = (machineUserName) => ({ namespace: ${JSON.stringify(authNamespace)}, machineUserName });\n`; } /** @@ -431,10 +431,9 @@ export function transformFunctionTriggers( if (workflowName) { // Resolve the source expression for authInvoker. const rawExpr = call.authInvoker.isShorthand ? "authInvoker" : call.authInvoker.valueText; - // Wrap with the runtime normalizer so any form (string literal, - // variable reference, function call, or `{ namespace, machineUserName }` - // object) becomes the object form the platform RPC expects. The - // normalizer is injected once at the top of the file. + // Wrap with the runtime normalizer so string expressions become the + // object form the platform RPC expects. The normalizer is injected + // once at the top of the file. // When no auth service is configured we can't expand a string, so // we pass through unchanged (platform will reject a string with a // clear error). diff --git a/packages/sdk/src/configure/services/auth/index.test.ts b/packages/sdk/src/configure/services/auth/index.test.ts index 0123e6dd1..058fea8d3 100644 --- a/packages/sdk/src/configure/services/auth/index.test.ts +++ b/packages/sdk/src/configure/services/auth/index.test.ts @@ -3,9 +3,8 @@ import { randomUUID } from "node:crypto"; import { describe, expect, test, expectTypeOf } from "vitest"; import { t } from "@/configure/types/type"; import { db } from "../tailordb/schema"; -import { defineAuth, type AuthInvoker } from "./index"; +import { defineAuth } from "./index"; import type { BeforeLoginHook } from "@/types/auth"; -import type { AuthInvoker as ProtoAuthInvoker } from "@tailor-proto/tailor/v1/auth_resource_pb"; import type { JsonObject } from "type-fest"; const userType = db.type("User", { @@ -63,27 +62,6 @@ describe("defineAuth", () => { expect(authConfig.machineUsers?.admin.attributes?.role).toBe("ADMIN"); }); - test("creates auth configuration with invoker method", () => { - const authConfig = defineAuth("test-service", { - userProfile: { - type: userType, - usernameField: "email", - }, - machineUsers: { - admin: {}, - worker: {}, - }, - }); - - const invoker = authConfig.invoker("admin"); - expect(invoker.namespace).toBe("test-service"); - expect(invoker.machineUserName).toBe("admin"); - - const workerInvoker = authConfig.invoker("worker"); - expect(workerInvoker.namespace).toBe("test-service"); - expect(workerInvoker.machineUserName).toBe("worker"); - }); - test("creates minimal auth configuration", () => { const authConfig = defineAuth("minimal", { userProfile: { @@ -253,8 +231,6 @@ describe("defineAuth", () => { }); expect(authConfig.hooks!.beforeLogin!.invoker).toBe("admin"); - // invoker should not narrow MachineUserNames — both machine users must remain valid - expectTypeOf(authConfig.invoker).parameter(0).toEqualTypeOf<"admin" | "worker">(); }); test("is optional — existing tests continue to pass without it", () => { @@ -268,67 +244,4 @@ describe("defineAuth", () => { expect(authConfig.hooks).toBeUndefined(); }); }); - - describe("AuthInvoker type compatibility with tailor-proto", () => { - test("AuthInvoker has namespace field compatible with proto", () => { - // Verify the field name matches tailor.v1.AuthInvoker - type HasNamespace = AuthInvoker extends { namespace: string } ? true : false; - expectTypeOf().toEqualTypeOf(); - - // Verify proto type has the same field - type ProtoHasNamespace = ProtoAuthInvoker extends { namespace: string } ? true : false; - expectTypeOf().toEqualTypeOf(); - }); - - test("AuthInvoker has machineUserName field compatible with proto", () => { - // Verify the field name matches tailor.v1.AuthInvoker - type HasMachineUserName = - AuthInvoker extends { - machineUserName: string; - } - ? true - : false; - expectTypeOf().toEqualTypeOf(); - - // Verify proto type has the same field - type ProtoHasMachineUserName = ProtoAuthInvoker extends { - machineUserName: string; - } - ? true - : false; - expectTypeOf().toEqualTypeOf(); - }); - - test("AuthInvoker is assignable to proto AuthInvoker fields", () => { - // This ensures that our AuthInvoker can be used where proto AuthInvoker is expected - // (checking the common properties) - type IsCompatible = - AuthInvoker extends Pick - ? true - : false; - expectTypeOf().toEqualTypeOf(); - }); - - test("invoker() returns AuthInvoker compatible object", () => { - const authConfig = defineAuth("test-auth", { - userProfile: { - type: userType, - usernameField: "email", - }, - machineUsers: { - admin: {}, - }, - }); - - const invoker = authConfig.invoker("admin"); - - // Verify at runtime that the object has the correct field names - expect(invoker).toHaveProperty("namespace"); - expect(invoker).toHaveProperty("machineUserName"); - - // Verify it does NOT have the old field names - expect(invoker).not.toHaveProperty("authName"); - expect(invoker).not.toHaveProperty("machineUser"); - }); - }); }); diff --git a/packages/sdk/src/configure/services/auth/index.ts b/packages/sdk/src/configure/services/auth/index.ts index 8fa0358c1..8ca30c959 100644 --- a/packages/sdk/src/configure/services/auth/index.ts +++ b/packages/sdk/src/configure/services/auth/index.ts @@ -7,7 +7,6 @@ import type { UserAttributeListKey, UserAttributeMap, } from "@/types/auth"; -import type { AuthInvoker as ParserAuthInvoker } from "@/types/auth.generated"; import type { DefinedFieldMetadata, FieldMetadata, TailorFieldType } from "@/types/field-types"; import type { TailorField } from "@/types/tailor-field"; @@ -88,15 +87,6 @@ export type { DefinedAuth, } from "@/types/auth"; -/** - * Invoker type compatible with tailor.v1.AuthInvoker - * - namespace: auth service name - * - machineUserName: machine user name - */ -export type AuthInvoker = Omit & { - machineUserName: M; -}; - /** * Define an auth service for the Tailor SDK. * @template Name @@ -104,7 +94,6 @@ export type AuthInvoker = Omit, ): DefinedAuth< Name, - UserProfileAuthInput, - MachineUserNames + UserProfileAuthInput >; export function defineAuth< const Name extends string, @@ -140,8 +128,7 @@ export function defineAuth< config: MachineUserOnlyAuthInput, ): DefinedAuth< Name, - MachineUserOnlyAuthInput, - MachineUserNames + MachineUserOnlyAuthInput >; /* @__NO_SIDE_EFFECTS__ */ export function defineAuth< @@ -161,9 +148,6 @@ export function defineAuth< const result = { ...config, name, - invoker(machineUser: M) { - return { namespace: name, machineUserName: machineUser } as const; - }, getConnectionToken(connectionName: C): Promise { return tailor.authconnection.getConnectionToken(connectionName); }, @@ -172,7 +156,6 @@ export function defineAuth< | MachineUserOnlyAuthInput ) & { name: string; - invoker(machineUser: M): AuthInvoker; getConnectionToken(connectionName: C): Promise; }; diff --git a/packages/sdk/src/configure/services/executor/executor.test.ts b/packages/sdk/src/configure/services/executor/executor.test.ts index a7fa6014c..9864fe4da 100644 --- a/packages/sdk/src/configure/services/executor/executor.test.ts +++ b/packages/sdk/src/configure/services/executor/executor.test.ts @@ -1373,7 +1373,7 @@ describe("workflowTarget", () => { kind: "workflow", workflow: testWorkflow, args: { orderId: "test-id" }, - authInvoker: { namespace: "my-auth", machineUserName: "admin" }, + authInvoker: "admin", }, }); }); diff --git a/packages/sdk/src/configure/services/executor/executor.ts b/packages/sdk/src/configure/services/executor/executor.ts index e3fcf3cd1..2870652c0 100644 --- a/packages/sdk/src/configure/services/executor/executor.ts +++ b/packages/sdk/src/configure/services/executor/executor.ts @@ -1,7 +1,6 @@ import { brandValue } from "@/utils/brand"; import type { Operation } from "./operation"; import type { Trigger } from "./trigger"; -import type { AuthInvoker } from "@/configure/services/auth"; import type { Workflow } from "@/configure/services/workflow/workflow"; import type { MachineUserName } from "@/configure/types/machine-user"; import type { ExecutorInput } from "@/types/executor.generated"; @@ -31,7 +30,7 @@ type Executor, O> = O extends { kind: "workflow"; workflow: W; args?: WorkflowInput | ((args: TriggerArgs) => WorkflowInput); - authInvoker?: AuthInvoker | MachineUserName; + authInvoker?: MachineUserName; }; } : ExecutorBase & { diff --git a/packages/sdk/src/configure/services/executor/operation.ts b/packages/sdk/src/configure/services/executor/operation.ts index 658fd2638..328131b8a 100644 --- a/packages/sdk/src/configure/services/executor/operation.ts +++ b/packages/sdk/src/configure/services/executor/operation.ts @@ -1,4 +1,3 @@ -import type { AuthInvoker } from "@/configure/services/auth"; import type { Workflow } from "@/configure/services/workflow/workflow"; import type { MachineUserName } from "@/configure/types/machine-user"; import type { @@ -13,7 +12,7 @@ import type { Client } from "@urql/core"; /** Function-based executor operation. The body receives the trigger args and the `invoker`. */ export type FunctionOperation = Omit & { body: (args: Args & { invoker?: TailorInvoker }) => void | Promise; - authInvoker?: AuthInvoker | MachineUserName; + authInvoker?: MachineUserName; }; type UrqlOperationArgs = Parameters; @@ -22,7 +21,7 @@ type UrqlOperationArgs = Parameters; export type GqlOperation = Omit & { query: UrqlOperationArgs[0]; variables?: (args: Args) => UrqlOperationArgs[1]; - authInvoker?: AuthInvoker | MachineUserName; + authInvoker?: MachineUserName; }; type RequestHeader = @@ -295,7 +294,7 @@ export type WorkflowOperation = Omit< > & { workflow: W; args?: WorkflowInput | ((args: Args) => WorkflowInput); - authInvoker?: AuthInvoker | MachineUserName; + authInvoker?: MachineUserName; }; export type Operation = diff --git a/packages/sdk/src/configure/services/resolver/resolver.test.ts b/packages/sdk/src/configure/services/resolver/resolver.test.ts index 53f6f9885..157072ab1 100644 --- a/packages/sdk/src/configure/services/resolver/resolver.test.ts +++ b/packages/sdk/src/configure/services/resolver/resolver.test.ts @@ -496,14 +496,11 @@ describe("createResolver", () => { operation: "query", output: outputType, body: () => ({ result: "ok" }), - authInvoker: { namespace: "my-auth", machineUserName: "batch-user" }, + authInvoker: "batch-user", }); expect(resolver.name).toBe("withAuthInvoker"); - expect(resolver.authInvoker).toEqual({ - namespace: "my-auth", - machineUserName: "batch-user", - }); + expect(resolver.authInvoker).toBe("batch-user"); }); test("creates minimal resolver without optional fields", () => { diff --git a/packages/sdk/src/configure/services/resolver/resolver.ts b/packages/sdk/src/configure/services/resolver/resolver.ts index f32e681be..c85dd0fe6 100644 --- a/packages/sdk/src/configure/services/resolver/resolver.ts +++ b/packages/sdk/src/configure/services/resolver/resolver.ts @@ -1,6 +1,5 @@ import { t } from "@/configure/types/type"; import { brandValue } from "@/utils/brand"; -import type { AuthInvoker } from "@/configure/services/auth"; import type { MachineUserName } from "@/configure/types/machine-user"; import type { TailorAnyField, TailorField } from "@/configure/types/type"; import type { TailorEnv } from "@/types/env"; @@ -42,7 +41,7 @@ type ResolverReturn< input?: Input; output: NormalizedOutput; body: (context: Context) => OutputType | Promise>; - authInvoker?: AuthInvoker | MachineUserName; + authInvoker?: MachineUserName; }>; /** @@ -93,7 +92,7 @@ export function createResolver< input?: Input; output: Output; body: (context: Context) => OutputType | Promise>; - authInvoker?: AuthInvoker | MachineUserName; + authInvoker?: MachineUserName; }>, ): ResolverReturn { // Check if output is already a TailorField using duck typing. diff --git a/packages/sdk/src/configure/services/workflow/workflow.ts b/packages/sdk/src/configure/services/workflow/workflow.ts index bb54512f9..eb2be3033 100644 --- a/packages/sdk/src/configure/services/workflow/workflow.ts +++ b/packages/sdk/src/configure/services/workflow/workflow.ts @@ -1,7 +1,6 @@ /* oxlint-disable typescript/no-explicit-any */ import { brandValue } from "@/utils/brand"; import { dispatchTriggerWorkflow, registerWorkflow } from "./registry"; -import type { AuthInvoker } from "../auth"; import type { WorkflowJob } from "./job"; import type { MachineUserName } from "@/configure/types/machine-user"; import type { ConcurrencyPolicy, RetryPolicy } from "@/types/workflow.generated"; @@ -24,7 +23,7 @@ export interface Workflow = WorkflowJob[0], - options?: { authInvoker: AuthInvoker | MachineUserName }, + options?: { authInvoker: MachineUserName }, ) => Promise; } diff --git a/packages/sdk/src/configure/types/machine-user.ts b/packages/sdk/src/configure/types/machine-user.ts index 924afecc2..456c4ce39 100644 --- a/packages/sdk/src/configure/types/machine-user.ts +++ b/packages/sdk/src/configure/types/machine-user.ts @@ -1,15 +1 @@ -// Interface for module augmentation -// Users can extend via: declare module "@tailor-platform/sdk" { interface MachineUserNameRegistry { ... } } -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface MachineUserNameRegistry {} - -/** - * Machine user name. - * - * When `tailor.d.ts` is generated (via `tailor-sdk deploy`/`generate`), this is narrowed - * to the union of defined machine user names. When no machine users are registered yet, - * falls back to `string` to avoid blocking editing before the first generate run. - */ -export type MachineUserName = keyof MachineUserNameRegistry extends never - ? string - : keyof MachineUserNameRegistry & string; +export type { MachineUserName, MachineUserNameRegistry } from "@/types/auth"; diff --git a/packages/sdk/src/types/auth.ts b/packages/sdk/src/types/auth.ts index dda7616f3..5dad684df 100644 --- a/packages/sdk/src/types/auth.ts +++ b/packages/sdk/src/types/auth.ts @@ -1,7 +1,6 @@ import type { AuthConnectionConfig } from "./auth-connection.generated"; import type { ValueOperand } from "./auth-value"; import type { - AuthInvoker, IdProvider as IdProviderConfig, OAuth2Client, OAuth2ClientInput, @@ -20,9 +19,21 @@ import type { IsAny, JsonObject } from "type-fest"; export type OAuth2ClientGrantType = OAuth2Client["grantTypes"][number]; export type SCIMAttributeType = SCIMAttribute["type"]; -export type AuthInvokerWithName = Omit & { - machineUserName: M; -}; +// Interface for module augmentation +// Users can extend via: declare module "@tailor-platform/sdk" { interface MachineUserNameRegistry { ... } } +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface MachineUserNameRegistry {} + +/** + * Machine user name. + * + * When `tailor.d.ts` is generated (via `tailor-sdk deploy`/`generate`), this is narrowed + * to the union of defined machine user names. When no machine users are registered yet, + * falls back to `string` to avoid blocking editing before the first generate run. + */ +export type MachineUserName = keyof MachineUserNameRegistry extends never + ? string + : keyof MachineUserNameRegistry & string; /** Result of retrieving a connection token at runtime. */ export type AuthConnectionTokenResult = { @@ -279,13 +290,8 @@ type ConnectionNames = Config extends { connections?: Record = Config & { +export type DefinedAuth = Config & { name: Name; - /** - * @deprecated Pass the machine user name directly as a string instead, e.g. `authInvoker: "machine-user-name"`. - * Using this function pulls config-layer (Node-only) dependencies into runtime bundles. - */ - invoker(machineUser: M): AuthInvokerWithName; getConnectionToken>( connectionName: C, ): Promise; @@ -301,8 +307,7 @@ export type AuthOwnConfig = DefinedAuth< // Intentionally permissive: AuthConfig is the "container" type for AppConfig.auth. // We want any concrete `defineAuth(...)` result to be assignable here, while the // strong typing remains on the `defineAuth` return type itself. - AuthServiceInputLoose, - string + AuthServiceInputLoose >; export type AuthConfig = AuthOwnConfig | AuthExternalConfig; From 531a3156dbbdcc20c42746baabee093427e9c958 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 05:31:59 +0900 Subject: [PATCH 016/618] fix(cli): preserve machine user narrowing in workflow helpers --- .../commands/workflow/start.api-types.test.ts | 15 +++++++++++++++ packages/sdk/src/cli/commands/workflow/start.ts | 17 ++++++++++++++++- packages/sdk/src/cli/lib.ts | 2 ++ .../sdk/src/cli/shared/type-generator.test.ts | 2 ++ packages/sdk/src/cli/shared/type-generator.ts | 4 ++++ 5 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts b/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts index b361b6bf6..ec8b11cb8 100644 --- a/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts +++ b/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts @@ -3,6 +3,14 @@ import { describe, test, expectTypeOf } from "vitest"; import { createWorkflow, createWorkflowJob } from "@/configure/services/workflow"; import { type StartWorkflowOptions, type StartWorkflowTypedOptions } from "./start"; +declare module "./start" { + interface MachineUserNameRegistry { + admin: true; + "typed-user": true; + worker: true; + } +} + const calculationJob = createWorkflowJob({ name: "calculation", body: (input: { a: number; b: number }) => ({ sum: input.a + input.b }), @@ -128,6 +136,13 @@ describe("startWorkflow API types", () => { authInvoker: "worker", arg: { a: 1, b: 2 }, }); + + acceptsCalculationWorkflowOptions({ + workflow: calculationWorkflow, + // @ts-expect-error - invalid machine user name + authInvoker: "invalid-machine-user", + arg: { a: 1, b: 2 }, + }); }); test("keeps default generic usable when StartWorkflowTypedOptions generic is omitted", () => { diff --git a/packages/sdk/src/cli/commands/workflow/start.ts b/packages/sdk/src/cli/commands/workflow/start.ts index b6e876b4d..bd8bb687e 100644 --- a/packages/sdk/src/cli/commands/workflow/start.ts +++ b/packages/sdk/src/cli/commands/workflow/start.ts @@ -18,10 +18,25 @@ import { nameArgs, waitArgs } from "./args"; import { getWorkflowExecution, printExecutionWithLogs } from "./executions"; import { resolveWorkflow } from "./get"; import { type WorkflowExecutionInfo, toWorkflowExecutionInfo } from "./transform"; -import type { MachineUserName } from "@/types/auth"; import type { WorkflowExecution } from "@tailor-proto/tailor/v1/workflow_resource_pb"; import type { Jsonifiable } from "type-fest"; +// Interface for module augmentation +// Users can extend via: declare module "@tailor-platform/sdk/cli" { interface MachineUserNameRegistry { ... } } +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface MachineUserNameRegistry {} + +/** + * Machine user name for CLI helper APIs. + * + * When `tailor.d.ts` is generated (via `tailor-sdk deploy`/`generate`), this is narrowed + * to the union of defined machine user names. When no machine users are registered yet, + * falls back to `string` to avoid blocking editing before the first generate run. + */ +export type MachineUserName = keyof MachineUserNameRegistry extends never + ? string + : keyof MachineUserNameRegistry & string; + type WorkflowLike = { name: string; mainJob: { diff --git a/packages/sdk/src/cli/lib.ts b/packages/sdk/src/cli/lib.ts index 818707706..141263bbb 100644 --- a/packages/sdk/src/cli/lib.ts +++ b/packages/sdk/src/cli/lib.ts @@ -93,6 +93,8 @@ export { } from "./commands/workflow/get"; export { startWorkflow, + type MachineUserName, + type MachineUserNameRegistry, type StartWorkflowOptions, type StartWorkflowTypedOptions, type StartWorkflowResultWithWait, diff --git a/packages/sdk/src/cli/shared/type-generator.test.ts b/packages/sdk/src/cli/shared/type-generator.test.ts index 7d4885173..5e990d199 100644 --- a/packages/sdk/src/cli/shared/type-generator.test.ts +++ b/packages/sdk/src/cli/shared/type-generator.test.ts @@ -86,6 +86,7 @@ describe("generateTypeDefinition", () => { const result = generateTypeDefinition(undefined, undefined); expect(result).toContain("interface MachineUserNameRegistry {}"); + expect(result).toContain('declare module "@tailor-platform/sdk/cli"'); }); test("should generate MachineUserNameRegistry with machine user names", () => { @@ -95,6 +96,7 @@ describe("generateTypeDefinition", () => { ]); expect(result).toContain("interface MachineUserNameRegistry"); + expect(result).toContain('declare module "@tailor-platform/sdk/cli"'); // Names with hyphens are quoted expect(result).toContain('"manager-machine-user": true;'); // Valid identifiers are emitted unquoted (matches formatter output) diff --git a/packages/sdk/src/cli/shared/type-generator.ts b/packages/sdk/src/cli/shared/type-generator.ts index 5806f8fa2..d093e8291 100644 --- a/packages/sdk/src/cli/shared/type-generator.ts +++ b/packages/sdk/src/cli/shared/type-generator.ts @@ -136,6 +136,10 @@ declare module "@tailor-platform/sdk" { interface IdpNameRegistry ${idpNameBody} } +declare module "@tailor-platform/sdk/cli" { + interface MachineUserNameRegistry ${machineUserBody} +} + export {}; `; From 97b57462e372a7a5aedd42239d69cbf26dee1209 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 05:47:49 +0900 Subject: [PATCH 017/618] fix(auth): resolve external auth invokers --- .../src/cli/commands/deploy/executor.test.ts | 44 ++++++++++++++++ .../sdk/src/cli/commands/deploy/executor.ts | 8 +-- .../src/cli/commands/deploy/resolver.test.ts | 52 +++++++++++++++++++ .../sdk/src/cli/commands/deploy/resolver.ts | 3 +- .../src/cli/commands/workflow/start.test.ts | 32 ++++++++++++ .../sdk/src/cli/commands/workflow/start.ts | 5 +- packages/sdk/src/cli/services/application.ts | 3 +- .../sdk/src/cli/shared/auth-namespace.test.ts | 25 +++++++++ packages/sdk/src/cli/shared/auth-namespace.ts | 17 ++++++ 9 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 packages/sdk/src/cli/shared/auth-namespace.test.ts create mode 100644 packages/sdk/src/cli/shared/auth-namespace.ts diff --git a/packages/sdk/src/cli/commands/deploy/executor.test.ts b/packages/sdk/src/cli/commands/deploy/executor.test.ts index af6c58145..f81e1aaa5 100644 --- a/packages/sdk/src/cli/commands/deploy/executor.test.ts +++ b/packages/sdk/src/cli/commands/deploy/executor.test.ts @@ -140,6 +140,7 @@ describe("planExecutor", () => { resolverNames?: Record; idpNames?: ReadonlyArray; authName?: string; + externalAuthName?: string; }, ): Application { const tailorDBServices = Object.entries( @@ -166,6 +167,9 @@ describe("planExecutor", () => { resolverServices, idpServices, authService: options?.authName ? { config: { name: options.authName } } : undefined, + config: options?.externalAuthName + ? { auth: { name: options.externalAuthName, external: true } } + : {}, } as unknown as Application; } @@ -672,6 +676,46 @@ describe("planExecutor", () => { expect(variablesExpr).toContain("result: args.succeeded?.result.resolver"); expect(variablesExpr).toContain("error: args.failed?.error"); }); + + test("string authInvoker uses external auth config name", async () => { + const client = createMockClient([]); + const executor: Executor = { + name: "test-executor", + description: "Executor test-executor", + disabled: false, + trigger: { + kind: "schedule", + timezone: "UTC", + cron: "0 * * * *", + }, + operation: { + kind: "function", + body: () => {}, + authInvoker: "batch-user", + }, + }; + const application = createMockApplication([executor], { + externalAuthName: "external-auth", + }); + + const result = await planExecutor({ + client, + workspaceId, + application, + forRemoval: false, + config: mockConfig, + }); + + const create = result.changeSet.creates[0]!; + const targetConfig = create.request.executor?.targetConfig?.config as { + case: "function"; + value: { invoker: { namespace: string; machineUserName: string } }; + }; + expect(targetConfig.value.invoker).toEqual({ + namespace: "external-auth", + machineUserName: "batch-user", + }); + }); }); describe("typed event config", () => { diff --git a/packages/sdk/src/cli/commands/deploy/executor.ts b/packages/sdk/src/cli/commands/deploy/executor.ts index 059932238..9300dcf5a 100644 --- a/packages/sdk/src/cli/commands/deploy/executor.ts +++ b/packages/sdk/src/cli/commands/deploy/executor.ts @@ -13,6 +13,7 @@ import { type ExecutorTriggerEventConfigSchema, ExecutorTriggerType, } from "@tailor-proto/tailor/v1/executor_resource_pb"; +import { getApplicationAuthNamespace } from "@/cli/shared/auth-namespace"; import { type OperatorClient } from "@/cli/shared/client"; import { buildExecutorArgsExpr } from "@/cli/shared/runtime-exprs"; import { stringifyFunction } from "@/parser/service/tailordb"; @@ -375,10 +376,11 @@ function resolveIdpNamespace( } function resolveAuthNamespace(application: Readonly): string { - if (!application.authService) { + const authNamespace = getApplicationAuthNamespace(application); + if (!authNamespace) { throw new Error("No Auth service configured"); } - return application.authService.config.name; + return authNamespace; } function protoExecutor( @@ -492,7 +494,7 @@ function protoExecutor( let targetType: ExecutorTargetType; let targetConfig: MessageInitShape; - const authNamespace = application.authService?.config.name; + const authNamespace = getApplicationAuthNamespace(application); const invokerContext = `Executor "${executor.name}"`; switch (target.kind) { diff --git a/packages/sdk/src/cli/commands/deploy/resolver.test.ts b/packages/sdk/src/cli/commands/deploy/resolver.test.ts index f7c98ac34..2099774ba 100644 --- a/packages/sdk/src/cli/commands/deploy/resolver.test.ts +++ b/packages/sdk/src/cli/commands/deploy/resolver.test.ts @@ -667,6 +667,58 @@ describe("processResolver authInvoker mapping", () => { await expect(planPipeline(ctx)).rejects.toThrow(/no Auth service is configured/); }); + test("string authInvoker uses external auth config name", async () => { + const client = createMockClient([{ name: "test-ns", label: appName }]); + + const resolverService = { + namespace: "test-ns", + config: {}, + resolvers: { + myResolver: { + name: "myResolver", + operation: "query", + body: () => "hello", + output: { type: "string", metadata: {}, fields: {} }, + authInvoker: "batch-user", + }, + }, + loadResolvers: vi.fn().mockResolvedValue(undefined), + } as unknown as ResolverService; + + const application = { + name: appName, + env: {}, + config: { + auth: { name: "external-auth", external: true }, + }, + resolverServices: [resolverService], + executorService: { + config: {}, + executors: {}, + loadExecutors: vi.fn().mockResolvedValue({}), + }, + } as unknown as Application; + + const ctx: PlanContext = { + client, + workspaceId, + application, + forRemoval: false, + config: { path: "/test/tailor.config.ts" } as LoadedConfig, + }; + + const result = await planPipeline(ctx); + + const resolverCreate = result.changeSet.resolver.creates[0]; + expect(resolverCreate).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const proto = (resolverCreate as any).request.pipelineResolver; + expect(proto.pipelines[0].invoker).toEqual({ + namespace: "external-auth", + machineUserName: "batch-user", + }); + }); + test("invoker is undefined when authInvoker is not set", async () => { const client = createMockClient([{ name: "test-ns", label: appName }]); diff --git a/packages/sdk/src/cli/commands/deploy/resolver.ts b/packages/sdk/src/cli/commands/deploy/resolver.ts index 05f56e071..fb9eb3e66 100644 --- a/packages/sdk/src/cli/commands/deploy/resolver.ts +++ b/packages/sdk/src/cli/commands/deploy/resolver.ts @@ -17,6 +17,7 @@ import { } from "@tailor-proto/tailor/v1/pipeline_resource_pb"; import * as inflection from "inflection"; import { type ResolverService } from "@/cli/services/resolver/service"; +import { getApplicationAuthNamespace } from "@/cli/shared/auth-namespace"; import { fetchAll, type OperatorClient } from "@/cli/shared/client"; import { buildResolverOperationHookExpr } from "@/cli/shared/runtime-exprs"; import { assertDefined } from "@/utils/assert"; @@ -137,7 +138,7 @@ export async function planPipeline(context: PlanContext) { executors, deletedServices, application.env, - application.authService?.config.name, + getApplicationAuthNamespace(application), forceApplyAll, ); diff --git a/packages/sdk/src/cli/commands/workflow/start.test.ts b/packages/sdk/src/cli/commands/workflow/start.test.ts index 8bb2bfd0c..88e1244e4 100644 --- a/packages/sdk/src/cli/commands/workflow/start.test.ts +++ b/packages/sdk/src/cli/commands/workflow/start.test.ts @@ -116,6 +116,38 @@ describe("startWorkflow runtime overload", () => { ); }); + test("typed shape falls back to external auth config name", async () => { + vi.mocked(loadConfig).mockResolvedValueOnce({ + config: { + name: "my-app", + auth: { name: "external-auth", external: true }, + }, + } as Awaited>); + getApplicationMock.mockResolvedValueOnce({ + application: {}, + }); + + await startWorkflow({ + workflow: { + name: "typed-workflow", + mainJob: { + body: () => undefined, + }, + }, + authInvoker: "typed-user", + }); + + expect(testStartWorkflowMock).toHaveBeenCalledWith( + expect.objectContaining({ + workflowId: "id:typed-workflow", + authInvoker: expect.objectContaining({ + namespace: "external-auth", + machineUserName: "typed-user", + }), + }), + ); + }); + test("start command with jsonMode emits only parseable JSON to stdout", async () => { using stdout = captureStdout(); using stderr = captureStderr(); diff --git a/packages/sdk/src/cli/commands/workflow/start.ts b/packages/sdk/src/cli/commands/workflow/start.ts index bd8bb687e..681ebc922 100644 --- a/packages/sdk/src/cli/commands/workflow/start.ts +++ b/packages/sdk/src/cli/commands/workflow/start.ts @@ -296,10 +296,11 @@ async function resolveApplicationAuthNamespace(options: { workspaceId: options.workspaceId, applicationName: config.name, }); - if (!application?.authNamespace) { + const authNamespace = application?.authNamespace || config.auth?.name; + if (!authNamespace) { throw new Error(`Application ${config.name} does not have an auth configuration.`); } - return application.authNamespace; + return authNamespace; } async function startWorkflowByName( diff --git a/packages/sdk/src/cli/services/application.ts b/packages/sdk/src/cli/services/application.ts index 35fbf8e07..1aef631a2 100644 --- a/packages/sdk/src/cli/services/application.ts +++ b/packages/sdk/src/cli/services/application.ts @@ -19,6 +19,7 @@ import { createTailorDBService, type TailorDBService } from "@/cli/services/tail import { assertUniqueLocalTailorDBTypeNames } from "@/cli/services/tailordb/type-name-validation"; import { bundleWorkflowJobs, type BundleWorkflowJobsResult } from "@/cli/services/workflow/bundler"; import { createWorkflowService, type WorkflowService } from "@/cli/services/workflow/service"; +import { getApplicationAuthNamespace } from "@/cli/shared/auth-namespace"; import { resolveBundleLogLevel } from "@/cli/shared/bundle-log-level"; import { type LoadedConfig } from "@/cli/shared/config-loader"; import { getDistDir } from "@/cli/shared/dist-dir"; @@ -488,7 +489,7 @@ export async function loadApplication( // 7. Build trigger context for workflow/job trigger transformation const triggerContext = await buildTriggerContext( config.workflow, - authResult.authService?.config.name, + getApplicationAuthNamespace({ authService: authResult.authService, config }), ); // 8. Resolve bundle settings diff --git a/packages/sdk/src/cli/shared/auth-namespace.test.ts b/packages/sdk/src/cli/shared/auth-namespace.test.ts new file mode 100644 index 000000000..b7f7edc07 --- /dev/null +++ b/packages/sdk/src/cli/shared/auth-namespace.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from "vitest"; +import { getApplicationAuthNamespace } from "./auth-namespace"; + +describe("getApplicationAuthNamespace", () => { + test("uses the local auth service name first", () => { + expect( + getApplicationAuthNamespace({ + authService: { config: { name: "local-auth" } }, + config: { auth: { name: "external-auth", external: true } }, + }), + ).toBe("local-auth"); + }); + + test("uses external auth config name when no local auth service exists", () => { + expect( + getApplicationAuthNamespace({ + config: { auth: { name: "external-auth", external: true } }, + }), + ).toBe("external-auth"); + }); + + test("returns undefined when no auth is configured", () => { + expect(getApplicationAuthNamespace({ config: {} })).toBeUndefined(); + }); +}); diff --git a/packages/sdk/src/cli/shared/auth-namespace.ts b/packages/sdk/src/cli/shared/auth-namespace.ts new file mode 100644 index 000000000..fcd7a39d7 --- /dev/null +++ b/packages/sdk/src/cli/shared/auth-namespace.ts @@ -0,0 +1,17 @@ +import type { AppConfig } from "@/types/app-config"; + +type AuthNamespaceApplication = { + authService?: { config: { name: string } }; + config?: Pick; +}; + +/** + * Resolve the auth namespace configured for an application. + * @param application - Loaded application with local or external Auth config + * @returns Auth namespace, or undefined when no Auth config is present + */ +export function getApplicationAuthNamespace( + application: AuthNamespaceApplication, +): string | undefined { + return application.authService?.config.name ?? application.config?.auth?.name; +} From 85d2e2bc6583a8f116980f945cc8de98270ac3d5 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 06:03:25 +0900 Subject: [PATCH 018/618] fix(create-sdk): update generated machine user registries --- packages/create-sdk/templates/executor/tailor.d.ts | 6 ++++++ packages/create-sdk/templates/generators/tailor.d.ts | 6 ++++++ packages/create-sdk/templates/hello-world/tailor.d.ts | 4 ++++ .../create-sdk/templates/inventory-management/tailor.d.ts | 7 +++++++ packages/create-sdk/templates/resolver/tailor.d.ts | 6 ++++++ packages/create-sdk/templates/static-web-site/tailor.d.ts | 6 ++++++ packages/create-sdk/templates/tailordb/tailor.d.ts | 7 +++++++ packages/create-sdk/templates/workflow/tailor.d.ts | 6 ++++++ 8 files changed, 48 insertions(+) diff --git a/packages/create-sdk/templates/executor/tailor.d.ts b/packages/create-sdk/templates/executor/tailor.d.ts index 9a473cac0..8052dbe2d 100644 --- a/packages/create-sdk/templates/executor/tailor.d.ts +++ b/packages/create-sdk/templates/executor/tailor.d.ts @@ -18,4 +18,10 @@ declare module "@tailor-platform/sdk" { } } +declare module "@tailor-platform/sdk/cli" { + interface MachineUserNameRegistry { + admin: true; + } +} + export {}; diff --git a/packages/create-sdk/templates/generators/tailor.d.ts b/packages/create-sdk/templates/generators/tailor.d.ts index 9a473cac0..8052dbe2d 100644 --- a/packages/create-sdk/templates/generators/tailor.d.ts +++ b/packages/create-sdk/templates/generators/tailor.d.ts @@ -18,4 +18,10 @@ declare module "@tailor-platform/sdk" { } } +declare module "@tailor-platform/sdk/cli" { + interface MachineUserNameRegistry { + admin: true; + } +} + export {}; diff --git a/packages/create-sdk/templates/hello-world/tailor.d.ts b/packages/create-sdk/templates/hello-world/tailor.d.ts index f618bd50c..fa0a41339 100644 --- a/packages/create-sdk/templates/hello-world/tailor.d.ts +++ b/packages/create-sdk/templates/hello-world/tailor.d.ts @@ -12,4 +12,8 @@ declare module "@tailor-platform/sdk" { interface IdpNameRegistry {} } +declare module "@tailor-platform/sdk/cli" { + interface MachineUserNameRegistry {} +} + export {}; diff --git a/packages/create-sdk/templates/inventory-management/tailor.d.ts b/packages/create-sdk/templates/inventory-management/tailor.d.ts index ee802f4a5..676f2ac82 100644 --- a/packages/create-sdk/templates/inventory-management/tailor.d.ts +++ b/packages/create-sdk/templates/inventory-management/tailor.d.ts @@ -17,4 +17,11 @@ declare module "@tailor-platform/sdk" { interface IdpNameRegistry {} } +declare module "@tailor-platform/sdk/cli" { + interface MachineUserNameRegistry { + manager: true; + staff: true; + } +} + export {}; diff --git a/packages/create-sdk/templates/resolver/tailor.d.ts b/packages/create-sdk/templates/resolver/tailor.d.ts index 67384d1c5..d4c7ca96a 100644 --- a/packages/create-sdk/templates/resolver/tailor.d.ts +++ b/packages/create-sdk/templates/resolver/tailor.d.ts @@ -19,4 +19,10 @@ declare module "@tailor-platform/sdk" { interface IdpNameRegistry {} } +declare module "@tailor-platform/sdk/cli" { + interface MachineUserNameRegistry { + admin: true; + } +} + export {}; diff --git a/packages/create-sdk/templates/static-web-site/tailor.d.ts b/packages/create-sdk/templates/static-web-site/tailor.d.ts index 080ae97b4..5470e9fb4 100644 --- a/packages/create-sdk/templates/static-web-site/tailor.d.ts +++ b/packages/create-sdk/templates/static-web-site/tailor.d.ts @@ -18,4 +18,10 @@ declare module "@tailor-platform/sdk" { } } +declare module "@tailor-platform/sdk/cli" { + interface MachineUserNameRegistry { + admin: true; + } +} + export {}; diff --git a/packages/create-sdk/templates/tailordb/tailor.d.ts b/packages/create-sdk/templates/tailordb/tailor.d.ts index fb3e4de56..ac0a43afc 100644 --- a/packages/create-sdk/templates/tailordb/tailor.d.ts +++ b/packages/create-sdk/templates/tailordb/tailor.d.ts @@ -17,4 +17,11 @@ declare module "@tailor-platform/sdk" { interface IdpNameRegistry {} } +declare module "@tailor-platform/sdk/cli" { + interface MachineUserNameRegistry { + admin: true; + viewer: true; + } +} + export {}; diff --git a/packages/create-sdk/templates/workflow/tailor.d.ts b/packages/create-sdk/templates/workflow/tailor.d.ts index 9016c307d..404b66593 100644 --- a/packages/create-sdk/templates/workflow/tailor.d.ts +++ b/packages/create-sdk/templates/workflow/tailor.d.ts @@ -16,4 +16,10 @@ declare module "@tailor-platform/sdk" { interface IdpNameRegistry {} } +declare module "@tailor-platform/sdk/cli" { + interface MachineUserNameRegistry { + admin: true; + } +} + export {}; From f0a92b4455ce47b55c5684e353d163139a9d3938 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 06:36:10 +0900 Subject: [PATCH 019/618] test(cli): update deploy command consumers --- packages/sdk/e2e/function-test-run.test.ts | 10 +++++----- packages/sdk/e2e/migration.test.ts | 20 +++++++++---------- .../deploy/config-id-ci-guard.test.ts | 2 +- .../cli/commands/deploy/config-id-injector.ts | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/sdk/e2e/function-test-run.test.ts b/packages/sdk/e2e/function-test-run.test.ts index 2ebd66785..8bc81eda1 100644 --- a/packages/sdk/e2e/function-test-run.test.ts +++ b/packages/sdk/e2e/function-test-run.test.ts @@ -5,7 +5,7 @@ * different function types on the Tailor Platform server via TestExecScript API. * * Uses internal APIs directly (detectFunctionType, bundleForTestRun, executeScript) - * instead of spawning CLI subprocesses. The `apply` step still uses subprocess + * instead of spawning CLI subprocesses. The deploy step still uses subprocess * since it orchestrates multiple services. * * Prerequisites: @@ -135,11 +135,11 @@ describe.sequential("E2E: function test-run", () => { console.log(`Workspace created: ${workspaceId}`); trackWorkspace(workspaceId); - // Apply example config to deploy DB schema, auth, machine users - console.log("Applying example config..."); + // Deploy example config to create DB schema, auth, machine users + console.log("Deploying example config..."); execFileSync( "node", - [cliPath, "apply", "--config", "tailor.config.ts", "--workspace-id", workspaceId, "--yes"], + [cliPath, "deploy", "--config", "tailor.config.ts", "--workspace-id", workspaceId, "--yes"], { cwd: exampleDir, stdio: ["ignore", "pipe", "pipe"], @@ -148,7 +148,7 @@ describe.sequential("E2E: function test-run", () => { timeout: 120000, }, ); - console.log("Apply completed."); + console.log("Deploy completed."); // Resolve machine user from API + config let machineUserId = "00000000-0000-0000-0000-000000000000"; diff --git a/packages/sdk/e2e/migration.test.ts b/packages/sdk/e2e/migration.test.ts index 170044233..6192c3e38 100644 --- a/packages/sdk/e2e/migration.test.ts +++ b/packages/sdk/e2e/migration.test.ts @@ -98,17 +98,17 @@ function runGenerateCli(configPath: string, cwd: string): void { } /** - * Run the apply CLI command via subprocess + * Run the deploy CLI command via subprocess * @param {string} configPath - Path to the config file * @param {string} workspaceId - Workspace ID * @param {string} cwd - Working directory */ -function runApplyCli(configPath: string, workspaceId: string, cwd: string): void { +function runDeployCli(configPath: string, workspaceId: string, cwd: string): void { const sdkRoot = path.resolve(__dirname, ".."); const cliPath = path.join(sdkRoot, "dist", "cli", "index.mjs"); try { - execSync(`node ${cliPath} apply --config ${configPath} --workspace-id ${workspaceId} --yes`, { + execSync(`node ${cliPath} deploy --config ${configPath} --workspace-id ${workspaceId} --yes`, { cwd, stdio: ["ignore", "pipe", "pipe"], // stdin ignored, stdout/stderr piped env: { @@ -116,13 +116,13 @@ function runApplyCli(configPath: string, workspaceId: string, cwd: string): void NODE_OPTIONS: "--experimental-vm-modules", }, encoding: "utf-8", - timeout: 120000, // 120 second timeout for apply operations + timeout: 120000, // 120 second timeout for deploy operations }); // Success - output captured but not logged to keep test output clean } catch (error: unknown) { // Log error details for debugging if (error && typeof error === "object" && "stderr" in error) { - console.error("Apply CLI error:", (error as { stderr?: Buffer }).stderr?.toString()); + console.error("Deploy CLI error:", (error as { stderr?: Buffer }).stderr?.toString()); } throw error; } @@ -364,7 +364,7 @@ describe.sequential("E2E: TailorDB Migrations", () => { test("applies initial migration to workspace", async () => { const configPath = createConfig(); - runApplyCli(configPath, workspaceId, tempDir); + runDeployCli(configPath, workspaceId, tempDir); // Verify: TailorDB service should exist const services = await listTailorDBServiceNames(); @@ -423,7 +423,7 @@ export type user = typeof user; test("applies non-breaking migration to workspace", async () => { const configPath = createConfig(); - runApplyCli(configPath, workspaceId, tempDir); + runDeployCli(configPath, workspaceId, tempDir); // Verify: phone field should be added to User type const fields = await getTailorDBTypeFields(tailordbName, "User"); @@ -487,7 +487,7 @@ export type user = typeof user; const configPath = createConfig(); - runApplyCli(configPath, workspaceId, tempDir); + runDeployCli(configPath, workspaceId, tempDir); // Verify: requiredField should be added to User type const fields = await getTailorDBTypeFields(tailordbName, "User"); @@ -551,7 +551,7 @@ export type user = typeof user; test("applies type addition to workspace", async () => { const configPath = createConfig(); - runApplyCli(configPath, workspaceId, tempDir); + runDeployCli(configPath, workspaceId, tempDir); // Verify: Post type should be added const types = await listTailorDBTypeNames(tailordbName); @@ -605,7 +605,7 @@ export type user = typeof user; test("applies field removal to workspace", async () => { const configPath = createConfig(); - runApplyCli(configPath, workspaceId, tempDir); + runDeployCli(configPath, workspaceId, tempDir); // Verify: requiredField should be removed from User type const fields = await getTailorDBTypeFields(tailordbName, "User"); diff --git a/packages/sdk/src/cli/commands/deploy/config-id-ci-guard.test.ts b/packages/sdk/src/cli/commands/deploy/config-id-ci-guard.test.ts index c0430c5ea..e8bb66123 100644 --- a/packages/sdk/src/cli/commands/deploy/config-id-ci-guard.test.ts +++ b/packages/sdk/src/cli/commands/deploy/config-id-ci-guard.test.ts @@ -46,7 +46,7 @@ describe("ensureConfigIdForDeploy", () => { const { ensureConfigIdForDeploy } = await load(true); await expect( ensureConfigIdForDeploy({ configPath: filePath, dryRun: false, buildOnly: false }), - ).rejects.toThrow(/missing an 'id'|setup github|apply/); + ).rejects.toThrow(/missing an 'id'|setup github|deploy/); // Must not have injected anything in CI. expect(await fs.promises.readFile(filePath, "utf-8")).toBe(configWithoutId); }); diff --git a/packages/sdk/src/cli/commands/deploy/config-id-injector.ts b/packages/sdk/src/cli/commands/deploy/config-id-injector.ts index 2a910027a..605dabe7f 100644 --- a/packages/sdk/src/cli/commands/deploy/config-id-injector.ts +++ b/packages/sdk/src/cli/commands/deploy/config-id-injector.ts @@ -169,7 +169,7 @@ async function assertConfigIdInCI(configPath: string): Promise { throw new Error( `tailor.config.ts is missing an 'id'. CI does not auto-generate one ` + `(each run would be treated as a separate app and break resource ownership). ` + - `Run 'tailor-sdk setup github' or 'tailor-sdk apply' locally and commit the injected id.`, + `Run 'tailor-sdk setup github' or 'tailor-sdk deploy' locally and commit the injected id.`, ); } // Keep CI and local behavior aligned: ensureConfigId() enforces the same From e4b825af0365042519f97e79e2331c84dfe509da Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 06:42:03 +0900 Subject: [PATCH 020/618] test(cli): use canonical machine user option --- packages/sdk/e2e/globalSetup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/e2e/globalSetup.ts b/packages/sdk/e2e/globalSetup.ts index 4dbcac566..ec92f1d06 100644 --- a/packages/sdk/e2e/globalSetup.ts +++ b/packages/sdk/e2e/globalSetup.ts @@ -8,7 +8,7 @@ import * as path from "node:path"; import { initOperatorClient, type OperatorClient } from "../src/cli/shared/client"; import { loadAccessToken } from "../src/cli/shared/context"; -// e2e must authenticate as the machine user from `tailor-sdk login --machineuser`, +// e2e must authenticate as the machine user from `tailor-sdk login --machine-user`, // never as the developer's locally configured profile. delete process.env.TAILOR_PLATFORM_PROFILE; From ac535a35b6b938abb7d833a1de5b300aa5eec199 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 06:48:25 +0900 Subject: [PATCH 021/618] ci: use canonical machine user option --- .github/workflows/cleanup-e2e-workspaces.yml | 2 +- .github/workflows/deploy.yml | 2 +- .github/workflows/migration.yml | 2 +- .github/workflows/sdk-e2e.yml | 4 ++-- .github/workflows/sdk-metrics.yml | 4 ++-- .github/workflows/test.yml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cleanup-e2e-workspaces.yml b/.github/workflows/cleanup-e2e-workspaces.yml index 31bb2acc7..a93300e42 100644 --- a/.github/workflows/cleanup-e2e-workspaces.yml +++ b/.github/workflows/cleanup-e2e-workspaces.yml @@ -32,7 +32,7 @@ jobs: uses: ./.github/actions/install-deps - name: Login as machine user - run: pnpm tailor-sdk login --machineuser + run: pnpm tailor-sdk login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 16bfe4ad5..33a3d80b2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -70,7 +70,7 @@ jobs: - name: Login as machine user if: runner.os != 'Windows' - run: pnpm tailor-sdk login --machineuser + run: pnpm tailor-sdk login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} diff --git a/.github/workflows/migration.yml b/.github/workflows/migration.yml index 7098dc5dc..030f11d1b 100644 --- a/.github/workflows/migration.yml +++ b/.github/workflows/migration.yml @@ -53,7 +53,7 @@ jobs: uses: ./.github/actions/install-deps - name: Login as machine user - run: pnpm tailor-sdk login --machineuser + run: pnpm tailor-sdk login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} diff --git a/.github/workflows/sdk-e2e.yml b/.github/workflows/sdk-e2e.yml index b26b11251..6b9fd19a9 100644 --- a/.github/workflows/sdk-e2e.yml +++ b/.github/workflows/sdk-e2e.yml @@ -52,7 +52,7 @@ jobs: uses: ./.github/actions/install-deps - name: Login as machine user - run: pnpm tailor-sdk login --machineuser + run: pnpm tailor-sdk login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} @@ -89,7 +89,7 @@ jobs: uses: ./.github/actions/install-deps - name: Login as machine user - run: pnpm tailor-sdk login --machineuser + run: pnpm tailor-sdk login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} diff --git a/.github/workflows/sdk-metrics.yml b/.github/workflows/sdk-metrics.yml index 41a4d17a0..ad8b337cc 100644 --- a/.github/workflows/sdk-metrics.yml +++ b/.github/workflows/sdk-metrics.yml @@ -40,7 +40,7 @@ jobs: uses: ./.github/actions/install-deps - name: Login as machine user - run: pnpm tailor-sdk login --machineuser + run: pnpm tailor-sdk login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} @@ -81,7 +81,7 @@ jobs: uses: ./.github/actions/install-deps - name: Login as machine user - run: pnpm tailor-sdk login --machineuser + run: pnpm tailor-sdk login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3e6726ecb..66982c1e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,7 +63,7 @@ jobs: node-version: ${{ matrix.node }} - name: Login as machine user - run: pnpm tailor-sdk login --machineuser + run: pnpm tailor-sdk login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} From 6b27e25fa35ee5152c35626e0216e20fcc101ed0 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 07:00:04 +0900 Subject: [PATCH 022/618] fix(cli): generate canonical setup github workflows --- .../commands/setup/github/branch.workflow.yml | 208 ++++++++++++++++-- .../cli/commands/setup/github/github.test.ts | 29 ++- .../cli/commands/setup/github/lock.test.ts | 2 +- .../commands/setup/github/tag.workflow.yml | 130 +++++++++-- .../cli/commands/setup/github/templates.ts | 24 +- 5 files changed, 350 insertions(+), 43 deletions(-) diff --git a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml index 709aa00dc..708cef6b0 100644 --- a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml @@ -52,17 +52,173 @@ jobs: echo "::error::Generated files are out of date. Run 'tailor-sdk generate' locally and commit the result." exit 1 fi - - id: tailor-plan + - id: tailor-mask-credentials # Fork PRs cannot read secrets; the checks above still run for them. if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork - uses: tailor-platform/actions/plan@4d0f160b6b5cc2f02594776665471497c297181e # v1.2.0 + run: | + echo "::add-mask::$CLIENT_ID" + echo "::add-mask::$CLIENT_SECRET" + env: + CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} + CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} + - id: tailor-login + if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork + # __WORKING_DIRECTORY__ + run: __PM_EXEC__ tailor-sdk login --machine-user + env: + TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} + TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} + - id: tailor-merge-base + if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork + env: + BASE_REF: ${{ github.base_ref }} + run: | + git fetch origin "$BASE_REF" + git merge --no-commit --no-ff "origin/$BASE_REF" || true + - id: tailor-plan + if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork + # __WORKING_DIRECTORY__ + run: | + if [ -z "$TAILOR_PLATFORM_WORKSPACE_ID" ]; then + echo "exit-code=" >> "$GITHUB_OUTPUT" + exit 0 + fi + + set +e + OUTPUT=$(__PM_EXEC__ tailor-sdk deploy --dry-run --yes 2>&1) + EXIT_CODE=$? + set -e + + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + { + echo "exit-code=$EXIT_CODE" + echo "output<<$EOF" + echo "$OUTPUT" + echo "$EOF" + } >> "$GITHUB_OUTPUT" + env: + TAILOR_PLATFORM_WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} + - id: tailor-plan-summary + if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork + run: | + if [ -n "$LABEL" ]; then + KEY="$LABEL" + elif [ -n "$WORKSPACE_ID" ]; then + KEY="$WORKSPACE_ID" + else + KEY="workspace" + fi + + if [ -z "$WORKSPACE_ID" ]; then + { + echo "## Tailor Platform Plan ($KEY)" + echo "" + echo "Workspace is not provisioned yet. Set TAILOR_PLATFORM_WORKSPACE_ID after provisioning the workspace." + } >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + if [ "$DRY_RUN_EXIT_CODE" = "0" ]; then + STATUS="PASS" + else + STATUS="FAIL" + fi + + { + echo "## $STATUS Tailor Platform Plan ($KEY)" + echo "" + echo "
" + echo "Plan output (exit code: $DRY_RUN_EXIT_CODE)" + echo "" + echo '```' + echo "$DRY_RUN_OUTPUT" + echo '```' + echo "" + echo "
" + } >> "$GITHUB_STEP_SUMMARY" + env: + WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} + LABEL: __WORKSPACE_NAME__ + DRY_RUN_OUTPUT: ${{ steps.tailor-plan.outputs.output }} + DRY_RUN_EXIT_CODE: ${{ steps.tailor-plan.outputs.exit-code }} + - id: tailor-plan-comment + if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} + LABEL: __WORKSPACE_NAME__ + DRY_RUN_OUTPUT: ${{ steps.tailor-plan.outputs.output }} + DRY_RUN_EXIT_CODE: ${{ steps.tailor-plan.outputs.exit-code }} with: - workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} - label: __WORKSPACE_NAME__ - # __WORKING_DIRECTORY__ - platform-client-id: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} - platform-client-secret: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const workspaceId = process.env.WORKSPACE_ID; + const label = process.env.LABEL; + const key = label || workspaceId || 'workspace'; + const marker = ``; + + let body; + if (!workspaceId) { + body = `${marker} + ## Tailor Platform Plan (${key}) + + Workspace is not provisioned yet. Set TAILOR_PLATFORM_WORKSPACE_ID after provisioning the workspace. + `; + } else { + const exitCode = process.env.DRY_RUN_EXIT_CODE; + let output = process.env.DRY_RUN_OUTPUT; + const MAX_OUTPUT = 60000; + if (output.length > MAX_OUTPUT) { + const note = `[output truncated to the last ${MAX_OUTPUT} characters - see the job's step summary for the full plan]\n\n`; + output = note + output.slice(output.length - MAX_OUTPUT); + } + const status = exitCode === '0' ? 'PASS' : 'FAIL'; + body = `${marker} + ## ${status} Tailor Platform Plan (${key}) + +
+ Plan output (exit code: ${exitCode}) + + \`\`\` + ${output} + \`\`\` + +
+ `; + } + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existingComment = comments.find(comment => comment.body.includes(marker)); + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + - id: tailor-plan-fail + if: >- + (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) && + vars.TAILOR_PLATFORM_WORKSPACE_ID != '' + run: | + if [ "$DRY_RUN_EXIT_CODE" != "0" ]; then + echo "::error::Plan dry-run failed with exit code $DRY_RUN_EXIT_CODE" + exit "$DRY_RUN_EXIT_CODE" + fi + env: + DRY_RUN_EXIT_CODE: ${{ steps.tailor-plan.outputs.exit-code }} # __PLAN_JOB_END__ tailor-deploy: # __DEPLOY_IF__ @@ -78,10 +234,34 @@ jobs: - id: tailor-checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # __SETUP_STEPS__ - - id: tailor-apply - uses: tailor-platform/actions/deploy@4d0f160b6b5cc2f02594776665471497c297181e # v1.2.0 - with: - workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} - # __WORKING_DIRECTORY__ - platform-client-id: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} - platform-client-secret: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} + - id: tailor-mask-credentials + run: | + echo "::add-mask::$CLIENT_ID" + echo "::add-mask::$CLIENT_SECRET" + env: + CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} + CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} + - id: tailor-login + # __WORKING_DIRECTORY__ + run: __PM_EXEC__ tailor-sdk login --machine-user + env: + TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} + TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} + - id: tailor-validate-workspace + env: + WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} + run: | + if [ -z "$WORKSPACE_ID" ]; then + echo "::error::Workspace is not provisioned: TAILOR_PLATFORM_WORKSPACE_ID is empty. Provision the workspace and set the variable." + exit 1 + fi + - id: tailor-generate + # __WORKING_DIRECTORY__ + run: __PM_EXEC__ tailor-sdk generate + env: + TAILOR_PLATFORM_WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} + - id: tailor-deploy + # __WORKING_DIRECTORY__ + run: __PM_EXEC__ tailor-sdk deploy --yes + env: + TAILOR_PLATFORM_WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} diff --git a/packages/sdk/src/cli/commands/setup/github/github.test.ts b/packages/sdk/src/cli/commands/setup/github/github.test.ts index 4b49000e6..3b6de51fc 100644 --- a/packages/sdk/src/cli/commands/setup/github/github.test.ts +++ b/packages/sdk/src/cli/commands/setup/github/github.test.ts @@ -68,12 +68,12 @@ describe("renderBranchWorkflow", () => { expect(content).toContain("name: Tailor (my-app)"); }); - test("pins actions with SHA + version comment, including the tailor actions", () => { + test("pins actions with SHA + version comment", () => { const { content } = renderBranchWorkflow(branchBase); expect(content).toMatch(/uses: actions\/checkout@[a-f0-9]+ # v\d+\.\d+\.\d+/); expect(content).toMatch(/uses: pnpm\/action-setup@[a-f0-9]+ # v\d+\.\d+\.\d+/); - expect(content).toMatch(/uses: tailor-platform\/actions\/plan@[0-9a-f]{40} # v\d+\.\d+\.\d+/); - expect(content).toMatch(/uses: tailor-platform\/actions\/deploy@[0-9a-f]{40} # v\d+\.\d+\.\d+/); + expect(content).toMatch(/uses: actions\/github-script@[a-f0-9]+ # v\d+/); + expect(content).not.toContain("tailor-platform/actions/"); }); test("pins every `uses:` to a full commit SHA (no moving tags/branches)", () => { @@ -99,12 +99,14 @@ describe("renderBranchWorkflow", () => { test("uses the unified secret names and targets the workspace-id variable", () => { const { content } = renderBranchWorkflow(branchBase); expect(content).toContain( - "platform-client-id: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }}", + "TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }}", ); expect(content).toContain( - "platform-client-secret: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }}", + "TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }}", + ); + expect(content).toContain( + "TAILOR_PLATFORM_WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}", ); - expect(content).toContain("workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}"); expect(content).not.toContain("secrets.PLATFORM_MACHINE_USER_CLIENT_ID"); }); @@ -123,7 +125,7 @@ describe("renderBranchWorkflow", () => { test("passes the workspace name as the plan label", () => { const { content } = renderBranchWorkflow(branchBase); - expect(content).toContain("label: my-app"); + expect(content).toContain("LABEL: my-app"); }); test("references the dry-run dispatch input with bracket notation", () => { @@ -147,11 +149,15 @@ describe("renderBranchWorkflow", () => { expect(content).toContain("cancel-in-progress: false"); }); - test("includes generate + generate-check in plan only (deploy delegates)", () => { + test("runs generate checks in plan and deploys with the canonical command", () => { const { content } = renderBranchWorkflow(branchBase); const parsed = parseYAML(content) as { jobs: Record }; expect(Object.keys(parsed.jobs)).toEqual(["tailor-plan", "tailor-deploy"]); + expect(content.match(/^ {6}- id: tailor-generate$/gm)).toHaveLength(2); expect(content.match(/id: tailor-generate-check/g)).toHaveLength(1); + expect(content).toContain("pnpm exec tailor-sdk deploy --dry-run --yes"); + expect(content).toContain("pnpm exec tailor-sdk deploy --yes"); + expect(content).not.toContain("tailor-sdk apply"); }); test("emits PM exec prefix for generate", () => { @@ -191,8 +197,8 @@ describe("renderBranchWorkflow", () => { const scoped = renderBranchWorkflow({ ...branchBase, workingDirectory: "apps/foo" }).content; expect(scoped).not.toMatch(NO_MARKER); expect(scoped).toContain('paths: ["apps/foo/**"]'); - // generate step + plan action + deploy action - expect(scoped.match(/working-directory: apps\/foo/g)).toHaveLength(3); + // plan generate/login/dry-run and deploy login/generate/deploy + expect(scoped.match(/working-directory: apps\/foo/g)).toHaveLength(6); expect(() => parseYAML(scoped)).not.toThrow(); }); @@ -215,7 +221,8 @@ describe("renderBranchWorkflow", () => { const { generatedIds } = renderBranchWorkflow(branchBase); expect(generatedIds).toContain("tailor-plan"); expect(generatedIds).toContain("tailor-plan/tailor-setup-pnpm"); - expect(generatedIds).toContain("tailor-deploy/tailor-apply"); + expect(generatedIds).toContain("tailor-plan/tailor-plan"); + expect(generatedIds).toContain("tailor-deploy/tailor-deploy"); }); test("preserves $ characters in values", () => { diff --git a/packages/sdk/src/cli/commands/setup/github/lock.test.ts b/packages/sdk/src/cli/commands/setup/github/lock.test.ts index 3958817a8..95807619d 100644 --- a/packages/sdk/src/cli/commands/setup/github/lock.test.ts +++ b/packages/sdk/src/cli/commands/setup/github/lock.test.ts @@ -20,7 +20,7 @@ function makeLock(): LockFile { packageManager: "pnpm", plan: true, }, - generatedIds: ["tailor-deploy", "tailor-deploy/tailor-apply"], + generatedIds: ["tailor-deploy", "tailor-deploy/tailor-deploy"], ejectedIds: [], contentHash: hashContent("hello"), }, diff --git a/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml b/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml index f6cf84d6c..1b1b65ae3 100644 --- a/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml @@ -64,14 +64,92 @@ jobs: echo "::error::Generated files are out of date. Run 'tailor-sdk generate' locally and commit the result." exit 1 fi + - id: tailor-mask-credentials + run: | + echo "::add-mask::$CLIENT_ID" + echo "::add-mask::$CLIENT_SECRET" + env: + CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} + CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} + - id: tailor-login + # __WORKING_DIRECTORY__ + run: __PM_EXEC__ tailor-sdk login --machine-user + env: + TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} + TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} - id: tailor-plan - uses: tailor-platform/actions/plan@4d0f160b6b5cc2f02594776665471497c297181e # v1.2.0 - with: - workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} - label: __WORKSPACE_NAME__ - # __WORKING_DIRECTORY__ - platform-client-id: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} - platform-client-secret: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} + # __WORKING_DIRECTORY__ + run: | + if [ -z "$TAILOR_PLATFORM_WORKSPACE_ID" ]; then + echo "exit-code=" >> "$GITHUB_OUTPUT" + exit 0 + fi + + set +e + OUTPUT=$(__PM_EXEC__ tailor-sdk deploy --dry-run --yes 2>&1) + EXIT_CODE=$? + set -e + + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + { + echo "exit-code=$EXIT_CODE" + echo "output<<$EOF" + echo "$OUTPUT" + echo "$EOF" + } >> "$GITHUB_OUTPUT" + env: + TAILOR_PLATFORM_WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} + - id: tailor-plan-summary + run: | + if [ -n "$LABEL" ]; then + KEY="$LABEL" + elif [ -n "$WORKSPACE_ID" ]; then + KEY="$WORKSPACE_ID" + else + KEY="workspace" + fi + + if [ -z "$WORKSPACE_ID" ]; then + { + echo "## Tailor Platform Plan ($KEY)" + echo "" + echo "Workspace is not provisioned yet. Set TAILOR_PLATFORM_WORKSPACE_ID after provisioning the workspace." + } >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + if [ "$DRY_RUN_EXIT_CODE" = "0" ]; then + STATUS="PASS" + else + STATUS="FAIL" + fi + + { + echo "## $STATUS Tailor Platform Plan ($KEY)" + echo "" + echo "
" + echo "Plan output (exit code: $DRY_RUN_EXIT_CODE)" + echo "" + echo '```' + echo "$DRY_RUN_OUTPUT" + echo '```' + echo "" + echo "
" + } >> "$GITHUB_STEP_SUMMARY" + env: + WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} + LABEL: __WORKSPACE_NAME__ + DRY_RUN_OUTPUT: ${{ steps.tailor-plan.outputs.output }} + DRY_RUN_EXIT_CODE: ${{ steps.tailor-plan.outputs.exit-code }} + - id: tailor-plan-fail + if: vars.TAILOR_PLATFORM_WORKSPACE_ID != '' + run: | + if [ "$DRY_RUN_EXIT_CODE" != "0" ]; then + echo "::error::Plan dry-run failed with exit code $DRY_RUN_EXIT_CODE" + exit "$DRY_RUN_EXIT_CODE" + fi + env: + DRY_RUN_EXIT_CODE: ${{ steps.tailor-plan.outputs.exit-code }} tailor-deploy: needs: tailor-plan @@ -88,10 +166,34 @@ jobs: - id: tailor-checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # __SETUP_STEPS__ - - id: tailor-apply - uses: tailor-platform/actions/deploy@4d0f160b6b5cc2f02594776665471497c297181e # v1.2.0 - with: - workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} - # __WORKING_DIRECTORY__ - platform-client-id: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} - platform-client-secret: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} + - id: tailor-mask-credentials + run: | + echo "::add-mask::$CLIENT_ID" + echo "::add-mask::$CLIENT_SECRET" + env: + CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} + CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} + - id: tailor-login + # __WORKING_DIRECTORY__ + run: __PM_EXEC__ tailor-sdk login --machine-user + env: + TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} + TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} + - id: tailor-validate-workspace + env: + WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} + run: | + if [ -z "$WORKSPACE_ID" ]; then + echo "::error::Workspace is not provisioned: TAILOR_PLATFORM_WORKSPACE_ID is empty. Provision the workspace and set the variable." + exit 1 + fi + - id: tailor-generate + # __WORKING_DIRECTORY__ + run: __PM_EXEC__ tailor-sdk generate + env: + TAILOR_PLATFORM_WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} + - id: tailor-deploy + # __WORKING_DIRECTORY__ + run: __PM_EXEC__ tailor-sdk deploy --yes + env: + TAILOR_PLATFORM_WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} diff --git a/packages/sdk/src/cli/commands/setup/github/templates.ts b/packages/sdk/src/cli/commands/setup/github/templates.ts index 327c828ee..90d0f098b 100644 --- a/packages/sdk/src/cli/commands/setup/github/templates.ts +++ b/packages/sdk/src/cli/commands/setup/github/templates.ts @@ -8,7 +8,7 @@ import setupYarn from "./setup-yarn.yml"; import tagTemplate from "./tag.workflow.yml"; /** Template schema version, tracked per target in the lock file. */ -export const TEMPLATE_VERSION = 1; +export const TEMPLATE_VERSION = 2; export type PackageManager = "pnpm" | "yarn" | "npm" | "bun"; @@ -194,14 +194,24 @@ export function renderBranchWorkflow(params: RenderBranchParams): RenderResult { ...setupIds("tailor-plan", packageManager), "tailor-plan/tailor-generate", "tailor-plan/tailor-generate-check", + "tailor-plan/tailor-mask-credentials", + "tailor-plan/tailor-login", + "tailor-plan/tailor-merge-base", "tailor-plan/tailor-plan", + "tailor-plan/tailor-plan-summary", + "tailor-plan/tailor-plan-comment", + "tailor-plan/tailor-plan-fail", ); } generatedIds.push( "tailor-deploy", "tailor-deploy/tailor-checkout", ...setupIds("tailor-deploy", packageManager), - "tailor-deploy/tailor-apply", + "tailor-deploy/tailor-mask-credentials", + "tailor-deploy/tailor-login", + "tailor-deploy/tailor-validate-workspace", + "tailor-deploy/tailor-generate", + "tailor-deploy/tailor-deploy", ); return { content: out, generatedIds }; @@ -249,11 +259,19 @@ export function renderTagWorkflow(params: RenderTagParams): RenderResult { ...setupIds("tailor-plan", packageManager), "tailor-plan/tailor-generate", "tailor-plan/tailor-generate-check", + "tailor-plan/tailor-mask-credentials", + "tailor-plan/tailor-login", "tailor-plan/tailor-plan", + "tailor-plan/tailor-plan-summary", + "tailor-plan/tailor-plan-fail", "tailor-deploy", "tailor-deploy/tailor-checkout", ...setupIds("tailor-deploy", packageManager), - "tailor-deploy/tailor-apply", + "tailor-deploy/tailor-mask-credentials", + "tailor-deploy/tailor-login", + "tailor-deploy/tailor-validate-workspace", + "tailor-deploy/tailor-generate", + "tailor-deploy/tailor-deploy", ); return { content: out, generatedIds }; From 8af00b5c59167a8689c08122e58bf805ccc4bff8 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 07:09:24 +0900 Subject: [PATCH 023/618] fix(cli): cap setup github plan output --- .../sdk/src/cli/commands/setup/github/branch.workflow.yml | 6 ++++++ packages/sdk/src/cli/commands/setup/github/tag.workflow.yml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml index 708cef6b0..d2512f6f8 100644 --- a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml @@ -89,6 +89,12 @@ jobs: EXIT_CODE=$? set -e + MAX_OUTPUT=60000 + if [ "${#OUTPUT}" -gt "$MAX_OUTPUT" ]; then + OUTPUT_TAIL="${OUTPUT: -$MAX_OUTPUT}" + OUTPUT=$(printf '%s\n\n%s' "[output truncated to the last $MAX_OUTPUT characters - see the workflow logs for the full plan]" "$OUTPUT_TAIL") + fi + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) { echo "exit-code=$EXIT_CODE" diff --git a/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml b/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml index 1b1b65ae3..75486a631 100644 --- a/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml @@ -90,6 +90,12 @@ jobs: EXIT_CODE=$? set -e + MAX_OUTPUT=60000 + if [ "${#OUTPUT}" -gt "$MAX_OUTPUT" ]; then + OUTPUT_TAIL="${OUTPUT: -$MAX_OUTPUT}" + OUTPUT=$(printf '%s\n\n%s' "[output truncated to the last $MAX_OUTPUT characters - see the workflow logs for the full plan]" "$OUTPUT_TAIL") + fi + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) { echo "exit-code=$EXIT_CODE" From 19be90d2cebee8beae601848a0348c9f71d7b23b Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 07:25:29 +0900 Subject: [PATCH 024/618] fix(cli): update setup github contracts --- docs/setup-github-contracts.md | 123 ++++++++++-------- .../commands/setup/github/branch.workflow.yml | 5 +- .../cli/commands/setup/github/github.test.ts | 6 + 3 files changed, 77 insertions(+), 57 deletions(-) diff --git a/docs/setup-github-contracts.md b/docs/setup-github-contracts.md index 3b66bf579..b41fc49d6 100644 --- a/docs/setup-github-contracts.md +++ b/docs/setup-github-contracts.md @@ -1,7 +1,7 @@ # `setup github` Contracts This document specifies the **beta graduation contracts** for `tailor-sdk setup -github` and the `tailor-platform/actions` that the generated workflows reference. +github` and the generated GitHub Actions workflows. > **Graduation criterion:** Beta is not finished when implementation is > complete — it is finished when all 13 contracts below are confirmed, specified @@ -33,28 +33,38 @@ status checks**. | ------------------ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tailor-tag-guard` | tag (with `--branch`) | Checks that the tagged commit is reachable from the configured branch. Output: `on-branch`. | | `tailor-plan` | branch, tag | Runs `generate`, `generate-check`, and the plan dry-run. On a branch target it posts a PR comment; on a tag target it writes the step summary only (no PR). | -| `tailor-deploy` | branch, tag | Runs the deploy (`tailor-sdk apply`). | +| `tailor-deploy` | branch, tag | Runs the deploy (`tailor-sdk deploy`). | #### Steps generated in P0 -| Qualified id (`/`) | Description | -| ----------------------------------- | --------------------------------------------------------------- | -| `tailor-tag-guard/tailor-checkout` | `actions/checkout` with `fetch-depth: 0` | -| `tailor-tag-guard/tailor-tag-guard` | Branch-reachability shell script | -| `tailor-plan/tailor-checkout` | `actions/checkout` | -| `tailor-plan/tailor-setup-pnpm` | `pnpm/action-setup` (pnpm projects only) | -| `tailor-plan/tailor-setup-node` | `actions/setup-node` | -| `tailor-plan/tailor-setup-bun` | `oven-sh/setup-bun` (bun projects only) | -| `tailor-plan/tailor-install` | Package install (`pnpm install --frozen-lockfile` / equivalent) | -| `tailor-plan/tailor-generate` | `tailor-sdk generate` | -| `tailor-plan/tailor-generate-check` | Checks generated files are committed | -| `tailor-plan/tailor-plan` | `tailor-platform/actions/plan` (SHA-pinned) | -| `tailor-deploy/tailor-checkout` | `actions/checkout` | -| `tailor-deploy/tailor-setup-pnpm` | pnpm projects only | -| `tailor-deploy/tailor-setup-node` | node projects | -| `tailor-deploy/tailor-setup-bun` | bun projects only | -| `tailor-deploy/tailor-install` | Package install | -| `tailor-deploy/tailor-apply` | `tailor-platform/actions/deploy` (SHA-pinned) | +| Qualified id (`/`) | Description | +| ----------------------------------------- | --------------------------------------------------------------- | +| `tailor-tag-guard/tailor-checkout` | `actions/checkout` with `fetch-depth: 0` | +| `tailor-tag-guard/tailor-tag-guard` | Branch-reachability shell script | +| `tailor-plan/tailor-checkout` | `actions/checkout` | +| `tailor-plan/tailor-setup-pnpm` | `pnpm/action-setup` (pnpm projects only) | +| `tailor-plan/tailor-setup-node` | `actions/setup-node` | +| `tailor-plan/tailor-setup-bun` | `oven-sh/setup-bun` (bun projects only) | +| `tailor-plan/tailor-install` | Package install (`pnpm install --frozen-lockfile` / equivalent) | +| `tailor-plan/tailor-generate` | `tailor-sdk generate` | +| `tailor-plan/tailor-generate-check` | Checks generated files are committed | +| `tailor-plan/tailor-mask-credentials` | Masks machine-user credentials | +| `tailor-plan/tailor-login` | `tailor-sdk login --machine-user` | +| `tailor-plan/tailor-merge-base` | Merges the PR base branch before branch-target dry-runs | +| `tailor-plan/tailor-plan` | `tailor-sdk deploy --dry-run --yes` | +| `tailor-plan/tailor-plan-summary` | Writes the plan result to the step summary | +| `tailor-plan/tailor-plan-comment` | Updates the PR plan comment on branch targets | +| `tailor-plan/tailor-plan-fail` | Fails the job when the dry-run exits non-zero | +| `tailor-deploy/tailor-checkout` | `actions/checkout` | +| `tailor-deploy/tailor-setup-pnpm` | pnpm projects only | +| `tailor-deploy/tailor-setup-node` | node projects | +| `tailor-deploy/tailor-setup-bun` | bun projects only | +| `tailor-deploy/tailor-install` | Package install | +| `tailor-deploy/tailor-mask-credentials` | Masks machine-user credentials | +| `tailor-deploy/tailor-login` | `tailor-sdk login --machine-user` | +| `tailor-deploy/tailor-validate-workspace` | Fails when `TAILOR_PLATFORM_WORKSPACE_ID` is empty | +| `tailor-deploy/tailor-generate` | `tailor-sdk generate` | +| `tailor-deploy/tailor-deploy` | `tailor-sdk deploy --yes` | #### Public outputs (P0 implemented) @@ -66,18 +76,18 @@ status checks**. The following ids and outputs are reserved and must not be used by user code: -| Future id / output | Planned role | -| ------------------------------------ | -------------------------------------------------------------------- | -| `tailor-drift-check` (step) | Warns when config has drifted from generated workflow | -| `tailor-seed-validate` (step) | Validates seed JSONL against schema | -| `tailor-staticwebsite-deploy` (step) | Deploys static website assets | -| `build-site-` (step) | User-owned slot for building a static site named `` | -| `seed-data` (step) | User-owned slot for seeding data (preview target) | -| `tailor-preview-comment` (step) | Posts workspace URL to PR | -| `tailor-preview-deploy` (job) | Deploys the per-PR preview workspace | -| `tailor-preview-cleanup` (job) | Deletes ephemeral preview workspace on PR close | -| `steps.tailor-apply.outputs.app-url` | Application URL after deploy (wired into `build-site-` inputs) | -| `TAILOR_SITE_DIST_` (env) | Path to built static site dist registered by `build-site-` | +| Future id / output | Planned role | +| ------------------------------------- | -------------------------------------------------------------------- | +| `tailor-drift-check` (step) | Warns when config has drifted from generated workflow | +| `tailor-seed-validate` (step) | Validates seed JSONL against schema | +| `tailor-staticwebsite-deploy` (step) | Deploys static website assets | +| `build-site-` (step) | User-owned slot for building a static site named `` | +| `seed-data` (step) | User-owned slot for seeding data (preview target) | +| `tailor-preview-comment` (step) | Posts workspace URL to PR | +| `tailor-preview-deploy` (job) | Deploys the per-PR preview workspace | +| `tailor-preview-cleanup` (job) | Deletes ephemeral preview workspace on PR close | +| `steps.tailor-deploy.outputs.app-url` | Application URL after deploy (wired into `build-site-` inputs) | +| `TAILOR_SITE_DIST_` (env) | Path to built static site dist registered by `build-site-` | Slot ids (`build-site-`, `seed-data`) are **user-owned in content** but **SDK-owned in name and position**. The SDK reserves the namespace; user code @@ -102,7 +112,7 @@ The lock file is JSON, 2-space indented, with a trailing newline. It is "kind": "branch", // "branch" | "tag" "workspaceName": "my-app-stg", "file": ".github/workflows/tailor-my-app-stg.yml", // repo-root-relative, POSIX separators - "templateVersion": 1, // internal constant TEMPLATE_VERSION + "templateVersion": 2, // internal constant TEMPLATE_VERSION "inputs": { "branch": "main", // null for tag target with no --branch "tagPattern": null, // non-null for tag target only @@ -120,7 +130,13 @@ The lock file is JSON, 2-space indented, with a trailing newline. It is "tailor-plan/tailor-install", "tailor-plan/tailor-generate", "tailor-plan/tailor-generate-check", + "tailor-plan/tailor-mask-credentials", + "tailor-plan/tailor-login", + "tailor-plan/tailor-merge-base", "tailor-plan/tailor-plan", + "tailor-plan/tailor-plan-summary", + "tailor-plan/tailor-plan-comment", + "tailor-plan/tailor-plan-fail", "tailor-deploy", "tailor-deploy/tailor-checkout", "...", @@ -220,27 +236,26 @@ can handle renaming and multi-trigger coexistence is a P2 enhancement. ### Principle -- **Workflow = configuration.** The generated workflow owns triggers, job - topology (`needs`, `if`, `concurrency`, `environment`, `permissions`), and - `with:` parameter wiring. -- **Actions = behaviour.** The execution logic of managed steps lives in - `tailor-platform/actions`, not in the generated workflow. This limits how - often users need to regenerate — only when configuration changes, not when - behaviour improves. +- **Workflow = contract.** The generated workflow owns triggers, job topology + (`needs`, `if`, `concurrency`, `environment`, `permissions`), secret/variable + wiring, and the managed CLI steps. +- **Actions = optional future extraction.** Behaviour can move to + `tailor-platform/actions` in a later release, but P0 generated workflows do + not depend on Tailor-owned composite actions. ### P0 state In P0, the setup steps (`tailor-setup-node`, `tailor-setup-pnpm`, -`tailor-setup-bun`, `tailor-install`) and the `tailor-generate` / -`tailor-generate-check` steps are inlined in the generated workflow. The -`tailor-plan` and `tailor-apply` steps already delegate to composite actions -(`tailor-platform/actions/plan` and `deploy`, SHA-pinned). +`tailor-setup-bun`, `tailor-install`), `tailor-generate` / +`tailor-generate-check`, plan dry-run/commenting, and deploy steps are inlined +in the generated workflow. They use the canonical CLI surface: +`tailor-sdk login --machine-user` and `tailor-sdk deploy`. ### P1 target state -All managed steps will be extracted into `tailor-platform/actions`. The -generated workflow will contain only a single composite-action call per managed -function. Users will receive behaviour improvements (e.g., new drift checks) +Managed steps may be extracted into `tailor-platform/actions`. If that happens, +the generated workflow will contain a composite-action call per managed +function. Users would receive behaviour improvements (e.g., new drift checks) via Renovate pin updates without needing to regenerate their workflows. ### No reusable workflows @@ -438,7 +453,7 @@ in `deploy`/`plan`, a multi-match guard is **not** needed there.) **Terraform ownership boundary.** Terraform manages only the `tailor_workspace` shell; the in-workspace resources (TailorDB types, auth, executors, …) are -owned by the SDK (`tailor-sdk apply`). The same resource must not be managed by +owned by the SDK (`tailor-sdk deploy`). The same resource must not be managed by both. **Caveat (B2 GitHub App).** The App needs the `Environments: write` permission, @@ -501,7 +516,7 @@ Rules that are fixed now so the namespace is stable: - The site `` is normalized to a step id-safe string (`[a-z0-9-]+`). Normalization collisions are an error at setup time. - The interface contract for `build-site-`: - - Input: `api-url` (the application GraphQL URL from `tailor-apply` outputs) + - Input: `api-url` (the application GraphQL URL from `tailor-deploy` outputs) - Output: registers `TAILOR_SITE_DIST_` in `$GITHUB_ENV` where `` is the upper-cased, non-alphanumeric-replaced site name (e.g., site `admin-portal` → `TAILOR_SITE_DIST_ADMIN_PORTAL`). @@ -541,16 +556,14 @@ Not generated in P0. Specified here so the workspace naming scheme is stable. | Adding an optional input to an existing action | Minor (v1.x) | | Removing an input or changing behaviour in a breaking way | Major (v2) | -Templates generated by `setup github` pin **all** actions — including the -tailor-platform ones — to a **full commit SHA with a version comment** (e.g. -`tailor-platform/actions/plan@ # v1.2.0`), exactly like every third-party +Templates generated by `setup github` pin **all** referenced actions to a +**full commit SHA with a version comment**, exactly like every third-party action in the template. Moving tags (`@v1`) are **not** used: a committed workflow must be reproducible and must pass the `unpinned-uses` / pin checks of zizmor and ghalint. A generated-workflow check fails if any `uses:` is not -SHA-pinned. Updates flow through Renovate (which bumps the SHA + comment), -the same path as the other actions — not by silently advancing a tag. The +SHA-pinned. If Tailor-managed composite actions are introduced later, their embedded SHA must correspond to a released `tailor-platform/actions` version -and is set/finalized as part of the coordinated release. +and be set/finalized as part of the coordinated release. ### SDK ↔ actions compatibility check (P1) diff --git a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml index d2512f6f8..45e57d588 100644 --- a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml @@ -193,12 +193,13 @@ jobs: `; } - const { data: comments } = await github.rest.issues.listComments({ + const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, + per_page: 100, }); - const existingComment = comments.find(comment => comment.body.includes(marker)); + const existingComment = comments.find(comment => comment.body?.includes(marker)); if (existingComment) { await github.rest.issues.updateComment({ owner: context.repo.owner, diff --git a/packages/sdk/src/cli/commands/setup/github/github.test.ts b/packages/sdk/src/cli/commands/setup/github/github.test.ts index 3b6de51fc..ba4d6c0a7 100644 --- a/packages/sdk/src/cli/commands/setup/github/github.test.ts +++ b/packages/sdk/src/cli/commands/setup/github/github.test.ts @@ -128,6 +128,12 @@ describe("renderBranchWorkflow", () => { expect(content).toContain("LABEL: my-app"); }); + test("paginates plan comment lookup", () => { + const { content } = renderBranchWorkflow(branchBase); + expect(content).toContain("github.paginate(github.rest.issues.listComments"); + expect(content).toContain("per_page: 100"); + }); + test("references the dry-run dispatch input with bracket notation", () => { // `inputs.dry-run` is ambiguous to readers/tools; the bracket form is // unambiguous for a hyphenated input name. From 8e47267ae36fe444ffa6b5f769632b8e044510fb Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 07:40:35 +0900 Subject: [PATCH 025/618] fix(cli): preserve full setup github plan logs --- .../cli/commands/setup/github/branch.workflow.yml | 6 ++++++ .../src/cli/commands/setup/github/github.test.ts | 13 +++++++++++++ .../src/cli/commands/setup/github/tag.workflow.yml | 6 ++++++ 3 files changed, 25 insertions(+) diff --git a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml index 45e57d588..5a6995113 100644 --- a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml @@ -91,6 +91,12 @@ jobs: MAX_OUTPUT=60000 if [ "${#OUTPUT}" -gt "$MAX_OUTPUT" ]; then + LOG_STOP_TOKEN=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + echo "::group::Full Tailor Platform plan output" + echo "::stop-commands::$LOG_STOP_TOKEN" + printf '%s\n' "$OUTPUT" + echo "::$LOG_STOP_TOKEN::" + echo "::endgroup::" OUTPUT_TAIL="${OUTPUT: -$MAX_OUTPUT}" OUTPUT=$(printf '%s\n\n%s' "[output truncated to the last $MAX_OUTPUT characters - see the workflow logs for the full plan]" "$OUTPUT_TAIL") fi diff --git a/packages/sdk/src/cli/commands/setup/github/github.test.ts b/packages/sdk/src/cli/commands/setup/github/github.test.ts index ba4d6c0a7..779da2787 100644 --- a/packages/sdk/src/cli/commands/setup/github/github.test.ts +++ b/packages/sdk/src/cli/commands/setup/github/github.test.ts @@ -134,6 +134,19 @@ describe("renderBranchWorkflow", () => { expect(content).toContain("per_page: 100"); }); + test("logs full plan output before truncating comments and outputs", () => { + const branch = renderBranchWorkflow(branchBase).content; + const tag = renderTagWorkflow(tagBase).content; + for (const content of [branch, tag]) { + expect(content).toContain("::group::Full Tailor Platform plan output"); + expect(content).toContain("::stop-commands::$LOG_STOP_TOKEN"); + expect(content).toContain("printf '%s\\n' \"$OUTPUT\""); + expect(content).toContain( + "[output truncated to the last $MAX_OUTPUT characters - see the workflow logs for the full plan]", + ); + } + }); + test("references the dry-run dispatch input with bracket notation", () => { // `inputs.dry-run` is ambiguous to readers/tools; the bracket form is // unambiguous for a hyphenated input name. diff --git a/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml b/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml index 75486a631..9add5ff13 100644 --- a/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml @@ -92,6 +92,12 @@ jobs: MAX_OUTPUT=60000 if [ "${#OUTPUT}" -gt "$MAX_OUTPUT" ]; then + LOG_STOP_TOKEN=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + echo "::group::Full Tailor Platform plan output" + echo "::stop-commands::$LOG_STOP_TOKEN" + printf '%s\n' "$OUTPUT" + echo "::$LOG_STOP_TOKEN::" + echo "::endgroup::" OUTPUT_TAIL="${OUTPUT: -$MAX_OUTPUT}" OUTPUT=$(printf '%s\n\n%s' "[output truncated to the last $MAX_OUTPUT characters - see the workflow logs for the full plan]" "$OUTPUT_TAIL") fi From 30d866094d7952cbbd10e5f946d244d447aa3f43 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 07:53:16 +0900 Subject: [PATCH 026/618] fix(cli): fetch setup github pull request base --- .../sdk/src/cli/commands/setup/github/branch.workflow.yml | 2 +- packages/sdk/src/cli/commands/setup/github/github.test.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml index 5a6995113..db955801b 100644 --- a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml @@ -73,7 +73,7 @@ jobs: env: BASE_REF: ${{ github.base_ref }} run: | - git fetch origin "$BASE_REF" + git fetch origin "$BASE_REF:refs/remotes/origin/$BASE_REF" git merge --no-commit --no-ff "origin/$BASE_REF" || true - id: tailor-plan if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork diff --git a/packages/sdk/src/cli/commands/setup/github/github.test.ts b/packages/sdk/src/cli/commands/setup/github/github.test.ts index 779da2787..49a1ccc4d 100644 --- a/packages/sdk/src/cli/commands/setup/github/github.test.ts +++ b/packages/sdk/src/cli/commands/setup/github/github.test.ts @@ -128,6 +128,12 @@ describe("renderBranchWorkflow", () => { expect(content).toContain("LABEL: my-app"); }); + test("fetches the pull request base into the remote-tracking ref before merge", () => { + const { content } = renderBranchWorkflow(branchBase); + expect(content).toContain('git fetch origin "$BASE_REF:refs/remotes/origin/$BASE_REF"'); + expect(content).toContain('git merge --no-commit --no-ff "origin/$BASE_REF" || true'); + }); + test("paginates plan comment lookup", () => { const { content } = renderBranchWorkflow(branchBase); expect(content).toContain("github.paginate(github.rest.issues.listComments"); From c49d3505dcbe283ee06b21bcad60e5ed13fccc0c Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 08:08:10 +0900 Subject: [PATCH 027/618] fix(cli): dedent setup github plan comments --- packages/sdk/docs/services/resolver.md | 2 +- packages/sdk/docs/services/tailordb.md | 2 +- .../commands/setup/github/branch.workflow.yml | 38 ++++++++++--------- .../cli/commands/setup/github/github.test.ts | 8 ++++ 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/sdk/docs/services/resolver.md b/packages/sdk/docs/services/resolver.md index cdc6ac61f..f84803d49 100644 --- a/packages/sdk/docs/services/resolver.md +++ b/packages/sdk/docs/services/resolver.md @@ -306,7 +306,7 @@ createResolver({ - When `publishEvents: true`, resolver execution events are published - When not specified, it is **automatically set to `true`** if an executor uses this resolver with `resolverExecutedTrigger` -- When explicitly set to `false` while an executor uses this resolver, an error is thrown during `tailor apply` +- When explicitly set to `false` while an executor uses this resolver, an error is thrown during `tailor-sdk deploy` **Use cases:** diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index 3bb171cc8..e9aad4ac0 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -461,7 +461,7 @@ db.type("User", { - When `publishEvents: true`, record creation/update/deletion events are published - When not specified, it is **automatically set to `true`** if an executor uses this type with `recordCreatedTrigger`, `recordUpdatedTrigger`, or `recordDeletedTrigger` -- When explicitly set to `false` while an executor uses this type, an error is thrown during `tailor apply` +- When explicitly set to `false` while an executor uses this type, an error is thrown during `tailor-sdk deploy` **Use cases:** diff --git a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml index db955801b..d60ba1ac6 100644 --- a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml @@ -171,11 +171,13 @@ jobs: let body; if (!workspaceId) { - body = `${marker} - ## Tailor Platform Plan (${key}) - - Workspace is not provisioned yet. Set TAILOR_PLATFORM_WORKSPACE_ID after provisioning the workspace. - `; + body = [ + marker, + `## Tailor Platform Plan (${key})`, + '', + 'Workspace is not provisioned yet. Set TAILOR_PLATFORM_WORKSPACE_ID after provisioning the workspace.', + '', + ].join('\n'); } else { const exitCode = process.env.DRY_RUN_EXIT_CODE; let output = process.env.DRY_RUN_OUTPUT; @@ -185,18 +187,20 @@ jobs: output = note + output.slice(output.length - MAX_OUTPUT); } const status = exitCode === '0' ? 'PASS' : 'FAIL'; - body = `${marker} - ## ${status} Tailor Platform Plan (${key}) - -
- Plan output (exit code: ${exitCode}) - - \`\`\` - ${output} - \`\`\` - -
- `; + body = [ + marker, + `## ${status} Tailor Platform Plan (${key})`, + '', + '
', + `Plan output (exit code: ${exitCode})`, + '', + '```', + output, + '```', + '', + '
', + '', + ].join('\n'); } const comments = await github.paginate(github.rest.issues.listComments, { diff --git a/packages/sdk/src/cli/commands/setup/github/github.test.ts b/packages/sdk/src/cli/commands/setup/github/github.test.ts index 49a1ccc4d..072cf6c90 100644 --- a/packages/sdk/src/cli/commands/setup/github/github.test.ts +++ b/packages/sdk/src/cli/commands/setup/github/github.test.ts @@ -140,6 +140,14 @@ describe("renderBranchWorkflow", () => { expect(content).toContain("per_page: 100"); }); + test("builds plan comment markdown without YAML indentation", () => { + const { content } = renderBranchWorkflow(branchBase); + expect(content).toContain("].join('\\n');"); + expect(content).toContain("`## ${status} Tailor Platform Plan (${key})`"); + expect(content).toContain("`Plan output (exit code: ${exitCode})`"); + expect(content).not.toContain(" ## ${status} Tailor Platform Plan"); + }); + test("logs full plan output before truncating comments and outputs", () => { const branch = renderBranchWorkflow(branchBase).content; const tag = renderTagWorkflow(tagBase).content; From 4bf34bd4035296b9dc96b8ad76d6ea3c4c45ebd2 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 08:24:42 +0900 Subject: [PATCH 028/618] fix(codemod): rewrite legacy machine user option --- .changeset/remove-v2-cli-aliases.md | 3 ++ .../v2/cli-rename/scripts/transform.ts | 31 +++++++++++-------- .../tests/basic-package-json/expected.json | 2 ++ .../tests/basic-package-json/input.json | 2 ++ .../cli-rename/tests/basic-shell/expected.sh | 1 + .../v2/cli-rename/tests/basic-shell/input.sh | 1 + .../cli-rename/tests/basic-yaml/expected.yml | 1 + .../v2/cli-rename/tests/basic-yaml/input.yml | 1 + .../v2/cli-rename/tests/no-match/input.sh | 2 ++ packages/sdk-codemod/src/registry.ts | 5 +-- 10 files changed, 34 insertions(+), 15 deletions(-) diff --git a/.changeset/remove-v2-cli-aliases.md b/.changeset/remove-v2-cli-aliases.md index 65b9a03e6..a5d3ebdc5 100644 --- a/.changeset/remove-v2-cli-aliases.md +++ b/.changeset/remove-v2-cli-aliases.md @@ -1,5 +1,8 @@ --- "@tailor-platform/sdk": major +"@tailor-platform/sdk-codemod": patch --- Remove deprecated CLI aliases for the v2 command surface. Use `tailor-sdk deploy` instead of `tailor-sdk apply`, `tailor-sdk crashreport` instead of `tailor-sdk crash-report`, and the hyphenated `--machine-user` option instead of the hidden `--machineuser` alias. + +Fix the v2 CLI rename codemod to migrate the hidden `--machineuser` option to `--machine-user`. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index fbe72152c..40f916e4d 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -2,25 +2,33 @@ import * as path from "pathe"; // Map of v1 multi-word command names to their v2 single-word replacements. const COMMAND_RENAMES: ReadonlyArray = [["crash-report", "crashreport"]]; +const OPTION_RENAMES: ReadonlyArray = [ + ["--machineuser", "--machine-user"], +]; const COMMAND_PATTERN = new RegExp( `\\btailor-sdk(@[^\\s'"\`]+)?(\\s+)(${COMMAND_RENAMES.map(([from]) => from).join("|")})\\b`, "g", ); +const OPTION_PATTERN = new RegExp( + `(? from).join("|")})(?=$|[\\s=])`, + "g", +); const COMMAND_MAP = new Map(COMMAND_RENAMES); +const OPTION_MAP = new Map(OPTION_RENAMES); function replaceAll(value: string): string { - return value.replace( - COMMAND_PATTERN, - (_match, ver: string | undefined, sep: string, cmd: string) => - `tailor-sdk${ver ?? ""}${sep}${COMMAND_MAP.get(cmd) ?? cmd}`, - ); + return value + .replace( + COMMAND_PATTERN, + (_match, ver: string | undefined, sep: string, cmd: string) => + `tailor-sdk${ver ?? ""}${sep}${COMMAND_MAP.get(cmd) ?? cmd}`, + ) + .replace(OPTION_PATTERN, (option: string) => OPTION_MAP.get(option) ?? option); } function transformText(source: string): string | null { - if (!COMMAND_PATTERN.test(source)) return null; - COMMAND_PATTERN.lastIndex = 0; const updated = replaceAll(source); return updated === source ? null : updated; } @@ -53,13 +61,10 @@ function transformPackageJson(source: string): string | null { /** * Apply v2 CLI naming conventions: multi-word commands collapse into a single - * word (`crash-report` → `crashreport`). Optional `@version` pins on the binary - * (`tailor-sdk@latest`) are preserved. + * word (`crash-report` → `crashreport`), and legacy option spellings are + * rewritten to kebab-case (`--machineuser` → `--machine-user`). Optional + * `@version` pins on the binary (`tailor-sdk@latest`) are preserved. * - * Long options (`--executionId`, `--executorName`, `--jobId`) and the - * positional argument keys with the same names are intentionally not rewritten: - * those tokens are positional in the SDK CLI and never appear as long flags in - * user scripts, so a transform here would have no real-world target. * @param source - File contents * @param filePath - Absolute path to the file (used to dispatch package.json vs text) * @returns Transformed source or null when nothing matched. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json index c8a36feda..8c2958d68 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json @@ -4,6 +4,8 @@ "scripts": { "report:tail": "tailor-sdk crashreport list", "report:send": "tailor-sdk crashreport send --file ./latest.crash.log", + "login": "tailor-sdk login --machine-user ci", + "query": "tailor-sdk query --machine-user=ci --json", "build": "tsc" } } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json index fe43c4b44..fc6a4a12d 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json @@ -4,6 +4,8 @@ "scripts": { "report:tail": "tailor-sdk crash-report list", "report:send": "tailor-sdk crash-report send --file ./latest.crash.log", + "login": "tailor-sdk login --machineuser ci", + "query": "tailor-sdk query --machineuser=ci --json", "build": "tsc" } } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh index 274b44990..1ee17395f 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh @@ -3,3 +3,4 @@ set -euo pipefail pnpm exec tailor-sdk crashreport list pnpm exec tailor-sdk crashreport send --file ./latest.crash.log +pnpm exec tailor-sdk workflow start approval --machine-user ci diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh index 6202aa2aa..bf70619dc 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh @@ -3,3 +3,4 @@ set -euo pipefail pnpm exec tailor-sdk crash-report list pnpm exec tailor-sdk crash-report send --file ./latest.crash.log +pnpm exec tailor-sdk workflow start approval --machineuser ci diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml index 0cddd575f..79f3d1867 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml @@ -8,3 +8,4 @@ jobs: - run: pnpm install - run: pnpm exec tailor-sdk crashreport list - run: pnpm exec tailor-sdk crashreport send --file latest.crash.log + - run: pnpm exec tailor-sdk login --machine-user ci diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml index 83ab2b5f5..5524b319d 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml @@ -8,3 +8,4 @@ jobs: - run: pnpm install - run: pnpm exec tailor-sdk crash-report list - run: pnpm exec tailor-sdk crash-report send --file latest.crash.log + - run: pnpm exec tailor-sdk login --machineuser ci diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh index a17ea544a..e7ba282d8 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh @@ -8,3 +8,5 @@ echo "Generated crash-report uploaded" # Should not match: positional/long-form camelCase identifiers are out of scope pnpm exec tailor-sdk function logs --executionId abc pnpm exec tailor-sdk executor jobs my-executor --jobId xyz +# Should not match: longer option names +pnpm exec tailor-sdk login --machineusername ci diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 886a22ab9..bb92febd9 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -59,13 +59,14 @@ const allCodemods: CodemodPackage[] = [ }, { id: "v2/cli-rename", - name: "v2 CLI rename (single-word commands)", + name: "v2 CLI rename", description: - "Rewrite `tailor-sdk crash-report` invocations to the v2 single-word `tailor-sdk crashreport` form across package.json scripts, shell scripts, CI configs, and docs", + "Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs", since: "1.0.0", until: "2.0.0", scriptPath: "v2/cli-rename/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], + legacyPatterns: ["tailor-sdk crash-report", "--machineuser"], }, { id: "v2/auth-invoker-unwrap", From c204311a1da1ec5064c125db1a5c34406c1710f0 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 08:25:02 +0900 Subject: [PATCH 029/618] fix(cli): merge setup github base before install --- docs/setup-github-contracts.md | 4 ++-- .../cli/commands/setup/github/branch.workflow.yml | 14 +++++++------- .../src/cli/commands/setup/github/github.test.ts | 3 +++ .../sdk/src/cli/commands/setup/github/templates.ts | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/setup-github-contracts.md b/docs/setup-github-contracts.md index b41fc49d6..fcd172e62 100644 --- a/docs/setup-github-contracts.md +++ b/docs/setup-github-contracts.md @@ -42,6 +42,7 @@ status checks**. | `tailor-tag-guard/tailor-checkout` | `actions/checkout` with `fetch-depth: 0` | | `tailor-tag-guard/tailor-tag-guard` | Branch-reachability shell script | | `tailor-plan/tailor-checkout` | `actions/checkout` | +| `tailor-plan/tailor-merge-base` | Merges the PR base branch before branch-target dry-runs | | `tailor-plan/tailor-setup-pnpm` | `pnpm/action-setup` (pnpm projects only) | | `tailor-plan/tailor-setup-node` | `actions/setup-node` | | `tailor-plan/tailor-setup-bun` | `oven-sh/setup-bun` (bun projects only) | @@ -50,7 +51,6 @@ status checks**. | `tailor-plan/tailor-generate-check` | Checks generated files are committed | | `tailor-plan/tailor-mask-credentials` | Masks machine-user credentials | | `tailor-plan/tailor-login` | `tailor-sdk login --machine-user` | -| `tailor-plan/tailor-merge-base` | Merges the PR base branch before branch-target dry-runs | | `tailor-plan/tailor-plan` | `tailor-sdk deploy --dry-run --yes` | | `tailor-plan/tailor-plan-summary` | Writes the plan result to the step summary | | `tailor-plan/tailor-plan-comment` | Updates the PR plan comment on branch targets | @@ -125,6 +125,7 @@ The lock file is JSON, 2-space indented, with a trailing newline. It is // history of managed ids written by this setup run "tailor-plan", // job id "tailor-plan/tailor-checkout", // job/step qualified form + "tailor-plan/tailor-merge-base", "tailor-plan/tailor-setup-pnpm", // pnpm projects only "tailor-plan/tailor-setup-node", "tailor-plan/tailor-install", @@ -132,7 +133,6 @@ The lock file is JSON, 2-space indented, with a trailing newline. It is "tailor-plan/tailor-generate-check", "tailor-plan/tailor-mask-credentials", "tailor-plan/tailor-login", - "tailor-plan/tailor-merge-base", "tailor-plan/tailor-plan", "tailor-plan/tailor-plan-summary", "tailor-plan/tailor-plan-comment", diff --git a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml index d60ba1ac6..e1eab8689 100644 --- a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml @@ -40,6 +40,13 @@ jobs: steps: - id: tailor-checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - id: tailor-merge-base + if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork + env: + BASE_REF: ${{ github.base_ref }} + run: | + git fetch origin "$BASE_REF:refs/remotes/origin/$BASE_REF" + git merge --no-commit --no-ff "origin/$BASE_REF" || true # __SETUP_STEPS__ - id: tailor-generate # __WORKING_DIRECTORY__ @@ -68,13 +75,6 @@ jobs: env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} - - id: tailor-merge-base - if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork - env: - BASE_REF: ${{ github.base_ref }} - run: | - git fetch origin "$BASE_REF:refs/remotes/origin/$BASE_REF" - git merge --no-commit --no-ff "origin/$BASE_REF" || true - id: tailor-plan if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork # __WORKING_DIRECTORY__ diff --git a/packages/sdk/src/cli/commands/setup/github/github.test.ts b/packages/sdk/src/cli/commands/setup/github/github.test.ts index 072cf6c90..0220f41ae 100644 --- a/packages/sdk/src/cli/commands/setup/github/github.test.ts +++ b/packages/sdk/src/cli/commands/setup/github/github.test.ts @@ -132,6 +132,9 @@ describe("renderBranchWorkflow", () => { const { content } = renderBranchWorkflow(branchBase); expect(content).toContain('git fetch origin "$BASE_REF:refs/remotes/origin/$BASE_REF"'); expect(content).toContain('git merge --no-commit --no-ff "origin/$BASE_REF" || true'); + expect(content.indexOf("id: tailor-merge-base")).toBeLessThan( + content.indexOf("id: tailor-setup-pnpm"), + ); }); test("paginates plan comment lookup", () => { diff --git a/packages/sdk/src/cli/commands/setup/github/templates.ts b/packages/sdk/src/cli/commands/setup/github/templates.ts index 90d0f098b..916562dc1 100644 --- a/packages/sdk/src/cli/commands/setup/github/templates.ts +++ b/packages/sdk/src/cli/commands/setup/github/templates.ts @@ -191,12 +191,12 @@ export function renderBranchWorkflow(params: RenderBranchParams): RenderResult { generatedIds.push( "tailor-plan", "tailor-plan/tailor-checkout", + "tailor-plan/tailor-merge-base", ...setupIds("tailor-plan", packageManager), "tailor-plan/tailor-generate", "tailor-plan/tailor-generate-check", "tailor-plan/tailor-mask-credentials", "tailor-plan/tailor-login", - "tailor-plan/tailor-merge-base", "tailor-plan/tailor-plan", "tailor-plan/tailor-plan-summary", "tailor-plan/tailor-plan-comment", From 09ecb40537be22bfe4247d7693db8d56f288e29f Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 08:36:23 +0900 Subject: [PATCH 030/618] fix(cli): address alias cleanup review gaps --- .../codemods/v2/cli-rename/scripts/transform.ts | 7 +++++-- .../v2/cli-rename/tests/basic-markdown/expected.md | 5 +++++ .../codemods/v2/cli-rename/tests/basic-markdown/input.md | 5 +++++ .../codemods/v2/cli-rename/tests/basic-yaml/expected.yml | 1 + .../codemods/v2/cli-rename/tests/basic-yaml/input.yml | 1 + .../codemods/v2/cli-rename/tests/no-match/input.sh | 2 ++ .../sdk/src/cli/commands/setup/github/branch.workflow.yml | 4 +++- packages/sdk/src/cli/commands/setup/github/github.test.ts | 3 ++- 8 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index 40f916e4d..2df606b7e 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -10,8 +10,9 @@ const COMMAND_PATTERN = new RegExp( `\\btailor-sdk(@[^\\s'"\`]+)?(\\s+)(${COMMAND_RENAMES.map(([from]) => from).join("|")})\\b`, "g", ); +const TAILOR_COMMAND_PATTERN = /\btailor-sdk(?:@[^\s'"`]+)?[^\n;&|'"`]*/g; const OPTION_PATTERN = new RegExp( - `(? from).join("|")})(?=$|[\\s=])`, + `(? from).join("|")})(?![\\w-])`, "g", ); @@ -25,7 +26,9 @@ function replaceAll(value: string): string { (_match, ver: string | undefined, sep: string, cmd: string) => `tailor-sdk${ver ?? ""}${sep}${COMMAND_MAP.get(cmd) ?? cmd}`, ) - .replace(OPTION_PATTERN, (option: string) => OPTION_MAP.get(option) ?? option); + .replace(TAILOR_COMMAND_PATTERN, (command: string) => + command.replace(OPTION_PATTERN, (option: string) => OPTION_MAP.get(option) ?? option), + ); } function transformText(source: string): string | null { diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md new file mode 100644 index 000000000..30431188d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md @@ -0,0 +1,5 @@ +# CLI migration + +Use `tailor-sdk login --machine-user` before running `tailor-sdk query --machine-user=ci`. + +Do not rewrite unrelated commands such as `other-cli --machineuser=ci`. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md new file mode 100644 index 000000000..e049b894b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md @@ -0,0 +1,5 @@ +# CLI migration + +Use `tailor-sdk login --machineuser` before running `tailor-sdk query --machineuser=ci`. + +Do not rewrite unrelated commands such as `other-cli --machineuser=ci`. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml index 79f3d1867..ee5021d33 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml @@ -9,3 +9,4 @@ jobs: - run: pnpm exec tailor-sdk crashreport list - run: pnpm exec tailor-sdk crashreport send --file latest.crash.log - run: pnpm exec tailor-sdk login --machine-user ci + - run: "pnpm exec tailor-sdk query --machine-user ci" diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml index 5524b319d..5e6d52da3 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml @@ -9,3 +9,4 @@ jobs: - run: pnpm exec tailor-sdk crash-report list - run: pnpm exec tailor-sdk crash-report send --file latest.crash.log - run: pnpm exec tailor-sdk login --machineuser ci + - run: "pnpm exec tailor-sdk query --machineuser ci" diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh index e7ba282d8..40f854a14 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh @@ -10,3 +10,5 @@ pnpm exec tailor-sdk function logs --executionId abc pnpm exec tailor-sdk executor jobs my-executor --jobId xyz # Should not match: longer option names pnpm exec tailor-sdk login --machineusername ci +# Should not match: same option spelling for another command +other-cli --machineuser=ci diff --git a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml index e1eab8689..f03489f0a 100644 --- a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml @@ -45,8 +45,10 @@ jobs: env: BASE_REF: ${{ github.base_ref }} run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git fetch origin "$BASE_REF:refs/remotes/origin/$BASE_REF" - git merge --no-commit --no-ff "origin/$BASE_REF" || true + git merge --no-edit "origin/$BASE_REF" # __SETUP_STEPS__ - id: tailor-generate # __WORKING_DIRECTORY__ diff --git a/packages/sdk/src/cli/commands/setup/github/github.test.ts b/packages/sdk/src/cli/commands/setup/github/github.test.ts index 0220f41ae..3758de065 100644 --- a/packages/sdk/src/cli/commands/setup/github/github.test.ts +++ b/packages/sdk/src/cli/commands/setup/github/github.test.ts @@ -131,7 +131,8 @@ describe("renderBranchWorkflow", () => { test("fetches the pull request base into the remote-tracking ref before merge", () => { const { content } = renderBranchWorkflow(branchBase); expect(content).toContain('git fetch origin "$BASE_REF:refs/remotes/origin/$BASE_REF"'); - expect(content).toContain('git merge --no-commit --no-ff "origin/$BASE_REF" || true'); + expect(content).toContain('git merge --no-edit "origin/$BASE_REF"'); + expect(content).not.toContain("git merge --no-commit"); expect(content.indexOf("id: tailor-merge-base")).toBeLessThan( content.indexOf("id: tailor-setup-pnpm"), ); From eb13c9f882357a9763f2d321230b108ba29ac414 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 08:51:07 +0900 Subject: [PATCH 031/618] fix(codemod): cover CLI alias migration edge cases --- .../v2/apply-to-deploy/scripts/transform.ts | 11 +- .../tests/basic-package-json/expected.json | 2 + .../tests/basic-package-json/input.json | 2 + .../tests/basic-shell/expected.sh | 2 + .../tests/basic-shell/input.sh | 2 + .../v2/cli-rename/scripts/transform.ts | 121 +++++++++++++++--- .../tests/basic-markdown/expected.md | 2 + .../cli-rename/tests/basic-markdown/input.md | 2 + .../tests/basic-package-json/expected.json | 1 + .../tests/basic-package-json/input.json | 1 + .../cli-rename/tests/basic-shell/expected.sh | 4 + .../v2/cli-rename/tests/basic-shell/input.sh | 4 + .../v2/cli-rename/tests/no-match/input.sh | 2 + 13 files changed, 138 insertions(+), 18 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts index bf50fec76..1f5f68996 100644 --- a/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts @@ -6,12 +6,19 @@ import * as path from "pathe"; // `apply` and `deploy` are the same subcommand on the same binary. // `(?![-\w])` excludes both word continuation (`applyConfig`) and dash-suffixed // names (`apply-foo`) so a hypothetical sibling subcommand is not rewritten. -const APPLY_PATTERN = /\btailor-sdk(@[^\s'"`]+)?(\s+)apply(?![-\w])/g; +const ARG_VALUE = `(?:[^\\s'"\`;&|]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`; +const BOOLEAN_GLOBAL_ARG = "(?:--verbose|--json|-j)"; +const VALUE_GLOBAL_ARG = "(?:--env-file|--env-file-if-exists|-e)"; +const GLOBAL_ARG_PATTERN = `(?:(?:\\s+${BOOLEAN_GLOBAL_ARG})|(?:\\s+${VALUE_GLOBAL_ARG}(?:=${ARG_VALUE}|\\s+${ARG_VALUE})))*`; +const APPLY_PATTERN = new RegExp( + `\\btailor-sdk(@[^\\s'"\`]+)?(${GLOBAL_ARG_PATTERN}\\s+)apply(?![-\\w])`, + "g", +); function replaceApply(value: string): string { return value.replace( APPLY_PATTERN, - (_match, ver: string | undefined, sep: string) => `tailor-sdk${ver ?? ""}${sep}deploy`, + (_match, ver: string | undefined, prefix: string) => `tailor-sdk${ver ?? ""}${prefix}deploy`, ); } diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/expected.json b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/expected.json index cb68f1c59..9cbc21b33 100644 --- a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/expected.json +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/expected.json @@ -4,6 +4,8 @@ "scripts": { "deploy:dev": "tailor-sdk deploy --profile dev", "deploy:prod": "tailor-sdk deploy --profile prod -y", + "deploy:json": "tailor-sdk --json deploy --dry-run", + "deploy:env": "tailor-sdk --env-file=.env deploy --yes", "build": "tsc" } } diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/input.json b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/input.json index 26cdfa98a..2c2ad6558 100644 --- a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/input.json +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/input.json @@ -4,6 +4,8 @@ "scripts": { "deploy:dev": "tailor-sdk apply --profile dev", "deploy:prod": "tailor-sdk apply --profile prod -y", + "deploy:json": "tailor-sdk --json apply --dry-run", + "deploy:env": "tailor-sdk --env-file=.env apply --yes", "build": "tsc" } } diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/expected.sh index c3705af3f..64f8adfff 100644 --- a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/expected.sh @@ -4,3 +4,5 @@ set -euo pipefail pnpm exec tailor-sdk deploy --dry-run npx tailor-sdk deploy -y --profile prod bunx tailor-sdk deploy --no-cache +tailor-sdk --env-file .env deploy --yes +tailor-sdk --json deploy --dry-run diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/input.sh index f197add5e..2ccbe800c 100644 --- a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/input.sh +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/input.sh @@ -4,3 +4,5 @@ set -euo pipefail pnpm exec tailor-sdk apply --dry-run npx tailor-sdk apply -y --profile prod bunx tailor-sdk apply --no-cache +tailor-sdk --env-file .env apply --yes +tailor-sdk --json apply --dry-run diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index 2df606b7e..04aea8809 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -6,29 +6,118 @@ const OPTION_RENAMES: ReadonlyArray = [ ["--machineuser", "--machine-user"], ]; +const ARG_VALUE = `(?:[^\\s'"\`;&|]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`; +const BOOLEAN_GLOBAL_ARG = "(?:--verbose|--json|-j)"; +const VALUE_GLOBAL_ARG = "(?:--env-file|--env-file-if-exists|-e)"; +const GLOBAL_ARG_PATTERN = `(?:(?:\\s+${BOOLEAN_GLOBAL_ARG})|(?:\\s+${VALUE_GLOBAL_ARG}(?:=${ARG_VALUE}|\\s+${ARG_VALUE})))*`; const COMMAND_PATTERN = new RegExp( - `\\btailor-sdk(@[^\\s'"\`]+)?(\\s+)(${COMMAND_RENAMES.map(([from]) => from).join("|")})\\b`, - "g", -); -const TAILOR_COMMAND_PATTERN = /\btailor-sdk(?:@[^\s'"`]+)?[^\n;&|'"`]*/g; -const OPTION_PATTERN = new RegExp( - `(? from).join("|")})(?![\\w-])`, + `\\btailor-sdk(@[^\\s'"\`]+)?(${GLOBAL_ARG_PATTERN}\\s+)(${COMMAND_RENAMES.map(([from]) => from).join("|")})\\b`, "g", ); +const TAILOR_BINARY_PATTERN = /\btailor-sdk(?:@[^\s'"`]+)?/g; const COMMAND_MAP = new Map(COMMAND_RENAMES); -const OPTION_MAP = new Map(OPTION_RENAMES); + +function isOptionBoundaryChar(value: string | undefined): boolean { + return value === undefined || !/[\w-]/.test(value); +} + +function findTailorCommandEnd(source: string, start: number): number { + if (source[start - 1] === "`") { + const codeSpanEnd = source.indexOf("`", start); + if (codeSpanEnd !== -1) return codeSpanEnd; + } + + let end = start; + while (end < source.length) { + const ch = source[end]; + const prev = source[end - 1]; + if ((ch === ";" || ch === "&" || ch === "|") && prev !== "\\") break; + if (ch === "\n" && prev !== "\\") break; + end += 1; + } + return end; +} + +function findOptionRename(command: string, index: number): readonly [string, string] | undefined { + return OPTION_RENAMES.find( + ([from]) => + command.startsWith(from, index) && + isOptionBoundaryChar(command[index - 1]) && + isOptionBoundaryChar(command[index + from.length]), + ); +} + +function replaceOptionsInCommand(command: string): string { + let updated = ""; + let index = 0; + let quote: "'" | '"' | null = null; + + while (index < command.length) { + const ch = command[index]; + + if (quote !== null) { + updated += ch; + if (ch === "\\" && quote === '"' && index + 1 < command.length) { + index += 1; + updated += command[index]; + } else if (ch === quote) { + quote = null; + } + index += 1; + continue; + } + + if (ch === "'" || ch === '"') { + quote = ch; + updated += ch; + index += 1; + continue; + } + + const rename = findOptionRename(command, index); + if (rename) { + updated += rename[1]; + index += rename[0].length; + continue; + } + + updated += ch; + index += 1; + } + + return updated; +} + +function replaceOptionsInTailorCommands(source: string): string { + let updated = ""; + let cursor = 0; + TAILOR_BINARY_PATTERN.lastIndex = 0; + + for (;;) { + const match = TAILOR_BINARY_PATTERN.exec(source); + if (!match) break; + + const start = match.index; + if (start < cursor) continue; + + const end = findTailorCommandEnd(source, start); + updated += source.slice(cursor, start); + updated += replaceOptionsInCommand(source.slice(start, end)); + cursor = end; + TAILOR_BINARY_PATTERN.lastIndex = end; + } + + return updated + source.slice(cursor); +} function replaceAll(value: string): string { - return value - .replace( - COMMAND_PATTERN, - (_match, ver: string | undefined, sep: string, cmd: string) => - `tailor-sdk${ver ?? ""}${sep}${COMMAND_MAP.get(cmd) ?? cmd}`, - ) - .replace(TAILOR_COMMAND_PATTERN, (command: string) => - command.replace(OPTION_PATTERN, (option: string) => OPTION_MAP.get(option) ?? option), - ); + const updated = value.replace( + COMMAND_PATTERN, + (_match, ver: string | undefined, prefix: string, cmd: string) => + `tailor-sdk${ver ?? ""}${prefix}${COMMAND_MAP.get(cmd) ?? cmd}`, + ); + return replaceOptionsInTailorCommands(updated); } function transformText(source: string): string | null { diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md index 30431188d..59309a4c6 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md @@ -2,4 +2,6 @@ Use `tailor-sdk login --machine-user` before running `tailor-sdk query --machine-user=ci`. +Use `tailor-sdk --json crashreport list` but leave `other-cli --machineuser=ci` alone. + Do not rewrite unrelated commands such as `other-cli --machineuser=ci`. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md index e049b894b..92d02b543 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md @@ -2,4 +2,6 @@ Use `tailor-sdk login --machineuser` before running `tailor-sdk query --machineuser=ci`. +Use `tailor-sdk --json crash-report list` but leave `other-cli --machineuser=ci` alone. + Do not rewrite unrelated commands such as `other-cli --machineuser=ci`. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json index 8c2958d68..0958528e7 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json @@ -4,6 +4,7 @@ "scripts": { "report:tail": "tailor-sdk crashreport list", "report:send": "tailor-sdk crashreport send --file ./latest.crash.log", + "report:json": "tailor-sdk --json crashreport list", "login": "tailor-sdk login --machine-user ci", "query": "tailor-sdk query --machine-user=ci --json", "build": "tsc" diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json index fc6a4a12d..92525985a 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json @@ -4,6 +4,7 @@ "scripts": { "report:tail": "tailor-sdk crash-report list", "report:send": "tailor-sdk crash-report send --file ./latest.crash.log", + "report:json": "tailor-sdk --json crash-report list", "login": "tailor-sdk login --machineuser ci", "query": "tailor-sdk query --machineuser=ci --json", "build": "tsc" diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh index 1ee17395f..eafa5d576 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh @@ -4,3 +4,7 @@ set -euo pipefail pnpm exec tailor-sdk crashreport list pnpm exec tailor-sdk crashreport send --file ./latest.crash.log pnpm exec tailor-sdk workflow start approval --machine-user ci +tailor-sdk query --query 'select 1' --machine-user ci +tailor-sdk workflow start approval --arg '{"ok":true}' \ + --machine-user ci +tailor-sdk --json crashreport list diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh index bf70619dc..f2686d787 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh @@ -4,3 +4,7 @@ set -euo pipefail pnpm exec tailor-sdk crash-report list pnpm exec tailor-sdk crash-report send --file ./latest.crash.log pnpm exec tailor-sdk workflow start approval --machineuser ci +tailor-sdk query --query 'select 1' --machineuser ci +tailor-sdk workflow start approval --arg '{"ok":true}' \ + --machineuser ci +tailor-sdk --json crash-report list diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh index 40f854a14..6043b350c 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh @@ -12,3 +12,5 @@ pnpm exec tailor-sdk executor jobs my-executor --jobId xyz pnpm exec tailor-sdk login --machineusername ci # Should not match: same option spelling for another command other-cli --machineuser=ci +# Should not match: option spelling inside quoted arguments +tailor-sdk query --query 'select --machineuser' From d3733b4d6da46ece4cbe489446aa202711c0fa73 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 09:03:08 +0900 Subject: [PATCH 032/618] fix(codemod): bound CLI option rewrites --- .../v2/cli-rename/scripts/transform.ts | 38 ++++++++++++++++--- .../tests/basic-markdown/expected.md | 2 + .../cli-rename/tests/basic-markdown/input.md | 2 + .../cli-rename/tests/basic-shell/expected.sh | 2 + .../v2/cli-rename/tests/basic-shell/input.sh | 2 + 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index 04aea8809..a69df5fbd 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -22,15 +22,41 @@ function isOptionBoundaryChar(value: string | undefined): boolean { return value === undefined || !/[\w-]/.test(value); } -function findTailorCommandEnd(source: string, start: number): number { - if (source[start - 1] === "`") { - const codeSpanEnd = source.indexOf("`", start); - if (codeSpanEnd !== -1) return codeSpanEnd; - } +function findInlineCodeSpanEnd(source: string, start: number): number | undefined { + const lineStart = source.lastIndexOf("\n", start - 1) + 1; + const ticksBefore = [...source.slice(lineStart, start).matchAll(/`/g)].length; + if (ticksBefore % 2 === 0) return undefined; + const codeSpanEnd = source.indexOf("`", start); + return codeSpanEnd === -1 ? undefined : codeSpanEnd; +} + +function findTailorCommandEnd(source: string, start: number): number { + const limit = findInlineCodeSpanEnd(source, start) ?? source.length; + let quote: "'" | '"' | null = null; let end = start; - while (end < source.length) { + + while (end < limit) { const ch = source[end]; + + if (quote !== null) { + if (ch === "\\" && quote === '"' && end + 1 < limit) { + end += 2; + continue; + } + if (ch === quote) { + quote = null; + } + end += 1; + continue; + } + + if (ch === "'" || ch === '"') { + quote = ch; + end += 1; + continue; + } + const prev = source[end - 1]; if ((ch === ";" || ch === "&" || ch === "|") && prev !== "\\") break; if (ch === "\n" && prev !== "\\") break; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md index 59309a4c6..eaabed9a9 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md @@ -4,4 +4,6 @@ Use `tailor-sdk login --machine-user` before running `tailor-sdk query --machine Use `tailor-sdk --json crashreport list` but leave `other-cli --machineuser=ci` alone. +Use `pnpm exec tailor-sdk login --machine-user` but leave `other-cli --machineuser=ci` alone. + Do not rewrite unrelated commands such as `other-cli --machineuser=ci`. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md index 92d02b543..47dcdac7e 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md @@ -4,4 +4,6 @@ Use `tailor-sdk login --machineuser` before running `tailor-sdk query --machineu Use `tailor-sdk --json crash-report list` but leave `other-cli --machineuser=ci` alone. +Use `pnpm exec tailor-sdk login --machineuser` but leave `other-cli --machineuser=ci` alone. + Do not rewrite unrelated commands such as `other-cli --machineuser=ci`. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh index eafa5d576..12b0536c9 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh @@ -5,6 +5,8 @@ pnpm exec tailor-sdk crashreport list pnpm exec tailor-sdk crashreport send --file ./latest.crash.log pnpm exec tailor-sdk workflow start approval --machine-user ci tailor-sdk query --query 'select 1' --machine-user ci +tailor-sdk query --query 'select 1;' --machine-user ci +tailor-sdk query --query "select 1 | 2" --machine-user ci tailor-sdk workflow start approval --arg '{"ok":true}' \ --machine-user ci tailor-sdk --json crashreport list diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh index f2686d787..276195502 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh @@ -5,6 +5,8 @@ pnpm exec tailor-sdk crash-report list pnpm exec tailor-sdk crash-report send --file ./latest.crash.log pnpm exec tailor-sdk workflow start approval --machineuser ci tailor-sdk query --query 'select 1' --machineuser ci +tailor-sdk query --query 'select 1;' --machineuser ci +tailor-sdk query --query "select 1 | 2" --machineuser ci tailor-sdk workflow start approval --arg '{"ok":true}' \ --machineuser ci tailor-sdk --json crash-report list From 03d17488e4c9f5b1570c181e2470709b291b9664 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 09:21:58 +0900 Subject: [PATCH 033/618] fix(cli): harden setup workflow migration --- .../v2/cli-rename/scripts/transform.ts | 100 ++++++++++++++-- .../cli-rename/tests/basic-yaml/expected.yml | 3 + .../v2/cli-rename/tests/basic-yaml/input.yml | 3 + packages/sdk-codemod/src/runner.test.ts | 22 ++++ packages/sdk-codemod/src/runner.ts | 110 +++++++++--------- .../commands/setup/github/branch.workflow.yml | 2 + .../cli/commands/setup/github/github.test.ts | 1 + 7 files changed, 178 insertions(+), 63 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index a69df5fbd..f88f539b1 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -18,6 +18,11 @@ const TAILOR_BINARY_PATTERN = /\btailor-sdk(?:@[^\s'"`]+)?/g; const COMMAND_MAP = new Map(COMMAND_RENAMES); +interface FoldedYamlRange { + start: number; + end: number; +} + function isOptionBoundaryChar(value: string | undefined): boolean { return value === undefined || !/[\w-]/.test(value); } @@ -31,16 +36,84 @@ function findInlineCodeSpanEnd(source: string, start: number): number | undefine return codeSpanEnd === -1 ? undefined : codeSpanEnd; } -function findTailorCommandEnd(source: string, start: number): number { +function lineIndent(line: string): number { + const match = line.match(/^ */); + return match?.[0].length ?? 0; +} + +function isFoldedScalarHeader(line: string): boolean { + return /^\s*(?:-\s*)?[^#\n]*:\s*>[+-]?(?:\s*(?:#.*)?)?$/.test(line); +} + +function findFoldedYamlRanges(source: string): FoldedYamlRange[] { + const ranges: FoldedYamlRange[] = []; + const lines = source.match(/^.*(?:\n|$)/gm) ?? []; + let offset = 0; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const body = line.replace(/\r?\n$/, ""); + offset += line.length; + + if (!isFoldedScalarHeader(body)) continue; + + const baseIndent = lineIndent(body); + let rangeStart: number | undefined; + let rangeEnd: number | undefined; + let cursor = offset; + + for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) { + const nextLine = lines[nextIndex]; + const nextBody = nextLine.replace(/\r?\n$/, ""); + const trimmed = nextBody.trim(); + const indent = lineIndent(nextBody); + + if (trimmed !== "" && indent <= baseIndent) break; + + if (trimmed === "") { + if (rangeStart !== undefined && rangeEnd !== undefined) { + ranges.push({ start: rangeStart, end: rangeEnd }); + rangeStart = undefined; + rangeEnd = undefined; + } + } else { + rangeStart ??= cursor; + rangeEnd = cursor + nextLine.length; + } + cursor += nextLine.length; + } + + if (rangeStart !== undefined && rangeEnd !== undefined) { + ranges.push({ start: rangeStart, end: rangeEnd }); + } + } + + return ranges; +} + +function findContainingFoldedYamlRange( + ranges: FoldedYamlRange[] | undefined, + index: number, +): FoldedYamlRange | undefined { + return ranges?.find((range) => range.start <= index && index < range.end); +} + +function findTailorCommandEnd( + source: string, + start: number, + foldedYamlRanges?: FoldedYamlRange[], +): number { const limit = findInlineCodeSpanEnd(source, start) ?? source.length; + const foldedYamlRange = findContainingFoldedYamlRange(foldedYamlRanges, start); + const commandLimit = foldedYamlRange ? Math.min(limit, foldedYamlRange.end) : limit; let quote: "'" | '"' | null = null; let end = start; - while (end < limit) { + while (end < commandLimit) { const ch = source[end]; if (quote !== null) { - if (ch === "\\" && quote === '"' && end + 1 < limit) { + if (ch === "\\" && quote === '"' && end + 1 < commandLimit) { end += 2; continue; } @@ -59,7 +132,7 @@ function findTailorCommandEnd(source: string, start: number): number { const prev = source[end - 1]; if ((ch === ";" || ch === "&" || ch === "|") && prev !== "\\") break; - if (ch === "\n" && prev !== "\\") break; + if (ch === "\n" && prev !== "\\" && !foldedYamlRange) break; end += 1; } return end; @@ -115,7 +188,10 @@ function replaceOptionsInCommand(command: string): string { return updated; } -function replaceOptionsInTailorCommands(source: string): string { +function replaceOptionsInTailorCommands( + source: string, + foldedYamlRanges?: FoldedYamlRange[], +): string { let updated = ""; let cursor = 0; TAILOR_BINARY_PATTERN.lastIndex = 0; @@ -127,7 +203,7 @@ function replaceOptionsInTailorCommands(source: string): string { const start = match.index; if (start < cursor) continue; - const end = findTailorCommandEnd(source, start); + const end = findTailorCommandEnd(source, start, foldedYamlRanges); updated += source.slice(cursor, start); updated += replaceOptionsInCommand(source.slice(start, end)); cursor = end; @@ -137,17 +213,19 @@ function replaceOptionsInTailorCommands(source: string): string { return updated + source.slice(cursor); } -function replaceAll(value: string): string { +function replaceAll(value: string, parseFoldedYaml = false): string { const updated = value.replace( COMMAND_PATTERN, (_match, ver: string | undefined, prefix: string, cmd: string) => `tailor-sdk${ver ?? ""}${prefix}${COMMAND_MAP.get(cmd) ?? cmd}`, ); - return replaceOptionsInTailorCommands(updated); + const foldedYamlRanges = parseFoldedYaml ? findFoldedYamlRanges(updated) : undefined; + return replaceOptionsInTailorCommands(updated, foldedYamlRanges); } -function transformText(source: string): string | null { - const updated = replaceAll(source); +function transformText(source: string, filePath: string): string | null { + const ext = path.extname(filePath).toLowerCase(); + const updated = replaceAll(source, ext === ".yml" || ext === ".yaml"); return updated === source ? null : updated; } @@ -190,5 +268,5 @@ function transformPackageJson(source: string): string | null { export default function transform(source: string, filePath: string): string | null { const ext = path.extname(filePath).toLowerCase(); if (ext === ".json") return transformPackageJson(source); - return transformText(source); + return transformText(source, filePath); } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml index ee5021d33..a38fcf830 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml @@ -9,4 +9,7 @@ jobs: - run: pnpm exec tailor-sdk crashreport list - run: pnpm exec tailor-sdk crashreport send --file latest.crash.log - run: pnpm exec tailor-sdk login --machine-user ci + - run: > + pnpm exec tailor-sdk login + --machine-user ci - run: "pnpm exec tailor-sdk query --machine-user ci" diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml index 5e6d52da3..4e5c8a35a 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml @@ -9,4 +9,7 @@ jobs: - run: pnpm exec tailor-sdk crash-report list - run: pnpm exec tailor-sdk crash-report send --file latest.crash.log - run: pnpm exec tailor-sdk login --machineuser ci + - run: > + pnpm exec tailor-sdk login + --machineuser ci - run: "pnpm exec tailor-sdk query --machineuser ci" diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 9e658cf58..06aa29aad 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -208,5 +208,27 @@ describe("runCodemods", () => { const jsonContent = await fs.promises.readFile(path.join(dir, "data.json"), "utf-8"); expect(jsonContent).toBe("WORLD"); }); + + test("should apply transforms to matching files under dot directories", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-dot-test-")); + tmpDir = dir; + const workflowPath = path.join(dir, ".github/workflows/test.yml"); + await fs.promises.mkdir(path.dirname(workflowPath), { recursive: true }); + await fs.promises.writeFile(workflowPath, "hello", "utf-8"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/upper", transformPath, ["**/*.yml"]), + scriptPath: transformPath, + }, + ], + dir, + false, + ); + + expect(result.filesModified).toEqual([workflowPath]); + await expect(fs.promises.readFile(workflowPath, "utf-8")).resolves.toBe("HELLO"); + }); }); }); diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index fb55fb2a7..4eb2afca5 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -1,5 +1,4 @@ import * as fs from "node:fs"; -import { glob } from "node:fs/promises"; import * as url from "node:url"; import chalk from "chalk"; import { structuredPatch } from "diff"; @@ -49,6 +48,28 @@ const DEFAULT_FILE_PATTERNS = ["**/*.{ts,tsx,mts,cts}"]; /** Directory names always excluded from file scanning. */ const EXCLUDE_DIRS = new Set(["node_modules", "dist", ".git"]); +async function* walkFiles(root: string, relativeDir = ""): AsyncGenerator { + const absoluteDir = path.join(root, relativeDir); + let entries: fs.Dirent[]; + try { + entries = await fs.promises.readdir(absoluteDir, { withFileTypes: true }); + } catch { + return; + } + + for (const entry of entries) { + const relative = relativeDir ? path.join(relativeDir, entry.name) : entry.name; + if (entry.isDirectory()) { + if (EXCLUDE_DIRS.has(entry.name)) continue; + yield* walkFiles(root, relative); + continue; + } + if (entry.isFile()) { + yield relative; + } + } +} + /** * Print a colorized unified diff for a single file to stderr. * @param filePath - Absolute path to the file @@ -123,70 +144,55 @@ export async function runCodemods( loaded.push({ id: codemod.id, transform: await loadTransform(scriptPath), - matches: picomatch(patterns), + matches: picomatch(patterns, { dot: true }), legacyPatterns: codemod.legacyPatterns ?? [], }); } - // Collect all unique file patterns for glob scanning - const allPatterns = new Set(); - for (const { codemod } of codemods) { - for (const p of codemod.filePatterns ?? DEFAULT_FILE_PATTERNS) { - allPatterns.add(p); - } - } - const filesModified: string[] = []; const warnings: string[] = []; const appliedCodemodIds = new Set(); const seen = new Set(); - // Iterate over all matching files (deduplicate across patterns) - for (const pattern of allPatterns) { - for await (const relative of glob(pattern, { - cwd: targetPath, - exclude: (name) => EXCLUDE_DIRS.has(name), - })) { - const absolute = path.resolve(targetPath, relative); - if (seen.has(absolute)) continue; - seen.add(absolute); - - let original: string; - try { - original = await fs.promises.readFile(absolute, "utf-8"); - } catch { - continue; - } + for await (const relative of walkFiles(targetPath)) { + const absolute = path.resolve(targetPath, relative); + if (seen.has(absolute)) continue; + seen.add(absolute); - // Chain only transforms whose filePatterns match this file - let current = original; - const matchedTransforms: LoadedTransform[] = []; - for (const lt of loaded) { - if (!lt.matches(relative)) continue; - matchedTransforms.push(lt); - const result = await lt.transform(current, absolute); - if (result != null) { - current = result; - appliedCodemodIds.add(lt.id); - } + let original: string; + try { + original = await fs.promises.readFile(absolute, "utf-8"); + } catch { + continue; + } + + let current = original; + const matchedTransforms: LoadedTransform[] = []; + for (const lt of loaded) { + if (!lt.matches(relative)) continue; + matchedTransforms.push(lt); + const result = await lt.transform(current, absolute); + if (result != null) { + current = result; + appliedCodemodIds.add(lt.id); } + } - if (current !== original) { - filesModified.push(absolute); - if (dryRun) { - printDiff(absolute, original, current); - } else { - await fs.promises.writeFile(absolute, current, "utf-8"); - } + if (current !== original) { + filesModified.push(absolute); + if (dryRun) { + printDiff(absolute, original, current); } else { - // Check each matched codemod's legacyPatterns for unmodified files - for (const lt of matchedTransforms) { - const found = lt.legacyPatterns.filter((p) => original.includes(p)); - if (found.length > 0) { - warnings.push( - `${relative}: contains ${found.join(", ")} but was not migrated automatically (rule: ${lt.id}). Manual migration may be needed.`, - ); - } + await fs.promises.writeFile(absolute, current, "utf-8"); + } + } else { + // Check each matched codemod's legacyPatterns for unmodified files + for (const lt of matchedTransforms) { + const found = lt.legacyPatterns.filter((p) => original.includes(p)); + if (found.length > 0) { + warnings.push( + `${relative}: contains ${found.join(", ")} but was not migrated automatically (rule: ${lt.id}). Manual migration may be needed.`, + ); } } } diff --git a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml index f03489f0a..41f0d66ed 100644 --- a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml @@ -40,6 +40,8 @@ jobs: steps: - id: tailor-checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 - id: tailor-merge-base if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork env: diff --git a/packages/sdk/src/cli/commands/setup/github/github.test.ts b/packages/sdk/src/cli/commands/setup/github/github.test.ts index 3758de065..ff3d310ef 100644 --- a/packages/sdk/src/cli/commands/setup/github/github.test.ts +++ b/packages/sdk/src/cli/commands/setup/github/github.test.ts @@ -130,6 +130,7 @@ describe("renderBranchWorkflow", () => { test("fetches the pull request base into the remote-tracking ref before merge", () => { const { content } = renderBranchWorkflow(branchBase); + expect(content).toContain("fetch-depth: 0"); expect(content).toContain('git fetch origin "$BASE_REF:refs/remotes/origin/$BASE_REF"'); expect(content).toContain('git merge --no-edit "origin/$BASE_REF"'); expect(content).not.toContain("git merge --no-commit"); From 8004d6820142b3dd5c7ddd6b6d4f10c49d4de58c Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 09:38:53 +0900 Subject: [PATCH 034/618] fix(codemod): preserve quoted yaml command bounds --- .../v2/cli-rename/scripts/transform.ts | 44 ++++++++++++++++++- .../cli-rename/tests/basic-yaml/expected.yml | 1 + .../v2/cli-rename/tests/basic-yaml/input.yml | 1 + packages/sdk-codemod/src/runner.test.ts | 2 + packages/sdk-codemod/src/runner.ts | 6 +-- 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index f88f539b1..494533601 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -36,6 +36,45 @@ function findInlineCodeSpanEnd(source: string, start: number): number | undefine return codeSpanEnd === -1 ? undefined : codeSpanEnd; } +function findEnclosingLineQuoteEnd(source: string, start: number): number | undefined { + const lineStart = source.lastIndexOf("\n", start - 1) + 1; + const lineEnd = source.indexOf("\n", start); + const limit = lineEnd === -1 ? source.length : lineEnd; + let quote: "'" | '"' | null = null; + + for (let index = lineStart; index < start; index += 1) { + const ch = source[index]; + if (quote !== null) { + if (ch === "\\" && quote === '"' && index + 1 < start) { + index += 1; + continue; + } + if (ch === quote) { + quote = null; + } + continue; + } + if (ch === "'" || ch === '"') { + quote = ch; + } + } + + if (quote === null) return undefined; + + for (let index = start; index < limit; index += 1) { + const ch = source[index]; + if (ch === "\\" && quote === '"' && index + 1 < limit) { + index += 1; + continue; + } + if (ch === quote) { + return index; + } + } + + return undefined; +} + function lineIndent(line: string): number { const match = line.match(/^ */); return match?.[0].length ?? 0; @@ -103,7 +142,10 @@ function findTailorCommandEnd( start: number, foldedYamlRanges?: FoldedYamlRange[], ): number { - const limit = findInlineCodeSpanEnd(source, start) ?? source.length; + const limit = Math.min( + findInlineCodeSpanEnd(source, start) ?? source.length, + findEnclosingLineQuoteEnd(source, start) ?? source.length, + ); const foldedYamlRange = findContainingFoldedYamlRange(foldedYamlRanges, start); const commandLimit = foldedYamlRange ? Math.min(limit, foldedYamlRange.end) : limit; let quote: "'" | '"' | null = null; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml index a38fcf830..c296b3623 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml @@ -13,3 +13,4 @@ jobs: pnpm exec tailor-sdk login --machine-user ci - run: "pnpm exec tailor-sdk query --machine-user ci" + - run: pnpm exec tailor-sdk workflow start approval --machine-user ci diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml index 4e5c8a35a..482d7dbcd 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml @@ -13,3 +13,4 @@ jobs: pnpm exec tailor-sdk login --machineuser ci - run: "pnpm exec tailor-sdk query --machineuser ci" + - run: pnpm exec tailor-sdk workflow start approval --machineuser ci diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 06aa29aad..13b64f117 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -184,6 +184,7 @@ describe("runCodemods", () => { tmpDir = dir; await fs.promises.writeFile(path.join(dir, "config.ts"), "hello", "utf-8"); await fs.promises.writeFile(path.join(dir, "data.json"), "world", "utf-8"); + using readFileSpy = vi.spyOn(fs.promises, "readFile"); const result = await runCodemods( [ @@ -199,6 +200,7 @@ describe("runCodemods", () => { // Only JSON file should be modified expect(result.filesModified).toHaveLength(1); expect(result.filesModified[0]).toContain("data.json"); + expect(readFileSpy).not.toHaveBeenCalledWith(path.join(dir, "config.ts"), "utf-8"); // TS file should be unchanged const tsContent = await fs.promises.readFile(path.join(dir, "config.ts"), "utf-8"); diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 4eb2afca5..e27a6de35 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -159,6 +159,9 @@ export async function runCodemods( if (seen.has(absolute)) continue; seen.add(absolute); + const matchedTransforms = loaded.filter((lt) => lt.matches(relative)); + if (matchedTransforms.length === 0) continue; + let original: string; try { original = await fs.promises.readFile(absolute, "utf-8"); @@ -167,10 +170,7 @@ export async function runCodemods( } let current = original; - const matchedTransforms: LoadedTransform[] = []; for (const lt of loaded) { - if (!lt.matches(relative)) continue; - matchedTransforms.push(lt); const result = await lt.transform(current, absolute); if (result != null) { current = result; From 0552f60f21f9c12858f5018ab9596dfba6aee8a4 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 09:51:45 +0900 Subject: [PATCH 035/618] fix(codemod): respect command migration boundaries --- .../v2/cli-rename/scripts/transform.ts | 26 ++++++++++---- .../tests/basic-markdown/expected.md | 2 ++ .../cli-rename/tests/basic-markdown/input.md | 2 ++ packages/sdk-codemod/src/runner.test.ts | 35 +++++++++++++++++++ packages/sdk-codemod/src/runner.ts | 2 +- 5 files changed, 60 insertions(+), 7 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index 494533601..a939ab326 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -23,6 +23,9 @@ interface FoldedYamlRange { end: number; } +const PROSE_BOUNDARY_PATTERN = + /^\s+(?:and|or|but|before|after|then|when|while|if|unless|because|so)\b/; + function isOptionBoundaryChar(value: string | undefined): boolean { return value === undefined || !/[\w-]/.test(value); } @@ -141,13 +144,21 @@ function findTailorCommandEnd( source: string, start: number, foldedYamlRanges?: FoldedYamlRange[], + stopAtProseBoundary = false, ): number { + const inlineCodeSpanEnd = findInlineCodeSpanEnd(source, start); + const enclosingLineQuoteEnd = findEnclosingLineQuoteEnd(source, start); const limit = Math.min( - findInlineCodeSpanEnd(source, start) ?? source.length, - findEnclosingLineQuoteEnd(source, start) ?? source.length, + inlineCodeSpanEnd ?? source.length, + enclosingLineQuoteEnd ?? source.length, ); const foldedYamlRange = findContainingFoldedYamlRange(foldedYamlRanges, start); const commandLimit = foldedYamlRange ? Math.min(limit, foldedYamlRange.end) : limit; + const shouldStopAtProseBoundary = + stopAtProseBoundary && + inlineCodeSpanEnd === undefined && + enclosingLineQuoteEnd === undefined && + foldedYamlRange === undefined; let quote: "'" | '"' | null = null; let end = start; @@ -175,6 +186,7 @@ function findTailorCommandEnd( const prev = source[end - 1]; if ((ch === ";" || ch === "&" || ch === "|") && prev !== "\\") break; if (ch === "\n" && prev !== "\\" && !foldedYamlRange) break; + if (shouldStopAtProseBoundary && PROSE_BOUNDARY_PATTERN.test(source.slice(end))) break; end += 1; } return end; @@ -233,6 +245,7 @@ function replaceOptionsInCommand(command: string): string { function replaceOptionsInTailorCommands( source: string, foldedYamlRanges?: FoldedYamlRange[], + stopAtProseBoundary = false, ): string { let updated = ""; let cursor = 0; @@ -245,7 +258,7 @@ function replaceOptionsInTailorCommands( const start = match.index; if (start < cursor) continue; - const end = findTailorCommandEnd(source, start, foldedYamlRanges); + const end = findTailorCommandEnd(source, start, foldedYamlRanges, stopAtProseBoundary); updated += source.slice(cursor, start); updated += replaceOptionsInCommand(source.slice(start, end)); cursor = end; @@ -255,19 +268,20 @@ function replaceOptionsInTailorCommands( return updated + source.slice(cursor); } -function replaceAll(value: string, parseFoldedYaml = false): string { +function replaceAll(value: string, parseFoldedYaml = false, stopAtProseBoundary = false): string { const updated = value.replace( COMMAND_PATTERN, (_match, ver: string | undefined, prefix: string, cmd: string) => `tailor-sdk${ver ?? ""}${prefix}${COMMAND_MAP.get(cmd) ?? cmd}`, ); const foldedYamlRanges = parseFoldedYaml ? findFoldedYamlRanges(updated) : undefined; - return replaceOptionsInTailorCommands(updated, foldedYamlRanges); + return replaceOptionsInTailorCommands(updated, foldedYamlRanges, stopAtProseBoundary); } function transformText(source: string, filePath: string): string | null { const ext = path.extname(filePath).toLowerCase(); - const updated = replaceAll(source, ext === ".yml" || ext === ".yaml"); + const isYaml = ext === ".yml" || ext === ".yaml"; + const updated = replaceAll(source, isYaml, ext === ".md"); return updated === source ? null : updated; } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md index eaabed9a9..b6cd05f2a 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md @@ -6,4 +6,6 @@ Use `tailor-sdk --json crashreport list` but leave `other-cli --machineuser=ci` Use `pnpm exec tailor-sdk login --machine-user` but leave `other-cli --machineuser=ci` alone. +Use tailor-sdk login --machine-user before running other-cli --machineuser=ci. + Do not rewrite unrelated commands such as `other-cli --machineuser=ci`. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md index 47dcdac7e..8be428c2f 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md @@ -6,4 +6,6 @@ Use `tailor-sdk --json crash-report list` but leave `other-cli --machineuser=ci` Use `pnpm exec tailor-sdk login --machineuser` but leave `other-cli --machineuser=ci` alone. +Use tailor-sdk login --machineuser before running other-cli --machineuser=ci. + Do not rewrite unrelated commands such as `other-cli --machineuser=ci`. diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 13b64f117..237237a68 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -164,6 +164,7 @@ describe("runCodemods", () => { describe("filePatterns filtering", () => { const transformPath = path.join(os.tmpdir(), "transform-upper.ts"); + const throwingTransformPath = path.join(os.tmpdir(), "transform-throw.ts"); beforeEach(async () => { await fs.promises.writeFile( @@ -173,10 +174,18 @@ describe("runCodemods", () => { }`, "utf-8", ); + await fs.promises.writeFile( + throwingTransformPath, + `export default function transform() { + throw new Error("nonmatching transform should not run"); + }`, + "utf-8", + ); }); afterEach(async () => { await fs.promises.rm(transformPath, { force: true }); + await fs.promises.rm(throwingTransformPath, { force: true }); }); test("should only apply transform to files matching filePatterns", async () => { @@ -211,6 +220,32 @@ describe("runCodemods", () => { expect(jsonContent).toBe("WORLD"); }); + test("should not run transforms whose filePatterns do not match a matched file", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-pattern-test-")); + tmpDir = dir; + await fs.promises.writeFile(path.join(dir, "data.json"), "world", "utf-8"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/upper", transformPath, ["**/*.json"]), + scriptPath: transformPath, + }, + { + codemod: makeCodemod("test/throw", throwingTransformPath, ["**/*.ts"]), + scriptPath: throwingTransformPath, + }, + ], + dir, + false, + ); + + expect(result.filesModified).toEqual([path.join(dir, "data.json")]); + await expect(fs.promises.readFile(path.join(dir, "data.json"), "utf-8")).resolves.toBe( + "WORLD", + ); + }); + test("should apply transforms to matching files under dot directories", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-dot-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index e27a6de35..16ae1a78a 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -170,7 +170,7 @@ export async function runCodemods( } let current = original; - for (const lt of loaded) { + for (const lt of matchedTransforms) { const result = await lt.transform(current, absolute); if (result != null) { current = result; From 69c0b62cc22e1fb0420222cd69b8522b61ac61db Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 10:14:13 +0900 Subject: [PATCH 036/618] fix(cli): guard v2 workflow and codemod edges --- docs/setup-github-contracts.md | 2 +- .../v2/apply-to-deploy/scripts/transform.ts | 11 ++--- .../apply-to-deploy/tests/no-match/input.sh | 3 ++ .../v2/cli-rename/scripts/transform.ts | 48 ++++++++++++------- .../tests/basic-markdown/expected.md | 2 +- .../v2/cli-rename/tests/no-match/input.sh | 4 ++ .../commands/setup/github/branch.workflow.yml | 28 ++++++----- .../cli/commands/setup/github/github.test.ts | 31 +++++++++++- .../commands/setup/github/tag.workflow.yml | 19 ++++---- .../cli/commands/setup/github/templates.ts | 4 +- 10 files changed, 102 insertions(+), 50 deletions(-) diff --git a/docs/setup-github-contracts.md b/docs/setup-github-contracts.md index fcd172e62..95635f6e3 100644 --- a/docs/setup-github-contracts.md +++ b/docs/setup-github-contracts.md @@ -60,9 +60,9 @@ status checks**. | `tailor-deploy/tailor-setup-node` | node projects | | `tailor-deploy/tailor-setup-bun` | bun projects only | | `tailor-deploy/tailor-install` | Package install | +| `tailor-deploy/tailor-validate-workspace` | Fails when `TAILOR_PLATFORM_WORKSPACE_ID` is empty | | `tailor-deploy/tailor-mask-credentials` | Masks machine-user credentials | | `tailor-deploy/tailor-login` | `tailor-sdk login --machine-user` | -| `tailor-deploy/tailor-validate-workspace` | Fails when `TAILOR_PLATFORM_WORKSPACE_ID` is empty | | `tailor-deploy/tailor-generate` | `tailor-sdk generate` | | `tailor-deploy/tailor-deploy` | `tailor-sdk deploy --yes` | diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts index 1f5f68996..0f792cec4 100644 --- a/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts @@ -10,16 +10,11 @@ const ARG_VALUE = `(?:[^\\s'"\`;&|]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`; const BOOLEAN_GLOBAL_ARG = "(?:--verbose|--json|-j)"; const VALUE_GLOBAL_ARG = "(?:--env-file|--env-file-if-exists|-e)"; const GLOBAL_ARG_PATTERN = `(?:(?:\\s+${BOOLEAN_GLOBAL_ARG})|(?:\\s+${VALUE_GLOBAL_ARG}(?:=${ARG_VALUE}|\\s+${ARG_VALUE})))*`; -const APPLY_PATTERN = new RegExp( - `\\btailor-sdk(@[^\\s'"\`]+)?(${GLOBAL_ARG_PATTERN}\\s+)apply(?![-\\w])`, - "g", -); +const TAILOR_BINARY = `(? `tailor-sdk${ver ?? ""}${prefix}deploy`, - ); + return value.replace(APPLY_PATTERN, (match) => `${match.slice(0, -"apply".length)}deploy`); } function transformText(source: string): string | null { diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/no-match/input.sh b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/no-match/input.sh index 004f8c9d8..9e883b8f5 100644 --- a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/no-match/input.sh +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/no-match/input.sh @@ -7,3 +7,6 @@ echo "How to apply this configuration" pnpm exec tailor-sdk apply-foo # Should not match: word continuation pnpm exec tailor-sdk applyConfig +# Should not match: wrapper or unrelated binary names +tailor-sdk-wrapper apply --yes +my-tailor-sdk apply --yes diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index a939ab326..9d8f66af7 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -10,11 +10,12 @@ const ARG_VALUE = `(?:[^\\s'"\`;&|]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`; const BOOLEAN_GLOBAL_ARG = "(?:--verbose|--json|-j)"; const VALUE_GLOBAL_ARG = "(?:--env-file|--env-file-if-exists|-e)"; const GLOBAL_ARG_PATTERN = `(?:(?:\\s+${BOOLEAN_GLOBAL_ARG})|(?:\\s+${VALUE_GLOBAL_ARG}(?:=${ARG_VALUE}|\\s+${ARG_VALUE})))*`; +const TAILOR_BINARY = `(? from).join("|")})\\b`, + `${TAILOR_BINARY}(${GLOBAL_ARG_PATTERN}\\s+)(${COMMAND_RENAMES.map(([from]) => from).join("|")})\\b`, "g", ); -const TAILOR_BINARY_PATTERN = /\btailor-sdk(?:@[^\s'"`]+)?/g; +const TAILOR_BINARY_PATTERN = new RegExp(TAILOR_BINARY, "g"); const COMMAND_MAP = new Map(COMMAND_RENAMES); @@ -23,9 +24,6 @@ interface FoldedYamlRange { end: number; } -const PROSE_BOUNDARY_PATTERN = - /^\s+(?:and|or|but|before|after|then|when|while|if|unless|because|so)\b/; - function isOptionBoundaryChar(value: string | undefined): boolean { return value === undefined || !/[\w-]/.test(value); } @@ -144,7 +142,6 @@ function findTailorCommandEnd( source: string, start: number, foldedYamlRanges?: FoldedYamlRange[], - stopAtProseBoundary = false, ): number { const inlineCodeSpanEnd = findInlineCodeSpanEnd(source, start); const enclosingLineQuoteEnd = findEnclosingLineQuoteEnd(source, start); @@ -154,11 +151,6 @@ function findTailorCommandEnd( ); const foldedYamlRange = findContainingFoldedYamlRange(foldedYamlRanges, start); const commandLimit = foldedYamlRange ? Math.min(limit, foldedYamlRange.end) : limit; - const shouldStopAtProseBoundary = - stopAtProseBoundary && - inlineCodeSpanEnd === undefined && - enclosingLineQuoteEnd === undefined && - foldedYamlRange === undefined; let quote: "'" | '"' | null = null; let end = start; @@ -186,12 +178,23 @@ function findTailorCommandEnd( const prev = source[end - 1]; if ((ch === ";" || ch === "&" || ch === "|") && prev !== "\\") break; if (ch === "\n" && prev !== "\\" && !foldedYamlRange) break; - if (shouldStopAtProseBoundary && PROSE_BOUNDARY_PATTERN.test(source.slice(end))) break; end += 1; } return end; } +function isDelimitedCommandContext( + source: string, + start: number, + foldedYamlRanges?: FoldedYamlRange[], +): boolean { + return ( + findInlineCodeSpanEnd(source, start) !== undefined || + findEnclosingLineQuoteEnd(source, start) !== undefined || + findContainingFoldedYamlRange(foldedYamlRanges, start) !== undefined + ); +} + function findOptionRename(command: string, index: number): readonly [string, string] | undefined { return OPTION_RENAMES.find( ([from]) => @@ -245,7 +248,7 @@ function replaceOptionsInCommand(command: string): string { function replaceOptionsInTailorCommands( source: string, foldedYamlRanges?: FoldedYamlRange[], - stopAtProseBoundary = false, + requireDelimitedContext = false, ): string { let updated = ""; let cursor = 0; @@ -258,7 +261,12 @@ function replaceOptionsInTailorCommands( const start = match.index; if (start < cursor) continue; - const end = findTailorCommandEnd(source, start, foldedYamlRanges, stopAtProseBoundary); + if (requireDelimitedContext && !isDelimitedCommandContext(source, start, foldedYamlRanges)) { + TAILOR_BINARY_PATTERN.lastIndex = start + match[0].length; + continue; + } + + const end = findTailorCommandEnd(source, start, foldedYamlRanges); updated += source.slice(cursor, start); updated += replaceOptionsInCommand(source.slice(start, end)); cursor = end; @@ -268,14 +276,18 @@ function replaceOptionsInTailorCommands( return updated + source.slice(cursor); } -function replaceAll(value: string, parseFoldedYaml = false, stopAtProseBoundary = false): string { +function replaceAll( + value: string, + parseFoldedYaml = false, + requireDelimitedContext = false, +): string { const updated = value.replace( COMMAND_PATTERN, - (_match, ver: string | undefined, prefix: string, cmd: string) => - `tailor-sdk${ver ?? ""}${prefix}${COMMAND_MAP.get(cmd) ?? cmd}`, + (match, _prefix: string, cmd: string) => + `${match.slice(0, -cmd.length)}${COMMAND_MAP.get(cmd) ?? cmd}`, ); const foldedYamlRanges = parseFoldedYaml ? findFoldedYamlRanges(updated) : undefined; - return replaceOptionsInTailorCommands(updated, foldedYamlRanges, stopAtProseBoundary); + return replaceOptionsInTailorCommands(updated, foldedYamlRanges, requireDelimitedContext); } function transformText(source: string, filePath: string): string | null { diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md index b6cd05f2a..a9015907a 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md @@ -6,6 +6,6 @@ Use `tailor-sdk --json crashreport list` but leave `other-cli --machineuser=ci` Use `pnpm exec tailor-sdk login --machine-user` but leave `other-cli --machineuser=ci` alone. -Use tailor-sdk login --machine-user before running other-cli --machineuser=ci. +Use tailor-sdk login --machineuser before running other-cli --machineuser=ci. Do not rewrite unrelated commands such as `other-cli --machineuser=ci`. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh index 6043b350c..c1fe89d8b 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh @@ -3,6 +3,8 @@ set -euo pipefail # Should not match: command name is part of a longer word pnpm exec tailor-sdk crash-reporter list +tailor-sdk-wrapper crash-report list +my-tailor-sdk crash-report list # Should not match: bare crash-report not preceded by tailor-sdk echo "Generated crash-report uploaded" # Should not match: positional/long-form camelCase identifiers are out of scope @@ -12,5 +14,7 @@ pnpm exec tailor-sdk executor jobs my-executor --jobId xyz pnpm exec tailor-sdk login --machineusername ci # Should not match: same option spelling for another command other-cli --machineuser=ci +tailor-sdk-wrapper --machineuser ci +my-tailor-sdk --machineuser ci # Should not match: option spelling inside quoted arguments tailor-sdk query --query 'select --machineuser' diff --git a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml index 41f0d66ed..80f0bc9fd 100644 --- a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml @@ -65,7 +65,9 @@ jobs: fi - id: tailor-mask-credentials # Fork PRs cannot read secrets; the checks above still run for them. - if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork + if: >- + (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) && + vars.TAILOR_PLATFORM_WORKSPACE_ID != '' run: | echo "::add-mask::$CLIENT_ID" echo "::add-mask::$CLIENT_SECRET" @@ -73,14 +75,18 @@ jobs: CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} - id: tailor-login - if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork + if: >- + (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) && + vars.TAILOR_PLATFORM_WORKSPACE_ID != '' # __WORKING_DIRECTORY__ run: __PM_EXEC__ tailor-sdk login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} - id: tailor-plan - if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork + if: >- + (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) && + vars.TAILOR_PLATFORM_WORKSPACE_ID != '' # __WORKING_DIRECTORY__ run: | if [ -z "$TAILOR_PLATFORM_WORKSPACE_ID" ]; then @@ -255,6 +261,14 @@ jobs: - id: tailor-checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # __SETUP_STEPS__ + - id: tailor-validate-workspace + env: + WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} + run: | + if [ -z "$WORKSPACE_ID" ]; then + echo "::error::Workspace is not provisioned: TAILOR_PLATFORM_WORKSPACE_ID is empty. Provision the workspace and set the variable." + exit 1 + fi - id: tailor-mask-credentials run: | echo "::add-mask::$CLIENT_ID" @@ -268,14 +282,6 @@ jobs: env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} - - id: tailor-validate-workspace - env: - WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} - run: | - if [ -z "$WORKSPACE_ID" ]; then - echo "::error::Workspace is not provisioned: TAILOR_PLATFORM_WORKSPACE_ID is empty. Provision the workspace and set the variable." - exit 1 - fi - id: tailor-generate # __WORKING_DIRECTORY__ run: __PM_EXEC__ tailor-sdk generate diff --git a/packages/sdk/src/cli/commands/setup/github/github.test.ts b/packages/sdk/src/cli/commands/setup/github/github.test.ts index ff3d310ef..2aea212fd 100644 --- a/packages/sdk/src/cli/commands/setup/github/github.test.ts +++ b/packages/sdk/src/cli/commands/setup/github/github.test.ts @@ -177,8 +177,22 @@ describe("renderBranchWorkflow", () => { test("includes the fork guard on the plan step", () => { const { content } = renderBranchWorkflow(branchBase); expect(content).toContain( - "if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork", + "(github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) &&", ); + expect(content).toContain("vars.TAILOR_PLATFORM_WORKSPACE_ID != ''"); + }); + + test("skips plan authentication and dry-run until the workspace is provisioned", () => { + const branch = renderBranchWorkflow(branchBase).content; + const tag = renderTagWorkflow(tagBase).content; + for (const content of [branch, tag]) { + for (const stepId of ["tailor-mask-credentials", "tailor-login", "tailor-plan"]) { + const start = content.indexOf(`- id: ${stepId}`); + const end = content.indexOf("\n - id:", start + 1); + const step = content.slice(start, end === -1 ? undefined : end); + expect(step).toContain("vars.TAILOR_PLATFORM_WORKSPACE_ID != ''"); + } + } }); test("sets cancel-in-progress: false on the deploy concurrency group", () => { @@ -198,6 +212,21 @@ describe("renderBranchWorkflow", () => { expect(content).not.toContain("tailor-sdk apply"); }); + test("validates deploy workspace before using machine-user credentials", () => { + for (const content of [ + renderBranchWorkflow(branchBase).content, + renderTagWorkflow(tagBase).content, + ]) { + const deployJob = content.slice(content.indexOf("tailor-deploy:")); + expect(deployJob.indexOf("id: tailor-validate-workspace")).toBeLessThan( + deployJob.indexOf("id: tailor-mask-credentials"), + ); + expect(deployJob.indexOf("id: tailor-validate-workspace")).toBeLessThan( + deployJob.indexOf("id: tailor-login"), + ); + } + }); + test("emits PM exec prefix for generate", () => { expect(renderBranchWorkflow(branchBase).content).toContain( "run: pnpm exec tailor-sdk generate", diff --git a/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml b/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml index 9add5ff13..a15b824b4 100644 --- a/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/tag.workflow.yml @@ -65,6 +65,7 @@ jobs: exit 1 fi - id: tailor-mask-credentials + if: vars.TAILOR_PLATFORM_WORKSPACE_ID != '' run: | echo "::add-mask::$CLIENT_ID" echo "::add-mask::$CLIENT_SECRET" @@ -72,12 +73,14 @@ jobs: CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} - id: tailor-login + if: vars.TAILOR_PLATFORM_WORKSPACE_ID != '' # __WORKING_DIRECTORY__ run: __PM_EXEC__ tailor-sdk login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} - id: tailor-plan + if: vars.TAILOR_PLATFORM_WORKSPACE_ID != '' # __WORKING_DIRECTORY__ run: | if [ -z "$TAILOR_PLATFORM_WORKSPACE_ID" ]; then @@ -178,6 +181,14 @@ jobs: - id: tailor-checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # __SETUP_STEPS__ + - id: tailor-validate-workspace + env: + WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} + run: | + if [ -z "$WORKSPACE_ID" ]; then + echo "::error::Workspace is not provisioned: TAILOR_PLATFORM_WORKSPACE_ID is empty. Provision the workspace and set the variable." + exit 1 + fi - id: tailor-mask-credentials run: | echo "::add-mask::$CLIENT_ID" @@ -191,14 +202,6 @@ jobs: env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }} - - id: tailor-validate-workspace - env: - WORKSPACE_ID: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} - run: | - if [ -z "$WORKSPACE_ID" ]; then - echo "::error::Workspace is not provisioned: TAILOR_PLATFORM_WORKSPACE_ID is empty. Provision the workspace and set the variable." - exit 1 - fi - id: tailor-generate # __WORKING_DIRECTORY__ run: __PM_EXEC__ tailor-sdk generate diff --git a/packages/sdk/src/cli/commands/setup/github/templates.ts b/packages/sdk/src/cli/commands/setup/github/templates.ts index 916562dc1..b9981859a 100644 --- a/packages/sdk/src/cli/commands/setup/github/templates.ts +++ b/packages/sdk/src/cli/commands/setup/github/templates.ts @@ -207,9 +207,9 @@ export function renderBranchWorkflow(params: RenderBranchParams): RenderResult { "tailor-deploy", "tailor-deploy/tailor-checkout", ...setupIds("tailor-deploy", packageManager), + "tailor-deploy/tailor-validate-workspace", "tailor-deploy/tailor-mask-credentials", "tailor-deploy/tailor-login", - "tailor-deploy/tailor-validate-workspace", "tailor-deploy/tailor-generate", "tailor-deploy/tailor-deploy", ); @@ -267,9 +267,9 @@ export function renderTagWorkflow(params: RenderTagParams): RenderResult { "tailor-deploy", "tailor-deploy/tailor-checkout", ...setupIds("tailor-deploy", packageManager), + "tailor-deploy/tailor-validate-workspace", "tailor-deploy/tailor-mask-credentials", "tailor-deploy/tailor-login", - "tailor-deploy/tailor-validate-workspace", "tailor-deploy/tailor-generate", "tailor-deploy/tailor-deploy", ); From b728d0479a5ca9977cd93d1d8696b8a5669ef1ae Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 10:26:09 +0900 Subject: [PATCH 037/618] fix(codemod): migrate markdown fenced CLI options --- .../v2/cli-rename/scripts/transform.ts | 85 +++++++++++++++---- .../tests/basic-markdown/expected.md | 5 ++ .../cli-rename/tests/basic-markdown/input.md | 5 ++ 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index 9d8f66af7..508c81893 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -19,7 +19,7 @@ const TAILOR_BINARY_PATTERN = new RegExp(TAILOR_BINARY, "g"); const COMMAND_MAP = new Map(COMMAND_RENAMES); -interface FoldedYamlRange { +interface TextRange { start: number; end: number; } @@ -85,8 +85,8 @@ function isFoldedScalarHeader(line: string): boolean { return /^\s*(?:-\s*)?[^#\n]*:\s*>[+-]?(?:\s*(?:#.*)?)?$/.test(line); } -function findFoldedYamlRanges(source: string): FoldedYamlRange[] { - const ranges: FoldedYamlRange[] = []; +function findFoldedYamlRanges(source: string): TextRange[] { + const ranges: TextRange[] = []; const lines = source.match(/^.*(?:\n|$)/gm) ?? []; let offset = 0; @@ -131,17 +131,52 @@ function findFoldedYamlRanges(source: string): FoldedYamlRange[] { return ranges; } -function findContainingFoldedYamlRange( - ranges: FoldedYamlRange[] | undefined, +function findMarkdownFencedCodeRanges(source: string): TextRange[] { + const ranges: TextRange[] = []; + const lines = source.match(/^.*(?:\n|$)/gm) ?? []; + let offset = 0; + let open: { char: string; length: number; start: number } | undefined; + + for (const line of lines) { + const body = line.replace(/\r?\n$/, ""); + + if (open) { + const close = body.match(/^ {0,3}(`{3,}|~{3,})\s*$/); + const marker = close?.[1]; + if (marker && marker[0] === open.char && marker.length >= open.length) { + ranges.push({ start: open.start, end: offset }); + open = undefined; + } + } else { + const start = body.match(/^ {0,3}(`{3,}|~{3,}).*$/); + const marker = start?.[1]; + if (marker) { + open = { char: marker[0], length: marker.length, start: offset + line.length }; + } + } + + offset += line.length; + } + + if (open) { + ranges.push({ start: open.start, end: source.length }); + } + + return ranges; +} + +function findContainingRange( + ranges: TextRange[] | undefined, index: number, -): FoldedYamlRange | undefined { +): TextRange | undefined { return ranges?.find((range) => range.start <= index && index < range.end); } function findTailorCommandEnd( source: string, start: number, - foldedYamlRanges?: FoldedYamlRange[], + foldedYamlRanges?: TextRange[], + markdownFencedCodeRanges?: TextRange[], ): number { const inlineCodeSpanEnd = findInlineCodeSpanEnd(source, start); const enclosingLineQuoteEnd = findEnclosingLineQuoteEnd(source, start); @@ -149,8 +184,10 @@ function findTailorCommandEnd( inlineCodeSpanEnd ?? source.length, enclosingLineQuoteEnd ?? source.length, ); - const foldedYamlRange = findContainingFoldedYamlRange(foldedYamlRanges, start); - const commandLimit = foldedYamlRange ? Math.min(limit, foldedYamlRange.end) : limit; + const foldedYamlRange = findContainingRange(foldedYamlRanges, start); + const markdownFencedCodeRange = findContainingRange(markdownFencedCodeRanges, start); + const delimitedRange = foldedYamlRange ?? markdownFencedCodeRange; + const commandLimit = delimitedRange ? Math.min(limit, delimitedRange.end) : limit; let quote: "'" | '"' | null = null; let end = start; @@ -186,12 +223,14 @@ function findTailorCommandEnd( function isDelimitedCommandContext( source: string, start: number, - foldedYamlRanges?: FoldedYamlRange[], + foldedYamlRanges?: TextRange[], + markdownFencedCodeRanges?: TextRange[], ): boolean { return ( findInlineCodeSpanEnd(source, start) !== undefined || findEnclosingLineQuoteEnd(source, start) !== undefined || - findContainingFoldedYamlRange(foldedYamlRanges, start) !== undefined + findContainingRange(foldedYamlRanges, start) !== undefined || + findContainingRange(markdownFencedCodeRanges, start) !== undefined ); } @@ -247,8 +286,9 @@ function replaceOptionsInCommand(command: string): string { function replaceOptionsInTailorCommands( source: string, - foldedYamlRanges?: FoldedYamlRange[], + foldedYamlRanges?: TextRange[], requireDelimitedContext = false, + markdownFencedCodeRanges?: TextRange[], ): string { let updated = ""; let cursor = 0; @@ -261,12 +301,15 @@ function replaceOptionsInTailorCommands( const start = match.index; if (start < cursor) continue; - if (requireDelimitedContext && !isDelimitedCommandContext(source, start, foldedYamlRanges)) { + if ( + requireDelimitedContext && + !isDelimitedCommandContext(source, start, foldedYamlRanges, markdownFencedCodeRanges) + ) { TAILOR_BINARY_PATTERN.lastIndex = start + match[0].length; continue; } - const end = findTailorCommandEnd(source, start, foldedYamlRanges); + const end = findTailorCommandEnd(source, start, foldedYamlRanges, markdownFencedCodeRanges); updated += source.slice(cursor, start); updated += replaceOptionsInCommand(source.slice(start, end)); cursor = end; @@ -280,6 +323,7 @@ function replaceAll( value: string, parseFoldedYaml = false, requireDelimitedContext = false, + parseMarkdownFencedCode = false, ): string { const updated = value.replace( COMMAND_PATTERN, @@ -287,13 +331,22 @@ function replaceAll( `${match.slice(0, -cmd.length)}${COMMAND_MAP.get(cmd) ?? cmd}`, ); const foldedYamlRanges = parseFoldedYaml ? findFoldedYamlRanges(updated) : undefined; - return replaceOptionsInTailorCommands(updated, foldedYamlRanges, requireDelimitedContext); + const markdownFencedCodeRanges = parseMarkdownFencedCode + ? findMarkdownFencedCodeRanges(updated) + : undefined; + return replaceOptionsInTailorCommands( + updated, + foldedYamlRanges, + requireDelimitedContext, + markdownFencedCodeRanges, + ); } function transformText(source: string, filePath: string): string | null { const ext = path.extname(filePath).toLowerCase(); const isYaml = ext === ".yml" || ext === ".yaml"; - const updated = replaceAll(source, isYaml, ext === ".md"); + const isMarkdown = ext === ".md"; + const updated = replaceAll(source, isYaml, isMarkdown, isMarkdown); return updated === source ? null : updated; } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md index a9015907a..9419f992e 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/expected.md @@ -6,6 +6,11 @@ Use `tailor-sdk --json crashreport list` but leave `other-cli --machineuser=ci` Use `pnpm exec tailor-sdk login --machine-user` but leave `other-cli --machineuser=ci` alone. +```sh +tailor-sdk login --machine-user +tailor-sdk query --machine-user=ci +``` + Use tailor-sdk login --machineuser before running other-cli --machineuser=ci. Do not rewrite unrelated commands such as `other-cli --machineuser=ci`. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md index 8be428c2f..e68df44b3 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-markdown/input.md @@ -6,6 +6,11 @@ Use `tailor-sdk --json crash-report list` but leave `other-cli --machineuser=ci` Use `pnpm exec tailor-sdk login --machineuser` but leave `other-cli --machineuser=ci` alone. +```sh +tailor-sdk login --machineuser +tailor-sdk query --machineuser=ci +``` + Use tailor-sdk login --machineuser before running other-cli --machineuser=ci. Do not rewrite unrelated commands such as `other-cli --machineuser=ci`. From 2029f2ff45bb5ba36ace94be971ffe0e39d0d80d Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 10:41:07 +0900 Subject: [PATCH 038/618] fix(codemod): handle escaped CLI option quotes --- .../v2/cli-rename/scripts/transform.ts | 51 ++++++++++++++++--- .../cli-rename/tests/basic-yaml/expected.yml | 1 + .../v2/cli-rename/tests/basic-yaml/input.yml | 1 + 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index 508c81893..8e5e18ad6 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -24,6 +24,11 @@ interface TextRange { end: number; } +interface ActiveQuote { + char: "'" | '"'; + escaped: boolean; +} + function isOptionBoundaryChar(value: string | undefined): boolean { return value === undefined || !/[\w-]/.test(value); } @@ -188,26 +193,41 @@ function findTailorCommandEnd( const markdownFencedCodeRange = findContainingRange(markdownFencedCodeRanges, start); const delimitedRange = foldedYamlRange ?? markdownFencedCodeRange; const commandLimit = delimitedRange ? Math.min(limit, delimitedRange.end) : limit; - let quote: "'" | '"' | null = null; + let quote: ActiveQuote | null = null; let end = start; while (end < commandLimit) { const ch = source[end]; if (quote !== null) { - if (ch === "\\" && quote === '"' && end + 1 < commandLimit) { + if (quote.escaped) { + if (ch === "\\" && source[end + 1] === quote.char) { + quote = null; + end += 2; + continue; + } + end += 1; + continue; + } + if (ch === "\\" && quote.char === '"' && end + 1 < commandLimit) { end += 2; continue; } - if (ch === quote) { + if (ch === quote.char) { quote = null; } end += 1; continue; } + if (ch === "\\" && source[end + 1] === '"') { + quote = { char: '"', escaped: true }; + end += 2; + continue; + } + if (ch === "'" || ch === '"') { - quote = ch; + quote = { char: ch, escaped: false }; end += 1; continue; } @@ -246,25 +266,40 @@ function findOptionRename(command: string, index: number): readonly [string, str function replaceOptionsInCommand(command: string): string { let updated = ""; let index = 0; - let quote: "'" | '"' | null = null; + let quote: ActiveQuote | null = null; while (index < command.length) { const ch = command[index]; if (quote !== null) { updated += ch; - if (ch === "\\" && quote === '"' && index + 1 < command.length) { + if (quote.escaped) { + if (ch === "\\" && command[index + 1] === quote.char) { + index += 1; + updated += command[index]; + quote = null; + } + } else if (ch === "\\" && quote.char === '"' && index + 1 < command.length) { index += 1; updated += command[index]; - } else if (ch === quote) { + } else if (ch === quote.char) { quote = null; } index += 1; continue; } + if (ch === "\\" && command[index + 1] === '"') { + quote = { char: '"', escaped: true }; + updated += ch; + index += 1; + updated += command[index]; + index += 1; + continue; + } + if (ch === "'" || ch === '"') { - quote = ch; + quote = { char: ch, escaped: false }; updated += ch; index += 1; continue; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml index c296b3623..af33e2c9b 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml @@ -13,4 +13,5 @@ jobs: pnpm exec tailor-sdk login --machine-user ci - run: "pnpm exec tailor-sdk query --machine-user ci" + - run: "tailor-sdk query --query \"select 1\" --machine-user ci" - run: pnpm exec tailor-sdk workflow start approval --machine-user ci diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml index 482d7dbcd..eebb22e0b 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml @@ -13,4 +13,5 @@ jobs: pnpm exec tailor-sdk login --machineuser ci - run: "pnpm exec tailor-sdk query --machineuser ci" + - run: "tailor-sdk query --query \"select 1\" --machineuser ci" - run: pnpm exec tailor-sdk workflow start approval --machineuser ci From 3e6d582a37d83a42302339cc4aea1d3dd11e8a81 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 16 Jun 2026 10:58:59 +0900 Subject: [PATCH 039/618] ci: set up v2 release workflow and rc prerelease line Enter changesets pre mode (next tag) and bootstrap the v2 major line so the first prerelease is 2.0.0-next.0. Extend the release and CI workflows to run on the v2 branch: v2 publishes prereleases to the next dist-tag while main keeps cutting stable releases. Generalize the changeset check to any base branch. --- .changeset/pre.json | 10 ++++++++++ .changeset/v2-baseline.md | 5 +++++ .github/workflows/changeset-check.yml | 6 ++++-- .github/workflows/check-license.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/lockfile-audit.yml | 2 +- .github/workflows/pkg-pr-new.yml | 2 +- .github/workflows/release.yml | 1 + .github/workflows/sdk-metrics.yml | 3 +-- .github/workflows/skills-sync-check.yml | 2 +- .github/workflows/template-tests.yml | 2 +- .github/workflows/test.yml | 2 +- 12 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 .changeset/pre.json create mode 100644 .changeset/v2-baseline.md diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000..67982f8b0 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,10 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "@tailor-platform/create-sdk": "1.64.0", + "@tailor-platform/sdk": "1.64.0", + "@tailor-platform/sdk-codemod": "0.2.7" + }, + "changesets": [] +} diff --git a/.changeset/v2-baseline.md b/.changeset/v2-baseline.md new file mode 100644 index 000000000..8f2e53ac9 --- /dev/null +++ b/.changeset/v2-baseline.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Start the v2 release line. v2 introduces breaking changes to the SDK API and CLI; run `tailor-sdk upgrade` to apply the bundled codemods when migrating. Prereleases are published to the `next` dist-tag — install with `@tailor-platform/sdk@next`. diff --git a/.github/workflows/changeset-check.yml b/.github/workflows/changeset-check.yml index f7afef374..9cde457db 100644 --- a/.github/workflows/changeset-check.yml +++ b/.github/workflows/changeset-check.yml @@ -14,7 +14,7 @@ jobs: if: >- github.event.pull_request.user.login != 'renovate[bot]' && - github.event.pull_request.head.ref != 'changeset-release/main' && + !startsWith(github.event.pull_request.head.ref, 'changeset-release/') && !contains(github.event.pull_request.labels.*.name, 'skip-changeset') steps: @@ -54,4 +54,6 @@ jobs: - name: Validate changeset packages if: steps.filter.outputs.changeset == 'true' - run: pnpm changeset status --since origin/main + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: pnpm changeset status --since "origin/$BASE_REF" diff --git a/.github/workflows/check-license.yml b/.github/workflows/check-license.yml index 1c28f5533..ffe4af5bb 100644 --- a/.github/workflows/check-license.yml +++ b/.github/workflows/check-license.yml @@ -2,7 +2,7 @@ name: Check Licenses on: push: - branches: [main] + branches: [main, v2] paths: - "pnpm-lock.yaml" - "scripts/check-license.js" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b36a60970..c7f4be165 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: Lint / Format / Type Check on: push: - branches: [main] + branches: [main, v2] pull_request: concurrency: diff --git a/.github/workflows/lockfile-audit.yml b/.github/workflows/lockfile-audit.yml index d0158f9e4..273ebd7c5 100644 --- a/.github/workflows/lockfile-audit.yml +++ b/.github/workflows/lockfile-audit.yml @@ -7,7 +7,7 @@ name: Lockfile Audit # and picomatch). This gate fails such PRs before they reach main. on: push: - branches: [main] + branches: [main, v2] paths: - pnpm-lock.yaml pull_request: diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml index 600cb96c4..7e7ac8fd4 100644 --- a/.github/workflows/pkg-pr-new.yml +++ b/.github/workflows/pkg-pr-new.yml @@ -1,7 +1,7 @@ name: Publish Any Commit on: push: - branches: [main] + branches: [main, v2] pull_request: paths: - packages/** diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b73a67957..5dbdb99ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - v2 concurrency: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/sdk-metrics.yml b/.github/workflows/sdk-metrics.yml index 41a4d17a0..6b8f2158f 100644 --- a/.github/workflows/sdk-metrics.yml +++ b/.github/workflows/sdk-metrics.yml @@ -2,14 +2,13 @@ name: SDK Metrics on: pull_request: - branches: [main] paths: - packages/** - package.json - pnpm-lock.yaml - .github/workflows/sdk-metrics.yml push: - branches: [main] + branches: [main, v2] workflow_dispatch: permissions: diff --git a/.github/workflows/skills-sync-check.yml b/.github/workflows/skills-sync-check.yml index 148e8b79b..f3e8ce687 100644 --- a/.github/workflows/skills-sync-check.yml +++ b/.github/workflows/skills-sync-check.yml @@ -6,7 +6,7 @@ on: - "packages/sdk/agent-skills/**" - ".github/workflows/skills-sync-check.yml" push: - branches: [main] + branches: [main, v2] paths: - "packages/sdk/agent-skills/**" diff --git a/.github/workflows/template-tests.yml b/.github/workflows/template-tests.yml index 3a205a88d..e178d64fe 100644 --- a/.github/workflows/template-tests.yml +++ b/.github/workflows/template-tests.yml @@ -2,7 +2,7 @@ name: Template Tests on: push: - branches: [main] + branches: [main, v2] pull_request: paths: - packages/** diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3e6726ecb..7293d6596 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: Test on: push: - branches: [main] + branches: [main, v2] pull_request: concurrency: From 4b32fa882e04ff1eb83a56541a762a6b96a4cf92 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 11:00:30 +0900 Subject: [PATCH 040/618] fix(codemod): bound command substitution rewrites --- .../sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts | 2 +- .../sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index 8e5e18ad6..b11c90fb1 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -233,7 +233,7 @@ function findTailorCommandEnd( } const prev = source[end - 1]; - if ((ch === ";" || ch === "&" || ch === "|") && prev !== "\\") break; + if ((ch === ";" || ch === "&" || ch === "|" || ch === ")") && prev !== "\\") break; if (ch === "\n" && prev !== "\\" && !foldedYamlRange) break; end += 1; } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh index c1fe89d8b..4ad7faf25 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh @@ -16,5 +16,7 @@ pnpm exec tailor-sdk login --machineusername ci other-cli --machineuser=ci tailor-sdk-wrapper --machineuser ci my-tailor-sdk --machineuser ci +# Should not match: same option spelling after a Tailor command substitution +TOKEN=$(tailor-sdk machineuser token ci) other-cli --machineuser=ci # Should not match: option spelling inside quoted arguments tailor-sdk query --query 'select --machineuser' From 8d1d72d7bd2dd07043dc8bcc07bb95cf6a9b6fda Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 11:17:57 +0900 Subject: [PATCH 041/618] fix(cli): harden v2 cleanup edge cases --- packages/sdk-codemod/src/runner.test.ts | 59 ++++++++++++++++++- packages/sdk-codemod/src/runner.ts | 26 ++++---- .../commands/setup/github/branch.workflow.yml | 6 +- .../cli/commands/setup/github/github.test.ts | 6 ++ 4 files changed, 85 insertions(+), 12 deletions(-) diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 237237a68..181573d61 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -21,7 +21,12 @@ async function createTestProject( return { tmpDir, filePath }; } -function makeCodemod(id: string, scriptPath: string, filePatterns?: string[]): CodemodPackage { +function makeCodemod( + id: string, + scriptPath: string, + filePatterns?: string[], + legacyPatterns?: string[], +): CodemodPackage { return { id, name: id, @@ -30,6 +35,7 @@ function makeCodemod(id: string, scriptPath: string, filePatterns?: string[]): C until: "2.0.0", scriptPath, filePatterns, + legacyPatterns, }; } @@ -268,4 +274,55 @@ describe("runCodemods", () => { await expect(fs.promises.readFile(workflowPath, "utf-8")).resolves.toBe("HELLO"); }); }); + + describe("legacy pattern warnings", () => { + const partialTransformPath = path.join(os.tmpdir(), "transform-partial.ts"); + + beforeEach(async () => { + await fs.promises.writeFile( + partialTransformPath, + `export default function transform(source) { + return source.replaceAll("tailor-sdk crash-report", "tailor-sdk crashreport"); + }`, + "utf-8", + ); + }); + + afterEach(async () => { + await fs.promises.rm(partialTransformPath, { force: true }); + }); + + test("warns when legacy patterns remain after a partial migration", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "README.md"), + "Run `tailor-sdk crash-report list`.\nRun tailor-sdk login --machineuser.\n", + "utf-8", + ); + + using _stderrSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true); + + const result = await runCodemods( + [ + { + codemod: makeCodemod( + "test/partial", + partialTransformPath, + ["**/*.md"], + ["tailor-sdk crash-report", "--machineuser"], + ), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.filesModified).toEqual([path.join(dir, "README.md")]); + expect(result.warnings).toEqual([ + "README.md: contains --machineuser but was not migrated automatically (rule: test/partial). Manual migration may be needed.", + ]); + }); + }); }); diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 16ae1a78a..ee3044e77 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -121,6 +121,20 @@ interface LoadedTransform { legacyPatterns: string[]; } +function legacyPatternWarnings( + relative: string, + content: string, + transforms: LoadedTransform[], +): string[] { + return transforms.flatMap((lt) => { + const found = lt.legacyPatterns.filter((p) => content.includes(p)); + if (found.length === 0) return []; + return [ + `${relative}: contains ${found.join(", ")} but was not migrated automatically (rule: ${lt.id}). Manual migration may be needed.`, + ]; + }); +} + /** * Run multiple codemods on a project directory using in-memory chaining. * Each file is processed through all transforms whose filePatterns match it. @@ -185,17 +199,9 @@ export async function runCodemods( } else { await fs.promises.writeFile(absolute, current, "utf-8"); } - } else { - // Check each matched codemod's legacyPatterns for unmodified files - for (const lt of matchedTransforms) { - const found = lt.legacyPatterns.filter((p) => original.includes(p)); - if (found.length > 0) { - warnings.push( - `${relative}: contains ${found.join(", ")} but was not migrated automatically (rule: ${lt.id}). Manual migration may be needed.`, - ); - } - } } + + warnings.push(...legacyPatternWarnings(relative, current, matchedTransforms)); } return { diff --git a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml index 80f0bc9fd..7f07a36e6 100644 --- a/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/branch.workflow.yml @@ -219,7 +219,11 @@ jobs: issue_number: context.issue.number, per_page: 100, }); - const existingComment = comments.find(comment => comment.body?.includes(marker)); + const existingComment = comments.find(comment => + comment.body?.includes(marker) && + comment.user?.type === 'Bot' && + comment.user?.login === 'github-actions[bot]' + ); if (existingComment) { await github.rest.issues.updateComment({ owner: context.repo.owner, diff --git a/packages/sdk/src/cli/commands/setup/github/github.test.ts b/packages/sdk/src/cli/commands/setup/github/github.test.ts index 2aea212fd..6e8ea180c 100644 --- a/packages/sdk/src/cli/commands/setup/github/github.test.ts +++ b/packages/sdk/src/cli/commands/setup/github/github.test.ts @@ -145,6 +145,12 @@ describe("renderBranchWorkflow", () => { expect(content).toContain("per_page: 100"); }); + test("updates only bot-owned plan comments", () => { + const { content } = renderBranchWorkflow(branchBase); + expect(content).toContain("comment.user?.type === 'Bot'"); + expect(content).toContain("comment.user?.login === 'github-actions[bot]'"); + }); + test("builds plan comment markdown without YAML indentation", () => { const { content } = renderBranchWorkflow(branchBase); expect(content).toContain("].join('\\n');"); From ab8db71dcae94919ebbad03c83877279000cb871 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 11:37:50 +0900 Subject: [PATCH 042/618] fix(codemod): preserve shell redirection scans --- .../codemods/v2/apply-to-deploy/codemod.yaml | 2 +- .../v2/apply-to-deploy/scripts/transform.ts | 6 +++--- .../codemods/v2/cli-rename/scripts/transform.ts | 15 ++++++++++++++- .../v2/cli-rename/tests/basic-shell/expected.sh | 1 + .../v2/cli-rename/tests/basic-shell/input.sh | 1 + packages/sdk-codemod/src/registry.ts | 2 +- 6 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/codemod.yaml b/packages/sdk-codemod/codemods/v2/apply-to-deploy/codemod.yaml index f9a072de5..23c63a08c 100644 --- a/packages/sdk-codemod/codemods/v2/apply-to-deploy/codemod.yaml +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/codemod.yaml @@ -1,6 +1,6 @@ name: "@tailor-platform/apply-to-deploy" version: "1.0.0" -description: "Rewrite `tailor-sdk apply` invocations to `tailor-sdk deploy` (the v2 recommended name)" +description: "Rewrite `tailor-sdk apply` invocations to the canonical v2 `tailor-sdk deploy` command" engine: jssg language: text since: "1.0.0" diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts index 0f792cec4..09b860926 100644 --- a/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts @@ -2,8 +2,8 @@ import * as path from "pathe"; // Match `tailor-sdk apply` plus the optional `@version` suffix that // package-manager run commands can add (`npx tailor-sdk@latest apply`, -// `pnpm dlx tailor-sdk@1.45.2 apply`). The version pin is preserved because -// `apply` and `deploy` are the same subcommand on the same binary. +// `pnpm dlx tailor-sdk@1.45.2 apply`). The version pin is preserved; only the +// command spelling changes. // `(?![-\w])` excludes both word continuation (`applyConfig`) and dash-suffixed // names (`apply-foo`) so a hypothetical sibling subcommand is not rewritten. const ARG_VALUE = `(?:[^\\s'"\`;&|]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`; @@ -54,7 +54,7 @@ function transformPackageJson(source: string): string | null { /** * Replace `tailor-sdk apply` invocations with `tailor-sdk deploy`. * - * `deploy` is a v1 alias of `apply` and the recommended name going forward. + * `deploy` is the canonical v2 command name. * @param source - File contents * @param filePath - Absolute path to the file (used to dispatch package.json vs text) * @returns Transformed source or null when nothing matched. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index b11c90fb1..b94582da2 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -170,6 +170,19 @@ function findMarkdownFencedCodeRanges(source: string): TextRange[] { return ranges; } +function isCommandSeparator(source: string, index: number): boolean { + const ch = source[index]; + const prev = source[index - 1]; + if (prev === "\\") return false; + + if (ch === "&") { + const next = source[index + 1]; + if (prev === ">" || prev === "<" || next === ">") return false; + } + + return ch === ";" || ch === "&" || ch === "|" || ch === ")"; +} + function findContainingRange( ranges: TextRange[] | undefined, index: number, @@ -233,7 +246,7 @@ function findTailorCommandEnd( } const prev = source[end - 1]; - if ((ch === ";" || ch === "&" || ch === "|" || ch === ")") && prev !== "\\") break; + if (isCommandSeparator(source, end)) break; if (ch === "\n" && prev !== "\\" && !foldedYamlRange) break; end += 1; } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh index 12b0536c9..2ccc0e54a 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh @@ -5,6 +5,7 @@ pnpm exec tailor-sdk crashreport list pnpm exec tailor-sdk crashreport send --file ./latest.crash.log pnpm exec tailor-sdk workflow start approval --machine-user ci tailor-sdk query --query 'select 1' --machine-user ci +tailor-sdk query 2>&1 --machine-user ci tailor-sdk query --query 'select 1;' --machine-user ci tailor-sdk query --query "select 1 | 2" --machine-user ci tailor-sdk workflow start approval --arg '{"ok":true}' \ diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh index 276195502..720969e7d 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh @@ -5,6 +5,7 @@ pnpm exec tailor-sdk crash-report list pnpm exec tailor-sdk crash-report send --file ./latest.crash.log pnpm exec tailor-sdk workflow start approval --machineuser ci tailor-sdk query --query 'select 1' --machineuser ci +tailor-sdk query 2>&1 --machineuser ci tailor-sdk query --query 'select 1;' --machineuser ci tailor-sdk query --query "select 1 | 2" --machineuser ci tailor-sdk workflow start approval --arg '{"ok":true}' \ diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index bb92febd9..26a27def8 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -51,7 +51,7 @@ const allCodemods: CodemodPackage[] = [ id: "v2/apply-to-deploy", name: "tailor-sdk apply → tailor-sdk deploy", description: - "Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the v2-recommended `tailor-sdk deploy` alias", + "Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor-sdk deploy` command", since: "1.0.0", until: "2.0.0", scriptPath: "v2/apply-to-deploy/scripts/transform.js", From d61aa416d6674b9488375d90163d321f6db2a5f3 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 11:58:18 +0900 Subject: [PATCH 043/618] fix(codemod): skip tool dot directories --- packages/sdk-codemod/src/runner.test.ts | 36 +++++++++++++++++++++++++ packages/sdk-codemod/src/runner.ts | 7 ++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 181573d61..8d38ee48b 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -273,6 +273,42 @@ describe("runCodemods", () => { expect(result.filesModified).toEqual([workflowPath]); await expect(fs.promises.readFile(workflowPath, "utf-8")).resolves.toBe("HELLO"); }); + + test("should skip unapproved tool dot directories", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-dot-test-")); + tmpDir = dir; + const workflowPath = path.join(dir, ".github/workflows/test.yml"); + const agentPackagePath = path.join(dir, ".agent/worktrees/demo/package.json"); + const nextYamlPath = path.join(dir, ".next/cache/workflow.yml"); + await fs.promises.mkdir(path.dirname(workflowPath), { recursive: true }); + await fs.promises.mkdir(path.dirname(agentPackagePath), { recursive: true }); + await fs.promises.mkdir(path.dirname(nextYamlPath), { recursive: true }); + await fs.promises.writeFile(workflowPath, "hello", "utf-8"); + await fs.promises.writeFile( + agentPackagePath, + '{"scripts":{"deploy":"tailor-sdk apply"}}', + "utf-8", + ); + await fs.promises.writeFile(nextYamlPath, "hello", "utf-8"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/upper", transformPath, ["**/*.yml", "**/package.json"]), + scriptPath: transformPath, + }, + ], + dir, + false, + ); + + expect(result.filesModified).toEqual([workflowPath]); + await expect(fs.promises.readFile(workflowPath, "utf-8")).resolves.toBe("HELLO"); + await expect(fs.promises.readFile(agentPackagePath, "utf-8")).resolves.toBe( + '{"scripts":{"deploy":"tailor-sdk apply"}}', + ); + await expect(fs.promises.readFile(nextYamlPath, "utf-8")).resolves.toBe("hello"); + }); }); describe("legacy pattern warnings", () => { diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index ee3044e77..b249cdb2b 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -47,6 +47,11 @@ const DEFAULT_FILE_PATTERNS = ["**/*.{ts,tsx,mts,cts}"]; /** Directory names always excluded from file scanning. */ const EXCLUDE_DIRS = new Set(["node_modules", "dist", ".git"]); +const ALLOWED_DOT_DIRS = new Set([".github", ".circleci"]); + +function shouldSkipDirectory(name: string): boolean { + return EXCLUDE_DIRS.has(name) || (name.startsWith(".") && !ALLOWED_DOT_DIRS.has(name)); +} async function* walkFiles(root: string, relativeDir = ""): AsyncGenerator { const absoluteDir = path.join(root, relativeDir); @@ -60,7 +65,7 @@ async function* walkFiles(root: string, relativeDir = ""): AsyncGenerator Date: Tue, 16 Jun 2026 12:18:02 +0900 Subject: [PATCH 044/618] fix(codemod): handle command substitutions --- .../v2/cli-rename/scripts/transform.ts | 106 +++++++++++++++++- .../cli-rename/tests/basic-shell/expected.sh | 2 + .../v2/cli-rename/tests/basic-shell/input.sh | 2 + 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index b94582da2..7a498fef4 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -180,7 +180,74 @@ function isCommandSeparator(source: string, index: number): boolean { if (prev === ">" || prev === "<" || next === ">") return false; } - return ch === ";" || ch === "&" || ch === "|" || ch === ")"; + return ch === ";" || ch === "&" || ch === "|"; +} + +function startsCommandSubstitution(source: string, index: number): boolean { + return source[index] === "$" && source[index + 1] === "(" && source[index - 1] !== "\\"; +} + +function findCommandSubstitutionEnd(source: string, start: number): number | undefined { + let depth = 1; + let index = start + 2; + let quote: ActiveQuote | null = null; + + while (index < source.length) { + const ch = source[index]; + + if (quote !== null) { + if (quote.escaped) { + if (ch === "\\" && source[index + 1] === quote.char) { + quote = null; + index += 2; + continue; + } + index += 1; + continue; + } + if (quote.char === '"' && startsCommandSubstitution(source, index)) { + depth += 1; + index += 2; + continue; + } + if (ch === "\\" && quote.char === '"' && index + 1 < source.length) { + index += 2; + continue; + } + if (ch === quote.char) { + quote = null; + } + index += 1; + continue; + } + + if (ch === "\\" && source[index + 1] === '"') { + quote = { char: '"', escaped: true }; + index += 2; + continue; + } + + if (ch === "'" || ch === '"') { + quote = { char: ch, escaped: false }; + index += 1; + continue; + } + + if (startsCommandSubstitution(source, index)) { + depth += 1; + index += 2; + continue; + } + + if (ch === ")") { + depth -= 1; + if (depth === 0) return index; + } + + index += 1; + } + + return undefined; } function findContainingRange( @@ -222,6 +289,13 @@ function findTailorCommandEnd( end += 1; continue; } + if (quote.char === '"' && startsCommandSubstitution(source, end)) { + const substitutionEnd = findCommandSubstitutionEnd(source, end); + if (substitutionEnd !== undefined) { + end = substitutionEnd + 1; + continue; + } + } if (ch === "\\" && quote.char === '"' && end + 1 < commandLimit) { end += 2; continue; @@ -245,7 +319,16 @@ function findTailorCommandEnd( continue; } + if (startsCommandSubstitution(source, end)) { + const substitutionEnd = findCommandSubstitutionEnd(source, end); + if (substitutionEnd !== undefined) { + end = substitutionEnd + 1; + continue; + } + } + const prev = source[end - 1]; + if (ch === ")") break; if (isCommandSeparator(source, end)) break; if (ch === "\n" && prev !== "\\" && !foldedYamlRange) break; end += 1; @@ -285,6 +368,16 @@ function replaceOptionsInCommand(command: string): string { const ch = command[index]; if (quote !== null) { + if (quote.char === '"' && startsCommandSubstitution(command, index)) { + const substitutionEnd = findCommandSubstitutionEnd(command, index); + if (substitutionEnd !== undefined) { + updated += "$("; + updated += replaceAll(command.slice(index + 2, substitutionEnd)); + updated += ")"; + index = substitutionEnd + 1; + continue; + } + } updated += ch; if (quote.escaped) { if (ch === "\\" && command[index + 1] === quote.char) { @@ -318,6 +411,17 @@ function replaceOptionsInCommand(command: string): string { continue; } + if (startsCommandSubstitution(command, index)) { + const substitutionEnd = findCommandSubstitutionEnd(command, index); + if (substitutionEnd !== undefined) { + updated += "$("; + updated += replaceAll(command.slice(index + 2, substitutionEnd)); + updated += ")"; + index = substitutionEnd + 1; + continue; + } + } + const rename = findOptionRename(command, index); if (rename) { updated += rename[1]; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh index 2ccc0e54a..faba9713c 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh @@ -6,8 +6,10 @@ pnpm exec tailor-sdk crashreport send --file ./latest.crash.log pnpm exec tailor-sdk workflow start approval --machine-user ci tailor-sdk query --query 'select 1' --machine-user ci tailor-sdk query 2>&1 --machine-user ci +tailor-sdk query $(build-query --machineuser=ci) --machine-user ci tailor-sdk query --query 'select 1;' --machine-user ci tailor-sdk query --query "select 1 | 2" --machine-user ci tailor-sdk workflow start approval --arg '{"ok":true}' \ --machine-user ci tailor-sdk --json crashreport list +TOKEN=$(tailor-sdk query --machine-user ci) other-cli --machineuser=ci diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh index 720969e7d..5b1573060 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh @@ -6,8 +6,10 @@ pnpm exec tailor-sdk crash-report send --file ./latest.crash.log pnpm exec tailor-sdk workflow start approval --machineuser ci tailor-sdk query --query 'select 1' --machineuser ci tailor-sdk query 2>&1 --machineuser ci +tailor-sdk query $(build-query --machineuser=ci) --machineuser ci tailor-sdk query --query 'select 1;' --machineuser ci tailor-sdk query --query "select 1 | 2" --machineuser ci tailor-sdk workflow start approval --arg '{"ok":true}' \ --machineuser ci tailor-sdk --json crash-report list +TOKEN=$(tailor-sdk query --machineuser ci) other-cli --machineuser=ci From 77e7f4512ddd141a8858ab37a3c48d8ad7e16543 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 14:05:38 +0900 Subject: [PATCH 045/618] feat(cli)!: require content hash for function log mapping --- .../require-function-log-content-hash.md | 5 + packages/sdk/docs/cli/function.md | 2 +- .../src/cli/commands/function/logs.test.ts | 101 +----------------- .../sdk/src/cli/commands/function/logs.ts | 71 +++--------- .../shared/function-script-download.test.ts | 27 +---- .../cli/shared/function-script-download.ts | 30 ++---- 6 files changed, 35 insertions(+), 201 deletions(-) create mode 100644 .changeset/require-function-log-content-hash.md diff --git a/.changeset/require-function-log-content-hash.md b/.changeset/require-function-log-content-hash.md new file mode 100644 index 000000000..e0b2c7cc5 --- /dev/null +++ b/.changeset/require-function-log-content-hash.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Require `FunctionExecution.contentHash` for `function logs` stack trace source mapping. Executions without a content hash now show raw stack traces instead of mapping against the current function bundle. diff --git a/packages/sdk/docs/cli/function.md b/packages/sdk/docs/cli/function.md index 57b8c3085..2e48c0c65 100644 --- a/packages/sdk/docs/cli/function.md +++ b/packages/sdk/docs/cli/function.md @@ -209,7 +209,7 @@ $ tailor-sdk function logs --json When viewing a specific execution that failed, the command displays error details with the stack trace mapped back to your original source files (clickable file links and code snippets, matching `function test-run` output). -Stack traces stay accurate even after later redeploys, because the trace is resolved against the exact build that produced the execution. If that build is no longer available, the command falls back to a plain-text error display. +Stack traces are mapped only when the execution includes a content hash for the exact build that ran. If the content hash is missing or the build is no longer available, the command falls back to a plain-text error display. diff --git a/packages/sdk/src/cli/commands/function/logs.test.ts b/packages/sdk/src/cli/commands/function/logs.test.ts index 30c42eb56..c602e9506 100644 --- a/packages/sdk/src/cli/commands/function/logs.test.ts +++ b/packages/sdk/src/cli/commands/function/logs.test.ts @@ -190,7 +190,6 @@ describe("downloadScriptForMapping", () => { scriptName: "test-run--throwError.js", executionType: FunctionExecution_Type.STANDARD, executionContentHash: "abc123", - executionStartedAt: new Date("2024-01-01T00:00:00Z"), }); expect(result).toBeNull(); @@ -207,7 +206,6 @@ describe("downloadScriptForMapping", () => { scriptName: "my-resolver.throwError.body.js", executionType: FunctionExecution_Type.STANDARD, executionContentHash: "abc123", - executionStartedAt: new Date("2024-02-01T00:00:00Z"), }); expect(result).toBe("pinned-code"); @@ -232,7 +230,6 @@ describe("downloadScriptForMapping", () => { scriptName: "my-resolver.throwError.body.js", executionType: FunctionExecution_Type.STANDARD, executionContentHash: "abc123", - executionStartedAt: new Date("2024-02-01T00:00:00Z"), }); expect(result).toBe("pinned-code"); @@ -247,7 +244,6 @@ describe("downloadScriptForMapping", () => { scriptName: "billing.retry.v2", executionType: FunctionExecution_Type.JOB, executionContentHash: "deadbeef", - executionStartedAt: new Date("2024-02-01T00:00:00Z"), }); expect(result).toBe("job-code"); @@ -267,90 +263,14 @@ describe("downloadScriptForMapping", () => { scriptName: "my-resolver.throwError.body.js", executionType: FunctionExecution_Type.STANDARD, executionContentHash: "abc123", - executionStartedAt: new Date("2024-02-01T00:00:00Z"), }); expect(result).toBeNull(); }); }); - describe("with updatedAt fallback (executionContentHash empty)", () => { - test("does not pin contentHash on the RPC", async () => { - const client = makeDownloadClient([new TextEncoder().encode("code")], { - updatedAt: new Date("2024-01-01T00:00:00Z"), - }); - - await downloadScriptForMapping({ - client, - workspaceId: "ws-1", - scriptName: "my-resolver.throwError.body.js", - executionType: FunctionExecution_Type.STANDARD, - executionContentHash: "", - executionStartedAt: new Date("2024-02-01T00:00:00Z"), - }); - - expect(client.downloadFunctionRegistryScript).toHaveBeenCalledWith({ - workspaceId: "ws-1", - name: "resolver--my-resolver--throwError", - contentHash: undefined, - }); - }); - - test("returns code when registry updatedAt is not newer than executionStartedAt", async () => { - const client = makeDownloadClient([new TextEncoder().encode("code")], { - updatedAt: new Date("2024-01-01T00:00:00Z"), - }); - - const result = await downloadScriptForMapping({ - client, - workspaceId: "ws-1", - scriptName: "my-resolver.throwError.body.js", - executionType: FunctionExecution_Type.STANDARD, - executionContentHash: "", - executionStartedAt: new Date("2024-02-01T00:00:00Z"), - }); - - expect(result).toBe("code"); - }); - - test("returns null when registry updatedAt is strictly newer than executionStartedAt", async () => { - // Registry was redeployed at 2024-03-01, but the execution we are - // viewing started at 2024-02-01. Mapping the old stack trace - // against the new bundle would show misleading source locations. - const client = makeDownloadClient([new TextEncoder().encode("new-code")], { - updatedAt: new Date("2024-03-01T00:00:00Z"), - }); - - const result = await downloadScriptForMapping({ - client, - workspaceId: "ws-1", - scriptName: "my-resolver.throwError.body.js", - executionType: FunctionExecution_Type.STANDARD, - executionContentHash: "", - executionStartedAt: new Date("2024-02-01T00:00:00Z"), - }); - - expect(result).toBeNull(); - }); - - test("returns code when executionStartedAt is null (no staleness check possible)", async () => { - const client = makeDownloadClient([new TextEncoder().encode("code")], { - updatedAt: new Date("2024-03-01T00:00:00Z"), - }); - - const result = await downloadScriptForMapping({ - client, - workspaceId: "ws-1", - scriptName: "my-resolver.throwError.body.js", - executionType: FunctionExecution_Type.STANDARD, - executionContentHash: "", - executionStartedAt: null, - }); - - expect(result).toBe("code"); - }); - - test("returns code when registry metadata omits updatedAt", async () => { + describe("without executionContentHash", () => { + test("skips mapping without downloading the current registry script", async () => { const client = makeDownloadClient([new TextEncoder().encode("code")]); const result = await downloadScriptForMapping({ @@ -359,25 +279,10 @@ describe("downloadScriptForMapping", () => { scriptName: "my-resolver.throwError.body.js", executionType: FunctionExecution_Type.STANDARD, executionContentHash: "", - executionStartedAt: new Date("2024-02-01T00:00:00Z"), - }); - - expect(result).toBe("code"); - }); - - test("returns null when download yields no chunks", async () => { - const client = makeDownloadClient([], { updatedAt: new Date("2024-01-01T00:00:00Z") }); - - const result = await downloadScriptForMapping({ - client, - workspaceId: "ws-1", - scriptName: "my-resolver.throwError.body.js", - executionType: FunctionExecution_Type.STANDARD, - executionContentHash: "", - executionStartedAt: new Date("2024-02-01T00:00:00Z"), }); expect(result).toBeNull(); + expect(client.downloadFunctionRegistryScript).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/sdk/src/cli/commands/function/logs.ts b/packages/sdk/src/cli/commands/function/logs.ts index 2eb9f5fbb..a55e606e4 100644 --- a/packages/sdk/src/cli/commands/function/logs.ts +++ b/packages/sdk/src/cli/commands/function/logs.ts @@ -211,19 +211,9 @@ interface DownloadScriptForMappingOptions { * FunctionExecution.contentHash. When non-empty, the registry * download is pinned to this exact bundle so the stack trace maps * against the code that actually ran, regardless of subsequent - * redeploys. Empty on older servers that did not populate the - * field; in that case the caller falls back to a timestamp-based - * staleness check using `executionStartedAt`. + * redeploys. Empty values cannot be mapped safely. */ executionContentHash: string; - /** - * When the execution started. Used as a fallback staleness signal - * only when `executionContentHash` is empty: if the current registry - * entry's `updatedAt` is strictly newer, the downloaded bundle may - * differ from what was actually executed, so mapping is skipped to - * avoid misleading source locations. - */ - executionStartedAt: Date | null; } /** @@ -234,8 +224,8 @@ interface DownloadScriptForMappingOptions { * * When `executionContentHash` is non-empty, the download is pinned to * that exact bundle so mapping stays correct across redeploys. When - * empty (older servers), falls back to comparing `registryUpdatedAt` - * against `executionStartedAt`. + * empty, mapping is skipped because the exact bundle cannot be + * identified. * * `FunctionExecution.scriptName` does not match the function registry * name directly; `scriptNameToRegistryName` translates between the two @@ -246,20 +236,12 @@ interface DownloadScriptForMappingOptions { * @param options.scriptName - Script name (matches FunctionExecution.scriptName) * @param options.executionType - Execution type used to discriminate registry name translation * @param options.executionContentHash - Content hash of the bundle that ran; pins the download when non-empty - * @param options.executionStartedAt - Execution start timestamp used as fallback staleness signal * @returns Bundled script content, or null when unavailable / stale */ export async function downloadScriptForMapping( options: DownloadScriptForMappingOptions, ): Promise { - const { - client, - workspaceId, - scriptName, - executionType, - executionContentHash, - executionStartedAt, - } = options; + const { client, workspaceId, scriptName, executionType, executionContentHash } = options; const registryName = scriptNameToRegistryName(scriptName, executionType); if (registryName == null) { logger.debug( @@ -268,44 +250,26 @@ export async function downloadScriptForMapping( return null; } - if (executionContentHash !== "") { - const pinned = await downloadFunctionScript({ - client, - workspaceId, - name: registryName, - contentHash: executionContentHash, - }); - if (pinned == null) { - logger.debug( - `Could not download pinned script "${scriptName}" (registry: "${registryName}", contentHash: "${executionContentHash}") for stack trace mapping; showing raw stack trace.`, - ); - return null; - } - return pinned.code; - } - - // Fallback for older servers that did not populate - // FunctionExecution.contentHash: download the current bundle and - // skip mapping if the registry was updated after the execution - // started. - const result = await downloadFunctionScript({ client, workspaceId, name: registryName }); - if (result == null) { + if (executionContentHash === "") { logger.debug( - `Could not download script "${scriptName}" (registry: "${registryName}") for stack trace mapping; showing raw stack trace.`, + `Function execution "${scriptName}" has no contentHash; skipping sourcemap mapping because the exact bundle cannot be identified.`, ); return null; } - if ( - executionStartedAt != null && - result.registryUpdatedAt != null && - result.registryUpdatedAt.getTime() > executionStartedAt.getTime() - ) { + + const pinned = await downloadFunctionScript({ + client, + workspaceId, + name: registryName, + contentHash: executionContentHash, + }); + if (pinned == null) { logger.debug( - `Registry script "${registryName}" was updated at ${result.registryUpdatedAt.toISOString()} after execution started at ${executionStartedAt.toISOString()}; skipping sourcemap mapping to avoid stale source locations.`, + `Could not download pinned script "${scriptName}" (registry: "${registryName}", contentHash: "${executionContentHash}") for stack trace mapping; showing raw stack trace.`, ); return null; } - return result.code; + return pinned.code; } export const logsCommand = defineAppCommand({ @@ -313,7 +277,7 @@ export const logsCommand = defineAppCommand({ description: "List or get function execution logs.", notes: `When viewing a specific execution that failed, the command displays error details with the stack trace mapped back to your original source files (clickable file links and code snippets, matching \`function test-run\` output). -Stack traces stay accurate even after later redeploys, because the trace is resolved against the exact build that produced the execution. If that build is no longer available, the command falls back to a plain-text error display.`, +Stack traces are mapped only when the execution includes a content hash for the exact build that ran. If the content hash is missing or the build is no longer available, the command falls back to a plain-text error display.`, examples: [ { cmd: "", @@ -378,7 +342,6 @@ Stack traces stay accurate even after later redeploys, because the trace is reso scriptName: detail.scriptName, executionType: execution.type, executionContentHash: execution.contentHash, - executionStartedAt: detail.startedAt, }) : null; printFunctionExecutionDetail({ detail, bundledCode }); diff --git a/packages/sdk/src/cli/shared/function-script-download.test.ts b/packages/sdk/src/cli/shared/function-script-download.test.ts index 34747bf05..764165631 100644 --- a/packages/sdk/src/cli/shared/function-script-download.test.ts +++ b/packages/sdk/src/cli/shared/function-script-download.test.ts @@ -1,4 +1,3 @@ -import { timestampFromDate } from "@bufbuild/protobuf/wkt"; import { FunctionExecution_Type } from "@tailor-proto/tailor/v1/function_resource_pb"; import { describe, test, expect, vi } from "vitest"; import { downloadFunctionScript, scriptNameToRegistryName } from "./function-script-download"; @@ -22,15 +21,8 @@ function makeStreamingClient(responses: DownloadResponse[]): OperatorClient { } describe("downloadFunctionScript", () => { - test("concatenates chunks into a UTF-8 string and returns registry updatedAt", async () => { - const updatedAt = new Date("2024-03-01T00:00:00Z"); + test("concatenates chunks into a UTF-8 string", async () => { const client = makeStreamingClient([ - { - payload: { - case: "metadata", - value: { function: { updatedAt: timestampFromDate(updatedAt) } }, - }, - }, { payload: { case: "chunk", value: new TextEncoder().encode("hello, ") } }, { payload: { case: "chunk", value: new TextEncoder().encode("world") } }, ]); @@ -41,22 +33,7 @@ describe("downloadFunctionScript", () => { name: "my-fn", }); - expect(result).toEqual({ code: "hello, world", registryUpdatedAt: updatedAt }); - }); - - test("returns registryUpdatedAt as null when metadata omits the timestamp", async () => { - const client = makeStreamingClient([ - { payload: { case: "metadata", value: {} } }, - { payload: { case: "chunk", value: new TextEncoder().encode("x") } }, - ]); - - const result = await downloadFunctionScript({ - client, - workspaceId: "ws-1", - name: "my-fn", - }); - - expect(result).toEqual({ code: "x", registryUpdatedAt: null }); + expect(result).toEqual({ code: "hello, world" }); }); test("returns null when no chunks are received", async () => { diff --git a/packages/sdk/src/cli/shared/function-script-download.ts b/packages/sdk/src/cli/shared/function-script-download.ts index 869e04ef0..e566d90d6 100644 --- a/packages/sdk/src/cli/shared/function-script-download.ts +++ b/packages/sdk/src/cli/shared/function-script-download.ts @@ -5,7 +5,6 @@ * concatenates content chunks into a single UTF-8 string. */ -import { timestampDate } from "@bufbuild/protobuf/wkt"; import { FunctionExecution_Type } from "@tailor-proto/tailor/v1/function_resource_pb"; import { logger } from "@/cli/shared/logger"; import type { OperatorClient } from "@/cli/shared/client"; @@ -96,25 +95,17 @@ export interface DownloadFunctionScriptOptions { export interface DownloadedFunctionScript { /** Bundled script content as a UTF-8 string */ code: string; - /** - * Server-side last-update timestamp of the registry entry, or null - * when the metadata message omitted it. Callers comparing against an - * execution timestamp use this to detect redeploys that happened - * after the execution ran. - */ - registryUpdatedAt: Date | null; } /** * Download a deployed function script. * - * Returns the bundled script content together with the registry - * entry's `updatedAt` timestamp. Returns null when the download fails - * (script removed, network error, etc.) or when no content chunks are - * received; errors are swallowed so callers can fall back to a - * non-sourcemap display. + * Returns the bundled script content. Returns null when the download + * fails (script removed, network error, etc.) or when no content + * chunks are received; errors are swallowed so callers can fall back + * to a non-sourcemap display. * @param options - Download options - * @returns Script content plus metadata, or null on failure / empty response + * @returns Script content, or null on failure / empty response */ export async function downloadFunctionScript( options: DownloadFunctionScriptOptions, @@ -122,24 +113,17 @@ export async function downloadFunctionScript( const { client, workspaceId, name, contentHash } = options; try { const chunks: Uint8Array[] = []; - let registryUpdatedAt: Date | null = null; for await (const response of client.downloadFunctionRegistryScript({ workspaceId, name, contentHash, })) { - if (response.payload.case === "metadata") { - const updatedAt = response.payload.value.function?.updatedAt; - if (updatedAt) registryUpdatedAt = timestampDate(updatedAt); - } else if (response.payload.case === "chunk") { + if (response.payload.case === "chunk") { chunks.push(response.payload.value); } } if (chunks.length === 0) return null; - return { - code: Buffer.concat(chunks).toString("utf-8"), - registryUpdatedAt, - }; + return { code: Buffer.concat(chunks).toString("utf-8") }; } catch (error) { logger.debug(`Failed to download function script "${options.name}": ${error}`); return null; From f40abae5491b881a1b58ad10937fdaa6853e1235 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 14:21:32 +0900 Subject: [PATCH 046/618] docs(cli): drop stale staleness references from function log mapping The timestamp-based staleness fallback was removed when contentHash became required; update the downloadScriptForMapping JSDoc and the redeploy test comment so they describe only the pinned-download behavior. --- packages/sdk/src/cli/commands/function/logs.test.ts | 5 ++--- packages/sdk/src/cli/commands/function/logs.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/sdk/src/cli/commands/function/logs.test.ts b/packages/sdk/src/cli/commands/function/logs.test.ts index c602e9506..278423137 100644 --- a/packages/sdk/src/cli/commands/function/logs.test.ts +++ b/packages/sdk/src/cli/commands/function/logs.test.ts @@ -217,9 +217,8 @@ describe("downloadScriptForMapping", () => { }); test("returns code even when registry was redeployed after the execution", async () => { - // Registry metadata reports updatedAt newer than the execution. - // The legacy timestamp-based check would skip this, but pinning - // by contentHash asks the server for the exact bundle that ran. + // Pinning by contentHash asks the server for the exact bundle that + // ran, so a newer registry updatedAt does not affect the result. const client = makeDownloadClient([new TextEncoder().encode("pinned-code")], { updatedAt: new Date("2024-03-01T00:00:00Z"), }); diff --git a/packages/sdk/src/cli/commands/function/logs.ts b/packages/sdk/src/cli/commands/function/logs.ts index a55e606e4..ddad88dc3 100644 --- a/packages/sdk/src/cli/commands/function/logs.ts +++ b/packages/sdk/src/cli/commands/function/logs.ts @@ -219,8 +219,7 @@ interface DownloadScriptForMappingOptions { /** * Download a deployed function script for sourcemap mapping. Logs a * debug message on failure but never throws. Error display falls back - * to a plain-text format when the script cannot be retrieved or when - * the current registry entry is stale relative to the execution. + * to a plain-text format when the script cannot be retrieved. * * When `executionContentHash` is non-empty, the download is pinned to * that exact bundle so mapping stays correct across redeploys. When @@ -236,7 +235,7 @@ interface DownloadScriptForMappingOptions { * @param options.scriptName - Script name (matches FunctionExecution.scriptName) * @param options.executionType - Execution type used to discriminate registry name translation * @param options.executionContentHash - Content hash of the bundle that ran; pins the download when non-empty - * @returns Bundled script content, or null when unavailable / stale + * @returns Bundled script content, or null when unavailable */ export async function downloadScriptForMapping( options: DownloadScriptForMappingOptions, From 1d5e7660532e4f306992baac0b541f27a43e278b Mon Sep 17 00:00:00 2001 From: "tailor-platform-pr-trigger[bot]" <247949890+tailor-platform-pr-trigger[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 05:23:26 +0000 Subject: [PATCH 047/618] Version Packages (next) --- .changeset/pre.json | 10 +++++++++- packages/create-sdk/CHANGELOG.md | 2 ++ packages/create-sdk/package.json | 2 +- packages/sdk-codemod/CHANGELOG.md | 7 +++++++ packages/sdk-codemod/package.json | 2 +- packages/sdk/CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ packages/sdk/package.json | 2 +- 7 files changed, 51 insertions(+), 4 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 67982f8b0..bdbcec2fe 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -6,5 +6,13 @@ "@tailor-platform/sdk": "1.64.0", "@tailor-platform/sdk-codemod": "0.2.7" }, - "changesets": [] + "changesets": [ + "docs-idp-user-auth-policy", + "fix-lockfile-audit-vulns", + "migration-rollback-on-failure", + "plugin-cli-import-codemod", + "proto-init-shape-optionality", + "v2-baseline", + "validate-migrate-json" + ] } diff --git a/packages/create-sdk/CHANGELOG.md b/packages/create-sdk/CHANGELOG.md index bad8cc996..09a77809d 100644 --- a/packages/create-sdk/CHANGELOG.md +++ b/packages/create-sdk/CHANGELOG.md @@ -1,5 +1,7 @@ # @tailor-platform/create-sdk +## 2.0.0-next.0 + ## 1.64.0 ## 1.63.0 diff --git a/packages/create-sdk/package.json b/packages/create-sdk/package.json index f15918490..57f0f8ab3 100644 --- a/packages/create-sdk/package.json +++ b/packages/create-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/create-sdk", - "version": "1.64.0", + "version": "2.0.0-next.0", "description": "A CLI tool to quickly create a new Tailor Platform SDK project", "license": "MIT", "repository": { diff --git a/packages/sdk-codemod/CHANGELOG.md b/packages/sdk-codemod/CHANGELOG.md index 5da5c8fef..69796bd5e 100644 --- a/packages/sdk-codemod/CHANGELOG.md +++ b/packages/sdk-codemod/CHANGELOG.md @@ -1,5 +1,12 @@ # @tailor-platform/sdk-codemod +## 0.3.0-next.0 +### Minor Changes + + + +- [#1435](https://github.com/tailor-platform/sdk/pull/1435) [`49c0cc9`](https://github.com/tailor-platform/sdk/commit/49c0cc99171d7e317a50a18804a21067d89f9493) Thanks [@dqn](https://github.com/dqn)! - Add the `v2/plugin-cli-import` codemod so `tailor-sdk upgrade` rewrites deprecated plugin imports from `@tailor-platform/sdk/cli` (`kyselyTypePlugin`, `enumConstantsPlugin`, `fileUtilsPlugin`, `seedPlugin`) to their dedicated `@tailor-platform/sdk/plugin/*` subpaths, splitting any non-plugin specifiers onto a separate import. + ## 0.2.7 ### Patch Changes diff --git a/packages/sdk-codemod/package.json b/packages/sdk-codemod/package.json index a50a2898a..0eaffe8e8 100644 --- a/packages/sdk-codemod/package.json +++ b/packages/sdk-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk-codemod", - "version": "0.2.7", + "version": "0.3.0-next.0", "description": "Codemod runner for Tailor Platform SDK upgrades", "license": "MIT", "repository": { diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 86cbfe363..60310e06a 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,5 +1,35 @@ # @tailor-platform/sdk +## 2.0.0-next.0 +### Major Changes + + + +- [#1451](https://github.com/tailor-platform/sdk/pull/1451) [`3e6d582`](https://github.com/tailor-platform/sdk/commit/3e6d582a37d83a42302339cc4aea1d3dd11e8a81) Thanks [@tailor-platform-pr-trigger](https://github.com/apps/tailor-platform-pr-trigger)! - Start the v2 release line. v2 introduces breaking changes to the SDK API and CLI; run `tailor-sdk upgrade` to apply the bundled codemods when migrating. Prereleases are published to the `next` dist-tag — install with `@tailor-platform/sdk@next`. + + +### Patch Changes + + + +- [#1449](https://github.com/tailor-platform/sdk/pull/1449) [`016aff6`](https://github.com/tailor-platform/sdk/commit/016aff6aab31c334c57a5e5244453f2dd559c008) Thanks [@k1LoW](https://github.com/k1LoW)! - Document the `userAuthPolicy`, `gqlOperations`, and `lang` options of `defineIdp()` in the IdP service guide, including the password policy fields, allowed email domains, Google/Microsoft social login, the read-only `"query"` shortcut, and the cross-field validation constraints. + + + +- [#1450](https://github.com/tailor-platform/sdk/pull/1450) [`162ba62`](https://github.com/tailor-platform/sdk/commit/162ba629e0d511593718f289b93788d5d56778da) Thanks [@toiroakr](https://github.com/toiroakr)! - Update OpenTelemetry runtime dependencies to 2.8.0 to resolve a moderate security advisory (GHSA-8988-4f7v-96qf) in `@opentelemetry/core` + + + +- [#1432](https://github.com/tailor-platform/sdk/pull/1432) [`3a854a3`](https://github.com/tailor-platform/sdk/commit/3a854a3a10b938ce3cf6fe7527de4ab56ecf48d5) Thanks [@toiroakr](https://github.com/toiroakr)! - Roll back a migration's pre-migration schema changes when its data migration (`migrate.ts`) fails during `apply`. A failed migration now leaves the workspace at its prior checkpoint and prior schema instead of half-applied, so subsequent deploys are no longer blocked by opaque "Remote schema drift detected" errors. + + + +- [#1422](https://github.com/tailor-platform/sdk/pull/1422) [`f3f8427`](https://github.com/tailor-platform/sdk/commit/f3f84277fe1942601d0fcbb8a64c2c26823b5624) Thanks [@dqn](https://github.com/dqn)! - Internal cleanup of proto field optionality handling. No behavior change. + + + +- [#1421](https://github.com/tailor-platform/sdk/pull/1421) [`b933f47`](https://github.com/tailor-platform/sdk/commit/b933f474d65f8dfed56f3991aae3a52589368b10) Thanks [@dqn](https://github.com/dqn)! - Corrupted or hand-edited TailorDB migration snapshot/diff files now fail with a clear validation error when loaded, instead of causing undefined behavior later. + ## 1.64.0 ### Minor Changes diff --git a/packages/sdk/package.json b/packages/sdk/package.json index da313453e..e6f2f7f46 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk", - "version": "1.64.0", + "version": "2.0.0-next.0", "description": "Tailor Platform SDK - The SDK to work with Tailor Platform", "license": "MIT", "repository": { From 0116de81f14d7de313215e4ba7b92dac516f1a49 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 14:24:56 +0900 Subject: [PATCH 048/618] refactor(cli): drop no-op resolver arg schema validation After removing the legacy input-wrapper support, the schema parse in the arg resolver no longer affects the returned value, leaving every call site to forward the original JSON string. Reduce it to a malformed-JSON fast-fail (assertResolverArgJson) and drop the now-unused schema/user parameters. --- packages/sdk/e2e/function-test-run.test.ts | 9 ++--- .../sdk/src/cli/commands/function/test-run.ts | 35 ++++--------------- 2 files changed, 8 insertions(+), 36 deletions(-) diff --git a/packages/sdk/e2e/function-test-run.test.ts b/packages/sdk/e2e/function-test-run.test.ts index 374b81eb6..af1fa4dd5 100644 --- a/packages/sdk/e2e/function-test-run.test.ts +++ b/packages/sdk/e2e/function-test-run.test.ts @@ -26,7 +26,7 @@ import { AuthInvokerSchema, type AuthInvoker } from "@tailor-proto/tailor/v1/aut import { describe, test, expect, beforeAll } from "vitest"; import { bundleForTestRun, type ResolvedMachineUser } from "../src/cli/commands/function/bundle"; import { detectFunctionType } from "../src/cli/commands/function/detect"; -import { validateResolverArg } from "../src/cli/commands/function/test-run"; +import { assertResolverArgJson } from "../src/cli/commands/function/test-run"; import { initOperatorClient, type OperatorClient } from "../src/cli/shared/client"; import { loadAccessToken } from "../src/cli/shared/context"; import { executeScript, type ScriptExecutionResult } from "../src/cli/shared/script-executor"; @@ -92,12 +92,7 @@ async function runTestRun( if (!detected.hasInput) { resolvedArg = undefined; } else if (detected.inputSchema) { - resolvedArg = validateResolverArg( - resolvedArg, - detected.inputSchema, - machineUser, - workspaceId, - ); + assertResolverArgJson(resolvedArg); } } diff --git a/packages/sdk/src/cli/commands/function/test-run.ts b/packages/sdk/src/cli/commands/function/test-run.ts index 0e0a96028..f1092ab6d 100644 --- a/packages/sdk/src/cli/commands/function/test-run.ts +++ b/packages/sdk/src/cli/commands/function/test-run.ts @@ -21,7 +21,7 @@ import { executeScript } from "@/cli/shared/script-executor"; import { formatErrorWithSourcemap } from "@/cli/shared/stack-trace"; import { assertDefined } from "@/utils/assert"; import { bundleForTestRun, type ResolvedMachineUser } from "./bundle"; -import { detectFunctionType, type DetectedFunction } from "./detect"; +import { detectFunctionType } from "./detect"; export const testRunCommand = defineAppCommand({ name: "test-run", @@ -136,7 +136,7 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file ); args.arg = undefined; } else if (detected.inputSchema) { - args.arg = validateResolverArg(args.arg, detected.inputSchema, machineUser, workspaceId); + assertResolverArgJson(args.arg); } } @@ -317,33 +317,10 @@ async function resolveMachineUser( } /** - * Validate resolver arg format against the detected input schema. + * Assert that the resolver `--arg` value is valid JSON, failing fast before the + * server round-trip. Schema validation is left to the server. * @param argStr - JSON string of the arg - * @param inputSchema - Pre-built schema object from detect (has .parse()) - * @param machineUser - Resolved machine user info - * @param workspaceId - Workspace ID - * @returns The original JSON string */ -export function validateResolverArg( - argStr: string, - inputSchema: NonNullable, - machineUser: ResolvedMachineUser, - workspaceId: string, -): string { - const parsed = JSON.parse(argStr); - const user = { - id: machineUser.id, - type: "machine_user" as const, - workspaceId, - attributes: machineUser.attributes ?? null, - attributeList: machineUser.attributeList, - }; - - const newResult = inputSchema.parse({ value: parsed, data: parsed, user }); - if (!newResult.issues) { - return argStr; - } - - // Pass as-is and let the server report the validation error. - return argStr; +export function assertResolverArgJson(argStr: string): void { + JSON.parse(argStr); } From 84890da15ee4a206c4764685baf463b9249825a1 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 14:25:34 +0900 Subject: [PATCH 049/618] test(sdk): migrate e2e migration fixture off defineGenerators The migration e2e fixture is written out as tailor.config.ts and loaded by the CLI at runtime. With defineGenerators removed from the SDK, its named import no longer resolves. Switch the fixture to definePlugins with kyselyTypePlugin so the config loads under the new API. --- packages/sdk/e2e/fixtures/migration/config.template.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/sdk/e2e/fixtures/migration/config.template.ts b/packages/sdk/e2e/fixtures/migration/config.template.ts index 1e08b0332..d569d729f 100644 --- a/packages/sdk/e2e/fixtures/migration/config.template.ts +++ b/packages/sdk/e2e/fixtures/migration/config.template.ts @@ -1,4 +1,5 @@ -import { defineConfig, defineAuth, defineGenerators } from "@tailor-platform/sdk"; +import { defineConfig, defineAuth, definePlugins } from "@tailor-platform/sdk"; +import { kyselyTypePlugin } from "@tailor-platform/sdk/plugin/kysely-type"; import { user } from "./tailordb/user"; const auth = defineAuth("migration-test-auth", { @@ -16,10 +17,7 @@ const auth = defineAuth("migration-test-auth", { }, }); -export const generators = defineGenerators([ - "@tailor-platform/kysely-type", - { distPath: "./generated/tailordb.ts" }, -]); +export const plugins = definePlugins(kyselyTypePlugin({ distPath: "./generated/tailordb.ts" })); export default defineConfig({ id: "dd52af75-a667-4751-806f-a6f3d16ee4c2", From 053fe5c7dc0c267ee96a8595d09c9a92055184f2 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 14:26:53 +0900 Subject: [PATCH 050/618] docs(sdk): note unauthenticatedTailorUser deprecation in testing guide --- packages/sdk/docs/testing.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index b3e8ebbe0..e3132d3e8 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -17,7 +17,7 @@ Unit-test entrypoints exposed by the SDK: Helpers under `@tailor-platform/sdk/test`: -- `unauthenticatedTailorUser` — default `user` value for resolver contexts +- `unauthenticatedTailorUser` — default `user` value for resolver contexts (deprecated; see the note under [Testing Resolvers](#testing-resolvers)) Platform API mocks under `@tailor-platform/sdk/vitest` (for use with the [`tailor-runtime` Vitest environment](#runtime-environment-emulation-beta) below): @@ -359,6 +359,8 @@ Unit tests call `.body()` (or `.trigger()`) directly on a resolver, workflow job ### Testing Resolvers +> **Note:** `unauthenticatedTailorUser` is deprecated. Represent an absent principal as `null` in your own application code going forward. The test helper and the examples below retain it until the resolver context `user` type accepts `null` in the next major version. + #### Simple resolver For pure logic with no external dependencies, invoke `.body()` directly: From 759612e6365fe145a3acc40dba4a098b95f6b05d Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 14:28:50 +0900 Subject: [PATCH 051/618] docs(runtime): clarify Tailordb migration and sync codemod docs Tie the v2/tailordb-namespace codemod step to the required `@tailor-platform/sdk/runtime/globals` opt-in in the changeset and runtime docs so the rewritten lowercase `tailordb.*` references resolve. Update the codemod's own description/comments to say the capital-cased namespace is removed (not deprecated) to match the registry entry. --- .../remove-runtime-globals-compatibility.md | 2 +- .../v2/tailordb-namespace/codemod.yaml | 2 +- .../tailordb-namespace/scripts/transform.ts | 20 ++++++++++--------- packages/sdk/docs/runtime.md | 2 +- packages/sdk/src/runtime/globals.test.ts | 9 +++++---- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/.changeset/remove-runtime-globals-compatibility.md b/.changeset/remove-runtime-globals-compatibility.md index decabff09..1917f11ee 100644 --- a/.changeset/remove-runtime-globals-compatibility.md +++ b/.changeset/remove-runtime-globals-compatibility.md @@ -5,4 +5,4 @@ Remove the v1 runtime globals compatibility layer. Importing from `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` declarations; opt into globals with `@tailor-platform/sdk/runtime/globals` or use the typed wrappers from `@tailor-platform/sdk/runtime`. -The capital-cased `Tailordb.*` namespace is removed. Run `pnpm dlx @tailor-platform/sdk-codemod v2/tailordb-namespace` before upgrading if your project still references `Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, or `typeof Tailordb.Client`. +The capital-cased `Tailordb.*` namespace is removed. If your project still references `Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, or `typeof Tailordb.Client`, migrate before upgrading: run `pnpm dlx @tailor-platform/sdk-codemod v2/tailordb-namespace` to rewrite them to lowercase `tailordb.*`, then add `import "@tailor-platform/sdk/runtime/globals"` so the rewritten references resolve. diff --git a/packages/sdk-codemod/codemods/v2/tailordb-namespace/codemod.yaml b/packages/sdk-codemod/codemods/v2/tailordb-namespace/codemod.yaml index 83219b1f8..dcd93c07b 100644 --- a/packages/sdk-codemod/codemods/v2/tailordb-namespace/codemod.yaml +++ b/packages/sdk-codemod/codemods/v2/tailordb-namespace/codemod.yaml @@ -1,6 +1,6 @@ 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`)" +description: "Rename the removed capital-cased `Tailordb` ambient namespace to lowercase `tailordb` (e.g. `Tailordb.QueryResult` → `tailordb.QueryResult`)" engine: jssg language: typescript since: "1.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 index a803bbea5..29cbf88a5 100644 --- a/packages/sdk-codemod/codemods/v2/tailordb-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/tailordb-namespace/scripts/transform.ts @@ -1,8 +1,9 @@ -// 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. +// Members of the removed capital-cased `Tailordb` ambient namespace +// (originally from `@tailor-platform/function-types`). Each is a type-only +// declaration now available under the lowercase `tailordb` namespace exposed +// by `@tailor-platform/sdk/runtime/globals`. 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("|"); @@ -14,10 +15,11 @@ const MEMBER_GROUP = TAILORDB_MEMBERS.join("|"); 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. + * Rewrite references to the capital-cased `Tailordb` ambient namespace to the + * lowercase `tailordb` namespace. The capital-cased namespace was inherited + * from `@tailor-platform/function-types`; the SDK kept it as a `@deprecated` + * alias in v1 and removed it in v2, leaving only the lowercase `tailordb.*` + * namespace exposed by `@tailor-platform/sdk/runtime/globals`. * * Only the known type-only members (`QueryResult`, `CommandType`, `Client`) * are rewritten so that unrelated user-defined symbols sharing the diff --git a/packages/sdk/docs/runtime.md b/packages/sdk/docs/runtime.md index 4858d3df1..32c84d23d 100644 --- a/packages/sdk/docs/runtime.md +++ b/packages/sdk/docs/runtime.md @@ -66,7 +66,7 @@ Or register the entry in `tsconfig.json`: } ``` -The globals entry exposes the lowercase `tailordb.*` namespace only. Use `pnpm dlx @tailor-platform/sdk-codemod v2/tailordb-namespace` before upgrading if your project still references the removed capital-cased `Tailordb.*` namespace from `@tailor-platform/function-types`. +The globals entry exposes the lowercase `tailordb.*` namespace only. If your project still references the removed capital-cased `Tailordb.*` namespace from `@tailor-platform/function-types`, migrate before upgrading in two steps: run `pnpm dlx @tailor-platform/sdk-codemod v2/tailordb-namespace` to rewrite `Tailordb.*` references to lowercase `tailordb.*`, then add the `import "@tailor-platform/sdk/runtime/globals"` opt-in above so the rewritten references resolve. ## Namespaces diff --git a/packages/sdk/src/runtime/globals.test.ts b/packages/sdk/src/runtime/globals.test.ts index ea8d39dab..c3b73a00f 100644 --- a/packages/sdk/src/runtime/globals.test.ts +++ b/packages/sdk/src/runtime/globals.test.ts @@ -1,10 +1,11 @@ /** * Type-level tests confirming that opting into `@tailor-platform/sdk/runtime/globals` - * activates the ambient `tailor.*` / `tailordb` declarations. + * activates the ambient `tailor.*` / `tailordb` declarations, and that the + * removed capital-cased `Tailordb` namespace no longer resolves. * - * 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. + * These assertions are type-only — the positive checks reference `tailor`, + * `tailordb`, and `TailorDBFileError` without depending on the platform + * runtime injecting those values into the unit test environment. */ import "@/runtime/globals"; import { describe, expectTypeOf, test } from "vitest"; From 1fa73ab3ef624ca2f79c9455d1e7b0fec051343a Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 14:32:50 +0900 Subject: [PATCH 052/618] test(cli): cover workflow-start auth namespace resolution and missing-auth error Assert the legacy startWorkflow path resolves the proto authInvoker namespace and machine user, and add a case for the error thrown when neither the deployed app nor the config provides an auth namespace. --- .../src/cli/commands/workflow/start.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/sdk/src/cli/commands/workflow/start.test.ts b/packages/sdk/src/cli/commands/workflow/start.test.ts index 88e1244e4..36ec8931d 100644 --- a/packages/sdk/src/cli/commands/workflow/start.test.ts +++ b/packages/sdk/src/cli/commands/workflow/start.test.ts @@ -85,6 +85,10 @@ describe("startWorkflow runtime overload", () => { expect(testStartWorkflowMock).toHaveBeenCalledWith( expect.objectContaining({ workflowId: "id:legacy-workflow", + authInvoker: expect.objectContaining({ + namespace: "auth-ns", + machineUserName: "legacy-user", + }), }), ); }); @@ -148,6 +152,25 @@ describe("startWorkflow runtime overload", () => { ); }); + test("throws when neither the deployed app nor the config has an auth namespace", async () => { + getApplicationMock.mockResolvedValueOnce({ + application: {}, + }); + + await expect( + startWorkflow({ + workflow: { + name: "typed-workflow", + mainJob: { + body: () => undefined, + }, + }, + authInvoker: "typed-user", + }), + ).rejects.toThrow("my-app does not have an auth configuration"); + expect(testStartWorkflowMock).not.toHaveBeenCalled(); + }); + test("start command with jsonMode emits only parseable JSON to stdout", async () => { using stdout = captureStdout(); using stderr = captureStderr(); From 419aaa8605bf5827cb9cd8b2268f31c41db11f49 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 14:53:54 +0900 Subject: [PATCH 053/618] refactor(sdk): source DependencyKind from the types layer Removing the generator-config Zod schemas left parser/generator-config/ schema.ts holding only a DependencyKind type alias, which violates the rule that parser schema files export Zod schemas only. Point the sole importer at the identical alias in @/types/generator-config and drop the now schema-less file. --- packages/sdk/src/parser/generator-config/schema.ts | 1 - packages/sdk/src/plugin/manager.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 packages/sdk/src/parser/generator-config/schema.ts diff --git a/packages/sdk/src/parser/generator-config/schema.ts b/packages/sdk/src/parser/generator-config/schema.ts deleted file mode 100644 index 14c4d1591..000000000 --- a/packages/sdk/src/parser/generator-config/schema.ts +++ /dev/null @@ -1 +0,0 @@ -export type DependencyKind = "tailordb" | "resolver" | "executor"; diff --git a/packages/sdk/src/plugin/manager.ts b/packages/sdk/src/plugin/manager.ts index 83557f4d6..9c0271cbe 100644 --- a/packages/sdk/src/plugin/manager.ts +++ b/packages/sdk/src/plugin/manager.ts @@ -5,7 +5,7 @@ import type { TailorTypePermission, TailorTypeGqlPermission, } from "@/configure/services/tailordb/permission"; -import type { DependencyKind } from "@/parser/generator-config/schema"; +import type { DependencyKind } from "@/types/generator-config"; import type { Plugin, PluginAttachment, From 282fdb7b7023f08a825da4a3fecd5a8afd3a1d02 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 03:58:12 +0900 Subject: [PATCH 054/618] feat: store CLI users by subject --- packages/sdk/docs/cli/user.md | 2 +- packages/sdk/docs/cli/workspace.md | 4 +- packages/sdk/src/cli/commands/login.ts | 13 +- .../sdk/src/cli/commands/profile/create.ts | 2 +- packages/sdk/src/cli/commands/user/switch.ts | 2 +- .../sdk/src/cli/commands/workspace/create.ts | 2 +- packages/sdk/src/cli/crashreport/report.ts | 30 ++- packages/sdk/src/cli/shared/client.ts | 1 + packages/sdk/src/cli/shared/context.test.ts | 122 +++++++++++- packages/sdk/src/cli/shared/context.ts | 185 ++++++++++++++---- 10 files changed, 307 insertions(+), 56 deletions(-) diff --git a/packages/sdk/docs/cli/user.md b/packages/sdk/docs/cli/user.md index 4e2206fd8..a7c389297 100644 --- a/packages/sdk/docs/cli/user.md +++ b/packages/sdk/docs/cli/user.md @@ -203,7 +203,7 @@ tailor-sdk user switch | Argument | Description | Required | | -------- | ----------- | -------- | -| `user` | User email | Yes | +| `user` | User ID | Yes | diff --git a/packages/sdk/docs/cli/workspace.md b/packages/sdk/docs/cli/workspace.md index 31ac40c7f..2a431d39c 100644 --- a/packages/sdk/docs/cli/workspace.md +++ b/packages/sdk/docs/cli/workspace.md @@ -79,7 +79,7 @@ tailor-sdk workspace create [options] | `--organization-id ` | `-o` | Organization ID to workspace associate with | No | - | `TAILOR_PLATFORM_ORGANIZATION_ID` | | `--folder-id ` | `-f` | Folder ID to workspace associate with | No | - | `TAILOR_PLATFORM_FOLDER_ID` | | `--profile-name ` | `-p` | Profile name to create | No | - | - | -| `--profile-user ` | - | User email for the profile (defaults to current user) | No | - | - | +| `--profile-user ` | - | User ID for the profile (defaults to current user) | No | - | - | | `--permission ` | - | Profile permission (requires --profile-name). 'read' blocks all write commands while the profile is active. | No | `"write"` | - | @@ -243,7 +243,7 @@ tailor-sdk profile create [options] | Option | Alias | Description | Required | Default | | ------------------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------- | -| `--user ` | `-u` | User email | Yes | - | +| `--user ` | `-u` | User ID | Yes | - | | `--workspace-id ` | `-w` | Workspace ID | Yes | - | | `--permission ` | - | Profile permission. 'read' blocks all write commands while the profile is active. | No | `"write"` | | `--machine-user ` | `-m` | Default machine user name for application-data commands (query, workflow start, function test-run, machineuser token). | No | - | diff --git a/packages/sdk/src/cli/commands/login.ts b/packages/sdk/src/cli/commands/login.ts index 8e1171549..67759dbe8 100644 --- a/packages/sdk/src/cli/commands/login.ts +++ b/packages/sdk/src/cli/commands/login.ts @@ -11,7 +11,12 @@ import { initOAuth2Client, } from "@/cli/shared/client"; import { defineAppCommand } from "@/cli/shared/command"; -import { readPlatformConfig, saveUserTokens, writePlatformConfig } from "@/cli/shared/context"; +import { + readPlatformConfig, + removeLegacyUserAlias, + saveUserTokens, + writePlatformConfig, +} from "@/cli/shared/context"; import { logger } from "@/cli/shared/logger"; import { prompt } from "@/cli/shared/prompt"; import { assertDefined } from "@/utils/assert"; @@ -47,7 +52,7 @@ const startAuthServer = async () => { const pfConfig = await readPlatformConfig(); await saveUserTokens( pfConfig, - userInfo.email, + userInfo.sub, { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken ?? undefined, @@ -55,8 +60,10 @@ const startAuthServer = async () => { new Date( assertDefined(tokens.expiresAt, "token response missing expiresAt"), ).toISOString(), + { email: userInfo.email }, ); - pfConfig.current_user = userInfo.email; + await removeLegacyUserAlias(pfConfig, userInfo.email, userInfo.sub); + pfConfig.current_user = userInfo.sub; writePlatformConfig(pfConfig); res.writeHead(200, { "Content-Type": "application/json" }); diff --git a/packages/sdk/src/cli/commands/profile/create.ts b/packages/sdk/src/cli/commands/profile/create.ts index ee99a0e72..c37c74157 100644 --- a/packages/sdk/src/cli/commands/profile/create.ts +++ b/packages/sdk/src/cli/commands/profile/create.ts @@ -17,7 +17,7 @@ export const createCommand = defineAppCommand({ }), user: arg(z.string(), { alias: "u", - description: "User email", + description: "User ID", }), "workspace-id": arg(z.string(), { alias: "w", diff --git a/packages/sdk/src/cli/commands/user/switch.ts b/packages/sdk/src/cli/commands/user/switch.ts index f8e3be0ae..73778ce5d 100644 --- a/packages/sdk/src/cli/commands/user/switch.ts +++ b/packages/sdk/src/cli/commands/user/switch.ts @@ -12,7 +12,7 @@ export const switchCommand = defineAppCommand({ .object({ user: arg(z.string(), { positional: true, - description: "User email", + description: "User ID", }), }) .strict(), diff --git a/packages/sdk/src/cli/commands/workspace/create.ts b/packages/sdk/src/cli/commands/workspace/create.ts index df1eaf5aa..59b363520 100644 --- a/packages/sdk/src/cli/commands/workspace/create.ts +++ b/packages/sdk/src/cli/commands/workspace/create.ts @@ -109,7 +109,7 @@ export const createCommand = defineAppCommand({ description: "Profile name to create", }), "profile-user": arg(z.string().optional(), { - description: "User email for the profile (defaults to current user)", + description: "User ID for the profile (defaults to current user)", }), permission: arg(z.enum(["write", "read"]).default("write"), { description: diff --git a/packages/sdk/src/cli/crashreport/report.ts b/packages/sdk/src/cli/crashreport/report.ts index ca879c4f6..f70b2f00d 100644 --- a/packages/sdk/src/cli/crashreport/report.ts +++ b/packages/sdk/src/cli/crashreport/report.ts @@ -84,26 +84,44 @@ export function buildCrashReport(options: BuildCrashReportOptions): CrashReport errorMessage: sanitizeMessage(rawMessage), stackTrace: sanitizeStackTrace(rawStack), errorType, - userId: currentUser, - userEmail: currentUser, + userId: currentUser?.id ?? null, + userEmail: currentUser?.email ?? null, }; } +type CurrentUser = { + id: string; + email: string | null; +}; + /** * Read current_user from Tailor Platform config without side effects. * Unlike readPlatformConfig(), this never triggers migration or logs warnings. - * @returns The current user email, or null if unavailable + * @returns The current user ID and email, or null if unavailable */ -function readCurrentUser(): string | null { +function readCurrentUser(): CurrentUser | null { try { if (!xdgConfig) return null; const configPath = path.join(xdgConfig, "tailor-platform", "config.yaml"); if (!fs.existsSync(configPath)) return null; - const raw = parseYAML(fs.readFileSync(configPath, "utf-8")) as { current_user?: string | null }; + const raw = parseYAML(fs.readFileSync(configPath, "utf-8")) as { + current_user?: string | null; + users?: Record; + }; // parseYAML returns null for empty documents // oxlint-disable-next-line typescript/no-unnecessary-condition - return raw?.current_user ?? null; + const currentUser = raw?.current_user ?? null; + if (!currentUser) return null; + const email = raw.users?.[currentUser]?.email; + return { + id: currentUser, + email: typeof email === "string" ? email : legacyEmail(currentUser), + }; } catch { return null; } } + +function legacyEmail(user: string): string | null { + return user.includes("@") ? user : null; +} diff --git a/packages/sdk/src/cli/shared/client.ts b/packages/sdk/src/cli/shared/client.ts index 8344df961..353f3fca2 100644 --- a/packages/sdk/src/cli/shared/client.ts +++ b/packages/sdk/src/cli/shared/client.ts @@ -476,6 +476,7 @@ export async function fetchUserInfo(accessToken: string) { const rawJson = await resp.json(); const schema = z.object({ + sub: z.string(), email: z.string(), }); return schema.parse(rawJson); diff --git a/packages/sdk/src/cli/shared/context.test.ts b/packages/sdk/src/cli/shared/context.test.ts index 2935a6485..106fe7781 100644 --- a/packages/sdk/src/cli/shared/context.test.ts +++ b/packages/sdk/src/cli/shared/context.test.ts @@ -8,11 +8,13 @@ import { loadConfigPath, loadMachineUserName, loadWorkspaceId, + readPlatformConfig, writePlatformConfig, } from "./context"; import { isCLIError } from "./errors"; import { logger } from "./logger"; import { resetKeyringState } from "./token-store"; +import type * as ClientModule from "./client"; const xdgTempDir = vi.hoisted(() => `/tmp/tailor-xdg-${Date.now()}-${Math.random()}`); @@ -30,6 +32,27 @@ vi.mock("@napi-rs/keyring", () => ({ }, })); +const clientMocks = vi.hoisted(() => ({ + fetchUserInfo: vi.fn(), + refreshToken: vi.fn(), +})); + +vi.mock("./client", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + fetchUserInfo: clientMocks.fetchUserInfo, + initOAuth2Client: () => ({ + refreshToken: clientMocks.refreshToken, + }), + }; +}); + +beforeEach(() => { + clientMocks.fetchUserInfo.mockReset(); + clientMocks.refreshToken.mockReset(); +}); + describe("loadConfigPath", () => { const originalEnv = process.env; let tempDir: string; @@ -462,6 +485,7 @@ describe("loadAccessToken", () => { const validToken = "valid-access-token"; const otherToken = "other-access-token"; const futureDate = new Date(Date.now() + 3600 * 1000).toISOString(); + const pastDate = new Date(Date.now() - 3600 * 1000).toISOString(); beforeEach(() => { vi.resetModules(); @@ -665,6 +689,53 @@ describe("loadAccessToken", () => { const result = await loadAccessToken(); expect(result).toBe(validToken); }); + + test("refreshes a legacy email-key user into a subject-key V3 config", async () => { + clientMocks.refreshToken.mockResolvedValue({ + accessToken: "new-access-token", + refreshToken: "new-refresh-token", + expiresAt: Date.now() + 3600 * 1000, + }); + clientMocks.fetchUserInfo.mockResolvedValue({ + sub: "platform-user-sub", + email: "legacy@example.com", + }); + writePlatformConfig({ + version: 2, + min_sdk_version: "1.29.0", + users: { + "legacy@example.com": { + access_token: "expired-access-token", + refresh_token: "refresh", + token_expires_at: pastDate, + storage: "file", + }, + }, + profiles: { + default: { + user: "legacy@example.com", + workspace_id: "12345678-1234-4abc-8def-123456789012", + }, + }, + current_user: "legacy@example.com", + }); + + const token = await loadAccessToken(); + const config = await readPlatformConfig(); + + expect(token).toBe("new-access-token"); + expect(clientMocks.fetchUserInfo).toHaveBeenCalledWith("new-access-token"); + expect(config.version).toBe(3); + expect(config.users["legacy@example.com"]).toBeUndefined(); + expect(config.users["platform-user-sub"]).toMatchObject({ + storage: "file", + access_token: "new-access-token", + refresh_token: "new-refresh-token", + email: "legacy@example.com", + }); + expect(config.current_user).toBe("platform-user-sub"); + expect(config.profiles.default?.user).toBe("platform-user-sub"); + }); }); describe("error case: no token source", () => { @@ -701,14 +772,14 @@ describe("profile readonly field", () => { }); }); -describe("V1 to V2 migration", () => { +describe("V1 to V3 migration", () => { const futureDate = new Date(Date.now() + 3600 * 1000).toISOString(); beforeEach(() => { resetKeyringState(); }); - test("migrates V1 config to V2 in memory without rewriting disk", async () => { + test("migrates V1 config to V3 in memory without rewriting disk", async () => { const configPath = path.join(xdgTempDir, "tailor-platform", "config.yaml"); writePlatformConfig({ version: 1, @@ -729,11 +800,12 @@ describe("V1 to V2 migration", () => { const { readPlatformConfig } = await import("./context"); const config = await readPlatformConfig(); - // In-memory: V2 with storage: "file" - expect(config.version).toBe(2); + // In-memory: V3 with storage: "file" and inferred legacy email metadata. + expect(config.version).toBe(3); const userEntry = config.users["user@example.com"]; expect(userEntry).toBeDefined(); expect(userEntry!.storage).toBe("file"); + expect(userEntry!.email).toBe("user@example.com"); expect(userEntry!.token_expires_at).toBe(futureDate); if (userEntry!.storage !== "file") { throw new Error("Expected file-backed user entry"); @@ -741,7 +813,7 @@ describe("V1 to V2 migration", () => { expect(userEntry!.access_token).toBe("v1-access-token"); expect(userEntry!.refresh_token).toBe("v1-refresh-token"); - // Disk: still V1 (not rewritten to V2) + // Disk: still V1 (not rewritten to V3) const diskConfig = parseYAML(fs.readFileSync(configPath, "utf-8")) as { version: number }; expect(diskConfig.version).toBe(1); }); @@ -793,14 +865,50 @@ describe("keyring user persistence on V2 -> V1 downgrade", () => { expect(diskConfig.users["file@example.com"]?.storage).toBe("file"); expect(diskConfig.current_user).toBe("keyring@example.com"); - // Round trip: the keyring user (and current_user) survive a re-read. + // Round trip: the keyring user (and current_user) survive a re-read and + // are exposed through the latest in-memory config version. const { readPlatformConfig } = await import("./context"); const config = await readPlatformConfig(); - expect(config.version).toBe(2); + expect(config.version).toBe(3); expect(config.users["keyring@example.com"]?.storage).toBe("keyring"); + expect(config.users["keyring@example.com"]?.email).toBe("keyring@example.com"); expect(config.current_user).toBe("keyring@example.com"); }); + test("keeps V3 configs as V3 because subject IDs and email metadata cannot downgrade", () => { + writePlatformConfig({ + version: 3, + min_sdk_version: "2.0.0", + users: { + "platform-user-sub": { + storage: "file", + access_token: "file-access-token", + refresh_token: "file-refresh-token", + token_expires_at: futureDate, + email: "user@example.com", + }, + }, + profiles: { + default: { + user: "platform-user-sub", + workspace_id: "12345678-1234-4abc-8def-123456789012", + }, + }, + current_user: "platform-user-sub", + }); + + const diskConfig = parseYAML(fs.readFileSync(configPath, "utf-8")) as { + version: number; + users: Record; + profiles: Record; + current_user: string | null; + }; + expect(diskConfig.version).toBe(3); + expect(diskConfig.users["platform-user-sub"]?.email).toBe("user@example.com"); + expect(diskConfig.profiles.default?.user).toBe("platform-user-sub"); + expect(diskConfig.current_user).toBe("platform-user-sub"); + }); + test("still downgrades a file-only config to V1 for backward compatibility", () => { writePlatformConfig({ version: 2, diff --git a/packages/sdk/src/cli/shared/context.ts b/packages/sdk/src/cli/shared/context.ts index eed2f3194..00f832ff1 100644 --- a/packages/sdk/src/cli/shared/context.ts +++ b/packages/sdk/src/cli/shared/context.ts @@ -8,7 +8,7 @@ import { xdgConfig } from "xdg-basedir"; import { z } from "zod"; import { assertDefined } from "@/utils/assert"; import ml from "@/utils/multiline"; -import { initOAuth2Client } from "./client"; +import { fetchUserInfo, initOAuth2Client } from "./client"; import { CLIError } from "./errors"; import { logger } from "./logger"; import { readPackageJson } from "./package-json"; @@ -48,7 +48,15 @@ const pfUserFileSchema = z.object({ const pfUserSchemaV2 = z.discriminatedUnion("storage", [pfUserKeyringSchema, pfUserFileSchema]); -type PfUserV2 = z.output; +const pfUserKeyringSchemaV3 = pfUserKeyringSchema.extend({ + email: z.string().optional(), +}); + +const pfUserFileSchemaV3 = pfUserFileSchema.extend({ + email: z.string().optional(), +}); + +const pfUserSchemaV3 = z.discriminatedUnion("storage", [pfUserKeyringSchemaV3, pfUserFileSchemaV3]); const pfConfigSchemaV1 = z.object({ version: z.literal(1), @@ -57,8 +65,10 @@ const pfConfigSchemaV1 = z.object({ current_user: z.string().nullable(), }); -const LATEST_CONFIG_VERSION = 2; +const V2_CONFIG_VERSION = 2; +const LATEST_CONFIG_VERSION = 3; const V2_MIN_SDK_VERSION = "1.29.0"; +const V3_MIN_SDK_VERSION = "2.0.0"; const semverSchema = z.templateLiteral([ z.number().int(), @@ -69,7 +79,7 @@ const semverSchema = z.templateLiteral([ ]); const pfConfigSchemaV2 = z.object({ - version: z.literal(LATEST_CONFIG_VERSION), + version: z.literal(V2_CONFIG_VERSION), min_sdk_version: semverSchema, latest_version: z.number().int().optional(), latest_min_sdk_version: semverSchema.optional(), @@ -78,8 +88,20 @@ const pfConfigSchemaV2 = z.object({ current_user: z.string().nullable(), }); +const pfConfigSchemaV3 = z.object({ + version: z.literal(LATEST_CONFIG_VERSION), + min_sdk_version: semverSchema, + latest_version: z.number().int().optional(), + latest_min_sdk_version: semverSchema.optional(), + users: z.partialRecord(z.string(), pfUserSchemaV3), + profiles: z.partialRecord(z.string(), pfProfileSchema), + current_user: z.string().nullable(), +}); + type PfConfigV1 = z.output; -type PfConfig = z.output; +type PfConfigV2 = z.output; +type PfConfig = z.output; +type PfUser = z.output; type LoadWorkspaceIdOptions = { workspaceId?: string; profile?: string; @@ -106,8 +128,8 @@ function platformConfigPath() { * @param v1Config - v1 configuration to migrate * @returns Migrated v2 configuration */ -function migrateV1ToV2(v1Config: PfConfigV1): PfConfig { - const users: PfConfig["users"] = {}; +function migrateV1ToV2(v1Config: PfConfigV1): PfConfigV2 { + const users: PfConfigV2["users"] = {}; for (const [name, v1User] of Object.entries(v1Config.users)) { if (!v1User) continue; @@ -121,7 +143,7 @@ function migrateV1ToV2(v1Config: PfConfigV1): PfConfig { } return { - version: LATEST_CONFIG_VERSION, + version: V2_CONFIG_VERSION, min_sdk_version: V2_MIN_SDK_VERSION, users, profiles: v1Config.profiles, @@ -129,6 +151,50 @@ function migrateV1ToV2(v1Config: PfConfigV1): PfConfig { }; } +function inferEmailFromUserId(user: string): string | undefined { + return z.email().safeParse(user).success ? user : undefined; +} + +function migrateV2ToV3(v2Config: PfConfigV2): PfConfig { + const users: PfConfig["users"] = {}; + + for (const [user, entry] of Object.entries(v2Config.users)) { + if (!entry) continue; + const email = inferEmailFromUserId(user); + users[user] = { + ...entry, + ...(email ? { email } : {}), + }; + } + + return { + version: LATEST_CONFIG_VERSION, + min_sdk_version: V3_MIN_SDK_VERSION, + users, + profiles: v2Config.profiles, + current_user: v2Config.current_user, + }; +} + +function migrateV1ToV3(v1Config: PfConfigV1): PfConfig { + return migrateV2ToV3(migrateV1ToV2(v1Config)); +} + +async function warnIfNewerConfigAvailable(config: { + latest_version?: number; + latest_min_sdk_version?: string; +}) { + if (!config.latest_min_sdk_version) return; + const packageJson = await readPackageJson(); + const sdkVersion = packageJson.version ?? "0.0.0"; + if (semverLt(sdkVersion, config.latest_min_sdk_version)) { + logger.warn(ml` + A newer config version (${String(config.latest_version)}) is available. + Please update your SDK to >= ${config.latest_min_sdk_version}: pnpm update @tailor-platform/sdk + `); + } +} + /** * Read Tailor Platform CLI configuration, migrating from tailorctl or v1 if necessary. * @returns Parsed platform configuration @@ -144,7 +210,7 @@ export async function readPlatformConfig(): Promise { ? fromTailorctlConfig(tcConfig) : ({ version: 1, users: {}, profiles: {}, current_user: null } as const); writePlatformConfig(v1Config); - return migrateV1ToV2(v1Config); + return migrateV1ToV3(v1Config); } const rawConfig = parseYAML(fs.readFileSync(configPath, "utf-8")); @@ -173,36 +239,34 @@ export async function readPlatformConfig(): Promise { `); } - // Try v2 first + // Try v3 first + const v3Result = pfConfigSchemaV3.safeParse(rawConfig); + if (v3Result.success) { + await warnIfNewerConfigAvailable(v3Result.data); + return v3Result.data; + } + + // Try v2 next const v2Result = pfConfigSchemaV2.safeParse(rawConfig); if (v2Result.success) { - if (v2Result.data.latest_min_sdk_version) { - const packageJson = await readPackageJson(); - const sdkVersion = packageJson.version ?? "0.0.0"; - if (semverLt(sdkVersion, v2Result.data.latest_min_sdk_version)) { - logger.warn(ml` - A newer config version (${String(v2Result.data.latest_version)}) is available. - Please update your SDK to >= ${v2Result.data.latest_min_sdk_version}: pnpm update @tailor-platform/sdk - `); - } - } - return v2Result.data; + await warnIfNewerConfigAvailable(v2Result.data); + return migrateV2ToV3(v2Result.data); } - // Fall back to v1 (convert to v2 in memory, but don't rewrite disk) + // Fall back to v1 (convert to v3 in memory, but don't rewrite disk) const v1Result = pfConfigSchemaV1.safeParse(rawConfig); if (v1Result.success) { - return migrateV1ToV2(v1Result.data); + return migrateV1ToV3(v1Result.data); } - // Neither v1 nor v2 + // Neither v1, v2, nor v3 throw new Error(ml` Failed to parse config file at ${configPath}. The file may be corrupted or created by an incompatible SDK version. `); } -function toV1ForDisk(config: PfConfig): PfConfigV1 { +function toV1ForDisk(config: PfConfigV2): PfConfigV1 { const users: PfConfigV1["users"] = {}; for (const [name, entry] of Object.entries(config.users)) { if (!entry || entry.storage === "keyring") continue; @@ -229,9 +293,9 @@ function toV1ForDisk(config: PfConfig): PfConfigV1 { * By default, V2 configs are converted to V1 for backward compatibility, so an * older SDK can still read the file. Configs containing a keyring user are kept * as V2 regardless, because the keyring storage variant is not representable in - * V1 and downgrading it would silently drop the user's login. Such configs are - * already V2 on disk (a keyring entry is only ever persisted with - * TAILOR_USE_KEYRING set), so keeping V2 does not regress backward compatibility. + * V1 and downgrading it would silently drop the user's login. V3 configs are + * kept as V3 because user keys are canonical subject IDs and may include email + * metadata that is not representable in older versions. * Set TAILOR_USE_KEYRING to write V2 format unconditionally. * * The config file may contain access/refresh tokens when the OS keyring is @@ -239,7 +303,7 @@ function toV1ForDisk(config: PfConfig): PfConfigV1 { * on the host cannot read it. * @param config - Platform configuration to write */ -export function writePlatformConfig(config: PfConfig | PfConfigV1) { +export function writePlatformConfig(config: PfConfig | PfConfigV2 | PfConfigV1) { const configPath = platformConfigPath(); const hasKeyringUser = config.version === 2 && Object.values(config.users).some((u) => u?.storage === "keyring"); @@ -460,7 +524,7 @@ export async function loadAccessToken(opts?: LoadAccessTokenOptions) { * @returns Access token and optional refresh token */ export async function resolveTokens( - userEntry: PfUserV2, + userEntry: PfUser, user: string, ): Promise<{ accessToken: string; refreshToken?: string }> { if (userEntry.storage === "keyring") { @@ -485,21 +549,25 @@ export async function resolveTokens( * @param config - Platform config * @param user - User identifier * @param tokens - Token data to save - * @param tokens.accessToken - * @param tokens.refreshToken + * @param tokens.accessToken - Access token to save + * @param tokens.refreshToken - Optional refresh token to save * @param expiresAt - Token expiration date + * @param metadata - Optional user metadata to persist with the token entry */ export async function saveUserTokens( config: PfConfig, user: string, tokens: { accessToken: string; refreshToken?: string }, expiresAt: string, + metadata?: { email?: string }, ): Promise { + const email = metadata?.email ?? config.users[user]?.email; if (process.env.TAILOR_USE_KEYRING && (await isKeyringAvailable())) { await saveKeyringTokens(user, tokens); config.users[user] = { token_expires_at: expiresAt, storage: "keyring", + ...(email ? { email } : {}), }; } else { config.users[user] = { @@ -507,6 +575,7 @@ export async function saveUserTokens( refresh_token: tokens.refreshToken, token_expires_at: expiresAt, storage: "file", + ...(email ? { email } : {}), }; } } @@ -523,10 +592,43 @@ export async function deleteUserTokens(config: PfConfig, user: string): Promise< } } +function updateUserReferences(config: PfConfig, fromUser: string, toUser: string) { + if (fromUser === toUser) return; + if (config.current_user === fromUser) { + config.current_user = toUser; + } + for (const profile of Object.values(config.profiles)) { + if (profile?.user === fromUser) { + profile.user = toUser; + } + } +} + +/** + * Remove a legacy alias after a canonical user ID has been written. + * @param config - Platform config + * @param legacyUser - Previous user key + * @param canonicalUser - Canonical user key + */ +export async function removeLegacyUserAlias( + config: PfConfig, + legacyUser: string, + canonicalUser: string, +): Promise { + if (legacyUser === canonicalUser) return; + updateUserReferences(config, legacyUser, canonicalUser); + await deleteUserTokens(config, legacyUser); + delete config.users[legacyUser]; +} + +function shouldResolveSubjectOnRefresh(user: string, userEntry: PfUser): boolean { + return Boolean(userEntry.email || inferEmailFromUserId(user)); +} + /** * Fetch the latest access token, refreshing if necessary. * @param config - Platform config - * @param user - User name + * @param user - User ID * @returns Latest access token */ export async function fetchLatestToken(config: PfConfig, user: string): Promise { @@ -569,15 +671,30 @@ export async function fetchLatestToken(config: PfConfig, user: string): Promise< const newExpiresAt = new Date( assertDefined(resp.expiresAt, "token refresh response missing expiresAt"), ).toISOString(); + + let resolvedUser = user; + let email = userEntry.email; + if (shouldResolveSubjectOnRefresh(user, userEntry)) { + try { + const userInfo = await fetchUserInfo(resp.accessToken); + resolvedUser = userInfo.sub; + email = userInfo.email; + } catch (error) { + logger.debug(`Failed to resolve refreshed token user info: ${String(error)}`); + } + } + await saveUserTokens( config, - user, + resolvedUser, { accessToken: resp.accessToken, refreshToken: resp.refreshToken ?? undefined, }, newExpiresAt, + { email }, ); + await removeLegacyUserAlias(config, user, resolvedUser); writePlatformConfig(config); return resp.accessToken; } From ad049131d61dfc9f96bff8ffe7c5e5e261523b14 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 13:43:11 +0900 Subject: [PATCH 055/618] chore: add changeset for CLI user subjects --- .changeset/store-cli-users-by-subject.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/store-cli-users-by-subject.md diff --git a/.changeset/store-cli-users-by-subject.md b/.changeset/store-cli-users-by-subject.md new file mode 100644 index 000000000..715a5ccda --- /dev/null +++ b/.changeset/store-cli-users-by-subject.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Store CLI human users by stable subject ID in the platform config instead of email, while preserving email for display and migrating legacy email-keyed entries on login or token refresh. From 939c8e5b6d5a83222ba5476f7f43720fb1bdf126 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 14:27:12 +0900 Subject: [PATCH 056/618] test(cli): cover legacy alias retention when subject resolution fails --- packages/sdk/src/cli/shared/context.test.ts | 42 +++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/sdk/src/cli/shared/context.test.ts b/packages/sdk/src/cli/shared/context.test.ts index 106fe7781..0cf78bf43 100644 --- a/packages/sdk/src/cli/shared/context.test.ts +++ b/packages/sdk/src/cli/shared/context.test.ts @@ -736,6 +736,48 @@ describe("loadAccessToken", () => { expect(config.current_user).toBe("platform-user-sub"); expect(config.profiles.default?.user).toBe("platform-user-sub"); }); + + test("keeps the legacy email key when subject resolution fails on refresh", async () => { + clientMocks.refreshToken.mockResolvedValue({ + accessToken: "new-access-token", + refreshToken: "new-refresh-token", + expiresAt: Date.now() + 3600 * 1000, + }); + clientMocks.fetchUserInfo.mockRejectedValue(new Error("network down")); + writePlatformConfig({ + version: 2, + min_sdk_version: "1.29.0", + users: { + "legacy@example.com": { + access_token: "expired-access-token", + refresh_token: "refresh", + token_expires_at: pastDate, + storage: "file", + }, + }, + profiles: { + default: { + user: "legacy@example.com", + workspace_id: "12345678-1234-4abc-8def-123456789012", + }, + }, + current_user: "legacy@example.com", + }); + + const token = await loadAccessToken(); + const config = await readPlatformConfig(); + + expect(token).toBe("new-access-token"); + expect(clientMocks.fetchUserInfo).toHaveBeenCalledWith("new-access-token"); + expect(config.users["platform-user-sub"]).toBeUndefined(); + expect(config.users["legacy@example.com"]).toMatchObject({ + storage: "file", + access_token: "new-access-token", + refresh_token: "new-refresh-token", + }); + expect(config.current_user).toBe("legacy@example.com"); + expect(config.profiles.default?.user).toBe("legacy@example.com"); + }); }); describe("error case: no token source", () => { From e37a9a8a12c80f79c2ed4f3f5a98c1e53d8512fe Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 14:46:54 +0900 Subject: [PATCH 057/618] fix(cli): persist resolved subject after refresh migration fetchLatestToken now returns the canonical user ID alongside the access token, so profile create/update write the migrated subject key instead of a legacy email key that the refresh just deleted. --- .../src/cli/commands/profile/create.test.ts | 111 ++++++++++++++++++ .../sdk/src/cli/commands/profile/create.ts | 6 +- .../src/cli/commands/profile/update.test.ts | 5 +- .../sdk/src/cli/commands/profile/update.ts | 10 +- .../sdk/src/cli/commands/user/pat/create.ts | 2 +- .../sdk/src/cli/commands/user/pat/delete.ts | 2 +- .../sdk/src/cli/commands/user/pat/list.ts | 2 +- .../sdk/src/cli/commands/user/pat/update.ts | 2 +- packages/sdk/src/cli/shared/context.ts | 15 ++- 9 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 packages/sdk/src/cli/commands/profile/create.test.ts diff --git a/packages/sdk/src/cli/commands/profile/create.test.ts b/packages/sdk/src/cli/commands/profile/create.test.ts new file mode 100644 index 000000000..a132006ab --- /dev/null +++ b/packages/sdk/src/cli/commands/profile/create.test.ts @@ -0,0 +1,111 @@ +import * as fs from "node:fs"; +import * as path from "pathe"; +import { runCommand } from "politty"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import { readPlatformConfig, writePlatformConfig } from "@/cli/shared/context"; +import { silenceLogger } from "@/cli/shared/test-helpers/silence-logger"; +import { resetKeyringState } from "@/cli/shared/token-store"; +import { createCommand } from "./create"; +import type * as ClientModule from "@/cli/shared/client"; + +const xdgTempDir = vi.hoisted(() => `/tmp/tailor-profile-create-${Date.now()}-${Math.random()}`); +const validUUID = "12345678-1234-4abc-8def-123456789012"; + +vi.mock("xdg-basedir", () => ({ xdgConfig: xdgTempDir })); + +vi.mock("@napi-rs/keyring", () => ({ + Entry: class { + setPassword() {} + getPassword(): string | null { + return null; + } + deletePassword() {} + }, +})); + +const clientMocks = vi.hoisted(() => ({ + fetchUserInfo: vi.fn(), + refreshToken: vi.fn(), + initOperatorClient: vi.fn(), + fetchAll: vi.fn(), +})); + +vi.mock("@/cli/shared/client", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + fetchUserInfo: clientMocks.fetchUserInfo, + initOperatorClient: clientMocks.initOperatorClient, + fetchAll: clientMocks.fetchAll, + initOAuth2Client: () => ({ refreshToken: clientMocks.refreshToken }), + }; +}); + +beforeAll(() => { + fs.mkdirSync(xdgTempDir, { recursive: true }); +}); + +afterAll(() => { + fs.rmSync(xdgTempDir, { recursive: true, force: true }); +}); + +describe("profile create with a migrating legacy email user", () => { + beforeEach(() => { + vi.clearAllMocks(); + resetKeyringState(); + vi.stubEnv("TAILOR_PLATFORM_PROFILE", undefined); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + const configPath = path.join(xdgTempDir, "tailor-platform", "config.yaml"); + if (fs.existsSync(configPath)) fs.rmSync(configPath); + }); + + test("writes the resolved subject as profile.user when the legacy email key migrates", async () => { + using _logger = silenceLogger("out", "success"); + + const pastDate = new Date(Date.now() - 3600 * 1000).toISOString(); + clientMocks.refreshToken.mockResolvedValue({ + accessToken: "new-access-token", + refreshToken: "new-refresh-token", + expiresAt: Date.now() + 3600 * 1000, + }); + clientMocks.fetchUserInfo.mockResolvedValue({ + sub: "platform-user-sub", + email: "legacy@example.com", + }); + clientMocks.initOperatorClient.mockResolvedValue({ + listWorkspaces: vi.fn(), + }); + clientMocks.fetchAll.mockResolvedValue([{ id: validUUID }]); + + writePlatformConfig({ + version: 2, + min_sdk_version: "1.29.0", + users: { + "legacy@example.com": { + access_token: "expired-access-token", + refresh_token: "refresh", + token_expires_at: pastDate, + storage: "file", + }, + }, + profiles: {}, + current_user: "legacy@example.com", + }); + + await runCommand(createCommand, [ + "myprofile", + "--user", + "legacy@example.com", + "--workspace-id", + validUUID, + ]); + + const config = await readPlatformConfig(); + expect(config.profiles.myprofile?.user).toBe("platform-user-sub"); + expect(config.users["platform-user-sub"]).toMatchObject({ storage: "file" }); + expect(config.users["legacy@example.com"]).toBeUndefined(); + }); +}); diff --git a/packages/sdk/src/cli/commands/profile/create.ts b/packages/sdk/src/cli/commands/profile/create.ts index c37c74157..13dd632eb 100644 --- a/packages/sdk/src/cli/commands/profile/create.ts +++ b/packages/sdk/src/cli/commands/profile/create.ts @@ -51,7 +51,7 @@ export const createCommand = defineAppCommand({ } // Check if user exists - const token = await fetchLatestToken(config, args.user); + const { accessToken: token, user: resolvedUser } = await fetchLatestToken(config, args.user); // Check if workspace exists const client = await initOperatorClient(token); @@ -70,7 +70,7 @@ export const createCommand = defineAppCommand({ // Create new profile config.profiles[args.name] = { - user: args.user, + user: resolvedUser, workspace_id: args["workspace-id"], ...(args.permission === "read" ? { readonly: true } : {}), ...(args["machine-user"] ? { machine_user: args["machine-user"] } : {}), @@ -87,7 +87,7 @@ export const createCommand = defineAppCommand({ // Show profile info const profileInfo: ProfileInfo = { name: args.name, - user: args.user, + user: resolvedUser, workspaceId: args["workspace-id"], permission: args.permission, ...(args["machine-user"] diff --git a/packages/sdk/src/cli/commands/profile/update.test.ts b/packages/sdk/src/cli/commands/profile/update.test.ts index 8fed53871..6b237600c 100644 --- a/packages/sdk/src/cli/commands/profile/update.test.ts +++ b/packages/sdk/src/cli/commands/profile/update.test.ts @@ -102,7 +102,10 @@ describe("profile update --permission", () => { test("performs remote validation when --user is also passed (permission does not bypass it)", async () => { using _logger = silenceLogger("out", "success"); - vi.mocked(fetchLatestToken).mockResolvedValue("mock-token"); + vi.mocked(fetchLatestToken).mockResolvedValue({ + accessToken: "mock-token", + user: "new@example.com", + }); vi.mocked(fetchAll).mockResolvedValue([{ id: validUUID }]); vi.mocked(initOperatorClient).mockResolvedValue({ listWorkspaces: vi.fn(), diff --git a/packages/sdk/src/cli/commands/profile/update.ts b/packages/sdk/src/cli/commands/profile/update.ts index 0cec45b9d..b245261e1 100644 --- a/packages/sdk/src/cli/commands/profile/update.ts +++ b/packages/sdk/src/cli/commands/profile/update.ts @@ -61,6 +61,7 @@ export const updateCommand = defineAppCommand({ const newUser = args.user || oldUser; const oldWorkspaceId = profile.workspace_id; const newWorkspaceId = args["workspace-id"] || oldWorkspaceId; + let resolvedUser = newUser; // Compute the final machine_user and machine_user_override to validate the combination. const finalMachineUser = @@ -89,10 +90,11 @@ export const updateCommand = defineAppCommand({ // removed, important so a user can always lift their own readonly flag. if (args.user !== undefined || args["workspace-id"] !== undefined) { // Check if user exists - const token = await fetchLatestToken(config, newUser); + const refreshed = await fetchLatestToken(config, newUser); + resolvedUser = refreshed.user; // Check if workspace exists - const client = await initOperatorClient(token); + const client = await initOperatorClient(refreshed.accessToken); const workspaces = await fetchAll(async (pageToken, maxPageSize) => { const { workspaces, nextPageToken } = await client.listWorkspaces({ pageToken, @@ -107,7 +109,7 @@ export const updateCommand = defineAppCommand({ } // Update properties - profile.user = newUser; + profile.user = resolvedUser; profile.workspace_id = newWorkspaceId; if (args.permission === "read") { profile.readonly = true; @@ -134,7 +136,7 @@ export const updateCommand = defineAppCommand({ // Show profile info const profileInfo: ProfileInfo = { name: args.name, - user: newUser, + user: resolvedUser, workspaceId: newWorkspaceId, permission: profile.readonly === true ? "read" : "write", ...(profile.machine_user diff --git a/packages/sdk/src/cli/commands/user/pat/create.ts b/packages/sdk/src/cli/commands/user/pat/create.ts index 438779171..802336e2e 100644 --- a/packages/sdk/src/cli/commands/user/pat/create.ts +++ b/packages/sdk/src/cli/commands/user/pat/create.ts @@ -33,7 +33,7 @@ export const createCommand = defineAppCommand({ `); } - const token = await fetchLatestToken(config, config.current_user); + const { accessToken: token } = await fetchLatestToken(config, config.current_user); const client = await initOperatorClient(token); const scopes = getScopesFromWriteFlag(args.write); diff --git a/packages/sdk/src/cli/commands/user/pat/delete.ts b/packages/sdk/src/cli/commands/user/pat/delete.ts index 4baeecbff..95184edd3 100644 --- a/packages/sdk/src/cli/commands/user/pat/delete.ts +++ b/packages/sdk/src/cli/commands/user/pat/delete.ts @@ -29,7 +29,7 @@ export const deleteCommand = defineAppCommand({ `); } - const token = await fetchLatestToken(config, config.current_user); + const { accessToken: token } = await fetchLatestToken(config, config.current_user); const client = await initOperatorClient(token); await client.deletePersonalAccessToken({ diff --git a/packages/sdk/src/cli/commands/user/pat/list.ts b/packages/sdk/src/cli/commands/user/pat/list.ts index b8504f4c2..4bfa5f730 100644 --- a/packages/sdk/src/cli/commands/user/pat/list.ts +++ b/packages/sdk/src/cli/commands/user/pat/list.ts @@ -22,7 +22,7 @@ export const listCommand = defineAppCommand({ `); } - const token = await fetchLatestToken(config, config.current_user); + const { accessToken: token } = await fetchLatestToken(config, config.current_user); const client = await initOperatorClient(token); const pageDirection = toPageDirection(args.order); diff --git a/packages/sdk/src/cli/commands/user/pat/update.ts b/packages/sdk/src/cli/commands/user/pat/update.ts index 52c298698..6f61eeb00 100644 --- a/packages/sdk/src/cli/commands/user/pat/update.ts +++ b/packages/sdk/src/cli/commands/user/pat/update.ts @@ -33,7 +33,7 @@ export const updateCommand = defineAppCommand({ `); } - const token = await fetchLatestToken(config, config.current_user); + const { accessToken: token } = await fetchLatestToken(config, config.current_user); const client = await initOperatorClient(token); // Delete the existing token diff --git a/packages/sdk/src/cli/shared/context.ts b/packages/sdk/src/cli/shared/context.ts index 00f832ff1..3cbcac9e0 100644 --- a/packages/sdk/src/cli/shared/context.ts +++ b/packages/sdk/src/cli/shared/context.ts @@ -514,7 +514,7 @@ export async function loadAccessToken(opts?: LoadAccessTokenOptions) { user = u; } - return await fetchLatestToken(pfConfig, user); + return (await fetchLatestToken(pfConfig, user)).accessToken; } /** @@ -629,9 +629,14 @@ function shouldResolveSubjectOnRefresh(user: string, userEntry: PfUser): boolean * Fetch the latest access token, refreshing if necessary. * @param config - Platform config * @param user - User ID - * @returns Latest access token + * @returns Latest access token and the canonical user ID it is stored under + * (the resolved subject when a legacy email key was migrated during refresh, + * otherwise the input user) */ -export async function fetchLatestToken(config: PfConfig, user: string): Promise { +export async function fetchLatestToken( + config: PfConfig, + user: string, +): Promise<{ accessToken: string; user: string }> { const userEntry = config.users[user]; if (!userEntry) { throw new Error(ml` @@ -643,7 +648,7 @@ export async function fetchLatestToken(config: PfConfig, user: string): Promise< const tokens = await resolveTokens(userEntry, user); if (new Date(userEntry.token_expires_at) > new Date()) { - return tokens.accessToken; + return { accessToken: tokens.accessToken, user }; } if (!tokens.refreshToken) { @@ -696,7 +701,7 @@ export async function fetchLatestToken(config: PfConfig, user: string): Promise< ); await removeLegacyUserAlias(config, user, resolvedUser); writePlatformConfig(config); - return resp.accessToken; + return { accessToken: resp.accessToken, user: resolvedUser }; } const DEFAULT_CONFIG_FILENAME = "tailor.config.ts"; From 6bdaad671101d66adfb7ab9dfc9bee2797b389c5 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 14:58:31 +0900 Subject: [PATCH 058/618] docs(cli): describe profile update --user as a user ID Align the profile update help text with the subject-keyed user model used by profile create, user switch, and workspace create. --- packages/sdk/docs/cli/workspace.md | 2 +- packages/sdk/src/cli/commands/profile/update.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/docs/cli/workspace.md b/packages/sdk/docs/cli/workspace.md index 2a431d39c..cc4dd5a57 100644 --- a/packages/sdk/docs/cli/workspace.md +++ b/packages/sdk/docs/cli/workspace.md @@ -324,7 +324,7 @@ tailor-sdk profile update [options] | Option | Alias | Description | Required | Default | | ------------------------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------- | -| `--user ` | `-u` | New user email | No | - | +| `--user ` | `-u` | New user ID | No | - | | `--workspace-id ` | `-w` | New workspace ID | No | - | | `--permission ` | - | Profile permission. 'read' blocks all write commands; 'write' lifts the restriction. | No | - | | `--machine-user ` | `-m` | Default machine user name for application-data commands (query, workflow start, function test-run, machineuser token). Pass an empty string to clear. | No | - | diff --git a/packages/sdk/src/cli/commands/profile/update.ts b/packages/sdk/src/cli/commands/profile/update.ts index b245261e1..2f7838372 100644 --- a/packages/sdk/src/cli/commands/profile/update.ts +++ b/packages/sdk/src/cli/commands/profile/update.ts @@ -17,7 +17,7 @@ export const updateCommand = defineAppCommand({ }), user: arg(z.string().optional(), { alias: "u", - description: "New user email", + description: "New user ID", }), "workspace-id": arg(z.string().optional(), { alias: "w", From b22bf1bb3ed05aaca3e2fc769f2979cf4e15ab04 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 15:16:55 +0900 Subject: [PATCH 059/618] test(cli): cover subject migration on workspace-only profile update --- .../src/cli/commands/profile/update.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/sdk/src/cli/commands/profile/update.test.ts b/packages/sdk/src/cli/commands/profile/update.test.ts index 6b237600c..f92271fe9 100644 --- a/packages/sdk/src/cli/commands/profile/update.test.ts +++ b/packages/sdk/src/cli/commands/profile/update.test.ts @@ -131,6 +131,40 @@ describe("profile update --permission", () => { expect(config.profiles.rw?.user).toBe("new@example.com"); expect(config.profiles.rw?.readonly).toBe(true); }); + + test("persists the resolved subject when only --workspace-id is updated for a legacy email user", async () => { + using _logger = silenceLogger("out", "success"); + const newUUID = "abcdef12-3456-4abc-8def-abcdef123456"; + vi.mocked(fetchLatestToken).mockResolvedValue({ + accessToken: "mock-token", + user: "platform-user-sub", + }); + vi.mocked(fetchAll).mockResolvedValue([{ id: newUUID }]); + vi.mocked(initOperatorClient).mockResolvedValue({ + listWorkspaces: vi.fn(), + } as unknown as Awaited>); + + writePlatformConfig({ + version: 2, + min_sdk_version: "1.29.0", + users: {}, + profiles: { + rw: { user: "legacy@example.com", workspace_id: validUUID }, + }, + current_user: null, + }); + + await runCommand(updateCommand, ["rw", "--workspace-id", newUUID]); + + expect(vi.mocked(fetchLatestToken)).toHaveBeenCalledWith( + expect.anything(), + "legacy@example.com", + ); + + const config = await readPlatformConfig(); + expect(config.profiles.rw?.user).toBe("platform-user-sub"); + expect(config.profiles.rw?.workspace_id).toBe(newUUID); + }); }); describe("profile update --machine-user", () => { From bf0646c6e925fdd610d040d22d8c78490aa68b66 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 04:26:47 +0900 Subject: [PATCH 060/618] feat: unify function principals --- .changeset/tailor-principal-type.md | 9 +- example/adapters/echo.ts | 4 +- example/adapters/whoami.ts | 6 +- example/e2e/httpAdapter.test.ts | 2 +- example/e2e/resolver.test.ts | 20 +-- example/resolvers/userInfo.ts | 14 +- example/tests/bundled_execution.test.ts | 6 +- .../src/executor/onUserCreated.test.ts | 8 + .../src/resolver/getProduct.test.ts | 7 +- .../apps/admin/db/adminNote.ts | 2 +- .../create-sdk/templates/resolver/README.md | 4 +- .../resolver/src/resolver/add.test.ts | 7 +- .../src/resolver/incrementUserAge.test.ts | 7 +- .../resolver/src/resolver/showEnv.test.ts | 4 +- .../src/resolver/showUserInfo.test.ts | 18 ++- .../resolver/src/resolver/showUserInfo.ts | 6 +- .../resolver/src/resolver/upsertUsers.test.ts | 4 +- .../src/resolver/resolveApproval.test.ts | 7 +- .../workflow/src/workflow/approval.test.ts | 10 +- .../src/workflow/order-fulfillment.test.ts | 28 +++- .../v2/principal-unify/scripts/transform.ts | 142 +++++++++++++++++- .../tests/tailordb-callbacks/expected.ts | 25 +++ .../tests/tailordb-callbacks/input.ts | 26 ++++ packages/sdk-codemod/src/registry.ts | 2 +- packages/sdk/docs/services/auth.md | 20 +-- packages/sdk/docs/services/resolver.md | 8 +- packages/sdk/docs/services/tailordb.md | 4 +- packages/sdk/docs/testing.md | 80 ++++++---- .../__test_fixtures__/resolvers/showInfo.ts | 8 +- .../src/cli/commands/function/bundle.test.ts | 2 +- .../sdk/src/cli/commands/function/bundle.ts | 48 +++--- .../sdk/src/cli/commands/function/detect.ts | 2 +- .../sdk/src/cli/commands/function/test-run.ts | 8 +- .../sdk/src/cli/services/resolver/bundler.ts | 2 +- .../tailordb/hooks-validate-bundler.ts | 6 +- .../sdk/src/cli/shared/runtime-exprs.test.ts | 17 ++- packages/sdk/src/cli/shared/runtime-exprs.ts | 40 +++-- packages/sdk/src/configure/index.ts | 9 +- .../services/executor/executor.test.ts | 6 +- .../configure/services/executor/operation.ts | 4 +- .../services/executor/trigger/event.ts | 4 +- .../services/resolver/resolver.test.ts | 31 ++-- .../configure/services/resolver/resolver.ts | 10 +- .../services/tailordb/schema.test.ts | 104 +++++++------ .../src/configure/services/tailordb/schema.ts | 24 +-- .../src/configure/services/tailordb/types.ts | 4 +- .../configure/services/workflow/job.test.ts | 4 +- .../src/configure/services/workflow/job.ts | 4 +- .../configure/services/workflow/registry.ts | 4 +- .../services/workflow/test-env-key.ts | 10 +- packages/sdk/src/configure/types/type.test.ts | 78 +++++----- packages/sdk/src/configure/types/type.ts | 26 ++-- .../sdk/src/parser/service/tailordb/field.ts | 28 +++- .../sdk/src/parser/service/tailordb/index.ts | 2 +- packages/sdk/src/plugin/with-context.ts | 6 +- packages/sdk/src/runtime/context.ts | 23 +-- packages/sdk/src/types/actor.ts | 35 ----- packages/sdk/src/types/runtime.ts | 2 +- packages/sdk/src/types/user.ts | 78 +--------- packages/sdk/src/types/validation.ts | 8 +- packages/sdk/src/utils/test/index.test.ts | 10 +- packages/sdk/src/utils/test/index.ts | 20 +-- packages/sdk/src/utils/test/mock.ts | 6 +- 63 files changed, 649 insertions(+), 504 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts delete mode 100644 packages/sdk/src/types/actor.ts diff --git a/.changeset/tailor-principal-type.md b/.changeset/tailor-principal-type.md index 26a8c9c97..645527978 100644 --- a/.changeset/tailor-principal-type.md +++ b/.changeset/tailor-principal-type.md @@ -1,10 +1,7 @@ --- -"@tailor-platform/sdk": minor +"@tailor-platform/sdk": major --- -Add `TailorPrincipal`, a unified type for the caller, actor, and invoker of a function execution. +Unify function principal context around `TailorPrincipal`. -The principal types are deprecated in favor of it and unified into `TailorPrincipal` (with absence represented as `null`) in the next major version: - -- `TailorUser`, `TailorInvoker`, and the `unauthenticatedTailorUser` constant (including the one from `@tailor-platform/sdk/test`). -- The event executor `actor` value, whose `userId`/`userType` fields become `id`/`type` with `"user"`/`"machine_user"` values. +Resolver contexts now use `caller` and `invoker` as `TailorPrincipal | null`, workflow and executor invokers also use `TailorPrincipal | null`, and event executor `actor` uses `TailorPrincipal | null` with `id`/`type` fields. The legacy `TailorUser`, `TailorInvoker`, `TailorActor`, `TailorActorType`, and `unauthenticatedTailorUser` exports are removed. diff --git a/example/adapters/echo.ts b/example/adapters/echo.ts index c2dd2d97a..8c1548ad6 100644 --- a/example/adapters/echo.ts +++ b/example/adapters/echo.ts @@ -8,10 +8,10 @@ export default createHttpAdapter({ pathPattern: "/echo", input: { get: () => ({ - query: `query { getResult: showUserInfo { user { role } } }`, + query: `query { getResult: showUserInfo { caller { role } } }`, }), post: () => ({ - query: `query { postResult: showUserInfo { user { role } } }`, + query: `query { postResult: showUserInfo { caller { role } } }`, }), }, output: (resp) => { diff --git a/example/adapters/whoami.ts b/example/adapters/whoami.ts index 4c6d22ece..b57e794a3 100644 --- a/example/adapters/whoami.ts +++ b/example/adapters/whoami.ts @@ -35,7 +35,7 @@ export default createHttpAdapter({ get: () => ({ query: `query Whoami { showUserInfo { - user { + caller { id type role @@ -53,7 +53,7 @@ export default createHttpAdapter({ const data = resp.data as | { showUserInfo?: { - user?: Record; + caller?: Record; invoker?: Record; }; } @@ -63,7 +63,7 @@ export default createHttpAdapter({ const xml = `\n` + `` + - actorXml("user", info?.user) + + actorXml("caller", info?.caller) + actorXml("invoker", info?.invoker) + ``; return { diff --git a/example/e2e/httpAdapter.test.ts b/example/e2e/httpAdapter.test.ts index 4271ff223..05f3890b0 100644 --- a/example/e2e/httpAdapter.test.ts +++ b/example/e2e/httpAdapter.test.ts @@ -21,7 +21,7 @@ describe("HTTP adapter routing", () => { expect(res.headers.get("content-type") ?? "").toContain("application/xml"); const body = await res.text(); expect(body).toContain(""); - expect(body).toMatch(/[\s\S]*<\/user>/); + expect(body).toMatch(/[\s\S]*<\/caller>/); }); test("POST /api/whoami fails with 404 because the adapter only declares GET", async () => { diff --git a/example/e2e/resolver.test.ts b/example/e2e/resolver.test.ts index a16448dcb..1bde34561 100644 --- a/example/e2e/resolver.test.ts +++ b/example/e2e/resolver.test.ts @@ -261,7 +261,7 @@ describe("dataplane", () => { const query = gql` query { showUserInfo { - user { + caller { id type workspaceId @@ -280,7 +280,7 @@ describe("dataplane", () => { expect(result.errors).toBeUndefined(); expect(result.data).toEqual({ showUserInfo: { - user: { + caller: { id: expect.any(String), type: "machine_user", workspaceId: expect.any(String), @@ -310,13 +310,15 @@ describe("dataplane", () => { const responseFields = userInfo?.response?.type?.fields ?? []; - const userField = responseFields.find((f) => f.name === "user"); - expect(userField?.description).toBe("Authenticated user"); - const userSubFields = userField?.type?.fields ?? []; - expect(userSubFields.find((f) => f.name === "id")?.description).toBe("User ID"); - expect(userSubFields.find((f) => f.name === "type")?.description).toBe("User type"); - expect(userSubFields.find((f) => f.name === "workspaceId")?.description).toBe("Workspace ID"); - expect(userSubFields.find((f) => f.name === "role")?.description).toBe("User role"); + const callerField = responseFields.find((f) => f.name === "caller"); + expect(callerField?.description).toBe("Authenticated caller"); + const callerSubFields = callerField?.type?.fields ?? []; + expect(callerSubFields.find((f) => f.name === "id")?.description).toBe("User ID"); + expect(callerSubFields.find((f) => f.name === "type")?.description).toBe("User type"); + expect(callerSubFields.find((f) => f.name === "workspaceId")?.description).toBe( + "Workspace ID", + ); + expect(callerSubFields.find((f) => f.name === "role")?.description).toBe("User role"); const invokerField = responseFields.find((f) => f.name === "invoker"); expect(invokerField?.description).toBe("Function invoker"); diff --git a/example/resolvers/userInfo.ts b/example/resolvers/userInfo.ts index 4d4943dc6..0678924be 100644 --- a/example/resolvers/userInfo.ts +++ b/example/resolvers/userInfo.ts @@ -6,11 +6,11 @@ export default createResolver({ operation: "query", body: (context) => { return { - user: { - id: context.user.id, - type: context.user.type, - workspaceId: context.user.workspaceId, - role: context.user.attributes?.role ?? "MANAGER", + caller: { + id: context.caller?.id ?? "", + type: context.caller?.type ?? "", + workspaceId: context.caller?.workspaceId ?? "", + role: context.caller?.attributes.role ?? "MANAGER", }, invoker: { id: context.invoker!.id, @@ -22,14 +22,14 @@ export default createResolver({ }, output: t .object({ - user: t + caller: t .object({ id: t.string().description("User ID"), type: t.string().description("User type"), workspaceId: t.string().description("Workspace ID"), role: t.enum(["MANAGER", "STAFF"]).description("User role"), }) - .description("Authenticated user"), + .description("Authenticated caller"), invoker: t .object({ id: t.string().description("Invoker ID"), diff --git a/example/tests/bundled_execution.test.ts b/example/tests/bundled_execution.test.ts index 846ebaf3e..7dfc54a6e 100644 --- a/example/tests/bundled_execution.test.ts +++ b/example/tests/bundled_execution.test.ts @@ -76,7 +76,7 @@ describe("bundled execution tests", () => { expect(result).toEqual(10); }); - test("resolvers/showUserInfo.js returns user and invoker information", async () => { + test("resolvers/showUserInfo.js returns caller and invoker information", async () => { using _invokerSpy = vi.spyOn(globalThis.tailor.context, "getInvoker").mockReturnValue({ id: "f1e2d3c4-b5a6-4798-89a0-1b2c3d4e5f60", type: "machine_user", @@ -87,7 +87,7 @@ describe("bundled execution tests", () => { const main = await importActualMain("resolvers/showUserInfo.js"); const payload = { - user: { + caller: { id: "57485cfe-fc74-4d46-8660-f0e95d1fbf98", type: "user", workspaceId: "b39bdd61-d442-4a4e-8599-33a78a4e19ab", @@ -96,7 +96,7 @@ describe("bundled execution tests", () => { }; const result = await main(payload); expect(result).toEqual({ - user: { + caller: { id: "57485cfe-fc74-4d46-8660-f0e95d1fbf98", type: "user", workspaceId: "b39bdd61-d442-4a4e-8599-33a78a4e19ab", diff --git a/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts b/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts index 22d1113fc..93f3f492f 100644 --- a/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts +++ b/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts @@ -10,6 +10,14 @@ describe("onUserCreated executor", () => { throw new Error("expected function operation"); } await onUserCreated.operation.body({ + workspaceId: "workspace-1", + appNamespace: "app", + env: {}, + actor: null, + invoker: null, + event: "created", + rawEvent: "tailordb.type_record.created", + typeName: "User", newRecord: { id: "user-1", name: "Alice", diff --git a/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts b/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts index c569ebcd0..598ee996f 100644 --- a/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts +++ b/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts @@ -1,4 +1,3 @@ -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { mockTailordb } from "@tailor-platform/sdk/vitest"; import { describe, expect, test } from "vitest"; import resolver from "./getProduct"; @@ -22,7 +21,8 @@ describe("getProduct resolver", () => { const result = await resolver.body({ input: { productId: "product-1" }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: {}, }); @@ -51,7 +51,8 @@ describe("getProduct resolver", () => { const result = await resolver.body({ input: { productId: "product-2" }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: {}, }); diff --git a/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts b/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts index b3f5997c3..0a39ee828 100644 --- a/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts +++ b/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts @@ -8,7 +8,7 @@ export const adminNote = db .type("AdminNote", { title: db.string(), content: db.string(), - authorId: db.uuid().hooks({ create: ({ user }) => user.id }), + authorId: db.uuid().hooks({ create: ({ invoker }) => invoker?.id ?? crypto.randomUUID() }), ...db.fields.timestamps(), }) // NOTE: This permits all operations for simplicity. diff --git a/packages/create-sdk/templates/resolver/README.md b/packages/create-sdk/templates/resolver/README.md index f0c531a61..f5c771dce 100644 --- a/packages/create-sdk/templates/resolver/README.md +++ b/packages/create-sdk/templates/resolver/README.md @@ -8,11 +8,11 @@ Demonstrates all resolver patterns with comprehensive testing approaches. - Database query resolver (Kysely with transactions) - Database mutation resolver (dependency injection pattern) - Environment variable access -- User context access +- Caller and invoker context access ## Testing Approaches -1. **Direct `body()` call** - Simple resolvers with `unauthenticatedTailorUser` +1. **Direct `body()` call** - Simple resolvers with explicit `caller` / `invoker` context values 2. **`tailor-runtime` environment + `mockTailordb`** - Database resolvers via `mockTailordb` from `@tailor-platform/sdk/vitest` (no `vi.stubGlobal` needed) 3. **Dependency injection** - Extract `DbOperations` interface for testability diff --git a/packages/create-sdk/templates/resolver/src/resolver/add.test.ts b/packages/create-sdk/templates/resolver/src/resolver/add.test.ts index 23ee62307..373e38462 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/add.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/add.test.ts @@ -1,4 +1,3 @@ -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { describe, expect, test } from "vitest"; import resolver from "./add"; @@ -6,7 +5,8 @@ describe("add resolver", () => { test("adds two positive numbers", async () => { const result = await resolver.body({ input: { left: 1, right: 2 }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); expect(result).toBe(3); @@ -15,7 +15,8 @@ describe("add resolver", () => { test("handles negative numbers", async () => { const result = await resolver.body({ input: { left: -5, right: 3 }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); expect(result).toBe(-2); diff --git a/packages/create-sdk/templates/resolver/src/resolver/incrementUserAge.test.ts b/packages/create-sdk/templates/resolver/src/resolver/incrementUserAge.test.ts index 3649f21fe..574aee83f 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/incrementUserAge.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/incrementUserAge.test.ts @@ -1,4 +1,3 @@ -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { mockTailordb } from "@tailor-platform/sdk/vitest"; import { describe, expect, test } from "vitest"; import resolver from "./incrementUserAge"; @@ -15,7 +14,8 @@ describe("incrementUserAge resolver", () => { const result = await resolver.body({ input: { email: "test@example.com" }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); expect(result).toEqual({ oldAge: 30, newAge: 31 }); @@ -32,7 +32,8 @@ describe("incrementUserAge resolver", () => { const result = resolver.body({ input: { email: "test@example.com" }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); await expect(result).rejects.toThrowError(/no result/i); diff --git a/packages/create-sdk/templates/resolver/src/resolver/showEnv.test.ts b/packages/create-sdk/templates/resolver/src/resolver/showEnv.test.ts index 1973897a9..19316895b 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/showEnv.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/showEnv.test.ts @@ -1,4 +1,3 @@ -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { describe, expect, test } from "vitest"; import resolver from "./showEnv"; @@ -6,7 +5,8 @@ describe("showEnv resolver", () => { test("returns environment variables", async () => { const result = await resolver.body({ input: undefined as never, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); expect(result).toEqual({ appName: "Resolver Template", version: 1 }); diff --git a/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts b/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts index 2a2f9739a..9fa5ee515 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts @@ -1,4 +1,3 @@ -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { describe, expect, test } from "vitest"; import resolver from "./showUserInfo"; @@ -6,26 +5,29 @@ describe("showUserInfo resolver", () => { test("returns default user info", async () => { const result = await resolver.body({ input: undefined as never, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); expect(result).toEqual({ - userId: unauthenticatedTailorUser.id, - userType: unauthenticatedTailorUser.type, - workspaceId: unauthenticatedTailorUser.workspaceId, + userId: "anonymous", + userType: "anonymous", + workspaceId: "", }); }); test("returns custom user info", async () => { - const customUser = { - ...unauthenticatedTailorUser, + const customCaller = { id: "user-123", type: "machine_user" as const, workspaceId: "ws-456", + attributes: {}, + attributeList: [], }; const result = await resolver.body({ input: undefined as never, - user: customUser, + caller: customCaller, + invoker: customCaller, env: { appName: "Resolver Template", version: 1 }, }); expect(result).toEqual({ diff --git a/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.ts b/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.ts index b848df640..6e33d0f8a 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.ts @@ -6,9 +6,9 @@ const resolver = createResolver({ operation: "query", body: (context) => { return { - userId: context.user.id, - userType: context.user.type, - workspaceId: context.user.workspaceId, + userId: context.caller?.id ?? "anonymous", + userType: context.caller?.type ?? "anonymous", + workspaceId: context.caller?.workspaceId ?? "", }; }, output: t.object({ diff --git a/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts b/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts index b953f1a33..a09b1fbd8 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts @@ -1,4 +1,3 @@ -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { createKyselyMock } from "@tailor-platform/sdk/vitest"; import { describe, expect, test, vi } from "vitest"; import { getDB, type Namespace } from "../generated/db"; @@ -30,7 +29,8 @@ describe("upsertUsers resolver", () => { { name: "Existing", email: "exists@example.com", age: 41 }, ], }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); diff --git a/packages/create-sdk/templates/workflow/src/resolver/resolveApproval.test.ts b/packages/create-sdk/templates/workflow/src/resolver/resolveApproval.test.ts index 39c9d8281..3b95af598 100644 --- a/packages/create-sdk/templates/workflow/src/resolver/resolveApproval.test.ts +++ b/packages/create-sdk/templates/workflow/src/resolver/resolveApproval.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from "vitest"; import { mockWorkflow } from "@tailor-platform/sdk/vitest"; -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import resolver from "./resolveApproval"; describe("resolveApproval resolver", () => { @@ -16,7 +15,8 @@ describe("resolveApproval resolver", () => { const result = await resolver.body({ input: { executionId: "exec-1", approved: true }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: {}, }); @@ -33,7 +33,8 @@ describe("resolveApproval resolver", () => { const result = await resolver.body({ input: { executionId: "exec-2", approved: false }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: {}, }); diff --git a/packages/create-sdk/templates/workflow/src/workflow/approval.test.ts b/packages/create-sdk/templates/workflow/src/workflow/approval.test.ts index a2a33c0cf..ace962bba 100644 --- a/packages/create-sdk/templates/workflow/src/workflow/approval.test.ts +++ b/packages/create-sdk/templates/workflow/src/workflow/approval.test.ts @@ -7,7 +7,10 @@ describe("approval workflow", () => { using wf = mockWorkflow(); wf.setWaitHandler((_key, _payload) => ({ approved: true })); - const result = await processWithApproval.body({ orderId: "order-1" }, { env: {} }); + const result = await processWithApproval.body( + { orderId: "order-1" }, + { env: {}, invoker: null }, + ); expect(result).toEqual({ orderId: "order-1", status: "approved" }); expect(wf.waitCalls).toEqual([ @@ -22,7 +25,10 @@ describe("approval workflow", () => { using wf = mockWorkflow(); wf.setWaitHandler({ approved: false }); - const result = await processWithApproval.body({ orderId: "order-2" }, { env: {} }); + const result = await processWithApproval.body( + { orderId: "order-2" }, + { env: {}, invoker: null }, + ); expect(result).toEqual({ orderId: "order-2", status: "rejected" }); }); diff --git a/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts b/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts index db1a1bc4b..bee4f602c 100644 --- a/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts +++ b/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts @@ -9,18 +9,24 @@ import workflow, { describe("order fulfillment workflow", () => { describe("individual job tests with .body()", () => { test("validateOrder accepts valid order", () => { - const result = validateOrder.body({ orderId: "order-1", amount: 100 }, { env: {} }); + const result = validateOrder.body( + { orderId: "order-1", amount: 100 }, + { env: {}, invoker: null }, + ); expect(result).toEqual({ valid: true, orderId: "order-1" }); }); test("validateOrder rejects zero amount", () => { - expect(() => validateOrder.body({ orderId: "order-1", amount: 0 }, { env: {} })).toThrow( - "Order amount must be positive", - ); + expect(() => + validateOrder.body({ orderId: "order-1", amount: 0 }, { env: {}, invoker: null }), + ).toThrow("Order amount must be positive"); }); test("processPayment returns transaction", () => { - const result = processPayment.body({ orderId: "order-1", amount: 100 }, { env: {} }); + const result = processPayment.body( + { orderId: "order-1", amount: 100 }, + { env: {}, invoker: null }, + ); expect(result).toEqual({ transactionId: "txn-order-1", amount: 100, @@ -31,7 +37,7 @@ describe("order fulfillment workflow", () => { test("sendConfirmation returns confirmation", () => { const result = sendConfirmation.body( { orderId: "order-1", transactionId: "txn-1" }, - { env: {} }, + { env: {}, invoker: null }, ); expect(result).toEqual({ orderId: "order-1", @@ -58,7 +64,10 @@ describe("order fulfillment workflow", () => { confirmed: true, }); - const result = await fulfillOrder.body({ orderId: "order-1", amount: 100 }, { env: {} }); + const result = await fulfillOrder.body( + { orderId: "order-1", amount: 100 }, + { env: {}, invoker: null }, + ); expect(validateOrder.trigger).toHaveBeenCalledWith({ orderId: "order-1", @@ -96,7 +105,10 @@ describe("order fulfillment workflow", () => { confirmed: true, }); - const result = await workflow.mainJob.body({ orderId: "order-2", amount: 200 }, { env: {} }); + const result = await workflow.mainJob.body( + { orderId: "order-2", amount: 200 }, + { env: {}, invoker: null }, + ); expect(result).toEqual({ orderId: "order-2", diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index af16cd7f0..86d8d3e61 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -9,7 +9,14 @@ const TYPE_RENAME_MAP: Record = { const UNAUTHENTICATED = "unauthenticatedTailorUser"; -const QUICK_FILTER_NEEDLES = [...Object.keys(TYPE_RENAME_MAP), UNAUTHENTICATED, "createResolver"]; +const QUICK_FILTER_NEEDLES = [ + ...Object.keys(TYPE_RENAME_MAP), + UNAUTHENTICATED, + "createResolver", + ".hooks", + ".validate", + ".parse", +]; function quickFilter(source: string): boolean { if (!source.includes("@tailor-platform/sdk")) return false; @@ -503,6 +510,131 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { } } +function findMemberCallName(call: SgNode): string | null { + const fn = call.field("function"); + if (!fn || fn.kind() !== "member_expression") return null; + const property = fn.field("property"); + return property?.text() ?? null; +} + +function getFirstFunctionParamPattern(fn: SgNode): SgNode | null { + const params = + fn.field("parameters") ?? + fn.field("parameter") ?? + fn.children().find((c: SgNode) => c.kind() === "formal_parameters"); + if (!params) return null; + + const firstParam = params + .children() + .find( + (c: SgNode) => + c.kind() === "required_parameter" || + c.kind() === "optional_parameter" || + c.kind() === "identifier" || + c.kind() === "object_pattern", + ); + if (!firstParam) return null; + if (firstParam.kind() === "object_pattern" || firstParam.kind() === "identifier") { + return firstParam; + } + return firstParam.field("pattern"); +} + +function transformPrincipalCallbackParam(fn: SgNode, edits: Edit[]): void { + const pattern = getFirstFunctionParamPattern(fn); + const body = fn.field("body"); + if (!pattern || pattern.kind() !== "object_pattern" || !body) return; + + let renamesBinding = false; + for (const child of pattern.children()) { + const kind = child.kind(); + if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { + renamesBinding = true; + } else if (kind === "object_assignment_pattern") { + const inner = child + .children() + .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); + if (inner?.text() === "user") renamesBinding = true; + } + } + + if (renamesBinding && patternBindsName(pattern, "invoker")) return; + + let renamedShorthandUser = false; + for (const child of pattern.children()) { + const kind = child.kind(); + if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { + edits.push(child.replace("invoker")); + renamedShorthandUser = true; + } else if (kind === "pair_pattern") { + const key = child.field("key"); + if (key?.text() === "user") { + edits.push(key.replace("invoker")); + } + } else if (kind === "object_assignment_pattern") { + const inner = child + .children() + .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); + if (inner?.text() === "user") { + edits.push(inner.replace("invoker")); + renamedShorthandUser = true; + } + } + } + + if (!renamedShorthandUser) return; + + const shadowRanges = collectAllShadowRanges(body, "user"); + const refs = body.findAll({ rule: { kind: "identifier", regex: "^user$" } }); + for (const ref of refs) { + const pos = ref.range().start.index; + if (isInsideAnyRange(pos, shadowRanges)) continue; + edits.push(ref.replace("invoker")); + } + + const shortRefs = body.findAll({ + rule: { kind: "shorthand_property_identifier", regex: "^user$" }, + }); + for (const ref of shortRefs) { + const pos = ref.range().start.index; + if (isInsideAnyRange(pos, shadowRanges)) continue; + edits.push(ref.replace("user: invoker")); + } +} + +function transformPrincipalCallbacksInCall(call: SgNode, edits: Edit[]): void { + const memberName = findMemberCallName(call); + if (memberName !== "hooks" && memberName !== "validate") return; + + const args = call.field("arguments"); + if (!args) return; + for (const kind of ["arrow_function", "function_expression"]) { + const callbacks = args.findAll({ rule: { kind } }); + for (const callback of callbacks) { + transformPrincipalCallbackParam(callback, edits); + } + } +} + +function transformParseArgsObject(call: SgNode, edits: Edit[]): void { + const memberName = findMemberCallName(call); + if (memberName !== "parse") return; + + const args = call.field("arguments"); + const objArg = args?.children().find((c: SgNode) => c.kind() === "object"); + if (!objArg) return; + + for (const child of objArg.children()) { + const kind = child.kind(); + if (kind === "pair") { + const key = child.field("key"); + if (key?.text() === "user") edits.push(key.replace("invoker")); + } else if (kind === "shorthand_property_identifier" && child.text() === "user") { + edits.push(child.replace("invoker: user")); + } + } +} + /** * Migrate user/actor/invoker types and identifiers to the unified TailorPrincipal. * @@ -516,6 +648,8 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { * handles aliased pairs (`{ user: currentUser }`) by rewriting only the property name, and * rewrites `.user` for non-destructured single-param bodies — respecting variable * shadowing in both directions. + * - Renames TailorDB hook/validator callback `user` parameters to `invoker`, and rewrites + * `.parse({ user: ... })` arguments to `.parse({ invoker: ... })`. * @param source - TypeScript source text. * @returns Transformed source or null when nothing matched. */ @@ -624,6 +758,12 @@ export default function transform(source: string): string | null { } } + const memberCalls = tree.findAll({ rule: { kind: "call_expression" } }); + for (const call of memberCalls) { + transformPrincipalCallbacksInCall(call, edits); + transformParseArgsObject(call, edits); + } + if (edits.length === 0) return null; let result = tree.commitEdits(edits); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts new file mode 100644 index 000000000..c4ee4ced5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -0,0 +1,25 @@ +import { db, t } from "@tailor-platform/sdk"; + +const role = db + .string() + .hooks({ + create: ({ value, invoker }) => (invoker?.attributes.role === "ADMIN" ? value : "user"), + }) + .validate([({ invoker }) => invoker?.type === "machine_user", "Machine user required"]); + +export const user = db + .type("User", { + role, + note: db.string(), + }) + .hooks({ + note: { + create: ({ invoker: currentUser }) => currentUser?.id ?? "anonymous", + }, + }); + +export const parsed = t.string().parse({ + value: "hello", + data: {}, + invoker: null, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts new file mode 100644 index 000000000..6f2de3741 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -0,0 +1,26 @@ +import { db, t } from "@tailor-platform/sdk"; +import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; + +const role = db + .string() + .hooks({ + create: ({ value, user }) => (user?.attributes.role === "ADMIN" ? value : "user"), + }) + .validate([({ user }) => user?.type === "machine_user", "Machine user required"]); + +export const user = db + .type("User", { + role, + note: db.string(), + }) + .hooks({ + note: { + create: ({ user: currentUser }) => currentUser?.id ?? "anonymous", + }, + }); + +export const parsed = t.string().parse({ + value: "hello", + data: {}, + user: unauthenticatedTailorUser, +}); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 886a22ab9..c57e29994 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -41,7 +41,7 @@ const allCodemods: CodemodPackage[] = [ id: "v2/principal-unify", name: "Unify TailorUser/TailorActor/TailorInvoker → TailorPrincipal", description: - "Rename TailorUser/TailorActor/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, and rename resolver body `user` to `caller`", + "Rename TailorUser/TailorActor/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, rename resolver body `user` to `caller`, and rename TailorDB callback `user` to `invoker`", since: "1.0.0", until: "2.0.0", scriptPath: "v2/principal-unify/scripts/transform.js", diff --git a/packages/sdk/docs/services/auth.md b/packages/sdk/docs/services/auth.md index 1db203bd5..5783a00d3 100644 --- a/packages/sdk/docs/services/auth.md +++ b/packages/sdk/docs/services/auth.md @@ -139,18 +139,18 @@ export const user = db.type("User", { }); ``` -The `attributeList` values are accessible via `user.attributeList` as a tuple: +The `attributeList` values are accessible via the runtime principal's `attributeList` as a tuple: ```typescript // In a resolver body: (context) => { - const [organizationId, teamId] = context.user.attributeList; + const [organizationId, teamId] = context.caller?.attributeList ?? []; }, // In TailorDB hooks .hooks({ field: { - create: ({ user }) => user.attributeList[0], // First UUID from list + create: ({ invoker }) => invoker?.attributeList[0] ?? null, // First UUID from list }, }) ``` @@ -160,7 +160,7 @@ body: (context) => { When you want to use machine users without defining a `userProfile`, define `machineUserAttributes` instead. These attributes are used for: - type-safe `machineUsers[*].attributes` -- `context.user.attributes` typing (via `tailor.d.ts`) +- runtime principal `attributes` typing (via `tailor.d.ts`) ```typescript import { defineAuth, t } from "@tailor-platform/sdk"; @@ -200,12 +200,12 @@ machineUsers: { }, ``` -**attributes**: Values for attributes enabled in `userProfile.attributes` (or all fields defined in `machineUserAttributes` when `userProfile` is omitted). All enabled fields must be set here. These values are accessible via `user.attributes`: +**attributes**: Values for attributes enabled in `userProfile.attributes` (or all fields defined in `machineUserAttributes` when `userProfile` is omitted). All enabled fields must be set here. These values are accessible via the runtime principal's `attributes`: ```typescript // In a resolver body: (context) => { - const role = context.user.attributes?.role; + const role = context.caller?.attributes.role; }, ``` @@ -230,25 +230,25 @@ machineUsers: { }, ``` -These values are accessible via `user.attributeList`: +These values are accessible via the runtime principal's `attributeList`: ```typescript // In a resolver body: (context) => { - const [organizationId, teamId] = context.user.attributeList; + const [organizationId, teamId] = context.caller?.attributeList ?? []; }, // In TailorDB hooks .hooks({ field: { - create: ({ user }) => user.attributes?.role === "ADMIN" ? "default" : null, + create: ({ invoker }) => invoker?.attributes.role === "ADMIN" ? "default" : null, }, }) // In TailorDB validate .validate({ field: [ - ({ user }) => user.attributes?.role === "ADMIN", + ({ invoker }) => invoker?.attributes.role === "ADMIN", "Only admins can set this field", ], }) diff --git a/packages/sdk/docs/services/resolver.md b/packages/sdk/docs/services/resolver.md index cdc6ac61f..e54834344 100644 --- a/packages/sdk/docs/services/resolver.md +++ b/packages/sdk/docs/services/resolver.md @@ -208,7 +208,7 @@ Validation functions receive: - `value` - The field value being validated - `data` - The entire input object -- `user` - The user performing the operation +- `invoker` - The principal performing the operation You can specify validation as: @@ -234,8 +234,8 @@ Validation runs automatically before the `body` function executes. When validati Define actual resolver logic in the `body` function. Function arguments include: - `input` - Input data from GraphQL request -- `user` - The user who called this resolver; unaffected by `authInvoker` -- `invoker` - The principal running this function; equals `user` by default, or the machine user set by `authInvoker`. `null` for anonymous calls. +- `caller` - The user or machine user who called this resolver; unaffected by `authInvoker`. `null` for anonymous calls. +- `invoker` - The principal running this function; equals `caller` by default, or the machine user set by `authInvoker`. `null` for anonymous calls. - `env` - Environment variables declared in `tailor.config.ts` ### Using Kysely for Database Access @@ -373,4 +373,4 @@ The machine user name is looked up in the auth service configured on your app (` > **Deprecated:** `auth.invoker("batch-processor")` still works, but is deprecated. Importing `auth` into runtime files pulls config-layer (Node-only) dependencies into the bundle. -**Note:** `authInvoker` controls the permissions for database operations and other platform actions. The `user` object passed to `body` still reflects the original caller, while `invoker` reflects the principal actually running the body. +**Note:** `authInvoker` controls the permissions for database operations and other platform actions. The `caller` object passed to `body` still reflects the original caller, while `invoker` reflects the principal actually running the body. diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index 3bb171cc8..30b55cfc9 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -260,7 +260,7 @@ Add hooks to execute functions during data creation or update. Hooks receive thr - `value`: User input if provided, otherwise existing value on update or null on create - `data`: Entire record data (for accessing other field values) -- `user`: User performing the operation +- `invoker`: Principal performing the operation #### Field-level Hooks @@ -268,7 +268,7 @@ Set hooks directly on individual fields: ```typescript db.string().hooks({ - create: ({ user }) => user.id, + create: ({ invoker }) => invoker?.id ?? "", update: ({ value }) => value, }); ``` diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index e3132d3e8..43bab3e23 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -11,13 +11,13 @@ Lean on unit tests for the day-to-day feedback loop — they run fast and exerci Unit-test entrypoints exposed by the SDK: -- `resolver.body({ input, user, env })` — invoke a resolver -- `workflowJob.body(input, { env })` / `workflowJob.trigger(input)` — invoke or chain a workflow job -- `executor.operation.body(args)` — invoke a function-kind executor +- `resolver.body({ input, caller, invoker, env })` — invoke a resolver +- `workflowJob.body(input, { env, invoker })` / `workflowJob.trigger(input)` — invoke or chain a workflow job +- `executor.operation.body({ ...args, invoker })` — invoke a function-kind executor -Helpers under `@tailor-platform/sdk/test`: +For anonymous direct calls: -- `unauthenticatedTailorUser` — default `user` value for resolver contexts (deprecated; see the note under [Testing Resolvers](#testing-resolvers)) +- Pass `null` for anonymous `caller` / `invoker` context in direct unit tests. Platform API mocks under `@tailor-platform/sdk/vitest` (for use with the [`tailor-runtime` Vitest environment](#runtime-environment-emulation-beta) below): @@ -96,7 +96,12 @@ test("resolver queries the database", async () => { [], // COMMIT ); - const result = await resolver.body({ input: { email: "test@example.com" } }); + const result = await resolver.body({ + input: { email: "test@example.com" }, + caller: null, + invoker: null, + env: {}, + }); expect(result).toEqual({ oldAge: 30, newAge: 31 }); expect(db.executedQueries).toHaveLength(3); @@ -118,7 +123,12 @@ test("content-based mock", async () => { return []; }); - const result = await resolver.body({ input: { userId: "1" } }); + const result = await resolver.body({ + input: { userId: "1" }, + caller: null, + invoker: null, + env: {}, + }); expect(db.executedQueries[0].query).toContain("SELECT"); }); @@ -359,14 +369,11 @@ Unit tests call `.body()` (or `.trigger()`) directly on a resolver, workflow job ### Testing Resolvers -> **Note:** `unauthenticatedTailorUser` is deprecated. Represent an absent principal as `null` in your own application code going forward. The test helper and the examples below retain it until the resolver context `user` type accepts `null` in the next major version. - #### Simple resolver For pure logic with no external dependencies, invoke `.body()` directly: ```typescript -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { describe, expect, test } from "vitest"; import resolver from "../src/resolver/add"; @@ -374,7 +381,8 @@ describe("add resolver", () => { test("adds two numbers", async () => { const result = await resolver.body({ input: { left: 1, right: 2 }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: {}, }); expect(result).toBe(3); @@ -391,7 +399,6 @@ Stub the global `tailordb.Client` and queue raw query results in order. Best for > If you are running with the [`tailor-runtime` Vitest environment](#runtime-environment-emulation-beta), acquire `using db = mockTailordb()` to install and drive the mock `tailordb.Client` instead of `vi.stubGlobal()`. ```typescript -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from "vitest"; import resolver from "../src/resolver/incrementUserAge"; @@ -421,7 +428,8 @@ describe("incrementUserAge resolver", () => { const result = await resolver.body({ input: { email: "test@example.com" }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: {}, }); @@ -492,7 +500,6 @@ describe("decrementUserAge", () => { Pass `mock.db` to functions that take a Kysely instance. When a resolver or executor calls `getDB()` internally there is no such seam, so spy the generated `getDB` and point it at the mock: ```typescript -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { createKyselyMock } from "@tailor-platform/sdk/vitest"; import { describe, expect, test, vi } from "vitest"; import { getDB, type Namespace } from "../generated/db"; @@ -524,7 +531,8 @@ describe("upsertUsers resolver", () => { { name: "Existing", email: "exists@example.com", age: 41 }, ], }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); @@ -543,7 +551,6 @@ Reach for [`mockTailordb`](#mocking-the-tailordb-client) instead when you want t Resolvers that call `waitPoint.resolve(...)` delegate to `tailor.workflow.resolve` at runtime. With the `tailor-runtime` environment active, use `mockWorkflow().setResolveHandler` to drive the user-supplied callback and inspect `resolveCalls`: ```typescript -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { mockWorkflow } from "@tailor-platform/sdk/vitest"; import { describe, expect, test } from "vitest"; import resolver from "./resolveApproval"; @@ -558,7 +565,8 @@ describe("resolveApproval resolver", () => { const result = await resolver.body({ input: { executionId: "exec-1", approved: true }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: {}, }); @@ -572,7 +580,7 @@ describe("resolveApproval resolver", () => { ### Testing Executors -Function-kind executors expose their handler as `executor.operation.body(args)`. The shape of `args` is determined by the trigger — for example, `recordCreatedTrigger({ type: user })` produces `{ newRecord }` typed against the type's output. GraphQL, webhook, and workflow operation kinds are declarative and don't expose a user-authored body to test. +Function-kind executors expose their handler as `executor.operation.body(args)`. The shape of `args` is determined by the trigger — for example, `recordCreatedTrigger({ type: user })` produces `{ newRecord }` typed against the type's output, plus runtime fields such as `env`, `actor`, and `invoker`. GraphQL, webhook, and workflow operation kinds are declarative and don't expose a user-authored body to test. The `executor` template extracts shared DB access into a helper (`shared.ts`) and tests the helper directly against a mocked `tailordb.Client` (same TailorDB-mocking pattern as the resolver section). Executor handlers themselves stay thin and can be tested by spying on the helper: @@ -589,6 +597,14 @@ describe("onUserCreated executor", () => { throw new Error("expected function operation"); } await onUserCreated.operation.body({ + workspaceId: "workspace-1", + appNamespace: "app", + env: {}, + actor: null, + invoker: null, + event: "created", + rawEvent: "tailordb.type_record.created", + typeName: "User", newRecord: { id: "user-1", name: "Alice", @@ -617,7 +633,7 @@ Workflow jobs expose the same `.body()` entrypoint as resolvers, plus `.trigger( #### Simple job -Call `.body()` with the input and a stub `{ env: {} }`: +Call `.body()` with the input and a stub `{ env: {}, invoker: null }`: ```typescript import { describe, expect, test } from "vitest"; @@ -625,14 +641,17 @@ import { validateOrder } from "./order-fulfillment"; describe("validateOrder", () => { test("accepts a valid order", () => { - const result = validateOrder.body({ orderId: "order-1", amount: 100 }, { env: {} }); + const result = validateOrder.body( + { orderId: "order-1", amount: 100 }, + { env: {}, invoker: null }, + ); expect(result).toEqual({ valid: true, orderId: "order-1" }); }); test("rejects a non-positive amount", () => { - expect(() => validateOrder.body({ orderId: "order-1", amount: 0 }, { env: {} })).toThrow( - "Order amount must be positive", - ); + expect(() => + validateOrder.body({ orderId: "order-1", amount: 0 }, { env: {}, invoker: null }), + ).toThrow("Order amount must be positive"); }); }); ``` @@ -662,7 +681,10 @@ describe("fulfillOrder", () => { confirmed: true, }); - const result = await fulfillOrder.body({ orderId: "order-1", amount: 100 }, { env: {} }); + const result = await fulfillOrder.body( + { orderId: "order-1", amount: 100 }, + { env: {}, invoker: null }, + ); expect(validateOrder.trigger).toHaveBeenCalledWith({ orderId: "order-1", amount: 100 }); expect(result).toMatchObject({ confirmed: true, paymentStatus: "completed" }); @@ -686,7 +708,10 @@ describe("processWithApproval", () => { using wf = mockWorkflow(); wf.setWaitHandler({ approved: true }); - const result = await processWithApproval.body({ orderId: "order-1" }, { env: {} }); + const result = await processWithApproval.body( + { orderId: "order-1" }, + { env: {}, invoker: null }, + ); expect(result).toEqual({ orderId: "order-1", status: "approved" }); expect(wf.waitCalls[0]).toEqual({ @@ -699,7 +724,10 @@ describe("processWithApproval", () => { using wf = mockWorkflow(); wf.setWaitHandler({ approved: false }); - const result = await processWithApproval.body({ orderId: "order-2" }, { env: {} }); + const result = await processWithApproval.body( + { orderId: "order-2" }, + { env: {}, invoker: null }, + ); expect(result.status).toBe("rejected"); }); diff --git a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/showInfo.ts b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/showInfo.ts index efac35219..0407f7fe1 100644 --- a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/showInfo.ts +++ b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/showInfo.ts @@ -6,12 +6,12 @@ export default createResolver({ operation: "query", body: (context) => { return { - id: context.user.id, - type: context.user.type, - workspaceId: context.user.workspaceId, + id: context.caller?.id ?? "", + type: context.caller?.type ?? "", + workspaceId: context.caller?.workspaceId ?? "", // platform response may omit the field // oxlint-disable-next-line typescript/no-unnecessary-condition - role: (context.user.attributes?.role as string) ?? "ADMIN", + role: (context.caller?.attributes.role as string | undefined) ?? "ADMIN", }; }, output: t diff --git a/packages/sdk/src/cli/commands/function/bundle.test.ts b/packages/sdk/src/cli/commands/function/bundle.test.ts index 963cf02b1..84d9bae0d 100644 --- a/packages/sdk/src/cli/commands/function/bundle.test.ts +++ b/packages/sdk/src/cli/commands/function/bundle.test.ts @@ -279,7 +279,7 @@ export default { workspaceId: defaultWorkspaceId, }); - expect(result.bundledCode).toContain("USER_TYPE_MACHINE_USER"); + expect(result.bundledCode).toContain("machine_user"); expect(result.bundledCode).toContain("ADMIN"); expect(result.bundledCode).toContain(defaultMachineUser.id); expect(result.bundledCode).toContain(defaultWorkspaceId); diff --git a/packages/sdk/src/cli/commands/function/bundle.ts b/packages/sdk/src/cli/commands/function/bundle.ts index d8972e095..9ec13aa16 100644 --- a/packages/sdk/src/cli/commands/function/bundle.ts +++ b/packages/sdk/src/cli/commands/function/bundle.ts @@ -24,7 +24,7 @@ import ml from "@/utils/multiline"; import type { DetectedFunction } from "./detect"; import type { LogLevelInput } from "@/types/app-config"; -/** Machine user info resolved from config and API for bundle-time user context. */ +/** Machine user info resolved from config and API for bundle-time principal context. */ export interface ResolvedMachineUser { /** Machine user name */ name: string; @@ -47,7 +47,7 @@ interface BundleForTestRunOptions { inlineSourcemap?: boolean; /** Log level config value from defineConfig */ logLevel?: LogLevelInput; - /** Machine user info for injecting user context into the bundle */ + /** Machine user info for injecting principal context into the bundle */ machineUser: ResolvedMachineUser; /** Workspace ID for user context */ workspaceId: string; @@ -152,23 +152,23 @@ function generateEntry( case "resolver": { // Mirrors the production resolver bundler (services/resolver/bundler.ts). - // In production, the operationHook injects user/env into context. + // In production, the operationHook injects caller/env into context. // For test-run, we embed machine user info since there's no operationHook. - const userExpr = buildMachineUserExpr(machineUser, workspaceId); + const principalExpr = buildMachinePrincipalExpr(machineUser, workspaceId); return ml /* js */ ` import _internalResolver from "${absoluteSourcePath}"; import { t } from "@tailor-platform/sdk"; const _env = ${JSON.stringify(env)}; - const _user = ${userExpr}; + const _caller = ${principalExpr}; const $tailor_resolver_body = async (context) => { - const _invoker = ${INVOKER_EXPR}; + const _invoker = ${INVOKER_EXPR} ?? _caller; if (_internalResolver.input) { const result = t.object(_internalResolver.input).parse({ value: context, data: context, - user: _user, + invoker: _invoker, }); if (result.issues) { @@ -179,7 +179,7 @@ function generateEntry( } } - const enrichedContext = { input: context, env: _env, user: _user, invoker: _invoker }; + const enrichedContext = { input: context, env: _env, caller: _caller, invoker: _invoker }; return _internalResolver.body(enrichedContext); }; @@ -191,15 +191,15 @@ function generateEntry( // Mirrors the production executor bundler (services/executor/bundler.ts). // In production, buildExecutorArgsExpr injects actor/env into args. // For test-run, we embed machine user as actor. - const actorExpr = buildMachineActorExpr(machineUser, workspaceId); + const principalExpr = buildMachinePrincipalExpr(machineUser, workspaceId); return ml /* js */ ` import _internalExecutor from "${absoluteSourcePath}"; const _env = ${JSON.stringify(env)}; - const _actor = ${actorExpr}; + const _actor = ${principalExpr}; const __executor_function = async (args) => { - const _invoker = ${INVOKER_EXPR}; + const _invoker = ${INVOKER_EXPR} ?? _actor; return _internalExecutor.operation.body({ ...args, env: _env, actor: _actor, invoker: _invoker }); }; @@ -212,13 +212,15 @@ function generateEntry( // Note: user context is not available in TestExecScript for workflow jobs. // The production workflow bundler's user mapping is being fixed in fix/workflow-user. const exportName = assertDefined(detected.exportName, "workflow job export name missing"); + const principalExpr = buildMachinePrincipalExpr(machineUser, workspaceId); return ml /* js */ ` import { ${exportName} } from "${absoluteSourcePath}"; const env = ${JSON.stringify(env)}; + const fallbackInvoker = ${principalExpr}; export async function main(input) { - const invoker = ${INVOKER_EXPR}; + const invoker = ${INVOKER_EXPR} ?? fallbackInvoker; return await ${exportName}.body(input, { env, invoker }); } `; @@ -227,33 +229,17 @@ function generateEntry( } /** - * Build a JSON expression for a machine user TailorUser object. + * Build a JSON expression for a machine user TailorPrincipal object. * @param machineUser - Resolved machine user info * @param workspaceId - Workspace ID * @returns JSON string for the user expression */ -function buildMachineUserExpr(machineUser: ResolvedMachineUser, workspaceId: string): string { +function buildMachinePrincipalExpr(machineUser: ResolvedMachineUser, workspaceId: string): string { return JSON.stringify({ id: machineUser.id, type: "machine_user", workspaceId, - attributes: machineUser.attributes, + attributes: machineUser.attributes ?? {}, attributeList: machineUser.attributeList, }); } - -/** - * Build a JSON expression for a machine user TailorActor object. - * @param machineUser - Resolved machine user info - * @param workspaceId - Workspace ID - * @returns JSON string for the actor expression - */ -function buildMachineActorExpr(machineUser: ResolvedMachineUser, workspaceId: string): string { - return JSON.stringify({ - workspaceId, - userId: machineUser.id, - attributes: machineUser.attributes, - attributeList: machineUser.attributeList, - userType: "USER_TYPE_MACHINE_USER", - }); -} diff --git a/packages/sdk/src/cli/commands/function/detect.ts b/packages/sdk/src/cli/commands/function/detect.ts index 020c5bfdb..45fd9343e 100644 --- a/packages/sdk/src/cli/commands/function/detect.ts +++ b/packages/sdk/src/cli/commands/function/detect.ts @@ -16,7 +16,7 @@ export type FunctionType = "resolver" | "executor" | "workflow-job" | "plain"; /** Minimal schema interface for local format detection (subset of TailorField) */ interface InputSchema { - parse(args: { value: unknown; data: unknown; user: Record }): { + parse(args: { value: unknown; data: unknown; invoker: Record | null }): { issues?: readonly { message: string; path?: readonly (string | number | symbol)[] }[]; }; } diff --git a/packages/sdk/src/cli/commands/function/test-run.ts b/packages/sdk/src/cli/commands/function/test-run.ts index 0fda018e9..883575c7a 100644 --- a/packages/sdk/src/cli/commands/function/test-run.ts +++ b/packages/sdk/src/cli/commands/function/test-run.ts @@ -333,15 +333,15 @@ export function resolveResolverArg( workspaceId: string, ): string { const parsed = JSON.parse(argStr); - const user = { + const invoker = { id: machineUser.id, type: "machine_user" as const, workspaceId, - attributes: machineUser.attributes ?? null, + attributes: machineUser.attributes ?? {}, attributeList: machineUser.attributeList, }; - const newResult = inputSchema.parse({ value: parsed, data: parsed, user }); + const newResult = inputSchema.parse({ value: parsed, data: parsed, invoker }); if (!newResult.issues) { return argStr; } @@ -353,7 +353,7 @@ export function resolveResolverArg( typeof parsed.input === "object" && !Array.isArray(parsed.input) ) { - const oldResult = inputSchema.parse({ value: parsed.input, data: parsed.input, user }); + const oldResult = inputSchema.parse({ value: parsed.input, data: parsed.input, invoker }); if (!oldResult.issues) { logger.warn( '[DEPRECATED] Wrapping args with "input" key (e.g. {"input":{...}}) is deprecated. Pass input fields directly (e.g. {"a":1}). The "input" wrapper will be removed in v2.', diff --git a/packages/sdk/src/cli/services/resolver/bundler.ts b/packages/sdk/src/cli/services/resolver/bundler.ts index dee56c40b..2a68eacf3 100644 --- a/packages/sdk/src/cli/services/resolver/bundler.ts +++ b/packages/sdk/src/cli/services/resolver/bundler.ts @@ -156,7 +156,7 @@ async function bundleSingleResolver( const result = t.object(_internalResolver.input).parse({ value: context.input, data: context.input, - user: context.user, + invoker, }); if (result.issues) { diff --git a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts index 61271d2dc..e21ba1928 100644 --- a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts +++ b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts @@ -4,7 +4,7 @@ import { join, resolve } from "pathe"; import * as rolldown from "rolldown"; import { getDistDir } from "@/cli/shared/dist-dir"; import { platformBundleDefinePlugin } from "@/cli/shared/platform-bundle-plugin"; -import { stringifyFunction, tailorUserMap } from "@/parser/service/tailordb/field"; +import { stringifyFunction, tailorPrincipalMap } from "@/parser/service/tailordb/field"; import { setPrecompiledScriptExpr } from "@/parser/service/tailordb/hooks-validate-precompiled-expr"; import { assertDefined } from "@/utils/assert"; import { ES_BUILTINS } from "./es-builtins"; @@ -391,7 +391,7 @@ function buildPrecompiledExpr(bundleCode: string): string { " const module = { exports: {} };\n" + " const exports = module.exports;\n" + `${bundleCode}\n` + - ` return module.exports.main({ value: _value, data: _data, user: ${tailorUserMap} });\n` + + ` return module.exports.main({ value: _value, data: _data, invoker: ${tailorPrincipalMap} });\n` + "})()" ); } @@ -439,7 +439,7 @@ async function bundleScriptTarget(args: { }): Promise { const { fn, kind, sourceFilePath, sourceBindings, tempDir, targetIndex, tsconfig } = args; const fnSource = stringifyFunction(fn); - const inlineExpr = `(${fnSource})({ value: _value, data: _data, user: ${tailorUserMap} })`; + const inlineExpr = `(${fnSource})({ value: _value, data: _data, invoker: ${tailorPrincipalMap} })`; // Check if the function has free variables that need bundling const freeVars = findUndefinedReferences(`const __fn = ${fnSource};`); diff --git a/packages/sdk/src/cli/shared/runtime-exprs.test.ts b/packages/sdk/src/cli/shared/runtime-exprs.test.ts index ee2e7abf4..d587de1f6 100644 --- a/packages/sdk/src/cli/shared/runtime-exprs.test.ts +++ b/packages/sdk/src/cli/shared/runtime-exprs.test.ts @@ -12,7 +12,9 @@ describe("buildExecutorArgsExpr", () => { const expr = buildExecutorArgsExpr(kind, env); expect(expr).toContain("...args"); expect(expr).toContain("appNamespace: args.namespaceName"); - expect(expr).toContain("actor: args.actor"); + expect(expr).toContain("actor: (($raw)"); + expect(expr).toContain("})(args.actor)"); + expect(expr).toContain("userType"); expect(expr).toContain("attributeMap"); expect(expr).toContain("attributeList"); expect(expr).toContain(`env: ${JSON.stringify(env)}`); @@ -44,7 +46,8 @@ describe("buildExecutorArgsExpr", () => { test("includes actor transform and appNamespace", () => { const expr = buildExecutorArgsExpr("resolverExecuted", env); - expect(expr).toContain("actor: args.actor"); + expect(expr).toContain("actor: (($raw)"); + expect(expr).toContain("})(args.actor)"); expect(expr).toContain("appNamespace: args.namespaceName"); }); @@ -98,12 +101,12 @@ describe("buildResolverOperationHookExpr", () => { expect(expr).toContain("input: context.args"); }); - test("includes user transformation via tailorUserMap", () => { + test("includes caller transformation via tailorPrincipalMap", () => { const expr = buildResolverOperationHookExpr(env); - expect(expr).toContain("user:"); - expect(expr).toContain("user.workspace_id"); - expect(expr).toContain("user.attribute_map"); - expect(expr).toContain("user.attributes"); + expect(expr).toContain("caller:"); + expect(expr).toContain("workspace_id"); + expect(expr).toContain("attribute_map"); + expect(expr).toContain("attributes"); }); test("includes env injection", () => { diff --git a/packages/sdk/src/cli/shared/runtime-exprs.ts b/packages/sdk/src/cli/shared/runtime-exprs.ts index 14365dde8..d6ea43fad 100644 --- a/packages/sdk/src/cli/shared/runtime-exprs.ts +++ b/packages/sdk/src/cli/shared/runtime-exprs.ts @@ -7,10 +7,10 @@ * - Bundle inline: interpolated into the generated `.entry.js` wrapper and * evaluated inside the bundled script at function entry. * - * The user field mapping (server → SDK) shared across services is defined in - * `@/parser/service/tailordb` as `tailorUserMap`. + * The principal field mapping (server → SDK) shared across services is defined + * in `@/parser/service/tailordb` as `tailorPrincipalMap`. */ -import { tailorUserMap } from "@/parser/service/tailordb"; +import { tailorPrincipalMap } from "@/parser/service/tailordb"; import type { Trigger } from "@/types/executor.generated"; // --------------------------------------------------------------------------- @@ -21,7 +21,8 @@ import type { Trigger } from "@/types/executor.generated"; * `invoker` value expression, inlined into bundler entry wrappers. * * Calls `tailor.context.getInvoker()` at function entry and maps the server - * shape to TailorInvoker. Anonymous callers (`null`) pass through as `null`. + * shape to `TailorPrincipal | null`. Anonymous callers (`null`) pass through + * as `null`. */ export const INVOKER_EXPR = `(($raw) => $raw ? ({ id: $raw.id, @@ -38,15 +39,26 @@ export const INVOKER_EXPR = `(($raw) => $raw ? ({ /** * Actor field transformation expression. * - * Transforms the server's actor object to match the SDK's TailorActor type: - * server `attributeMap` → SDK `attributes` - * server `attributes` → SDK `attributeList` - * other fields → passed through - * null/undefined actor → null + * Transforms the server's actor object to match `TailorPrincipal | null`. */ -const ACTOR_TRANSFORM_EXPR = - `actor: args.actor ? (({ attributeMap, attributes: attrList, ...rest }) => ` + - `({ ...rest, attributes: attributeMap, attributeList: attrList }))(args.actor) : null`; +const ACTOR_TRANSFORM_EXPR = `actor: (($raw) => { + const type = $raw?.userType === "USER_TYPE_USER" + ? "user" + : $raw?.userType === "USER_TYPE_MACHINE_USER" + ? "machine_user" + : $raw?.type; + const id = $raw?.userId ?? $raw?.id; + if (!$raw || !id || !type || type === "USER_TYPE_UNSPECIFIED" || id === "00000000-0000-0000-0000-000000000000") { + return null; + } + return { + id, + type, + workspaceId: $raw.workspaceId, + attributes: $raw.attributeMap ?? {}, + attributeList: $raw.attributes ?? [], + }; +})(args.actor)`; /** * Build the JavaScript expression that transforms server-format executor event @@ -92,7 +104,7 @@ export function buildExecutorArgsExpr( * Transforms server context to SDK resolver context: * context.args → input * context.pipeline → spread into result - * user (global var) → TailorUser (via tailorUserMap: workspace_id→workspaceId, attribute_map→attributes, attributes→attributeList) + * user (global var) → caller (`TailorPrincipal | null`) * env → injected as JSON * @param env - Application env record to embed in the expression * @returns A JavaScript expression string for the operationHook @@ -100,5 +112,5 @@ export function buildExecutorArgsExpr( export function buildResolverOperationHookExpr( env: Record, ): string { - return `({ ...context.pipeline, input: context.args, user: ${tailorUserMap}, env: ${JSON.stringify(env)} });`; + return `({ ...context.pipeline, input: context.args, caller: ${tailorPrincipalMap}, env: ${JSON.stringify(env)} });`; } diff --git a/packages/sdk/src/configure/index.ts b/packages/sdk/src/configure/index.ts index a5b7f695d..77af0697b 100644 --- a/packages/sdk/src/configure/index.ts +++ b/packages/sdk/src/configure/index.ts @@ -16,14 +16,7 @@ export namespace t { } export { type TailorField } from "@/configure/types/type"; -export { - type TailorPrincipal, - type TailorUser, - type TailorInvoker, - unauthenticatedTailorUser, - type AttributeMap, - type AttributeList, -} from "@/types/user"; +export { type TailorPrincipal, type AttributeMap, type AttributeList } from "@/types/user"; export { type Env } from "@/types/env"; export { type MachineUserNameRegistry, type MachineUserName } from "@/configure/types/machine-user"; export { type IdpNameRegistry, type IdpName } from "@/configure/types/idp-name"; diff --git a/packages/sdk/src/configure/services/executor/executor.test.ts b/packages/sdk/src/configure/services/executor/executor.test.ts index a7fa6014c..e2bc6ccf7 100644 --- a/packages/sdk/src/configure/services/executor/executor.test.ts +++ b/packages/sdk/src/configure/services/executor/executor.test.ts @@ -17,7 +17,7 @@ import { import { scheduleTrigger } from "./trigger/schedule"; import { incomingWebhookTrigger } from "./trigger/webhook"; import type { Operation } from "./operation"; -import type { TailorInvoker } from "@/types/user"; +import type { TailorPrincipal } from "@/types/user"; describe("createExecutor", () => { test("can disable executor", () => { @@ -87,7 +87,7 @@ describe("createExecutor", () => { operation: { kind: "function", body: (args) => { - expectTypeOf(args).toEqualTypeOf(); + expectTypeOf(args).toEqualTypeOf(); }, }, }); @@ -1073,7 +1073,7 @@ describe("functionTarget", () => { operation: { kind: "function", body: (args) => { - expectTypeOf(args.invoker).toEqualTypeOf(); + expectTypeOf(args.invoker).toEqualTypeOf(); }, }, }); diff --git a/packages/sdk/src/configure/services/executor/operation.ts b/packages/sdk/src/configure/services/executor/operation.ts index 658fd2638..867dc51e4 100644 --- a/packages/sdk/src/configure/services/executor/operation.ts +++ b/packages/sdk/src/configure/services/executor/operation.ts @@ -7,12 +7,12 @@ import type { WebhookOperation as ParserWebhookOperation, WorkflowOperation as ParserWorkflowOperation, } from "@/types/executor.generated"; -import type { TailorInvoker } from "@/types/user"; +import type { TailorPrincipal } from "@/types/user"; import type { Client } from "@urql/core"; /** Function-based executor operation. The body receives the trigger args and the `invoker`. */ export type FunctionOperation = Omit & { - body: (args: Args & { invoker?: TailorInvoker }) => void | Promise; + body: (args: Args & { invoker: TailorPrincipal | null }) => void | Promise; authInvoker?: AuthInvoker | MachineUserName; }; diff --git a/packages/sdk/src/configure/services/executor/trigger/event.ts b/packages/sdk/src/configure/services/executor/trigger/event.ts index 315036f24..a6227b987 100644 --- a/packages/sdk/src/configure/services/executor/trigger/event.ts +++ b/packages/sdk/src/configure/services/executor/trigger/event.ts @@ -1,7 +1,6 @@ import type { ResolverConfig } from "@/configure/services/resolver/resolver"; import type { TailorDBType } from "@/configure/services/tailordb/schema"; import type { IdpName } from "@/configure/types/idp-name"; -import type { TailorActor } from "@/types/actor"; import type { TailorEnv } from "@/types/env"; import type { TailorDBTrigger as ParserTailorDBTrigger, @@ -10,12 +9,13 @@ import type { AuthAccessTokenTrigger as ParserAuthAccessTokenTrigger, } from "@/types/executor.generated"; import type { output } from "@/types/helpers"; +import type { TailorPrincipal } from "@/types/user"; interface EventArgs { workspaceId: string; appNamespace: string; env: TailorEnv; - actor: TailorActor | null; + actor: TailorPrincipal | null; } interface RecordArgs extends EventArgs { diff --git a/packages/sdk/src/configure/services/resolver/resolver.test.ts b/packages/sdk/src/configure/services/resolver/resolver.test.ts index 53f6f9885..077c6b8dc 100644 --- a/packages/sdk/src/configure/services/resolver/resolver.test.ts +++ b/packages/sdk/src/configure/services/resolver/resolver.test.ts @@ -4,7 +4,7 @@ import { t } from "@/configure/types"; import { createResolver } from "./resolver"; import type { output } from "@/types/helpers"; import type { ResolverInput } from "@/types/resolver.generated"; -import type { TailorInvoker, TailorUser } from "@/types/user"; +import type { TailorPrincipal } from "@/types/user"; describe("createResolver", () => { describe("type inference", () => { @@ -16,11 +16,11 @@ describe("createResolver", () => { result: t.string(), }), body: (context) => { - expectTypeOf(context).toHaveProperty("user"); + expectTypeOf(context).toHaveProperty("caller"); expectTypeOf(context).toHaveProperty("input"); expectTypeOf(context).toHaveProperty("invoker"); - expectTypeOf(context.user).toEqualTypeOf(); - expectTypeOf(context.invoker).toEqualTypeOf(); + expectTypeOf(context.caller).toEqualTypeOf(); + expectTypeOf(context.invoker).toEqualTypeOf(); expectTypeOf(context.input).toBeNever(); return { result: "hello" }; }, @@ -35,9 +35,9 @@ describe("createResolver", () => { success: t.bool(), }), body: (context) => { - expectTypeOf(context).toHaveProperty("user"); + expectTypeOf(context).toHaveProperty("caller"); expectTypeOf(context).toHaveProperty("input"); - expectTypeOf(context.user).toEqualTypeOf(); + expectTypeOf(context.caller).toEqualTypeOf(); expectTypeOf(context.input).toBeNever(); return { success: true }; }, @@ -59,7 +59,7 @@ describe("createResolver", () => { }), body: (context) => { expectTypeOf(context).toHaveProperty("input"); - expectTypeOf(context).toHaveProperty("user"); + expectTypeOf(context).toHaveProperty("caller"); expectTypeOf(context.input).toEqualTypeOf<{ name: string; age: number; @@ -241,7 +241,7 @@ describe("createResolver", () => { }), body: async (context) => { expectTypeOf(context).toHaveProperty("input"); - expectTypeOf(context).toHaveProperty("user"); + expectTypeOf(context).toHaveProperty("caller"); await new Promise((resolve) => setTimeout(resolve, 0)); return { data: context.input.id }; }, @@ -260,13 +260,13 @@ describe("createResolver", () => { }), body: async (context) => { expectTypeOf(context).toHaveProperty("input"); - expectTypeOf(context).toHaveProperty("user"); + expectTypeOf(context).toHaveProperty("caller"); return { success: true }; }, }); }); - test("user context always available", () => { + test("caller context is nullable", () => { createResolver({ name: "withUser", operation: "query", @@ -274,11 +274,12 @@ describe("createResolver", () => { userId: t.string(), }), body: (context) => { - expectTypeOf(context.user).toEqualTypeOf(); - expectTypeOf(context.user.id).toBeString(); - expectTypeOf(context.user.type).toBeString(); - expectTypeOf(context.user.workspaceId).toBeString(); - return { userId: context.user.id }; + expectTypeOf(context.caller).toEqualTypeOf(); + if (!context.caller) return { userId: "anonymous" }; + expectTypeOf(context.caller.id).toBeString(); + expectTypeOf(context.caller.type).toEqualTypeOf<"user" | "machine_user">(); + expectTypeOf(context.caller.workspaceId).toBeString(); + return { userId: context.caller.id }; }, }); }); diff --git a/packages/sdk/src/configure/services/resolver/resolver.ts b/packages/sdk/src/configure/services/resolver/resolver.ts index f32e681be..91a30bd8b 100644 --- a/packages/sdk/src/configure/services/resolver/resolver.ts +++ b/packages/sdk/src/configure/services/resolver/resolver.ts @@ -6,12 +6,12 @@ import type { TailorAnyField, TailorField } from "@/configure/types/type"; import type { TailorEnv } from "@/types/env"; import type { InferFieldsOutput, output } from "@/types/helpers"; import type { ResolverInput } from "@/types/resolver.generated"; -import type { TailorInvoker, TailorUser } from "@/types/user"; +import type { TailorPrincipal } from "@/types/user"; type Context | undefined> = { input: Input extends Record ? InferFieldsOutput : never; - user: TailorUser; - invoker?: TailorInvoker; + caller: TailorPrincipal | null; + invoker: TailorPrincipal | null; env: TailorEnv; }; @@ -49,7 +49,7 @@ type ResolverReturn< * Create a resolver definition for the Tailor SDK. * * The `body` function receives a context with `input` (typed from `config.input`), - * `user`, `invoker` (reflects `authInvoker` delegation), and `env`. + * `caller`, `invoker` (reflects `authInvoker` delegation), and `env`. * The return value of `body` must match the `output` type. * * `output` accepts either a single TailorField (e.g. `t.string()`) or a @@ -72,7 +72,7 @@ type ResolverReturn< * input: { * id: t.string(), * }, - * body: async ({ input, user }) => { + * body: async ({ input, caller }) => { * const db = getDB("tailordb"); * const result = await db.selectFrom("User").selectAll().where("id", "=", input.id).executeTakeFirst(); * return { name: result?.name ?? "", email: result?.email ?? "" }; diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 18713ff80..0c52ffddd 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -1,10 +1,10 @@ // oxlint-disable vitest/expect-expect -- Type-only assertions are checked by TypeScript. import { describe, expectTypeOf, expect, test } from "vitest"; import { t } from "@/configure/types"; -import { type TailorUser, unauthenticatedTailorUser } from "@/types/user"; import { db } from "./schema"; import type { Hook } from "./types"; import type { output } from "@/types/helpers"; +import type { TailorPrincipal } from "@/types/user"; import type { FieldValidateInput, ValidateConfig } from "@/types/validation"; describe("TailorDBField basic field type tests", () => { @@ -607,7 +607,7 @@ describe("TailorDBType withTimestamps option tests", () => { }>(); }); - const timestampHookUser = unauthenticatedTailorUser; + const timestampHookInvoker = null; test("createdAt create hook respects a user-specified value", () => { const { createdAt } = db.fields.timestamps(); @@ -615,7 +615,7 @@ describe("TailorDBType withTimestamps option tests", () => { expect(createHook).toBeDefined(); const specified = new Date("2025-02-10T09:00:00Z"); - const result = createHook!({ value: specified, data: {}, user: timestampHookUser }); + const result = createHook!({ value: specified, data: {}, invoker: timestampHookInvoker }); expect(result).toBe(specified); }); @@ -625,7 +625,7 @@ describe("TailorDBType withTimestamps option tests", () => { expect(createHook).toBeDefined(); const before = Date.now(); - const result = createHook!({ value: null, data: {}, user: timestampHookUser }); + const result = createHook!({ value: null, data: {}, invoker: timestampHookInvoker }); const after = Date.now(); expect(result).toBeInstanceOf(Date); expect((result as Date).getTime()).toBeGreaterThanOrEqual(before); @@ -1374,7 +1374,7 @@ describe("TailorDBType files method tests", () => { }); describe("TailorDBField runtime validation tests", () => { - const user: TailorUser = { + const invoker: TailorPrincipal = { id: "test", type: "user", workspaceId: "workspace-test", @@ -1385,66 +1385,66 @@ describe("TailorDBField runtime validation tests", () => { test("validates string field values", () => { const field = db.string(); - const result = field.parse({ value: "hello", data, user }); + const result = field.parse({ value: "hello", data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); } expect(result.value).toBe("hello"); - const bad = field.parse({ value: 123, data, user }); + const bad = field.parse({ value: 123, data, invoker }); expect(bad.issues?.[0]?.message).toBe("Expected a string: received 123"); }); test("validates enum values", () => { const field = db.enum(["active", "inactive"]); - const result = field.parse({ value: "active", data, user }); + const result = field.parse({ value: "active", data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); } expect(result.value).toBe("active"); - const bad = field.parse({ value: "unknown", data, user }); + const bad = field.parse({ value: "unknown", data, invoker }); expect(bad.issues?.[0]?.message).toBe("Must be one of [active, inactive]: received unknown"); }); test("validates integer values", () => { const field = db.int(); - const ok = field.parse({ value: 42, data, user }); + const ok = field.parse({ value: 42, data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); } expect(ok.value).toBe(42); - const bad = field.parse({ value: "not-a-number", data, user }); + const bad = field.parse({ value: "not-a-number", data, invoker }); expect(bad.issues?.[0]?.message).toBe("Expected an integer: received not-a-number"); }); test("validates float values", () => { const field = db.float(); - const ok = field.parse({ value: 3.14, data, user }); + const ok = field.parse({ value: 3.14, data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); } expect(ok.value).toBe(3.14); - const bad = field.parse({ value: "not-a-number", data, user }); + const bad = field.parse({ value: "not-a-number", data, invoker }); expect(bad.issues?.[0]?.message).toBe("Expected a number: received not-a-number"); }); test("validates boolean values", () => { const field = db.bool(); - const ok = field.parse({ value: true, data, user }); + const ok = field.parse({ value: true, data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); } expect(ok.value).toBe(true); - const bad = field.parse({ value: "true", data, user }); + const bad = field.parse({ value: "true", data, invoker }); expect(bad.issues?.[0]?.message).toBe("Expected a boolean: received true"); }); @@ -1453,21 +1453,21 @@ describe("TailorDBField runtime validation tests", () => { name: db.string(), age: db.int({ optional: true }), }); - const ok = field.parse({ value: { name: "test", age: 30 }, data, user }); + const ok = field.parse({ value: { name: "test", age: 30 }, data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); } expect(ok.value).toEqual({ name: "test", age: 30 }); - const bad = field.parse({ value: { name: 123 }, data, user }); + const bad = field.parse({ value: { name: 123 }, data, invoker }); expect(bad.issues?.[0]?.path).toEqual(["name"]); expect(bad.issues?.[0]?.message).toBe("Expected a string: received 123"); }); test("validates array values", () => { const field = db.int({ array: true }); - const ok = field.parse({ value: [1, 2, 3], data, user }); + const ok = field.parse({ value: [1, 2, 3], data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); @@ -1477,27 +1477,27 @@ describe("TailorDBField runtime validation tests", () => { test("validates UUID format", () => { const field = db.uuid(); - const ok = field.parse({ value: "123e4567-e89b-12d3-a456-426614174000", data, user }); + const ok = field.parse({ value: "123e4567-e89b-12d3-a456-426614174000", data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); } expect(ok.value).toBe("123e4567-e89b-12d3-a456-426614174000"); - const bad = field.parse({ value: "not-a-uuid", data, user }); + const bad = field.parse({ value: "not-a-uuid", data, invoker }); expect(bad.issues?.[0]?.message).toBe("Expected a valid UUID: received not-a-uuid"); }); test("validates date format", () => { const field = db.date(); - const ok = field.parse({ value: "2025-01-01", data, user }); + const ok = field.parse({ value: "2025-01-01", data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); } expect(ok.value).toBe("2025-01-01"); - const bad = field.parse({ value: "2025/01/01", data, user }); + const bad = field.parse({ value: "2025/01/01", data, invoker }); expect(bad.issues?.[0]?.message).toBe( 'Expected to match "yyyy-MM-dd" format: received 2025/01/01', ); @@ -1505,24 +1505,24 @@ describe("TailorDBField runtime validation tests", () => { test("validates time format", () => { const field = db.time(); - const ok = field.parse({ value: "10:11", data, user }); + const ok = field.parse({ value: "10:11", data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); } expect(ok.value).toBe("10:11"); - const bad = field.parse({ value: "10:11:12", data, user }); + const bad = field.parse({ value: "10:11:12", data, invoker }); expect(bad.issues?.[0]?.message).toBe('Expected to match "HH:mm" format: received 10:11:12'); }); test("validates required and optional handling", () => { const requiredField = db.string(); - const requiredMissing = requiredField.parse({ value: undefined, data, user }); + const requiredMissing = requiredField.parse({ value: undefined, data, invoker }); expect(requiredMissing.issues?.[0]?.message).toBe("Required field is missing"); const optionalField = db.string({ optional: true }); - const optionalNull = optionalField.parse({ value: undefined, data, user }); + const optionalNull = optionalField.parse({ value: undefined, data, invoker }); expect(optionalNull.issues).toBeUndefined(); if (optionalNull.issues) { throw new Error("Unexpected issues"); @@ -1971,44 +1971,56 @@ describe("TailorDBField decimal type tests", () => { test("decimal parse validates valid decimal strings", () => { const field = db.decimal(); - const user = { id: "test", _loggedIn: true } as unknown as TailorUser; - expect(field.parse({ value: "123.45", data: {}, user })).toEqual({ value: "123.45" }); - expect(field.parse({ value: "0", data: {}, user })).toEqual({ value: "0" }); - expect(field.parse({ value: "-99.99", data: {}, user })).toEqual({ value: "-99.99" }); - expect(field.parse({ value: "1000", data: {}, user })).toEqual({ value: "1000" }); - expect(field.parse({ value: ".5", data: {}, user })).toEqual({ value: ".5" }); - expect(field.parse({ value: "5.", data: {}, user })).toEqual({ value: "5." }); - expect(field.parse({ value: "4.321e+4", data: {}, user })).toEqual({ value: "4.321e+4" }); - expect(field.parse({ value: "1E-5", data: {}, user })).toEqual({ value: "1E-5" }); - expect(field.parse({ value: "2.41E-3", data: {}, user })).toEqual({ value: "2.41E-3" }); - expect(field.parse({ value: "-1.5e10", data: {}, user })).toEqual({ value: "-1.5e10" }); + const invoker: TailorPrincipal = { + id: "test", + type: "user", + workspaceId: "workspace-test", + attributes: {}, + attributeList: [], + }; + expect(field.parse({ value: "123.45", data: {}, invoker })).toEqual({ value: "123.45" }); + expect(field.parse({ value: "0", data: {}, invoker })).toEqual({ value: "0" }); + expect(field.parse({ value: "-99.99", data: {}, invoker })).toEqual({ value: "-99.99" }); + expect(field.parse({ value: "1000", data: {}, invoker })).toEqual({ value: "1000" }); + expect(field.parse({ value: ".5", data: {}, invoker })).toEqual({ value: ".5" }); + expect(field.parse({ value: "5.", data: {}, invoker })).toEqual({ value: "5." }); + expect(field.parse({ value: "4.321e+4", data: {}, invoker })).toEqual({ value: "4.321e+4" }); + expect(field.parse({ value: "1E-5", data: {}, invoker })).toEqual({ value: "1E-5" }); + expect(field.parse({ value: "2.41E-3", data: {}, invoker })).toEqual({ value: "2.41E-3" }); + expect(field.parse({ value: "-1.5e10", data: {}, invoker })).toEqual({ value: "-1.5e10" }); }); test("decimal parse rejects invalid decimal strings", () => { const field = db.decimal(); - const user = { id: "test", _loggedIn: true } as unknown as TailorUser; - const result1 = field.parse({ value: "abc", data: {}, user }); + const invoker: TailorPrincipal = { + id: "test", + type: "user", + workspaceId: "workspace-test", + attributes: {}, + attributeList: [], + }; + const result1 = field.parse({ value: "abc", data: {}, invoker }); expect(result1).toHaveProperty("issues"); - const result2 = field.parse({ value: 123, data: {}, user }); + const result2 = field.parse({ value: 123, data: {}, invoker }); expect(result2).toHaveProperty("issues"); - const result3 = field.parse({ value: "", data: {}, user }); + const result3 = field.parse({ value: "", data: {}, invoker }); expect(result3).toHaveProperty("issues"); - const result4 = field.parse({ value: "1_000_000", data: {}, user }); + const result4 = field.parse({ value: "1_000_000", data: {}, invoker }); expect(result4).toHaveProperty("issues"); - const result5 = field.parse({ value: "0b1.1p-5", data: {}, user }); + const result5 = field.parse({ value: "0b1.1p-5", data: {}, invoker }); expect(result5).toHaveProperty("issues"); - const result6 = field.parse({ value: "1e", data: {}, user }); + const result6 = field.parse({ value: "1e", data: {}, invoker }); expect(result6).toHaveProperty("issues"); - const result7 = field.parse({ value: "e5", data: {}, user }); + const result7 = field.parse({ value: "e5", data: {}, invoker }); expect(result7).toHaveProperty("issues"); - const result8 = field.parse({ value: ".", data: {}, user }); + const result8 = field.parse({ value: ".", data: {}, invoker }); expect(result8).toHaveProperty("issues"); }); }); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 01a742c28..7b755636e 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -25,7 +25,7 @@ import type { RelationType, } from "@/types/tailordb"; import type { RawPermissions } from "@/types/tailordb.generated"; -import type { InferredAttributeMap, TailorUser } from "@/types/user"; +import type { InferredAttributeMap, TailorPrincipal } from "@/types/user"; import type { FieldValidateInput, ValidateConfig, Validators } from "@/types/validation"; import type { StandardSchemaV1 } from "@standard-schema/spec"; @@ -308,13 +308,13 @@ const regex = { type FieldParseArgs = { value: unknown; data: unknown; - user: TailorUser; + invoker: TailorPrincipal | null; }; type FieldValidateValueArgs = { value: TailorToTs[T]; data: unknown; - user: TailorUser; + invoker: TailorPrincipal | null; pathArray: string[]; }; @@ -323,7 +323,7 @@ type FieldParseInternalArgs = { // oxlint-disable-next-line no-explicit-any value: any; data: unknown; - user: TailorUser; + invoker: TailorPrincipal | null; pathArray: string[]; }; @@ -371,11 +371,11 @@ function createTailorDBField< /** * Validate a single value (not an array element) * Used internally for array element validation - * @param args - Value, context data, and user + * @param args - Value, context data, and invoker * @returns Array of validation issues */ function validateValue(args: FieldValidateValueArgs): StandardSchemaV1.Issue[] { - const { value, data, user, pathArray } = args; + const { value, data, invoker, pathArray } = args; const issues: StandardSchemaV1.Issue[] = []; // Type-specific validation @@ -490,7 +490,7 @@ function createTailorDBField< const result = nestedField._parseInternal({ value: fieldValue, data, - user, + invoker, pathArray: pathArray.concat(fieldName), }); if (result.issues) { @@ -510,7 +510,7 @@ function createTailorDBField< ? { fn: validateInput, message: "Validation failed" } : { fn: validateInput[0], message: validateInput[1] }; - if (!fn({ value, data, user })) { + if (!fn({ value, data, invoker })) { issues.push({ message, path: pathArray.length > 0 ? pathArray : undefined, @@ -530,7 +530,7 @@ function createTailorDBField< function parseInternal( args: FieldParseInternalArgs, ): StandardSchemaV1.Result> { - const { value, data, user, pathArray } = args; + const { value, data, invoker, pathArray } = args; const issues: StandardSchemaV1.Issue[] = []; // 1. Check required/optional @@ -567,7 +567,7 @@ function createTailorDBField< const elementIssues = validateValue({ value: elementValue, data, - user, + invoker, pathArray: elementPath, }); if (elementIssues.length > 0) { @@ -582,7 +582,7 @@ function createTailorDBField< } // 3. Type-specific validation and custom validation - const valueIssues = validateValue({ value, data, user, pathArray }); + const valueIssues = validateValue({ value, data, invoker, pathArray }); issues.push(...valueIssues); if (issues.length > 0) { @@ -633,7 +633,7 @@ function createTailorDBField< return parseInternal({ value: args.value, data: args.data, - user: args.user, + invoker: args.invoker, pathArray: [], }); }, diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index fa3c29b59..e5be01140 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -1,7 +1,7 @@ import type { output } from "@/types/helpers"; import type { TailorAnyDBField, TailorDBField } from "@/types/tailor-db-field"; import type { GqlOperationsConfig } from "@/types/tailordb"; -import type { TailorUser } from "@/types/user"; +import type { TailorPrincipal } from "@/types/user"; import type { NonEmptyObject } from "type-fest"; // --- Hook types (UX-focused, for configure layer) --- @@ -11,7 +11,7 @@ type HookFn = (args: { data: TData extends Record ? { readonly [K in keyof TData]?: TData[K] | null | undefined } : unknown; - user: TailorUser; + invoker: TailorPrincipal | null; }) => TReturn; export type Hook = { diff --git a/packages/sdk/src/configure/services/workflow/job.test.ts b/packages/sdk/src/configure/services/workflow/job.test.ts index 14032279e..8e3d2bc0b 100644 --- a/packages/sdk/src/configure/services/workflow/job.test.ts +++ b/packages/sdk/src/configure/services/workflow/job.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test, expectTypeOf } from "vitest"; import { createWorkflowJob, type WorkflowJob } from "./job"; import { createWorkflow } from "./workflow"; -import type { TailorInvoker } from "@/types/user"; +import type { TailorPrincipal } from "@/types/user"; describe("WorkflowJob type inference", () => { test("preserves literal types in output when using as const", () => { @@ -55,7 +55,7 @@ describe("WorkflowJob type inference", () => { body: (_input: undefined, context) => { expectTypeOf(context).toHaveProperty("env"); expectTypeOf(context).toHaveProperty("invoker"); - expectTypeOf(context.invoker).toEqualTypeOf(); + expectTypeOf(context.invoker).toEqualTypeOf(); }, }); }); diff --git a/packages/sdk/src/configure/services/workflow/job.ts b/packages/sdk/src/configure/services/workflow/job.ts index 33e94327c..ff0f359fe 100644 --- a/packages/sdk/src/configure/services/workflow/job.ts +++ b/packages/sdk/src/configure/services/workflow/job.ts @@ -2,14 +2,14 @@ import { brandValue } from "@/utils/brand"; import { dispatchTriggerJob, registerJob, type RegisteredJobBody } from "./registry"; import type { TailorEnv } from "@/types/env"; import type { JsonCompatible } from "@/types/helpers"; -import type { TailorInvoker } from "@/types/user"; +import type { TailorPrincipal } from "@/types/user"; /** * Context object passed as the second argument to workflow job body functions. */ export type WorkflowJobContext = { env: TailorEnv; - invoker?: TailorInvoker; + invoker: TailorPrincipal | null; }; /** diff --git a/packages/sdk/src/configure/services/workflow/registry.ts b/packages/sdk/src/configure/services/workflow/registry.ts index 8b0905441..f31969b29 100644 --- a/packages/sdk/src/configure/services/workflow/registry.ts +++ b/packages/sdk/src/configure/services/workflow/registry.ts @@ -1,7 +1,7 @@ import { platformSerialize } from "@/utils/test/platform-serialize"; import { buildJobContext } from "./test-env-key"; import type { TailorEnv } from "@/types/env"; -import type { TailorInvoker } from "@/types/user"; +import type { TailorPrincipal } from "@/types/user"; /** * Body signature shared by workflow jobs at registry-write time. @@ -10,7 +10,7 @@ import type { TailorInvoker } from "@/types/user"; */ export type RegisteredJobBody = ( args: unknown, - context: { env: TailorEnv; invoker?: TailorInvoker }, + context: { env: TailorEnv; invoker: TailorPrincipal | null }, ) => unknown | Promise; export interface RegisteredWorkflow { diff --git a/packages/sdk/src/configure/services/workflow/test-env-key.ts b/packages/sdk/src/configure/services/workflow/test-env-key.ts index a7db0eba9..626cc6fdd 100644 --- a/packages/sdk/src/configure/services/workflow/test-env-key.ts +++ b/packages/sdk/src/configure/services/workflow/test-env-key.ts @@ -10,7 +10,7 @@ * @internal */ import type { TailorEnv } from "../../../types/env"; -import type { TailorInvoker } from "../../../types/user"; +import type { TailorPrincipal } from "../../../types/user"; const SLOT_KEY = "__tailorWorkflowTestEnv"; @@ -49,11 +49,11 @@ export const WORKFLOW_TEST_ENV_KEY = "TAILOR_TEST_WORKFLOW_ENV"; // env from `mockWorkflow().setEnv()`, else the deprecated env-var. Shallow-copied // to isolate against cross-trigger mutation. -export function buildJobContext(): { env: TailorEnv; invoker?: TailorInvoker } { +export function buildJobContext(): { env: TailorEnv; invoker: TailorPrincipal | null } { const fromGlobal = readWorkflowTestEnv(); - if (fromGlobal !== undefined) return { env: { ...fromGlobal } }; + if (fromGlobal !== undefined) return { env: { ...fromGlobal }, invoker: null }; const raw = process.env[WORKFLOW_TEST_ENV_KEY]; - if (!raw) return { env: {} as TailorEnv }; + if (!raw) return { env: {} as TailorEnv, invoker: null }; let parsed: unknown; try { parsed = JSON.parse(raw); @@ -68,5 +68,5 @@ export function buildJobContext(): { env: TailorEnv; invoker?: TailorInvoker } { `${WORKFLOW_TEST_ENV_KEY} must be a JSON object; provide a record or use mockWorkflow().setEnv().`, ); } - return { env: { ...(parsed as TailorEnv) } }; + return { env: { ...(parsed as TailorEnv) }, invoker: null }; } diff --git a/packages/sdk/src/configure/types/type.test.ts b/packages/sdk/src/configure/types/type.test.ts index d86acc4ff..59d395db3 100644 --- a/packages/sdk/src/configure/types/type.test.ts +++ b/packages/sdk/src/configure/types/type.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test, expectTypeOf } from "vitest"; import { t } from "./type"; import type { AllowedValues } from "./field"; import type { output } from "@/types/helpers"; -import type { TailorUser } from "@/types/user"; +import type { TailorPrincipal } from "@/types/user"; describe("TailorType basic field type tests", () => { test("string field outputs string type correctly", () => { @@ -471,7 +471,7 @@ describe("t.object tests", () => { }); describe("TailorField runtime validation tests", () => { - const user: TailorUser = { + const invoker: TailorPrincipal = { id: "test", type: "user", workspaceId: "workspace-test", @@ -483,7 +483,7 @@ describe("TailorField runtime validation tests", () => { describe("validates primitive types", () => { test("validates string type", () => { { - const result = t.string().parse({ value: "valid string", data, user }); + const result = t.string().parse({ value: "valid string", data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -492,7 +492,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.string().parse({ value: 123, data, user }); + const result = t.string().parse({ value: 123, data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected a string: received 123"); expect(result.issues?.[0]?.path).toBeUndefined(); @@ -501,7 +501,7 @@ describe("TailorField runtime validation tests", () => { test("validates integer type", () => { { - const result = t.int().parse({ value: 123, data, user }); + const result = t.int().parse({ value: 123, data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -510,13 +510,13 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.int().parse({ value: "invalid string", data, user }); + const result = t.int().parse({ value: "invalid string", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected an integer: received invalid string"); } { - const result = t.int().parse({ value: 1.5, data, user }); + const result = t.int().parse({ value: 1.5, data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected an integer: received 1.5"); } @@ -524,7 +524,7 @@ describe("TailorField runtime validation tests", () => { test("validates float type", () => { { - const result = t.float().parse({ value: 1.5, data, user }); + const result = t.float().parse({ value: 1.5, data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -533,13 +533,13 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.float().parse({ value: Number.NaN, data, user }); + const result = t.float().parse({ value: Number.NaN, data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected a number: received NaN"); } { - const result = t.float().parse({ value: "invalid string", data, user }); + const result = t.float().parse({ value: "invalid string", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected a number: received invalid string"); } @@ -547,7 +547,7 @@ describe("TailorField runtime validation tests", () => { test("validates boolean type", () => { { - const result = t.bool().parse({ value: true, data, user }); + const result = t.bool().parse({ value: true, data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -556,7 +556,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.bool().parse({ value: "true", data, user }); + const result = t.bool().parse({ value: "true", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected a boolean: received true"); } @@ -569,7 +569,7 @@ describe("TailorField runtime validation tests", () => { const result = t.uuid().parse({ value: "550e8400-e29b-41d4-a716-446655440000", data, - user, + invoker, }); expect(result.issues).toBeUndefined(); if (result.issues) { @@ -579,7 +579,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.uuid().parse({ value: "not-a-uuid", data, user }); + const result = t.uuid().parse({ value: "not-a-uuid", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected a valid UUID: received not-a-uuid"); } @@ -587,7 +587,7 @@ describe("TailorField runtime validation tests", () => { test("validates date format", () => { { - const result = t.date().parse({ value: "2025-12-21", data, user }); + const result = t.date().parse({ value: "2025-12-21", data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -596,7 +596,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.date().parse({ value: "2025/12/21", data, user }); + const result = t.date().parse({ value: "2025/12/21", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual( 'Expected to match "yyyy-MM-dd" format: received 2025/12/21', @@ -609,7 +609,7 @@ describe("TailorField runtime validation tests", () => { const result = t.datetime().parse({ value: "2025-12-21T10:11:12.123Z", data, - user, + invoker, }); expect(result.issues).toBeUndefined(); if (result.issues) { @@ -619,7 +619,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.datetime().parse({ value: "2025-12-21 10:11:12", data, user }); + const result = t.datetime().parse({ value: "2025-12-21 10:11:12", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual( "Expected to match ISO format: received 2025-12-21 10:11:12", @@ -629,7 +629,7 @@ describe("TailorField runtime validation tests", () => { test("vlidates time format", () => { { - const result = t.time().parse({ value: "10:11", data, user }); + const result = t.time().parse({ value: "10:11", data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -638,7 +638,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.time().parse({ value: "10:11:12", data, user }); + const result = t.time().parse({ value: "10:11:12", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual( 'Expected to match "HH:mm" format: received 10:11:12', @@ -651,7 +651,7 @@ describe("TailorField runtime validation tests", () => { test("validates enum values", () => { const status = t.enum(["active", "inactive"]); { - const result = status.parse({ value: "active", data, user }); + const result = status.parse({ value: "active", data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -660,7 +660,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = status.parse({ value: "pending", data, user }); + const result = status.parse({ value: "pending", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual( "Must be one of [active, inactive]: received pending", @@ -682,7 +682,7 @@ describe("TailorField runtime validation tests", () => { gender: "male", }, data, - user, + invoker, }); expect(result.issues).toBeUndefined(); if (result.issues) { @@ -699,7 +699,7 @@ describe("TailorField runtime validation tests", () => { const result = schema.parse({ value: { age: 1, gender: "invalid" }, data, - user, + invoker, }); expect(result.issues).toBeDefined(); expect(result.issues).toEqual([ @@ -719,7 +719,7 @@ describe("TailorField runtime validation tests", () => { value: t.string({ optional: true }), }); const now = new Date(); - const result = schema.parse({ value: now, data, user }); + const result = schema.parse({ value: now, data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual(`Expected an object: received ${String(now)}`); } @@ -728,7 +728,7 @@ describe("TailorField runtime validation tests", () => { test("validates array fields and element paths", () => { const schema = t.int({ array: true }); { - const result = schema.parse({ value: [1, 2, 3], data, user }); + const result = schema.parse({ value: [1, 2, 3], data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -737,13 +737,13 @@ describe("TailorField runtime validation tests", () => { } { - const result = schema.parse({ value: "invalid", data, user }); + const result = schema.parse({ value: "invalid", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected an array"); } { - const result = schema.parse({ value: [1, "x"], data, user }); + const result = schema.parse({ value: [1, "x"], data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]).toEqual({ path: ["[1]"], @@ -755,14 +755,14 @@ describe("TailorField runtime validation tests", () => { test("treats null/undefined as missing when required, and allowed when optional", () => { { const schema = t.string(); - const result = schema.parse({ value: null, data, user }); + const result = schema.parse({ value: null, data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Required field is missing"); } { const schema = t.string({ optional: true }); - const result = schema.parse({ value: null, data, user }); + const result = schema.parse({ value: null, data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -775,7 +775,7 @@ describe("TailorField runtime validation tests", () => { const result = schema.parse({ value: null, data, - user, + invoker, }); expect(result.issues).toBeUndefined(); if (result.issues) { @@ -800,7 +800,7 @@ describe("TailorField runtime validation tests", () => { "2.41E-3", "-1.5e10", ]) { - const result = t.decimal().parse({ value, data, user }); + const result = t.decimal().parse({ value, data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error(`Unexpected issues for "${value}"`); @@ -811,11 +811,11 @@ describe("TailorField runtime validation tests", () => { test("rejects invalid decimal values", () => { for (const value of ["abc", "", "1_000_000", "0b1.1p-5", "1e", "e5", "."]) { - const result = t.decimal().parse({ value, data, user }); + const result = t.decimal().parse({ value, data, invoker }); expect(result.issues).toBeDefined(); } { - const result = t.decimal().parse({ value: 123, data, user }); + const result = t.decimal().parse({ value: 123, data, invoker }); expect(result.issues).toBeDefined(); } }); @@ -823,7 +823,7 @@ describe("TailorField runtime validation tests", () => { }); describe("TailorField clone-on-write / no aliasing", () => { - const user: TailorUser = { + const invoker: TailorPrincipal = { id: "test", type: "user", workspaceId: "workspace-test", @@ -923,10 +923,10 @@ describe("TailorField clone-on-write / no aliasing", () => { const status = t.enum(["active", "inactive"]); const cloned = status.description("status field"); - const ok = cloned.parse({ value: "active", data, user }); + const ok = cloned.parse({ value: "active", data, invoker }); expect(ok.issues).toBeUndefined(); - const ng = cloned.parse({ value: "pending", data, user }); + const ng = cloned.parse({ value: "pending", data, invoker }); expect(ng.issues).toBeDefined(); expect(ng.issues?.[0]?.message).toEqual("Must be one of [active, inactive]: received pending"); }); @@ -938,7 +938,7 @@ describe("TailorField clone-on-write / no aliasing", () => { return args.value.length > 0; }); - const result = field.parse({ value: "x", data, user }); + const result = field.parse({ value: "x", data, invoker }); expect(result.issues).toBeUndefined(); expect(calls).toEqual(["x"]); }); @@ -953,7 +953,7 @@ describe("TailorField clone-on-write / no aliasing", () => { expect(described.metadata.description).toBe("name"); expect(validated.metadata.description).toBeUndefined(); - const failed = described.parse({ value: "", data, user }); + const failed = described.parse({ value: "", data, invoker }); expect(failed.issues).toBeDefined(); }); }); diff --git a/packages/sdk/src/configure/types/type.ts b/packages/sdk/src/configure/types/type.ts index 5055987c5..fe7dca9c7 100644 --- a/packages/sdk/src/configure/types/type.ts +++ b/packages/sdk/src/configure/types/type.ts @@ -9,7 +9,7 @@ import type { } from "@/types/field-types"; import type { InferFieldsOutput, Prettify } from "@/types/helpers"; import type { TailorField as TailorFieldBase } from "@/types/tailor-field"; -import type { TailorUser } from "@/types/user"; +import type { TailorPrincipal } from "@/types/user"; import type { FieldValidateInput } from "@/types/validation"; import type { StandardSchemaV1 } from "@standard-schema/spec"; @@ -73,7 +73,7 @@ export interface TailorField< /** * Parse and validate a value against this field's validation rules * Returns StandardSchema Result type with success or failure - * @param args - Value, context data, and user + * @param args - Value, context data, and invoker * @returns Validation result */ parse(args: FieldParseArgs): StandardSchemaV1.Result; @@ -110,13 +110,13 @@ const regex = { type FieldParseArgs = { value: unknown; data: unknown; - user: TailorUser; + invoker: TailorPrincipal | null; }; type FieldValidateValueArgs = { value: TailorToTs[T]; data: unknown; - user: TailorUser; + invoker: TailorPrincipal | null; pathArray: string[]; }; @@ -125,7 +125,7 @@ type FieldParseInternalArgs = { // oxlint-disable-next-line no-explicit-any value: any; data: unknown; - user: TailorUser; + invoker: TailorPrincipal | null; pathArray: string[]; }; @@ -186,11 +186,11 @@ function createTailorField< /** * Validate a single value (not an array element) * Used internally for array element validation - * @param args - Value, context data, and user + * @param args - Value, context data, and invoker * @returns Array of validation issues */ function validateValue(args: FieldValidateValueArgs): StandardSchemaV1.Issue[] { - const { value, data, user, pathArray } = args; + const { value, data, invoker, pathArray } = args; const issues: StandardSchemaV1.Issue[] = []; const path = pathArray.length > 0 ? pathArray : undefined; @@ -306,7 +306,7 @@ function createTailorField< const result = nestedField._parseInternal({ value: fieldValue, data, - user, + invoker, pathArray: pathArray.concat(fieldName), }); if (result.issues) { @@ -326,7 +326,7 @@ function createTailorField< ? { fn: validateInput, message: "Validation failed" } : { fn: validateInput[0], message: validateInput[1] }; - if (!fn({ value, data, user })) { + if (!fn({ value, data, invoker })) { issues.push({ message, path, @@ -346,7 +346,7 @@ function createTailorField< function parseInternal( args: FieldParseInternalArgs, ): StandardSchemaV1.Result> { - const { value, data, user, pathArray } = args; + const { value, data, invoker, pathArray } = args; const issues: StandardSchemaV1.Issue[] = []; const path = pathArray.length > 0 ? pathArray : undefined; @@ -384,7 +384,7 @@ function createTailorField< const elementIssues = validateValue({ value: elementValue, data, - user, + invoker, pathArray: elementPath, }); if (elementIssues.length > 0) { @@ -399,7 +399,7 @@ function createTailorField< } // 3. Type-specific validation and custom validation - const valueIssues = validateValue({ value, data, user, pathArray }); + const valueIssues = validateValue({ value, data, invoker, pathArray }); issues.push(...valueIssues); if (issues.length > 0) { @@ -462,7 +462,7 @@ function createTailorField< return parseInternal({ value: args.value, data: args.data, - user: args.user, + invoker: args.invoker, pathArray: [], }); }, diff --git a/packages/sdk/src/parser/service/tailordb/field.ts b/packages/sdk/src/parser/service/tailordb/field.ts index 139e0f858..4a0e1d342 100644 --- a/packages/sdk/src/parser/service/tailordb/field.ts +++ b/packages/sdk/src/parser/service/tailordb/field.ts @@ -7,9 +7,27 @@ import type { } from "@/types/tailordb"; import type { TailorDBTypeRaw as TailorDBTypeSchemaOutput } from "@/types/tailordb.generated"; -// Since there's naming difference between platform and sdk, -// use this mapping in all scripts to provide variables that match sdk types. -export const tailorUserMap = /* js */ `{ id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes }`; +const NIL_UUID = "00000000-0000-0000-0000-000000000000"; + +// Since there's naming difference between platform and SDK, use this mapping in +// all scripts to provide variables that match `TailorPrincipal | null`. +export const tailorPrincipalMap = /* js */ `(($raw) => { + const type = $raw?.type === "USER_TYPE_USER" + ? "user" + : $raw?.type === "USER_TYPE_MACHINE_USER" + ? "machine_user" + : $raw?.type; + if (!$raw || !type || type === "USER_TYPE_UNSPECIFIED" || $raw.id === "${NIL_UUID}") { + return null; + } + return { + id: $raw.id, + type, + workspaceId: $raw.workspace_id ?? $raw.workspaceId, + attributes: $raw.attribute_map ?? $raw.attributeMap ?? {}, + attributeList: $raw.attributes ?? [], + }; +})(user)`; /** * Convert a function to a string representation. @@ -46,7 +64,7 @@ const convertHookToExpr = (fn: Function): string => { return precompiledExpr; } const normalized = stringifyFunction(fn); - return `(${normalized})({ value: _value, data: _data, user: ${tailorUserMap} })`; + return `(${normalized})({ value: _value, data: _data, invoker: ${tailorPrincipalMap} })`; }; /** @@ -89,7 +107,7 @@ export function parseFieldConfig( script: { expr: getPrecompiledScriptExpr(fn) ?? - `(${fn.toString().trim()})({ value: _value, data: _data, user: ${tailorUserMap} })`, + `(${fn.toString().trim()})({ value: _value, data: _data, invoker: ${tailorPrincipalMap} })`, }, errorMessage: message, }; diff --git a/packages/sdk/src/parser/service/tailordb/index.ts b/packages/sdk/src/parser/service/tailordb/index.ts index 5cb0f781b..53451acf5 100644 --- a/packages/sdk/src/parser/service/tailordb/index.ts +++ b/packages/sdk/src/parser/service/tailordb/index.ts @@ -1,3 +1,3 @@ -export { stringifyFunction, tailorUserMap } from "./field"; +export { stringifyFunction, tailorPrincipalMap } from "./field"; export { parseTypes } from "./type-parser"; export { TailorDBServiceConfigSchema, TailorDBTypeSchema } from "./schema"; diff --git a/packages/sdk/src/plugin/with-context.ts b/packages/sdk/src/plugin/with-context.ts index f390fabc0..7ef102df3 100644 --- a/packages/sdk/src/plugin/with-context.ts +++ b/packages/sdk/src/plugin/with-context.ts @@ -4,7 +4,7 @@ * context (like type references and namespace) at runtime. */ -import type { TailorActor, TailorEnv } from "@/types/runtime"; +import type { TailorEnv, TailorPrincipal } from "@/types/runtime"; /** * Plugin executor factory function type. @@ -28,8 +28,8 @@ export interface PluginFunctionArgs { appNamespace: string; /** Environment variables */ env: TailorEnv; - /** Actor (user) who triggered the event, null for system events */ - actor: TailorActor | null; + /** Principal that triggered the event, null for system events */ + actor: TailorPrincipal | null; /** Name of the TailorDB type */ typeName: string; /** TailorDB connections by namespace */ diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index 5fac684a5..a1de9f4bb 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -12,24 +12,15 @@ * } */ +import type { TailorPrincipal } from "@/types/user"; + /** * Information about the invoker of the current function execution. * - * Matches the shape of `TailorUser` and `TailorActor` — `attributes` is the - * attribute map and `attributeList` is the array of attribute IDs. + * Matches the public `TailorPrincipal` shape — `attributes` is the attribute + * map and `attributeList` is the array of attribute IDs. */ -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[]; -} +export type Invoker = TailorPrincipal; /** * Raw platform-side invoker payload returned by `tailor.context.getInvoker()`. @@ -70,7 +61,7 @@ export function getInvoker(): Invoker | null { id: raw.id, type: raw.type, workspaceId: raw.workspaceId, - attributes: raw.attributeMap, - attributeList: raw.attributes, + attributes: raw.attributeMap as Invoker["attributes"], + attributeList: raw.attributes as Invoker["attributeList"], }; } diff --git a/packages/sdk/src/types/actor.ts b/packages/sdk/src/types/actor.ts deleted file mode 100644 index 1597d00de..000000000 --- a/packages/sdk/src/types/actor.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { InferredAttributeList, InferredAttributeMap } from "./user"; - -/** - * User type enum values from the Tailor Platform server. - * - * @deprecated `TailorPrincipal` represents the type as `"user"` or - * `"machine_user"`. This enum is removed in the next major version. - */ -export type TailorActorType = "USER_TYPE_USER" | "USER_TYPE_MACHINE_USER" | "USER_TYPE_UNSPECIFIED"; - -/** - * Represents an actor in event triggers. - * - * @deprecated Use `TailorPrincipal` instead. `TailorActor` is unified into - * `TailorPrincipal` in the next major version, where `userId`/`userType` become - * `id`/`type` with `"user"`/`"machine_user"` values. - */ -export type TailorActor = { - /** The ID of the workspace the user belongs to. */ - workspaceId: string; - /** The ID of the user. */ - userId: string; - /** - * A map of the user's attributes. - * Maps from server's `attributeMap` field. - */ - attributes: InferredAttributeMap | null; - /** - * A list of the user's attributes. - * Maps from server's `attributes` field. - */ - attributeList: InferredAttributeList; - /** The type of the user. */ - userType: TailorActorType; -}; diff --git a/packages/sdk/src/types/runtime.ts b/packages/sdk/src/types/runtime.ts index 54a19e3c1..4d1deebae 100644 --- a/packages/sdk/src/types/runtime.ts +++ b/packages/sdk/src/types/runtime.ts @@ -1,2 +1,2 @@ -export type { TailorActor, TailorActorType } from "./actor"; export type { TailorEnv } from "./env"; +export type { TailorPrincipal } from "./user"; diff --git a/packages/sdk/src/types/user.ts b/packages/sdk/src/types/user.ts index ac2ccfd35..72df0db93 100644 --- a/packages/sdk/src/types/user.ts +++ b/packages/sdk/src/types/user.ts @@ -14,12 +14,7 @@ export type InferredAttributeList = AttributeList["__tuple"] extends [] ? string[] : AttributeList["__tuple"]; -/** - * Represents the principal associated with a function execution. - * - * Unifies the caller, actor, and invoker shapes into a single type: a stable - * `id`, a `type` of `"user"` or `"machine_user"`, and non-null attributes. - */ +/** Represents a user or machine user principal in the Tailor Platform. */ export type TailorPrincipal = { /** The ID of the principal. */ id: string; @@ -32,74 +27,3 @@ export type TailorPrincipal = { /** A list of the principal's attribute IDs. */ attributeList: InferredAttributeList; }; - -/** - * Represents a user in the Tailor platform. - * - * @deprecated Use {@link TailorPrincipal} instead. `TailorUser` is unified into - * `TailorPrincipal` in the next major version. - */ -export type TailorUser = { - /** - * The ID of the user. - * For unauthenticated users, this will be a nil UUID. - */ - id: string; - /** - * The type of the user. - * For unauthenticated users, this will be an empty string. - */ - type: "user" | "machine_user" | ""; - /** The ID of the workspace the user belongs to. */ - workspaceId: string; - /** - * A map of the user's attributes. - * For unauthenticated users, this will be null. - */ - attributes: InferredAttributeMap | null; - /** - * A list of the user's attributes. - * For unauthenticated users, this will be an empty array. - */ - attributeList: InferredAttributeList; -}; - -/** - * Represents an unauthenticated user in the Tailor platform. - * - * @deprecated Represent an absent principal as `null` instead. This constant is - * removed in the next major version. - */ -export const unauthenticatedTailorUser: TailorUser = { - id: "00000000-0000-0000-0000-000000000000", - type: "", - workspaceId: "00000000-0000-0000-0000-000000000000", - attributes: null, - attributeList: [], -}; - -/** - * The invoker of the current function execution. - * - * Reflects `authInvoker` delegation: when `authInvoker` is specified, this is - * the machine user; otherwise it is the calling user. - * Distinct from resolver's `user` (the authenticated caller) and executor's - * `actor` (the subject of the event). - * - * `null` for anonymous requests. - * - * @deprecated Use {@link TailorPrincipal} (as `TailorPrincipal | null`) instead. - * `TailorInvoker` is unified into `TailorPrincipal` in the next major version. - */ -export type TailorInvoker = { - /** The ID of the invoker (user ID or machine user ID). */ - id: string; - /** The type of the invoker. */ - type: "user" | "machine_user"; - /** The ID of the workspace the invoker belongs to. */ - workspaceId: string; - /** A map of the invoker's attributes. */ - attributes: InferredAttributeMap; - /** A list of the invoker's attribute IDs. */ - attributeList: InferredAttributeList; -} | null; diff --git a/packages/sdk/src/types/validation.ts b/packages/sdk/src/types/validation.ts index 521e26520..7f3ad54c4 100644 --- a/packages/sdk/src/types/validation.ts +++ b/packages/sdk/src/types/validation.ts @@ -1,11 +1,15 @@ import type { output, InferFieldsOutput } from "./helpers"; -import type { TailorUser } from "./user"; +import type { TailorPrincipal } from "./user"; import type { NonEmptyObject } from "type-fest"; /** * Validation function type */ -export type ValidateFn = (args: { value: O; data: D; user: TailorUser }) => boolean; +export type ValidateFn = (args: { + value: O; + data: D; + invoker: TailorPrincipal | null; +}) => boolean; /** * Validation configuration with custom error message diff --git a/packages/sdk/src/utils/test/index.test.ts b/packages/sdk/src/utils/test/index.test.ts index a9a0ee38b..c9e777636 100644 --- a/packages/sdk/src/utils/test/index.test.ts +++ b/packages/sdk/src/utils/test/index.test.ts @@ -184,12 +184,12 @@ describe("createTailorDBHook", () => { }); describe("create hook on a top-level field", () => { - test("invokes the create hook with value, full data, and the unauthenticated user", () => { - const seen: { value: unknown; data: unknown; userId: string }[] = []; + test("invokes the create hook with value, full data, and a null invoker", () => { + const seen: { value: unknown; data: unknown; invoker: unknown }[] = []; const type = db.type("Order", { total: db.float(), tax: db.float() }).hooks({ tax: { - create: ({ value, data, user }) => { - seen.push({ value, data, userId: user.id }); + create: ({ value, data, invoker }) => { + seen.push({ value, data, invoker }); return (data as { total: number }).total * 0.1; }, }, @@ -200,7 +200,7 @@ describe("createTailorDBHook", () => { { value: undefined, data: { total: 100, tax: undefined }, - userId: "00000000-0000-0000-0000-000000000000", + invoker: null, }, ]); }); diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index 353d0631a..cbcb239d5 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -1,4 +1,4 @@ -import type { output, TailorUser } from "@/configure"; +import type { output } from "@/configure"; import type { TailorDBType } from "@/configure/services/tailordb/schema"; import type { TailorField } from "@/configure/types/type"; import type { StandardSchemaV1 } from "@standard-schema/spec"; @@ -13,20 +13,6 @@ export { createImportMain, } from "./mock"; -/** - * Represents an unauthenticated user in the Tailor platform. - * - * @deprecated Represent an absent principal as `null` instead. This constant is - * removed in the next major version. - */ -export const unauthenticatedTailorUser = { - id: "00000000-0000-0000-0000-000000000000", - type: "", - workspaceId: "00000000-0000-0000-0000-000000000000", - attributes: null, - attributeList: [], -} as const satisfies TailorUser; - /** * Creates a hook function that processes TailorDB type fields * - Uses existing id from data if provided, otherwise generates UUID for id fields @@ -66,7 +52,7 @@ export function createTailorDBHook>(type: T) { hooked[key] = field.metadata.hooks.create({ value: (data as Record)[key], data: data, - user: unauthenticatedTailorUser, + invoker: null, }); if (hooked[key] instanceof Date) { hooked[key] = hooked[key].toISOString(); @@ -103,7 +89,7 @@ export function createStandardSchema>( const result = schemaType.parse({ value: hooked, data: hooked, - user: unauthenticatedTailorUser, + invoker: null, }); if (result.issues) { return result; diff --git a/packages/sdk/src/utils/test/mock.ts b/packages/sdk/src/utils/test/mock.ts index 210fa3cf7..09c3d2506 100644 --- a/packages/sdk/src/utils/test/mock.ts +++ b/packages/sdk/src/utils/test/mock.ts @@ -1,7 +1,7 @@ import * as path from "node:path"; import { pathToFileURL } from "node:url"; import type { ContextInvoker } from "@/runtime/context"; -import type { TailorInvoker } from "@/types/user"; +import type { TailorPrincipal } from "@/types/user"; type MainFunction = (args: Record) => unknown | Promise; type QueryResolver = (query: string, params: unknown[]) => unknown[]; @@ -134,9 +134,9 @@ export function setupWorkflowMock(handler: JobHandler): { * Sets up a mock for `globalThis.tailor.context.getInvoker` used in bundled * resolver/executor/workflow tests. * @deprecated With the `tailor-runtime` environment from `@tailor-platform/sdk/vitest`, drive the invoker via `vi.spyOn(globalThis.tailor.context, "getInvoker").mockReturnValue(...)` for bundled tests, or pass `invoker` directly to `.body()` when unit-testing resolvers/executors/workflow jobs against the TypeScript source. - * @param invoker - The `TailorInvoker` value to return, or `null` for anonymous. + * @param invoker - The `TailorPrincipal` value to return, or `null` for anonymous. */ -export function setupInvokerMock(invoker: TailorInvoker): void { +export function setupInvokerMock(invoker: TailorPrincipal | null): void { const raw: ContextInvoker | null = invoker ? { id: invoker.id, From 238d203835be24923e1b21eb09697baaf51626c0 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 16:30:54 +0900 Subject: [PATCH 061/618] fix(codemod): narrow principal callback migration --- .../v2/principal-unify/scripts/transform.ts | 178 +++++++++++++++++- .../tests/tailordb-callbacks/expected.ts | 22 ++- .../tests/tailordb-callbacks/input.ts | 22 ++- 3 files changed, 210 insertions(+), 12 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 86d8d3e61..5c1d9713e 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -517,6 +517,16 @@ function findMemberCallName(call: SgNode): string | null { return property?.text() ?? null; } +function findMemberCallObject(call: SgNode): SgNode | null { + const fn = call.field("function"); + if (!fn || fn.kind() !== "member_expression") return null; + return fn.field("object"); +} + +function isFunctionNode(node: SgNode): boolean { + return node.kind() === "arrow_function" || node.kind() === "function_expression"; +} + function getFirstFunctionParamPattern(fn: SgNode): SgNode | null { const params = fn.field("parameters") ?? @@ -543,7 +553,53 @@ function getFirstFunctionParamPattern(fn: SgNode): SgNode | null { function transformPrincipalCallbackParam(fn: SgNode, edits: Edit[]): void { const pattern = getFirstFunctionParamPattern(fn); const body = fn.field("body"); - if (!pattern || pattern.kind() !== "object_pattern" || !body) return; + if (!pattern || !body) return; + + if (pattern.kind() === "identifier") { + const ctxName = pattern.text(); + const ctxShadowRanges = collectCtxShadowRanges(body, ctxName, fn); + const propertyAccesses = body.findAll({ + rule: { kind: "property_identifier", regex: "^user$" }, + }); + for (const propId of propertyAccesses) { + const parent = propId.parent(); + if (!parent || parent.kind() !== "member_expression") continue; + const obj = parent.field("object"); + if (!(obj && obj.kind() === "identifier" && obj.text() === ctxName)) continue; + const pos = obj.range().start.index; + if (isInsideAnyRange(pos, ctxShadowRanges)) continue; + edits.push(propId.replace("invoker")); + } + + const ctxDestructures = body.findAll({ + rule: { + kind: "variable_declarator", + has: { + field: "value", + kind: "identifier", + regex: `^${escapeRegex(ctxName)}$`, + }, + }, + }); + for (const decl of ctxDestructures) { + const pos = decl.range().start.index; + if (isInsideAnyRange(pos, ctxShadowRanges)) continue; + const pat = decl.field("name"); + if (!pat || pat.kind() !== "object_pattern") continue; + for (const child of pat.children()) { + const kind = child.kind(); + if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { + edits.push(child.replace("invoker: user")); + } else if (kind === "pair_pattern") { + const key = child.field("key"); + if (key?.text() === "user") edits.push(key.replace("invoker")); + } + } + } + return; + } + + if (pattern.kind() !== "object_pattern") return; let renamesBinding = false; for (const child of pattern.children()) { @@ -602,23 +658,120 @@ function transformPrincipalCallbackParam(fn: SgNode, edits: Edit[]): void { } } -function transformPrincipalCallbacksInCall(call: SgNode, edits: Edit[]): void { +function isSdkFieldExpression( + node: SgNode, + sdkFieldRootNames: Set, + sdkFieldLocalNames: Set, +): boolean { + if (node.kind() === "identifier") { + return sdkFieldRootNames.has(node.text()) || sdkFieldLocalNames.has(node.text()); + } + if (node.kind() === "member_expression") { + const object = node.field("object"); + return object ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalNames) : false; + } + if (node.kind() === "call_expression") { + const object = findMemberCallObject(node); + return object ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalNames) : false; + } + return false; +} + +function collectSdkFieldLocalNames(root: SgNode, sdkFieldRootNames: Set): Set { + const localNames = new Set(); + let changed = true; + while (changed) { + changed = false; + const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarators) { + const name = decl.field("name"); + const value = decl.field("value"); + if (!name || name.kind() !== "identifier" || !value) continue; + if (localNames.has(name.text())) continue; + if (!isSdkFieldExpression(value, sdkFieldRootNames, localNames)) continue; + localNames.add(name.text()); + changed = true; + } + } + return localNames; +} + +function isSdkFieldMemberCall( + call: SgNode, + sdkFieldRootNames: Set, + sdkFieldLocalNames: Set, +): boolean { + const object = findMemberCallObject(call); + return object ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalNames) : false; +} + +function transformHookCallbackObject(node: SgNode, edits: Edit[]): void { + if (node.kind() !== "object") return; + const pairs = node.children().filter((child: SgNode) => child.kind() === "pair"); + for (const pair of pairs) { + const key = pair.field("key")?.text(); + const value = pair.field("value"); + if (!value) continue; + if ((key === "create" || key === "update") && isFunctionNode(value)) { + transformPrincipalCallbackParam(value, edits); + } else if (value.kind() === "object") { + transformHookCallbackObject(value, edits); + } + } +} + +function transformValidateCallbackNode(node: SgNode, edits: Edit[]): void { + if (isFunctionNode(node)) { + transformPrincipalCallbackParam(node, edits); + return; + } + + if (node.kind() === "array") { + const callback = node.children().find(isFunctionNode); + if (callback) transformPrincipalCallbackParam(callback, edits); + return; + } + + if (node.kind() !== "object") return; + const pairs = node.children().filter((child: SgNode) => child.kind() === "pair"); + for (const pair of pairs) { + const value = pair.field("value"); + if (value) transformValidateCallbackNode(value, edits); + } +} + +function transformPrincipalCallbacksInCall( + call: SgNode, + edits: Edit[], + sdkFieldRootNames: Set, + sdkFieldLocalNames: Set, +): void { const memberName = findMemberCallName(call); if (memberName !== "hooks" && memberName !== "validate") return; + if (!isSdkFieldMemberCall(call, sdkFieldRootNames, sdkFieldLocalNames)) return; const args = call.field("arguments"); if (!args) return; - for (const kind of ["arrow_function", "function_expression"]) { - const callbacks = args.findAll({ rule: { kind } }); - for (const callback of callbacks) { - transformPrincipalCallbackParam(callback, edits); - } + if (memberName === "hooks") { + const objArg = args.children().find((c: SgNode) => c.kind() === "object"); + if (objArg) transformHookCallbackObject(objArg, edits); + return; + } + + for (const arg of args.children()) { + transformValidateCallbackNode(arg, edits); } } -function transformParseArgsObject(call: SgNode, edits: Edit[]): void { +function transformParseArgsObject( + call: SgNode, + edits: Edit[], + sdkFieldRootNames: Set, + sdkFieldLocalNames: Set, +): void { const memberName = findMemberCallName(call); if (memberName !== "parse") return; + if (!isSdkFieldMemberCall(call, sdkFieldRootNames, sdkFieldLocalNames)) return; const args = call.field("arguments"); const objArg = args?.children().find((c: SgNode) => c.kind() === "object"); @@ -729,11 +882,15 @@ export default function transform(source: string): string | null { // and unrelated local helpers named `createResolver` (when the SDK import // does not actually bring `createResolver` in) are not. const createResolverLocalNames = new Set(); + const sdkFieldRootNames = new Set(); for (const importStmt of sdkImports) { for (const { importedName, localName } of iterateImportSpecs(importStmt)) { if (importedName === "createResolver") { createResolverLocalNames.add(localName); } + if (importedName === "t" || importedName === "db") { + sdkFieldRootNames.add(localName); + } } } for (const localName of createResolverLocalNames) { @@ -758,10 +915,11 @@ export default function transform(source: string): string | null { } } + const sdkFieldLocalNames = collectSdkFieldLocalNames(tree, sdkFieldRootNames); const memberCalls = tree.findAll({ rule: { kind: "call_expression" } }); for (const call of memberCalls) { - transformPrincipalCallbacksInCall(call, edits); - transformParseArgsObject(call, edits); + transformPrincipalCallbacksInCall(call, edits, sdkFieldRootNames, sdkFieldLocalNames); + transformParseArgsObject(call, edits, sdkFieldRootNames, sdkFieldLocalNames); } if (edits.length === 0) return null; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index c4ee4ced5..82cd94ab2 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -4,9 +4,13 @@ const role = db .string() .hooks({ create: ({ value, invoker }) => (invoker?.attributes.role === "ADMIN" ? value : "user"), + update: (ctx) => ctx.invoker?.id ?? "anonymous", }) .validate([({ invoker }) => invoker?.type === "machine_user", "Machine user required"]); +const reviewer = t.string(); +const zodLike = { parse: (arg: unknown) => arg }; + export const user = db .type("User", { role, @@ -14,8 +18,14 @@ export const user = db }) .hooks({ note: { - create: ({ invoker: currentUser }) => currentUser?.id ?? "anonymous", + create: ({ invoker: currentUser }) => { + const audit = [{ user: { id: "data-user" } }].map(({ user }) => user.id); + return currentUser?.id ?? audit[0] ?? "anonymous"; + }, }, + }) + .validate({ + note: (ctx) => ctx.invoker?.type !== "machine_user", }); export const parsed = t.string().parse({ @@ -23,3 +33,13 @@ export const parsed = t.string().parse({ data: {}, invoker: null, }); + +export const parsedLocal = reviewer.parse({ + value: "hello", + data: {}, + invoker: null, +}); + +export const parsedOther = zodLike.parse({ + user: null, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index 6f2de3741..981c353a2 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -5,9 +5,13 @@ const role = db .string() .hooks({ create: ({ value, user }) => (user?.attributes.role === "ADMIN" ? value : "user"), + update: (ctx) => ctx.user?.id ?? "anonymous", }) .validate([({ user }) => user?.type === "machine_user", "Machine user required"]); +const reviewer = t.string(); +const zodLike = { parse: (arg: unknown) => arg }; + export const user = db .type("User", { role, @@ -15,8 +19,14 @@ export const user = db }) .hooks({ note: { - create: ({ user: currentUser }) => currentUser?.id ?? "anonymous", + create: ({ user: currentUser }) => { + const audit = [{ user: { id: "data-user" } }].map(({ user }) => user.id); + return currentUser?.id ?? audit[0] ?? "anonymous"; + }, }, + }) + .validate({ + note: (ctx) => ctx.user?.type !== "machine_user", }); export const parsed = t.string().parse({ @@ -24,3 +34,13 @@ export const parsed = t.string().parse({ data: {}, user: unauthenticatedTailorUser, }); + +export const parsedLocal = reviewer.parse({ + value: "hello", + data: {}, + user: unauthenticatedTailorUser, +}); + +export const parsedOther = zodLike.parse({ + user: unauthenticatedTailorUser, +}); From 725be7ce57134d3990db27072f948b1177fd7523 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 16 Jun 2026 16:36:00 +0900 Subject: [PATCH 062/618] refactor(cli): unify machine-user registry into a single sdk augmentation The `@tailor-platform/sdk/cli` entry declared its own `MachineUserNameRegistry`, forcing the generated `tailor.d.ts` to augment two modules with identical contents. Reference `MachineUserName` from the public `@tailor-platform/sdk` entry (kept external in the cli dts bundle so it is not inlined) so a single `declare module "@tailor-platform/sdk"` augmentation narrows both the resolver and workflow-start `authInvoker`. Drop the program-global `declare module` from the workflow-start type unit test (it leaked the augmentation across the whole tsc program once the registry was shared) and verify `MachineUserName` narrowing for both entries against a real generated `tailor.d.ts` in example/ instead. --- example/tests/auth-invoker.types.ts | 17 ++++++++++++++++ .../commands/workflow/start.api-types.test.ts | 20 +++++-------------- .../sdk/src/cli/commands/workflow/start.ts | 20 ++++--------------- packages/sdk/src/cli/lib.ts | 3 +-- .../sdk/src/cli/shared/type-generator.test.ts | 4 ++-- packages/sdk/src/cli/shared/type-generator.ts | 4 ---- packages/sdk/tsconfig.json | 3 ++- packages/sdk/tsdown.config.ts | 8 ++++++-- 8 files changed, 37 insertions(+), 42 deletions(-) create mode 100644 example/tests/auth-invoker.types.ts diff --git a/example/tests/auth-invoker.types.ts b/example/tests/auth-invoker.types.ts new file mode 100644 index 000000000..b8f2f088d --- /dev/null +++ b/example/tests/auth-invoker.types.ts @@ -0,0 +1,17 @@ +// Type-level checks against the generated `tailor.d.ts`. Once `tailor-sdk +// generate` augments `MachineUserNameRegistry`, `MachineUserName` narrows to the +// registered machine user union for both SDK entries — `@tailor-platform/sdk` +// (resolver `authInvoker`) and `@tailor-platform/sdk/cli` (workflow-start +// `authInvoker`), which share the single `@tailor-platform/sdk` augmentation. +import type { MachineUserName as SdkMachineUserName } from "@tailor-platform/sdk"; +import type { MachineUserName as CliMachineUserName } from "@tailor-platform/sdk/cli"; + +const sdkInvoker: SdkMachineUserName = "manager-machine-user"; +const cliInvoker: CliMachineUserName = "manager-machine-user"; + +// @ts-expect-error - unknown machine user names are rejected once tailor.d.ts is generated +const unknownSdkInvoker: SdkMachineUserName = "unknown-machine-user"; +// @ts-expect-error - unknown machine user names are rejected once tailor.d.ts is generated +const unknownCliInvoker: CliMachineUserName = "unknown-machine-user"; + +export const authInvokerTypeChecks = [sdkInvoker, cliInvoker, unknownSdkInvoker, unknownCliInvoker]; diff --git a/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts b/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts index ec8b11cb8..b8e87c0e8 100644 --- a/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts +++ b/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts @@ -3,14 +3,11 @@ import { describe, test, expectTypeOf } from "vitest"; import { createWorkflow, createWorkflowJob } from "@/configure/services/workflow"; import { type StartWorkflowOptions, type StartWorkflowTypedOptions } from "./start"; -declare module "./start" { - interface MachineUserNameRegistry { - admin: true; - "typed-user": true; - worker: true; - } -} - +// `authInvoker` is typed as `MachineUserName`, which falls back to `string` until +// `tailor.d.ts` augments `MachineUserNameRegistry`. Narrowing to the registered +// machine user union (and rejection of unknown names) is covered against a real +// generated `tailor.d.ts` in `example/`; here we only assert arg inference and +// that machine user names are accepted as strings. const calculationJob = createWorkflowJob({ name: "calculation", body: (input: { a: number; b: number }) => ({ sum: input.a + input.b }), @@ -136,13 +133,6 @@ describe("startWorkflow API types", () => { authInvoker: "worker", arg: { a: 1, b: 2 }, }); - - acceptsCalculationWorkflowOptions({ - workflow: calculationWorkflow, - // @ts-expect-error - invalid machine user name - authInvoker: "invalid-machine-user", - arg: { a: 1, b: 2 }, - }); }); test("keeps default generic usable when StartWorkflowTypedOptions generic is omitted", () => { diff --git a/packages/sdk/src/cli/commands/workflow/start.ts b/packages/sdk/src/cli/commands/workflow/start.ts index 2591920bd..d24bafa69 100644 --- a/packages/sdk/src/cli/commands/workflow/start.ts +++ b/packages/sdk/src/cli/commands/workflow/start.ts @@ -18,25 +18,13 @@ import { nameArgs, waitArgs } from "./args"; import { getWorkflowExecution, printExecutionWithLogs } from "./executions"; import { resolveWorkflow } from "./get"; import { type WorkflowExecutionInfo, toWorkflowExecutionInfo } from "./transform"; +// Import from the public entry (not `@/types/auth`) so the `./cli` d.ts references +// `@tailor-platform/sdk` externally instead of inlining the registry — a single +// generated `declare module "@tailor-platform/sdk"` then narrows both entries. +import type { MachineUserName } from "@tailor-platform/sdk"; import type { WorkflowExecution } from "@tailor-proto/tailor/v1/workflow_resource_pb"; import type { Jsonifiable } from "type-fest"; -// Interface for module augmentation -// Users can extend via: declare module "@tailor-platform/sdk/cli" { interface MachineUserNameRegistry { ... } } -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface MachineUserNameRegistry {} - -/** - * Machine user name for CLI helper APIs. - * - * When `tailor.d.ts` is generated (via `tailor-sdk deploy`/`generate`), this is narrowed - * to the union of defined machine user names. When no machine users are registered yet, - * falls back to `string` to avoid blocking editing before the first generate run. - */ -export type MachineUserName = keyof MachineUserNameRegistry extends never - ? string - : keyof MachineUserNameRegistry & string; - type WorkflowLike = { name: string; mainJob: { diff --git a/packages/sdk/src/cli/lib.ts b/packages/sdk/src/cli/lib.ts index 141263bbb..e1e8db450 100644 --- a/packages/sdk/src/cli/lib.ts +++ b/packages/sdk/src/cli/lib.ts @@ -91,10 +91,9 @@ export { type GetWorkflowOptions, type GetWorkflowTypedOptions, } from "./commands/workflow/get"; +export type { MachineUserName } from "@tailor-platform/sdk"; export { startWorkflow, - type MachineUserName, - type MachineUserNameRegistry, type StartWorkflowOptions, type StartWorkflowTypedOptions, type StartWorkflowResultWithWait, diff --git a/packages/sdk/src/cli/shared/type-generator.test.ts b/packages/sdk/src/cli/shared/type-generator.test.ts index 5e990d199..79a7a811e 100644 --- a/packages/sdk/src/cli/shared/type-generator.test.ts +++ b/packages/sdk/src/cli/shared/type-generator.test.ts @@ -86,7 +86,7 @@ describe("generateTypeDefinition", () => { const result = generateTypeDefinition(undefined, undefined); expect(result).toContain("interface MachineUserNameRegistry {}"); - expect(result).toContain('declare module "@tailor-platform/sdk/cli"'); + expect(result).not.toContain('declare module "@tailor-platform/sdk/cli"'); }); test("should generate MachineUserNameRegistry with machine user names", () => { @@ -96,7 +96,7 @@ describe("generateTypeDefinition", () => { ]); expect(result).toContain("interface MachineUserNameRegistry"); - expect(result).toContain('declare module "@tailor-platform/sdk/cli"'); + expect(result).not.toContain('declare module "@tailor-platform/sdk/cli"'); // Names with hyphens are quoted expect(result).toContain('"manager-machine-user": true;'); // Valid identifiers are emitted unquoted (matches formatter output) diff --git a/packages/sdk/src/cli/shared/type-generator.ts b/packages/sdk/src/cli/shared/type-generator.ts index d093e8291..5806f8fa2 100644 --- a/packages/sdk/src/cli/shared/type-generator.ts +++ b/packages/sdk/src/cli/shared/type-generator.ts @@ -136,10 +136,6 @@ declare module "@tailor-platform/sdk" { interface IdpNameRegistry ${idpNameBody} } -declare module "@tailor-platform/sdk/cli" { - interface MachineUserNameRegistry ${machineUserBody} -} - export {}; `; diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index f0715f03d..f1db34816 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -9,7 +9,8 @@ "tsBuildInfoFile": "./.tsbuildinfo", "paths": { "@/*": ["./src/*"], - "@tailor-proto/*": ["../tailor-proto/src/*"] + "@tailor-proto/*": ["../tailor-proto/src/*"], + "@tailor-platform/sdk": ["./src/configure/index.ts"] } }, "include": [ diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index e713eab8c..bc3c8b57c 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -102,8 +102,12 @@ export default defineConfig({ banner: { dts: '/// ', }, - // peer dependencies: prevent bundling, resolve at runtime - deps: { neverBundle: ["vite", "vitest"] }, + // peer dependencies: prevent bundling, resolve at runtime. + // `@tailor-platform/sdk` (self-name) is kept external so the `./cli` entry's + // types reference `MachineUserName`/`MachineUserNameRegistry` from the main + // entry instead of inlining them, letting a single + // `declare module "@tailor-platform/sdk"` augmentation narrow both entries. + deps: { neverBundle: ["vite", "vitest", /^@tailor-platform\/sdk$/] }, sourcemap: true, plugins, onSuccess: (config) => { From b73c14f87d897aaa54b8526d4b9f3a8f6a90df7d Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 16:43:14 +0900 Subject: [PATCH 063/618] fix(codemod): cover principal validator callbacks --- .../codemods/v2/principal-unify/scripts/transform.ts | 12 ++++++++++-- .../tests/tailordb-callbacks/expected.ts | 7 +++++-- .../tests/tailordb-callbacks/input.ts | 7 +++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 5c1d9713e..4343cd9f0 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -533,6 +533,9 @@ function getFirstFunctionParamPattern(fn: SgNode): SgNode | null { fn.field("parameter") ?? fn.children().find((c: SgNode) => c.kind() === "formal_parameters"); if (!params) return null; + if (params.kind() === "object_pattern" || params.kind() === "identifier") { + return params; + } const firstParam = params .children() @@ -727,8 +730,13 @@ function transformValidateCallbackNode(node: SgNode, edits: Edit[]): void { } if (node.kind() === "array") { - const callback = node.children().find(isFunctionNode); - if (callback) transformPrincipalCallbackParam(callback, edits); + for (const child of node.children()) { + if (isFunctionNode(child)) { + transformPrincipalCallbackParam(child, edits); + } else if (child.kind() === "array") { + transformValidateCallbackNode(child, edits); + } + } return; } diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index 82cd94ab2..c6d9ceb56 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -4,9 +4,12 @@ const role = db .string() .hooks({ create: ({ value, invoker }) => (invoker?.attributes.role === "ADMIN" ? value : "user"), - update: (ctx) => ctx.invoker?.id ?? "anonymous", + update: ctx => ctx.invoker?.id ?? "anonymous", }) - .validate([({ invoker }) => invoker?.type === "machine_user", "Machine user required"]); + .validate([ + [({ invoker }) => invoker?.type === "machine_user", "Machine user required"], + ctx => ctx.invoker?.id !== "", + ]); const reviewer = t.string(); const zodLike = { parse: (arg: unknown) => arg }; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index 981c353a2..556286ff0 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -5,9 +5,12 @@ const role = db .string() .hooks({ create: ({ value, user }) => (user?.attributes.role === "ADMIN" ? value : "user"), - update: (ctx) => ctx.user?.id ?? "anonymous", + update: ctx => ctx.user?.id ?? "anonymous", }) - .validate([({ user }) => user?.type === "machine_user", "Machine user required"]); + .validate([ + [({ user }) => user?.type === "machine_user", "Machine user required"], + ctx => ctx.user?.id !== "", + ]); const reviewer = t.string(); const zodLike = { parse: (arg: unknown) => arg }; From be753f17112216a0e685385438ed4b537fb90fec Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 16 Jun 2026 16:48:34 +0900 Subject: [PATCH 064/618] chore(create-sdk): regenerate template tailor.d.ts after registry unification The generator no longer emits a separate `declare module "@tailor-platform/sdk/cli"` block, so regenerate every template's `tailor.d.ts` to drop the now-stale block. --- packages/create-sdk/templates/executor/tailor.d.ts | 6 ------ packages/create-sdk/templates/generators/tailor.d.ts | 6 ------ packages/create-sdk/templates/hello-world/tailor.d.ts | 4 ---- .../create-sdk/templates/inventory-management/tailor.d.ts | 7 ------- packages/create-sdk/templates/resolver/tailor.d.ts | 6 ------ packages/create-sdk/templates/static-web-site/tailor.d.ts | 6 ------ packages/create-sdk/templates/tailordb/tailor.d.ts | 7 ------- packages/create-sdk/templates/workflow/tailor.d.ts | 6 ------ 8 files changed, 48 deletions(-) diff --git a/packages/create-sdk/templates/executor/tailor.d.ts b/packages/create-sdk/templates/executor/tailor.d.ts index 8052dbe2d..9a473cac0 100644 --- a/packages/create-sdk/templates/executor/tailor.d.ts +++ b/packages/create-sdk/templates/executor/tailor.d.ts @@ -18,10 +18,4 @@ declare module "@tailor-platform/sdk" { } } -declare module "@tailor-platform/sdk/cli" { - interface MachineUserNameRegistry { - admin: true; - } -} - export {}; diff --git a/packages/create-sdk/templates/generators/tailor.d.ts b/packages/create-sdk/templates/generators/tailor.d.ts index 8052dbe2d..9a473cac0 100644 --- a/packages/create-sdk/templates/generators/tailor.d.ts +++ b/packages/create-sdk/templates/generators/tailor.d.ts @@ -18,10 +18,4 @@ declare module "@tailor-platform/sdk" { } } -declare module "@tailor-platform/sdk/cli" { - interface MachineUserNameRegistry { - admin: true; - } -} - export {}; diff --git a/packages/create-sdk/templates/hello-world/tailor.d.ts b/packages/create-sdk/templates/hello-world/tailor.d.ts index fa0a41339..f618bd50c 100644 --- a/packages/create-sdk/templates/hello-world/tailor.d.ts +++ b/packages/create-sdk/templates/hello-world/tailor.d.ts @@ -12,8 +12,4 @@ declare module "@tailor-platform/sdk" { interface IdpNameRegistry {} } -declare module "@tailor-platform/sdk/cli" { - interface MachineUserNameRegistry {} -} - export {}; diff --git a/packages/create-sdk/templates/inventory-management/tailor.d.ts b/packages/create-sdk/templates/inventory-management/tailor.d.ts index 676f2ac82..ee802f4a5 100644 --- a/packages/create-sdk/templates/inventory-management/tailor.d.ts +++ b/packages/create-sdk/templates/inventory-management/tailor.d.ts @@ -17,11 +17,4 @@ declare module "@tailor-platform/sdk" { interface IdpNameRegistry {} } -declare module "@tailor-platform/sdk/cli" { - interface MachineUserNameRegistry { - manager: true; - staff: true; - } -} - export {}; diff --git a/packages/create-sdk/templates/resolver/tailor.d.ts b/packages/create-sdk/templates/resolver/tailor.d.ts index d4c7ca96a..67384d1c5 100644 --- a/packages/create-sdk/templates/resolver/tailor.d.ts +++ b/packages/create-sdk/templates/resolver/tailor.d.ts @@ -19,10 +19,4 @@ declare module "@tailor-platform/sdk" { interface IdpNameRegistry {} } -declare module "@tailor-platform/sdk/cli" { - interface MachineUserNameRegistry { - admin: true; - } -} - export {}; diff --git a/packages/create-sdk/templates/static-web-site/tailor.d.ts b/packages/create-sdk/templates/static-web-site/tailor.d.ts index 5470e9fb4..080ae97b4 100644 --- a/packages/create-sdk/templates/static-web-site/tailor.d.ts +++ b/packages/create-sdk/templates/static-web-site/tailor.d.ts @@ -18,10 +18,4 @@ declare module "@tailor-platform/sdk" { } } -declare module "@tailor-platform/sdk/cli" { - interface MachineUserNameRegistry { - admin: true; - } -} - export {}; diff --git a/packages/create-sdk/templates/tailordb/tailor.d.ts b/packages/create-sdk/templates/tailordb/tailor.d.ts index ac0a43afc..fb3e4de56 100644 --- a/packages/create-sdk/templates/tailordb/tailor.d.ts +++ b/packages/create-sdk/templates/tailordb/tailor.d.ts @@ -17,11 +17,4 @@ declare module "@tailor-platform/sdk" { interface IdpNameRegistry {} } -declare module "@tailor-platform/sdk/cli" { - interface MachineUserNameRegistry { - admin: true; - viewer: true; - } -} - export {}; diff --git a/packages/create-sdk/templates/workflow/tailor.d.ts b/packages/create-sdk/templates/workflow/tailor.d.ts index 404b66593..9016c307d 100644 --- a/packages/create-sdk/templates/workflow/tailor.d.ts +++ b/packages/create-sdk/templates/workflow/tailor.d.ts @@ -16,10 +16,4 @@ declare module "@tailor-platform/sdk" { interface IdpNameRegistry {} } -declare module "@tailor-platform/sdk/cli" { - interface MachineUserNameRegistry { - admin: true; - } -} - export {}; From 0dd710ef0057eac3646fddb85b38bd44b8acad3a Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 17:03:12 +0900 Subject: [PATCH 065/618] fix(codemod): respect shadowed principal fields --- .../v2/principal-unify/scripts/transform.ts | 109 +++++++++++++++--- .../tests/tailordb-callbacks/expected.ts | 4 + .../tests/tailordb-callbacks/input.ts | 4 + 3 files changed, 98 insertions(+), 19 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 4343cd9f0..a8ea97ebb 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -661,27 +661,90 @@ function transformPrincipalCallbackParam(fn: SgNode, edits: Edit[]): void { } } +interface SdkFieldLocalBinding { + name: string; + bindingStart: number; + scope: [number, number]; +} + +function rangeContains([start, end]: [number, number], pos: number): boolean { + return pos >= start && pos < end; +} + +function isShadowedLocalReference( + root: SgNode, + name: string, + pos: number, + bindingStart: number, +): boolean { + const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarators) { + const nameNode = decl.field("name"); + if (!nameNode || !patternBindsName(nameNode, name)) continue; + if (nameNode.range().start.index === bindingStart) continue; + const scope = enclosingScopeRange(decl); + if (scope && rangeContains(scope, pos)) return true; + } + + for (const kind of NESTED_FN_KINDS) { + const fns = root.findAll({ rule: { kind } }); + for (const fn of fns) { + if (!functionRebindsName(fn, name)) continue; + const range = fn.range(); + if (pos >= range.start.index && pos < range.end.index) return true; + } + } + + return false; +} + +function resolvesToSdkFieldLocal( + node: SgNode, + bindings: SdkFieldLocalBinding[], + root: SgNode, +): boolean { + if (node.kind() !== "identifier") return false; + const pos = node.range().start.index; + return bindings.some( + (binding) => + binding.name === node.text() && + rangeContains(binding.scope, pos) && + !isShadowedLocalReference(root, binding.name, pos, binding.bindingStart), + ); +} + function isSdkFieldExpression( node: SgNode, sdkFieldRootNames: Set, - sdkFieldLocalNames: Set, + sdkFieldLocalBindings: SdkFieldLocalBinding[], + root: SgNode, ): boolean { if (node.kind() === "identifier") { - return sdkFieldRootNames.has(node.text()) || sdkFieldLocalNames.has(node.text()); + return ( + sdkFieldRootNames.has(node.text()) || + resolvesToSdkFieldLocal(node, sdkFieldLocalBindings, root) + ); } if (node.kind() === "member_expression") { const object = node.field("object"); - return object ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalNames) : false; + return object + ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalBindings, root) + : false; } if (node.kind() === "call_expression") { const object = findMemberCallObject(node); - return object ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalNames) : false; + return object + ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalBindings, root) + : false; } return false; } -function collectSdkFieldLocalNames(root: SgNode, sdkFieldRootNames: Set): Set { - const localNames = new Set(); +function collectSdkFieldLocalBindings( + root: SgNode, + sdkFieldRootNames: Set, +): SdkFieldLocalBinding[] { + const bindings: SdkFieldLocalBinding[] = []; let changed = true; while (changed) { changed = false; @@ -690,22 +753,28 @@ function collectSdkFieldLocalNames(root: SgNode, sdkFieldRootNames: Set) const name = decl.field("name"); const value = decl.field("value"); if (!name || name.kind() !== "identifier" || !value) continue; - if (localNames.has(name.text())) continue; - if (!isSdkFieldExpression(value, sdkFieldRootNames, localNames)) continue; - localNames.add(name.text()); + const bindingStart = name.range().start.index; + if (bindings.some((binding) => binding.bindingStart === bindingStart)) continue; + if (!isSdkFieldExpression(value, sdkFieldRootNames, bindings, root)) continue; + const rootRange = root.range(); + const scope = enclosingScopeRange(decl) ?? [rootRange.start.index, rootRange.end.index]; + bindings.push({ name: name.text(), bindingStart, scope }); changed = true; } } - return localNames; + return bindings; } function isSdkFieldMemberCall( call: SgNode, sdkFieldRootNames: Set, - sdkFieldLocalNames: Set, + sdkFieldLocalBindings: SdkFieldLocalBinding[], + root: SgNode, ): boolean { const object = findMemberCallObject(call); - return object ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalNames) : false; + return object + ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalBindings, root) + : false; } function transformHookCallbackObject(node: SgNode, edits: Edit[]): void { @@ -752,11 +821,12 @@ function transformPrincipalCallbacksInCall( call: SgNode, edits: Edit[], sdkFieldRootNames: Set, - sdkFieldLocalNames: Set, + sdkFieldLocalBindings: SdkFieldLocalBinding[], + root: SgNode, ): void { const memberName = findMemberCallName(call); if (memberName !== "hooks" && memberName !== "validate") return; - if (!isSdkFieldMemberCall(call, sdkFieldRootNames, sdkFieldLocalNames)) return; + if (!isSdkFieldMemberCall(call, sdkFieldRootNames, sdkFieldLocalBindings, root)) return; const args = call.field("arguments"); if (!args) return; @@ -775,11 +845,12 @@ function transformParseArgsObject( call: SgNode, edits: Edit[], sdkFieldRootNames: Set, - sdkFieldLocalNames: Set, + sdkFieldLocalBindings: SdkFieldLocalBinding[], + root: SgNode, ): void { const memberName = findMemberCallName(call); if (memberName !== "parse") return; - if (!isSdkFieldMemberCall(call, sdkFieldRootNames, sdkFieldLocalNames)) return; + if (!isSdkFieldMemberCall(call, sdkFieldRootNames, sdkFieldLocalBindings, root)) return; const args = call.field("arguments"); const objArg = args?.children().find((c: SgNode) => c.kind() === "object"); @@ -923,11 +994,11 @@ export default function transform(source: string): string | null { } } - const sdkFieldLocalNames = collectSdkFieldLocalNames(tree, sdkFieldRootNames); + const sdkFieldLocalBindings = collectSdkFieldLocalBindings(tree, sdkFieldRootNames); const memberCalls = tree.findAll({ rule: { kind: "call_expression" } }); for (const call of memberCalls) { - transformPrincipalCallbacksInCall(call, edits, sdkFieldRootNames, sdkFieldLocalNames); - transformParseArgsObject(call, edits, sdkFieldRootNames, sdkFieldLocalNames); + transformPrincipalCallbacksInCall(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); + transformParseArgsObject(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); } if (edits.length === 0) return null; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index c6d9ceb56..c5c7a32e9 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -46,3 +46,7 @@ export const parsedLocal = reviewer.parse({ export const parsedOther = zodLike.parse({ user: null, }); + +export function parseWithShadow(reviewer: { parse(arg: unknown): unknown }, user: unknown) { + return reviewer.parse({ user }); +} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index 556286ff0..19ddd3799 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -47,3 +47,7 @@ export const parsedLocal = reviewer.parse({ export const parsedOther = zodLike.parse({ user: unauthenticatedTailorUser, }); + +export function parseWithShadow(reviewer: { parse(arg: unknown): unknown }, user: unknown) { + return reviewer.parse({ user }); +} From 3149e4571b130a508359a56701979cc81ec3fce1 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 17:26:55 +0900 Subject: [PATCH 066/618] fix(sdk): complete principal migration paths --- .../v2/principal-unify/scripts/transform.ts | 86 +++++++++++++++++-- .../tests/body-direct-call/expected.ts | 11 +++ .../tests/body-direct-call/input.ts | 11 +++ .../tests/tailordb-callbacks/expected.ts | 6 ++ .../tests/tailordb-callbacks/input.ts | 6 ++ .../configure/services/workflow/job.test.ts | 45 ++++++++++ .../src/configure/services/workflow/job.ts | 5 +- .../services/workflow/test-env-key.ts | 76 +++++++++++++++- 8 files changed, 234 insertions(+), 12 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index a8ea97ebb..8afbf0aec 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -524,7 +524,15 @@ function findMemberCallObject(call: SgNode): SgNode | null { } function isFunctionNode(node: SgNode): boolean { - return node.kind() === "arrow_function" || node.kind() === "function_expression"; + return ( + node.kind() === "arrow_function" || + node.kind() === "function_expression" || + node.kind() === "method_definition" + ); +} + +function propertyName(node: SgNode): string | null { + return node.field("key")?.text() ?? node.field("name")?.text() ?? null; } function getFirstFunctionParamPattern(fn: SgNode): SgNode | null { @@ -779,10 +787,15 @@ function isSdkFieldMemberCall( function transformHookCallbackObject(node: SgNode, edits: Edit[]): void { if (node.kind() !== "object") return; - const pairs = node.children().filter((child: SgNode) => child.kind() === "pair"); - for (const pair of pairs) { - const key = pair.field("key")?.text(); - const value = pair.field("value"); + for (const child of node.children()) { + if (child.kind() === "method_definition") { + const key = propertyName(child); + if (key === "create" || key === "update") transformPrincipalCallbackParam(child, edits); + continue; + } + if (child.kind() !== "pair") continue; + const key = child.field("key")?.text(); + const value = child.field("value"); if (!value) continue; if ((key === "create" || key === "update") && isFunctionNode(value)) { transformPrincipalCallbackParam(value, edits); @@ -810,9 +823,13 @@ function transformValidateCallbackNode(node: SgNode, edits: Edit[]): void { } if (node.kind() !== "object") return; - const pairs = node.children().filter((child: SgNode) => child.kind() === "pair"); - for (const pair of pairs) { - const value = pair.field("value"); + for (const child of node.children()) { + if (child.kind() === "method_definition") { + transformPrincipalCallbackParam(child, edits); + continue; + } + if (child.kind() !== "pair") continue; + const value = child.field("value"); if (value) transformValidateCallbackNode(value, edits); } } @@ -867,6 +884,58 @@ function transformParseArgsObject( } } +function transformDirectBodyContext(call: SgNode, edits: Edit[]): void { + const memberName = findMemberCallName(call); + if (memberName !== "body") return; + + const args = call.field("arguments"); + const objArg = args?.children().find((c: SgNode) => c.kind() === "object"); + if (!objArg) return; + + let hasInput = false; + let hasEnv = false; + let hasInvoker = false; + let userNode: SgNode | null = null; + let envNode: SgNode | null = null; + + for (const child of objArg.children()) { + const kind = child.kind(); + if (kind === "pair") { + const key = child.field("key")?.text(); + if (key === "input") hasInput = true; + if (key === "env") { + hasEnv = true; + envNode = child; + } + if (key === "invoker") hasInvoker = true; + if (key === "user") userNode = child.field("key") ?? child; + } else if (kind === "shorthand_property_identifier") { + const name = child.text(); + if (name === "input") hasInput = true; + if (name === "env") { + hasEnv = true; + envNode = child; + } + if (name === "invoker") hasInvoker = true; + if (name === "user") userNode = child; + } + } + + if (!hasInput || !hasEnv || !userNode) return; + edits.push( + userNode.kind() === "shorthand_property_identifier" + ? userNode.replace("caller: user") + : userNode.replace("caller"), + ); + if (hasInvoker || !envNode) return; + const indent = " ".repeat(envNode.range().start.column); + edits.push( + envNode.kind() === "shorthand_property_identifier" + ? envNode.replace(`invoker: null,\n${indent}env`) + : envNode.replace(`invoker: null,\n${indent}${envNode.text()}`), + ); +} + /** * Migrate user/actor/invoker types and identifiers to the unified TailorPrincipal. * @@ -999,6 +1068,7 @@ export default function transform(source: string): string | null { for (const call of memberCalls) { transformPrincipalCallbacksInCall(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); transformParseArgsObject(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); + transformDirectBodyContext(call, edits); } if (edits.length === 0) return null; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts new file mode 100644 index 000000000..ba7ddc296 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts @@ -0,0 +1,11 @@ +import resolver from "./resolver"; + +export async function run() { + return await resolver.body({ + input: { id: "user-1" }, + caller: null, + invoker: null, + env: {}, + }); +} + diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts new file mode 100644 index 000000000..74f4beb80 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts @@ -0,0 +1,11 @@ +import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; +import resolver from "./resolver"; + +export async function run() { + return await resolver.body({ + input: { id: "user-1" }, + user: unauthenticatedTailorUser, + env: {}, + }); +} + diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index c5c7a32e9..7d37c8529 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -5,6 +5,9 @@ const role = db .hooks({ create: ({ value, invoker }) => (invoker?.attributes.role === "ADMIN" ? value : "user"), update: ctx => ctx.invoker?.id ?? "anonymous", + delete({ user }) { + return user?.id ?? "anonymous"; + }, }) .validate([ [({ invoker }) => invoker?.type === "machine_user", "Machine user required"], @@ -25,6 +28,9 @@ export const user = db const audit = [{ user: { id: "data-user" } }].map(({ user }) => user.id); return currentUser?.id ?? audit[0] ?? "anonymous"; }, + update({ invoker }) { + return invoker?.id ?? "anonymous"; + }, }, }) .validate({ diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index 19ddd3799..80e8306ab 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -6,6 +6,9 @@ const role = db .hooks({ create: ({ value, user }) => (user?.attributes.role === "ADMIN" ? value : "user"), update: ctx => ctx.user?.id ?? "anonymous", + delete({ user }) { + return user?.id ?? "anonymous"; + }, }) .validate([ [({ user }) => user?.type === "machine_user", "Machine user required"], @@ -26,6 +29,9 @@ export const user = db const audit = [{ user: { id: "data-user" } }].map(({ user }) => user.id); return currentUser?.id ?? audit[0] ?? "anonymous"; }, + update({ user }) { + return user?.id ?? "anonymous"; + }, }, }) .validate({ diff --git a/packages/sdk/src/configure/services/workflow/job.test.ts b/packages/sdk/src/configure/services/workflow/job.test.ts index 8e3d2bc0b..96c371805 100644 --- a/packages/sdk/src/configure/services/workflow/job.test.ts +++ b/packages/sdk/src/configure/services/workflow/job.test.ts @@ -59,6 +59,51 @@ describe("WorkflowJob type inference", () => { }, }); }); + + test("direct body calls propagate invoker to triggered child jobs", async () => { + const invoker: TailorPrincipal = { + id: "principal-1", + type: "user", + workspaceId: "workspace-1", + attributes: { role: "ADMIN" }, + attributeList: [], + }; + const child = createWorkflowJob({ + name: "capture-child-invoker", + body: (_input: undefined, context) => context.invoker?.id ?? "anonymous", + }); + const parent = createWorkflowJob({ + name: "propagate-parent-invoker", + body: async () => await child.trigger(), + }); + + await expect(parent.body(undefined, { env: {}, invoker })).resolves.toBe("principal-1"); + }); + + test("trigger reads the runtime invoker when no body context is active", async () => { + const previousTailor = (globalThis as { tailor?: unknown }).tailor; + (globalThis as { tailor?: unknown }).tailor = { + context: { + getInvoker: () => ({ + id: "runtime-principal", + type: "machine_user", + workspaceId: "workspace-1", + attributes: ["role"], + attributeMap: { role: "SYSTEM" }, + }), + }, + }; + try { + const job = createWorkflowJob({ + name: "capture-runtime-invoker", + body: (_input: undefined, context) => context.invoker?.id ?? "anonymous", + }); + + await expect(job.trigger()).resolves.toBe("runtime-principal"); + } finally { + (globalThis as { tailor?: unknown }).tailor = previousTailor; + } + }); }); describe("WorkflowJob type constraints", () => { diff --git a/packages/sdk/src/configure/services/workflow/job.ts b/packages/sdk/src/configure/services/workflow/job.ts index ff0f359fe..ff1424556 100644 --- a/packages/sdk/src/configure/services/workflow/job.ts +++ b/packages/sdk/src/configure/services/workflow/job.ts @@ -1,5 +1,6 @@ import { brandValue } from "@/utils/brand"; import { dispatchTriggerJob, registerJob, type RegisteredJobBody } from "./registry"; +import { withWorkflowTestInvoker } from "./test-env-key"; import type { TailorEnv } from "@/types/env"; import type { JsonCompatible } from "@/types/helpers"; import type { TailorPrincipal } from "@/types/user"; @@ -100,7 +101,9 @@ interface CreateWorkflowJobConfig { export function createWorkflowJob( config: CreateWorkflowJobConfig, ): WorkflowJob> { - const body = config.body as (input: I, context: WorkflowJobContext) => O | Promise; + const userBody = config.body as (input: I, context: WorkflowJobContext) => O | Promise; + const body = (input: I, context: WorkflowJobContext): O | Promise => + withWorkflowTestInvoker(context.invoker, () => userBody(input, context)); // Test-only registry/trigger shim; the platform bundle sets the flag so it is DCE'd. if (!process.env.TAILOR_PLATFORM_BUNDLE) { diff --git a/packages/sdk/src/configure/services/workflow/test-env-key.ts b/packages/sdk/src/configure/services/workflow/test-env-key.ts index 626cc6fdd..b45164fcf 100644 --- a/packages/sdk/src/configure/services/workflow/test-env-key.ts +++ b/packages/sdk/src/configure/services/workflow/test-env-key.ts @@ -13,6 +13,7 @@ import type { TailorEnv } from "../../../types/env"; import type { TailorPrincipal } from "../../../types/user"; const SLOT_KEY = "__tailorWorkflowTestEnv"; +const INVOKER_SLOT_KEY = "__tailorWorkflowTestInvoker"; /** * Read the test-time env slot. @@ -40,6 +41,46 @@ export function clearWorkflowTestEnv(): void { delete (globalThis as unknown as Record)[SLOT_KEY]; } +function invokerSlot(): { hasValue: boolean; value: TailorPrincipal | null | undefined } { + const slots = globalThis as unknown as Record; + return { + hasValue: Object.hasOwn(slots, INVOKER_SLOT_KEY), + value: slots[INVOKER_SLOT_KEY], + }; +} + +function writeWorkflowTestInvoker(invoker: TailorPrincipal | null): void { + (globalThis as unknown as Record)[INVOKER_SLOT_KEY] = invoker; +} + +function restoreWorkflowTestInvoker(previous: { + hasValue: boolean; + value: TailorPrincipal | null | undefined; +}): void { + const slots = globalThis as unknown as Record; + if (previous.hasValue) { + slots[INVOKER_SLOT_KEY] = previous.value; + } else { + delete slots[INVOKER_SLOT_KEY]; + } +} + +export function withWorkflowTestInvoker(invoker: TailorPrincipal | null, run: () => T): T { + const previous = invokerSlot(); + writeWorkflowTestInvoker(invoker); + try { + const result = run(); + if (result instanceof Promise) { + return result.finally(() => restoreWorkflowTestInvoker(previous)) as T; + } + restoreWorkflowTestInvoker(previous); + return result; + } catch (cause) { + restoreWorkflowTestInvoker(previous); + throw cause; + } +} + /** * Env-var fallback read by `.trigger()` when `mockWorkflow().setEnv()` is unset. * @deprecated Use `mockWorkflow().setEnv()` from `@tailor-platform/sdk/vitest`. @@ -47,13 +88,42 @@ export function clearWorkflowTestEnv(): void { */ export const WORKFLOW_TEST_ENV_KEY = "TAILOR_TEST_WORKFLOW_ENV"; +type RuntimeInvoker = { + id: string; + type: "user" | "machine_user"; + workspaceId: string; + attributes?: string[] | TailorPrincipal["attributes"]; + attributeMap?: TailorPrincipal["attributes"]; + attributeList?: TailorPrincipal["attributeList"]; +}; + +function readRuntimeInvoker(): TailorPrincipal | null { + const runtime = ( + globalThis as unknown as { + tailor?: { context?: { getInvoker?: () => RuntimeInvoker | null } }; + } + ).tailor?.context?.getInvoker; + const raw = runtime?.(); + if (!raw) return null; + return { + id: raw.id, + type: raw.type, + workspaceId: raw.workspaceId, + attributes: raw.attributeMap ?? (Array.isArray(raw.attributes) ? {} : (raw.attributes ?? {})), + attributeList: (raw.attributeList ?? + (Array.isArray(raw.attributes) ? raw.attributes : [])) as TailorPrincipal["attributeList"], + }; +} + // env from `mockWorkflow().setEnv()`, else the deprecated env-var. Shallow-copied // to isolate against cross-trigger mutation. export function buildJobContext(): { env: TailorEnv; invoker: TailorPrincipal | null } { + const currentInvoker = invokerSlot(); + const invoker = currentInvoker.hasValue ? (currentInvoker.value ?? null) : readRuntimeInvoker(); const fromGlobal = readWorkflowTestEnv(); - if (fromGlobal !== undefined) return { env: { ...fromGlobal }, invoker: null }; + if (fromGlobal !== undefined) return { env: { ...fromGlobal }, invoker }; const raw = process.env[WORKFLOW_TEST_ENV_KEY]; - if (!raw) return { env: {} as TailorEnv, invoker: null }; + if (!raw) return { env: {} as TailorEnv, invoker }; let parsed: unknown; try { parsed = JSON.parse(raw); @@ -68,5 +138,5 @@ export function buildJobContext(): { env: TailorEnv; invoker: TailorPrincipal | `${WORKFLOW_TEST_ENV_KEY} must be a JSON object; provide a record or use mockWorkflow().setEnv().`, ); } - return { env: { ...(parsed as TailorEnv) }, invoker: null }; + return { env: { ...(parsed as TailorEnv) }, invoker }; } From d7ff7e489505f21a5d99cec86aa9ea0ddf1e1f96 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 17:42:10 +0900 Subject: [PATCH 067/618] fix(codemod): preserve principal migration values --- .../v2/principal-unify/scripts/transform.ts | 57 +++++++++++++++---- .../tests/body-direct-call/expected.ts | 16 ++++++ .../tests/body-direct-call/input.ts | 15 +++++ .../tests/tailordb-callbacks/expected.ts | 9 ++- .../tests/tailordb-callbacks/input.ts | 7 ++- 5 files changed, 90 insertions(+), 14 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 8afbf0aec..4a8f9d8e8 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -627,12 +627,18 @@ function transformPrincipalCallbackParam(fn: SgNode, edits: Edit[]): void { if (renamesBinding && patternBindsName(pattern, "invoker")) return; + const aliasRenamedUser = renamesBinding && collectAllShadowRanges(body, "invoker").length > 0; + let renamedShorthandUser = false; for (const child of pattern.children()) { const kind = child.kind(); if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { - edits.push(child.replace("invoker")); - renamedShorthandUser = true; + if (aliasRenamedUser) { + edits.push(child.replace("invoker: user")); + } else { + edits.push(child.replace("invoker")); + renamedShorthandUser = true; + } } else if (kind === "pair_pattern") { const key = child.field("key"); if (key?.text() === "user") { @@ -643,8 +649,12 @@ function transformPrincipalCallbackParam(fn: SgNode, edits: Edit[]): void { .children() .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); if (inner?.text() === "user") { - edits.push(inner.replace("invoker")); - renamedShorthandUser = true; + if (aliasRenamedUser) { + edits.push(inner.replace("invoker: user")); + } else { + edits.push(inner.replace("invoker")); + renamedShorthandUser = true; + } } } } @@ -884,7 +894,24 @@ function transformParseArgsObject( } } -function transformDirectBodyContext(call: SgNode, edits: Edit[]): void { +function directBodyInvokerValue( + value: SgNode, + unauthenticatedLocalNames: Set, + root: SgNode, +): string { + const text = value.text(); + if (!unauthenticatedLocalNames.has(text)) return text; + const shadowRanges = collectAllShadowRanges(root, text); + const pos = value.range().start.index; + return isInsideAnyRange(pos, shadowRanges) ? text : "null"; +} + +function transformDirectBodyContext( + call: SgNode, + edits: Edit[], + unauthenticatedLocalNames: Set, + root: SgNode, +): void { const memberName = findMemberCallName(call); if (memberName !== "body") return; @@ -896,6 +923,7 @@ function transformDirectBodyContext(call: SgNode, edits: Edit[]): void { let hasEnv = false; let hasInvoker = false; let userNode: SgNode | null = null; + let userValue: SgNode | null = null; let envNode: SgNode | null = null; for (const child of objArg.children()) { @@ -908,7 +936,10 @@ function transformDirectBodyContext(call: SgNode, edits: Edit[]): void { envNode = child; } if (key === "invoker") hasInvoker = true; - if (key === "user") userNode = child.field("key") ?? child; + if (key === "user") { + userNode = child.field("key") ?? child; + userValue = child.field("value"); + } } else if (kind === "shorthand_property_identifier") { const name = child.text(); if (name === "input") hasInput = true; @@ -917,11 +948,14 @@ function transformDirectBodyContext(call: SgNode, edits: Edit[]): void { envNode = child; } if (name === "invoker") hasInvoker = true; - if (name === "user") userNode = child; + if (name === "user") { + userNode = child; + userValue = child; + } } } - if (!hasInput || !hasEnv || !userNode) return; + if (!hasInput || !hasEnv || !userNode || !userValue) return; edits.push( userNode.kind() === "shorthand_property_identifier" ? userNode.replace("caller: user") @@ -929,10 +963,11 @@ function transformDirectBodyContext(call: SgNode, edits: Edit[]): void { ); if (hasInvoker || !envNode) return; const indent = " ".repeat(envNode.range().start.column); + const invokerValue = directBodyInvokerValue(userValue, unauthenticatedLocalNames, root); edits.push( envNode.kind() === "shorthand_property_identifier" - ? envNode.replace(`invoker: null,\n${indent}env`) - : envNode.replace(`invoker: null,\n${indent}${envNode.text()}`), + ? envNode.replace(`invoker: ${invokerValue},\n${indent}env`) + : envNode.replace(`invoker: ${invokerValue},\n${indent}${envNode.text()}`), ); } @@ -1068,7 +1103,7 @@ export default function transform(source: string): string | null { for (const call of memberCalls) { transformPrincipalCallbacksInCall(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); transformParseArgsObject(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); - transformDirectBodyContext(call, edits); + transformDirectBodyContext(call, edits, unauthenticatedLocalNames, tree); } if (edits.length === 0) return null; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts index ba7ddc296..d67e906bd 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts @@ -1,5 +1,13 @@ import resolver from "./resolver"; +const customUser = { + id: "user-2", + type: "user", + workspaceId: "workspace-1", + attributes: {}, + attributeList: [], +}; + export async function run() { return await resolver.body({ input: { id: "user-1" }, @@ -9,3 +17,11 @@ export async function run() { }); } +export async function runAsCustomUser() { + return await resolver.body({ + input: { id: "user-2" }, + caller: customUser, + invoker: customUser, + env: {}, + }); +} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts index 74f4beb80..5c81ebfc5 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts @@ -1,6 +1,14 @@ import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import resolver from "./resolver"; +const customUser = { + id: "user-2", + type: "user", + workspaceId: "workspace-1", + attributes: {}, + attributeList: [], +}; + export async function run() { return await resolver.body({ input: { id: "user-1" }, @@ -9,3 +17,10 @@ export async function run() { }); } +export async function runAsCustomUser() { + return await resolver.body({ + input: { id: "user-2" }, + user: customUser, + env: {}, + }); +} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index 7d37c8529..1c1347ada 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -28,13 +28,18 @@ export const user = db const audit = [{ user: { id: "data-user" } }].map(({ user }) => user.id); return currentUser?.id ?? audit[0] ?? "anonymous"; }, - update({ invoker }) { - return invoker?.id ?? "anonymous"; + update({ invoker: user }) { + const invoker = user?.id ?? "anonymous"; + return invoker; }, }, }) .validate({ note: (ctx) => ctx.invoker?.type !== "machine_user", + fallback: ({ invoker: user = null }) => { + const labels = ["anonymous"].map((invoker) => invoker); + return user?.id !== labels[0]; + }, }); export const parsed = t.string().parse({ diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index 80e8306ab..b6ab1688a 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -30,12 +30,17 @@ export const user = db return currentUser?.id ?? audit[0] ?? "anonymous"; }, update({ user }) { - return user?.id ?? "anonymous"; + const invoker = user?.id ?? "anonymous"; + return invoker; }, }, }) .validate({ note: (ctx) => ctx.user?.type !== "machine_user", + fallback: ({ user = unauthenticatedTailorUser }) => { + const labels = ["anonymous"].map((invoker) => invoker); + return user?.id !== labels[0]; + }, }); export const parsed = t.string().parse({ From 046ec87d94d7af2c6544d9a01ecb2fbb2847e060 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 17:52:52 +0900 Subject: [PATCH 068/618] docs: clarify workflow trigger mocking guidance --- .changeset/workflow-trigger-dispatch.md | 2 +- packages/sdk/docs/testing.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/workflow-trigger-dispatch.md b/.changeset/workflow-trigger-dispatch.md index 76984874a..553133b4b 100644 --- a/.changeset/workflow-trigger-dispatch.md +++ b/.changeset/workflow-trigger-dispatch.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk": major --- -Align workflow job `.trigger()` with the platform runtime. Job triggers now delegate to `tailor.workflow.triggerJobFunction()` instead of running job bodies locally, and `trigger()` returns the job result directly instead of a Promise wrapper. Use `mockWorkflow()` to mock trigger results in tests, or `runWorkflowLocally()` for full-chain local workflow tests. +Align workflow job `.trigger()` with the platform runtime. Job triggers now require a mocked workflow runtime in tests instead of running job bodies locally, and `trigger()` returns the job result directly instead of a Promise wrapper. Use `mockWorkflow()` to mock trigger results in tests, or `runWorkflowLocally()` for full-chain local workflow tests. diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index a6e9d64c8..61ca724a6 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -129,7 +129,7 @@ test("content-based mock", async () => { ### Workflow Mock -Workflow job `.trigger()` calls delegate to `tailor.workflow.triggerJobFunction`. Acquire `mockWorkflow()` when you want to provide trigger responses with `setJobHandler` / `enqueueResult` or assert on `triggeredJobs`. If no response is configured, the mock throws so missing job mocks fail loudly: +Workflow job `.trigger()` calls use the platform workflow runtime. Acquire `mockWorkflow()` when you want to provide trigger responses with `setJobHandler` / `enqueueResult` or assert on `triggeredJobs`. If no response is configured, the mock throws so missing job mocks fail loudly: ```typescript import { mockWorkflow } from "@tailor-platform/sdk/vitest"; From f6cd8541111803c848e0f6b88ca95de127ed2918 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 18:04:40 +0900 Subject: [PATCH 069/618] fix(sdk): isolate workflow principal context --- .../v2/principal-unify/scripts/transform.ts | 10 ++-- .../tests/quick-filter-body-call/expected.ts | 17 +++++++ .../tests/quick-filter-body-call/input.ts | 16 +++++++ .../tests/shadowed-sdk-field/input.ts | 7 +++ .../configure/services/workflow/job.test.ts | 48 +++++++++++++++++++ .../services/workflow/test-env-key.ts | 45 ++--------------- 6 files changed, 99 insertions(+), 44 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/shadowed-sdk-field/input.ts diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 4a8f9d8e8..009089966 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -16,6 +16,7 @@ const QUICK_FILTER_NEEDLES = [ ".hooks", ".validate", ".parse", + ".body", ]; function quickFilter(source: string): boolean { @@ -738,10 +739,11 @@ function isSdkFieldExpression( root: SgNode, ): boolean { if (node.kind() === "identifier") { - return ( - sdkFieldRootNames.has(node.text()) || - resolvesToSdkFieldLocal(node, sdkFieldLocalBindings, root) - ); + const pos = node.range().start.index; + const isSdkRoot = + sdkFieldRootNames.has(node.text()) && + !isInsideAnyRange(pos, collectAllShadowRanges(root, node.text())); + return isSdkRoot || resolvesToSdkFieldLocal(node, sdkFieldLocalBindings, root); } if (node.kind() === "member_expression") { const object = node.field("object"); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/expected.ts new file mode 100644 index 000000000..8af676f63 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/expected.ts @@ -0,0 +1,17 @@ +import type { TailorPrincipal } from "@tailor-platform/sdk"; +import resolver from "./resolver"; + +const principal: TailorPrincipal = { + id: "user-1", + type: "user", + workspaceId: "workspace-1", + attributes: {}, + attributeList: [], +}; + +export const result = resolver.body({ + input: { id: "user-1" }, + caller: principal, + invoker: principal, + env: {}, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/input.ts new file mode 100644 index 000000000..0cf5c6a59 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/input.ts @@ -0,0 +1,16 @@ +import type { TailorPrincipal } from "@tailor-platform/sdk"; +import resolver from "./resolver"; + +const principal: TailorPrincipal = { + id: "user-1", + type: "user", + workspaceId: "workspace-1", + attributes: {}, + attributeList: [], +}; + +export const result = resolver.body({ + input: { id: "user-1" }, + user: principal, + env: {}, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadowed-sdk-field/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadowed-sdk-field/input.ts new file mode 100644 index 000000000..11951b8a7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadowed-sdk-field/input.ts @@ -0,0 +1,7 @@ +import { db } from "@tailor-platform/sdk"; + +export function buildField(db: { string(): { hooks(config: unknown): unknown } }) { + return db.string().hooks({ + create: ({ user }: { user: { id: string } }) => user.id, + }); +} diff --git a/packages/sdk/src/configure/services/workflow/job.test.ts b/packages/sdk/src/configure/services/workflow/job.test.ts index 96c371805..9ef58e0d7 100644 --- a/packages/sdk/src/configure/services/workflow/job.test.ts +++ b/packages/sdk/src/configure/services/workflow/job.test.ts @@ -80,6 +80,54 @@ describe("WorkflowJob type inference", () => { await expect(parent.body(undefined, { env: {}, invoker })).resolves.toBe("principal-1"); }); + test("concurrent direct body calls isolate invokers for child triggers", async () => { + const firstInvoker: TailorPrincipal = { + id: "principal-1", + type: "user", + workspaceId: "workspace-1", + attributes: {}, + attributeList: [], + }; + const secondInvoker: TailorPrincipal = { + id: "principal-2", + type: "machine_user", + workspaceId: "workspace-1", + attributes: {}, + attributeList: [], + }; + let releaseFirst: () => void = () => {}; + let releaseSecond: () => void = () => {}; + const firstGate = new Promise((resolve) => { + releaseFirst = resolve; + }); + const secondGate = new Promise((resolve) => { + releaseSecond = resolve; + }); + const gates = { + first: firstGate, + second: secondGate, + }; + const child = createWorkflowJob({ + name: "capture-concurrent-child-invoker", + body: (_input: undefined, context) => context.invoker?.id ?? "anonymous", + }); + const parent = createWorkflowJob({ + name: "capture-concurrent-parent-invoker", + body: async (input: { gate: "first" | "second" }) => { + await gates[input.gate]; + return await child.trigger(); + }, + }); + + const first = parent.body({ gate: "first" }, { env: {}, invoker: firstInvoker }); + const second = parent.body({ gate: "second" }, { env: {}, invoker: secondInvoker }); + + releaseFirst(); + await expect(first).resolves.toBe("principal-1"); + releaseSecond(); + await expect(second).resolves.toBe("principal-2"); + }); + test("trigger reads the runtime invoker when no body context is active", async () => { const previousTailor = (globalThis as { tailor?: unknown }).tailor; (globalThis as { tailor?: unknown }).tailor = { diff --git a/packages/sdk/src/configure/services/workflow/test-env-key.ts b/packages/sdk/src/configure/services/workflow/test-env-key.ts index b45164fcf..1f68c86ec 100644 --- a/packages/sdk/src/configure/services/workflow/test-env-key.ts +++ b/packages/sdk/src/configure/services/workflow/test-env-key.ts @@ -9,11 +9,12 @@ * it from nested Vitest configs that do not resolve `@/` aliases. * @internal */ +import { AsyncLocalStorage } from "node:async_hooks"; import type { TailorEnv } from "../../../types/env"; import type { TailorPrincipal } from "../../../types/user"; const SLOT_KEY = "__tailorWorkflowTestEnv"; -const INVOKER_SLOT_KEY = "__tailorWorkflowTestInvoker"; +const invokerStorage = new AsyncLocalStorage(); /** * Read the test-time env slot. @@ -41,44 +42,8 @@ export function clearWorkflowTestEnv(): void { delete (globalThis as unknown as Record)[SLOT_KEY]; } -function invokerSlot(): { hasValue: boolean; value: TailorPrincipal | null | undefined } { - const slots = globalThis as unknown as Record; - return { - hasValue: Object.hasOwn(slots, INVOKER_SLOT_KEY), - value: slots[INVOKER_SLOT_KEY], - }; -} - -function writeWorkflowTestInvoker(invoker: TailorPrincipal | null): void { - (globalThis as unknown as Record)[INVOKER_SLOT_KEY] = invoker; -} - -function restoreWorkflowTestInvoker(previous: { - hasValue: boolean; - value: TailorPrincipal | null | undefined; -}): void { - const slots = globalThis as unknown as Record; - if (previous.hasValue) { - slots[INVOKER_SLOT_KEY] = previous.value; - } else { - delete slots[INVOKER_SLOT_KEY]; - } -} - export function withWorkflowTestInvoker(invoker: TailorPrincipal | null, run: () => T): T { - const previous = invokerSlot(); - writeWorkflowTestInvoker(invoker); - try { - const result = run(); - if (result instanceof Promise) { - return result.finally(() => restoreWorkflowTestInvoker(previous)) as T; - } - restoreWorkflowTestInvoker(previous); - return result; - } catch (cause) { - restoreWorkflowTestInvoker(previous); - throw cause; - } + return invokerStorage.run(invoker, run); } /** @@ -118,8 +83,8 @@ function readRuntimeInvoker(): TailorPrincipal | null { // env from `mockWorkflow().setEnv()`, else the deprecated env-var. Shallow-copied // to isolate against cross-trigger mutation. export function buildJobContext(): { env: TailorEnv; invoker: TailorPrincipal | null } { - const currentInvoker = invokerSlot(); - const invoker = currentInvoker.hasValue ? (currentInvoker.value ?? null) : readRuntimeInvoker(); + const storedInvoker = invokerStorage.getStore(); + const invoker = storedInvoker === undefined ? readRuntimeInvoker() : storedInvoker; const fromGlobal = readWorkflowTestEnv(); if (fromGlobal !== undefined) return { env: { ...fromGlobal }, invoker }; const raw = process.env[WORKFLOW_TEST_ENV_KEY]; From 5f91fdc52dcbc04fa6e0190a7428c5ae22fc6c44 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 18:12:16 +0900 Subject: [PATCH 070/618] test(create-sdk): raise workflow template e2e timeout to 60s The workflow template e2e suite ran with Vitest's default 5s timeout, so the first resolver invocation after deploy could exceed it on a cold start and fail flakily. Match the example project's e2e timeout (60s). --- packages/create-sdk/templates/workflow/vitest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/create-sdk/templates/workflow/vitest.config.ts b/packages/create-sdk/templates/workflow/vitest.config.ts index cc5905a97..d57ae2c43 100644 --- a/packages/create-sdk/templates/workflow/vitest.config.ts +++ b/packages/create-sdk/templates/workflow/vitest.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ name: { label: "e2e", color: "green" }, include: ["e2e/**/*.test.ts"], globalSetup: "e2e/globalSetup.ts", + testTimeout: 60_000, }, }, ], From 14a85348ffc4c4b493f726bb7d57df0670ce16b5 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 18:19:35 +0900 Subject: [PATCH 071/618] fix(sdk): keep workflow test context out of bundles --- .../src/cli/services/workflow/bundler.test.ts | 1 + .../src/configure/services/workflow/job.ts | 6 +++-- .../services/workflow/test-env-key.ts | 23 +++++++++++++++---- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/sdk/src/cli/services/workflow/bundler.test.ts b/packages/sdk/src/cli/services/workflow/bundler.test.ts index d7e08f785..4fafbf10c 100644 --- a/packages/sdk/src/cli/services/workflow/bundler.test.ts +++ b/packages/sdk/src/cli/services/workflow/bundler.test.ts @@ -110,6 +110,7 @@ export default createWorkflow({ expect(code).not.toContain("job-registry"); expect(code).not.toContain("registerJob"); expect(code).not.toContain("platformSerialize"); + expect(code).not.toContain("async_hooks"); } }); diff --git a/packages/sdk/src/configure/services/workflow/job.ts b/packages/sdk/src/configure/services/workflow/job.ts index ff1424556..0e314f2a3 100644 --- a/packages/sdk/src/configure/services/workflow/job.ts +++ b/packages/sdk/src/configure/services/workflow/job.ts @@ -102,8 +102,10 @@ export function createWorkflowJob, ): WorkflowJob> { const userBody = config.body as (input: I, context: WorkflowJobContext) => O | Promise; - const body = (input: I, context: WorkflowJobContext): O | Promise => - withWorkflowTestInvoker(context.invoker, () => userBody(input, context)); + const body = process.env.TAILOR_PLATFORM_BUNDLE + ? userBody + : (input: I, context: WorkflowJobContext): O | Promise => + withWorkflowTestInvoker(context.invoker, () => userBody(input, context)); // Test-only registry/trigger shim; the platform bundle sets the flag so it is DCE'd. if (!process.env.TAILOR_PLATFORM_BUNDLE) { diff --git a/packages/sdk/src/configure/services/workflow/test-env-key.ts b/packages/sdk/src/configure/services/workflow/test-env-key.ts index 1f68c86ec..4cbc7fe84 100644 --- a/packages/sdk/src/configure/services/workflow/test-env-key.ts +++ b/packages/sdk/src/configure/services/workflow/test-env-key.ts @@ -9,12 +9,27 @@ * it from nested Vitest configs that do not resolve `@/` aliases. * @internal */ -import { AsyncLocalStorage } from "node:async_hooks"; import type { TailorEnv } from "../../../types/env"; import type { TailorPrincipal } from "../../../types/user"; const SLOT_KEY = "__tailorWorkflowTestEnv"; -const invokerStorage = new AsyncLocalStorage(); + +type AsyncLocalStorageLike = { + getStore(): T | undefined; + run(store: T, callback: () => R): R; +}; + +let invokerStorage: AsyncLocalStorageLike | undefined; + +function workflowInvokerStorage(): AsyncLocalStorageLike { + if (!invokerStorage) { + const { AsyncLocalStorage } = process.getBuiltinModule("node:async_hooks") as { + AsyncLocalStorage: new () => AsyncLocalStorageLike; + }; + invokerStorage = new AsyncLocalStorage(); + } + return invokerStorage; +} /** * Read the test-time env slot. @@ -43,7 +58,7 @@ export function clearWorkflowTestEnv(): void { } export function withWorkflowTestInvoker(invoker: TailorPrincipal | null, run: () => T): T { - return invokerStorage.run(invoker, run); + return workflowInvokerStorage().run(invoker, run); } /** @@ -83,7 +98,7 @@ function readRuntimeInvoker(): TailorPrincipal | null { // env from `mockWorkflow().setEnv()`, else the deprecated env-var. Shallow-copied // to isolate against cross-trigger mutation. export function buildJobContext(): { env: TailorEnv; invoker: TailorPrincipal | null } { - const storedInvoker = invokerStorage.getStore(); + const storedInvoker = invokerStorage?.getStore(); const invoker = storedInvoker === undefined ? readRuntimeInvoker() : storedInvoker; const fromGlobal = readWorkflowTestEnv(); if (fromGlobal !== undefined) return { env: { ...fromGlobal }, invoker }; From 5a641a1a1b3b836ae89bff861b725d8f5d75728b Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 18:36:10 +0900 Subject: [PATCH 072/618] fix(codemod): narrow direct body migration --- .changeset/tailor-principal-type.md | 1 + .../v2/principal-unify/scripts/transform.ts | 80 ++++++++++++++++++- .../tests/body-direct-call/expected.ts | 12 +++ .../tests/body-direct-call/input.ts | 12 +++ .../tests/non-resolver-body-call/input.ts | 12 +++ 5 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/non-resolver-body-call/input.ts diff --git a/.changeset/tailor-principal-type.md b/.changeset/tailor-principal-type.md index 645527978..e2946b79c 100644 --- a/.changeset/tailor-principal-type.md +++ b/.changeset/tailor-principal-type.md @@ -1,5 +1,6 @@ --- "@tailor-platform/sdk": major +"@tailor-platform/sdk-codemod": patch --- Unify function principal context around `TailorPrincipal`. diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 009089966..fc0f79d48 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -896,6 +896,59 @@ function transformParseArgsObject( } } +function importDefaultName(importStmt: SgNode): string | null { + const importText = importStmt.text(); + if (/^\s*import\s+type\b/.test(importText)) return null; + const match = importText.match(/^\s*import\s+([A-Za-z_$][\w$]*)\s*(?:,|\s+from\b)/); + return match?.[1] ?? null; +} + +function collectResolverBodyObjectNames( + root: SgNode, + createResolverLocalNames: Set, +): Set { + const names = new Set(); + const imports = root.findAll({ rule: { kind: "import_statement" } }); + for (const importStmt of imports) { + const source = extractModuleSource(importStmt.text()); + if (source.startsWith("@tailor-platform/sdk") || !/resolver/i.test(source)) continue; + const defaultName = importDefaultName(importStmt); + if (defaultName) names.add(defaultName); + for (const { localName } of iterateImportSpecs(importStmt)) { + names.add(localName); + } + } + + const declarations = root.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarations) { + const name = decl.field("name"); + const value = decl.field("value"); + if (!name || name.kind() !== "identifier" || !value || value.kind() !== "call_expression") { + continue; + } + const callee = value.field("function"); + if (!callee || callee.kind() !== "identifier") continue; + if (!createResolverLocalNames.has(callee.text())) continue; + const pos = callee.range().start.index; + if (isInsideAnyRange(pos, collectAllShadowRanges(root, callee.text()))) continue; + names.add(name.text()); + } + + return names; +} + +function isResolverBodyCall( + call: SgNode, + resolverBodyObjectNames: Set, + root: SgNode, +): boolean { + const object = findMemberCallObject(call); + if (!object || object.kind() !== "identifier") return false; + if (!resolverBodyObjectNames.has(object.text())) return false; + const pos = object.range().start.index; + return !isInsideAnyRange(pos, collectAllShadowRanges(root, object.text())); +} + function directBodyInvokerValue( value: SgNode, unauthenticatedLocalNames: Set, @@ -908,14 +961,20 @@ function directBodyInvokerValue( return isInsideAnyRange(pos, shadowRanges) ? text : "null"; } +function canDuplicateDirectBodyValue(value: SgNode): boolean { + return value.kind() === "identifier" || value.text() === "null"; +} + function transformDirectBodyContext( call: SgNode, edits: Edit[], unauthenticatedLocalNames: Set, + resolverBodyObjectNames: Set, root: SgNode, ): void { const memberName = findMemberCallName(call); if (memberName !== "body") return; + if (!isResolverBodyCall(call, resolverBodyObjectNames, root)) return; const args = call.field("arguments"); const objArg = args?.children().find((c: SgNode) => c.kind() === "object"); @@ -925,6 +984,7 @@ function transformDirectBodyContext( let hasEnv = false; let hasInvoker = false; let userNode: SgNode | null = null; + let userEntry: SgNode | null = null; let userValue: SgNode | null = null; let envNode: SgNode | null = null; @@ -940,6 +1000,7 @@ function transformDirectBodyContext( if (key === "invoker") hasInvoker = true; if (key === "user") { userNode = child.field("key") ?? child; + userEntry = child; userValue = child.field("value"); } } else if (kind === "shorthand_property_identifier") { @@ -952,12 +1013,20 @@ function transformDirectBodyContext( if (name === "invoker") hasInvoker = true; if (name === "user") { userNode = child; + userEntry = child; userValue = child; } } } - if (!hasInput || !hasEnv || !userNode || !userValue) return; + if (!hasInput || !hasEnv || !userNode || !userEntry || !userValue) return; + if (!hasInvoker && !canDuplicateDirectBodyValue(userValue)) { + edits.push( + userEntry.replace(`...((user) => ({ caller: user, invoker: user }))(${userValue.text()})`), + ); + return; + } + edits.push( userNode.kind() === "shorthand_property_identifier" ? userNode.replace("caller: user") @@ -1101,11 +1170,18 @@ export default function transform(source: string): string | null { } const sdkFieldLocalBindings = collectSdkFieldLocalBindings(tree, sdkFieldRootNames); + const resolverBodyObjectNames = collectResolverBodyObjectNames(tree, createResolverLocalNames); const memberCalls = tree.findAll({ rule: { kind: "call_expression" } }); for (const call of memberCalls) { transformPrincipalCallbacksInCall(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); transformParseArgsObject(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); - transformDirectBodyContext(call, edits, unauthenticatedLocalNames, tree); + transformDirectBodyContext( + call, + edits, + unauthenticatedLocalNames, + resolverBodyObjectNames, + tree, + ); } if (edits.length === 0) return null; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts index d67e906bd..4684f102a 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts @@ -25,3 +25,15 @@ export async function runAsCustomUser() { env: {}, }); } + +export async function runWithFactory() { + return await resolver.body({ + input: { id: "user-2" }, + ...((user) => ({ caller: user, invoker: user }))(makeUser()), + env: {}, + }); +} + +function makeUser() { + return customUser; +} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts index 5c81ebfc5..6211e3803 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts @@ -24,3 +24,15 @@ export async function runAsCustomUser() { env: {}, }); } + +export async function runWithFactory() { + return await resolver.body({ + input: { id: "user-2" }, + user: makeUser(), + env: {}, + }); +} + +function makeUser() { + return customUser; +} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/non-resolver-body-call/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/non-resolver-body-call/input.ts new file mode 100644 index 000000000..05598d5d5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/non-resolver-body-call/input.ts @@ -0,0 +1,12 @@ +import type { TailorPrincipal } from "@tailor-platform/sdk"; + +declare const client: { + body(args: { input: unknown; user: TailorPrincipal | null; env: Record }): unknown; +}; +declare const principal: TailorPrincipal; + +export const result = client.body({ + input: { id: "user-1" }, + user: principal, + env: {}, +}); From f968a5c71f7c2c8c63713811ab528dca988200a9 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 18:59:37 +0900 Subject: [PATCH 073/618] fix(codemod): migrate resolver body test calls --- example/migrations/0002/diff.json | 644 ++++++++++++++++++ .../v2/principal-unify/scripts/transform.ts | 12 +- .../expected.ts | 16 + .../resolver-body-without-sdk-import/input.ts | 15 + 4 files changed, 683 insertions(+), 4 deletions(-) create mode 100644 example/migrations/0002/diff.json create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/input.ts diff --git a/example/migrations/0002/diff.json b/example/migrations/0002/diff.json new file mode 100644 index 000000000..99742e58a --- /dev/null +++ b/example/migrations/0002/diff.json @@ -0,0 +1,644 @@ +{ + "version": 1, + "namespace": "tailordb", + "createdAt": "2026-06-16T09:57:56.775Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "name", + "before": { + "type": "string", + "required": true, + "validate": [ + { + "script": { + "expr": "(({value})=>value.length>5)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "Name must be longer than 5 characters" + } + ] + }, + "after": { + "type": "string", + "required": true, + "validate": [ + { + "script": { + "expr": "(({value})=>value.length>5)({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "Name must be longer than 5 characters" + } + ] + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "city", + "before": { + "type": "string", + "required": false, + "validate": [ + { + "script": { + "expr": "(({value})=>value?value.length>1:true)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "failed by `({value})=>value?value.length>1:true`" + }, + { + "script": { + "expr": "(({value})=>value?value.length<100:true)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "failed by `({value})=>value?value.length<100:true`" + } + ] + }, + "after": { + "type": "string", + "required": false, + "validate": [ + { + "script": { + "expr": "(({value})=>value?value.length>1:true)({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({value})=>value?value.length>1:true`" + }, + { + "script": { + "expr": "(({value})=>value?value.length<100:true)({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({value})=>value?value.length<100:true`" + } + ] + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "fullAddress", + "before": { + "type": "string", + "required": true, + "hooks": { + "create": { + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "update": { + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "string", + "required": true, + "hooks": { + "create": { + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "attachedFiles", + "before": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "size": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({value})=>value>0)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "failed by `({value})=>value>0`" + } + ] + }, + "type": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "text" + }, + { + "value": "image" + } + ] + } + } + }, + "after": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "size": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({value})=>value>0)({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({value})=>value>0`" + } + ] + }, + "type": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "text" + }, + { + "value": "image" + } + ] + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": false +} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index fc0f79d48..c1e528efc 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -20,6 +20,7 @@ const QUICK_FILTER_NEEDLES = [ ]; function quickFilter(source: string): boolean { + if (source.includes(".body")) return true; if (!source.includes("@tailor-platform/sdk")) return false; return QUICK_FILTER_NEEDLES.some((needle) => source.includes(needle)); } @@ -911,12 +912,15 @@ function collectResolverBodyObjectNames( const imports = root.findAll({ rule: { kind: "import_statement" } }); for (const importStmt of imports) { const source = extractModuleSource(importStmt.text()); - if (source.startsWith("@tailor-platform/sdk") || !/resolver/i.test(source)) continue; const defaultName = importDefaultName(importStmt); + const specNames = [...iterateImportSpecs(importStmt)].map(({ localName }) => localName); + const looksLikeResolver = + /resolver/i.test(source) || + (defaultName !== null && /resolver/i.test(defaultName)) || + specNames.some((localName) => /resolver/i.test(localName)); + if (source.startsWith("@tailor-platform/sdk") || !looksLikeResolver) continue; if (defaultName) names.add(defaultName); - for (const { localName } of iterateImportSpecs(importStmt)) { - names.add(localName); - } + for (const localName of specNames) names.add(localName); } const declarations = root.findAll({ rule: { kind: "variable_declarator" } }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/expected.ts new file mode 100644 index 000000000..90b753cb7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/expected.ts @@ -0,0 +1,16 @@ +import resolver from "./add"; + +const principal = { + id: "user-1", + type: "user", + workspaceId: "workspace-1", + attributes: {}, + attributeList: [], +}; + +export const result = resolver.body({ + input: { id: "user-1" }, + caller: principal, + invoker: principal, + env: {}, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/input.ts new file mode 100644 index 000000000..2bd036313 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/input.ts @@ -0,0 +1,15 @@ +import resolver from "./add"; + +const principal = { + id: "user-1", + type: "user", + workspaceId: "workspace-1", + attributes: {}, + attributeList: [], +}; + +export const result = resolver.body({ + input: { id: "user-1" }, + user: principal, + env: {}, +}); From 6ef6fa4558c4abcc309303bf66b1e2996890b01c Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 19:57:07 +0900 Subject: [PATCH 074/618] fix(codemod): preserve nested principal migrations --- .../v2/principal-unify/scripts/transform.ts | 100 +++++++++++++++--- .../tests/body-direct-call/expected.ts | 6 +- .../tests/body-direct-call/input.ts | 6 +- .../tests/tailordb-callbacks/expected.ts | 3 +- .../tests/tailordb-callbacks/input.ts | 3 +- 5 files changed, 94 insertions(+), 24 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index c1e528efc..7a67431b4 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -537,7 +537,7 @@ function propertyName(node: SgNode): string | null { return node.field("key")?.text() ?? node.field("name")?.text() ?? null; } -function getFirstFunctionParamPattern(fn: SgNode): SgNode | null { +function getFirstFunctionParam(fn: SgNode): SgNode | null { const params = fn.field("parameters") ?? fn.field("parameter") ?? @@ -547,24 +547,46 @@ function getFirstFunctionParamPattern(fn: SgNode): SgNode | null { return params; } - const firstParam = params - .children() - .find( - (c: SgNode) => - c.kind() === "required_parameter" || - c.kind() === "optional_parameter" || - c.kind() === "identifier" || - c.kind() === "object_pattern", - ); + return ( + params + .children() + .find( + (c: SgNode) => + c.kind() === "required_parameter" || + c.kind() === "optional_parameter" || + c.kind() === "identifier" || + c.kind() === "object_pattern", + ) ?? null + ); +} + +function getFunctionParamPattern(param: SgNode): SgNode | null { + if (param.kind() === "object_pattern" || param.kind() === "identifier") return param; + return param.field("pattern"); +} + +function getFirstFunctionParamPattern(fn: SgNode): SgNode | null { + const firstParam = getFirstFunctionParam(fn); if (!firstParam) return null; - if (firstParam.kind() === "object_pattern" || firstParam.kind() === "identifier") { - return firstParam; + return getFunctionParamPattern(firstParam); +} + +function transformPrincipalCallbackParamType(param: SgNode, edits: Edit[]): void { + const typeAnnotation = param.field("type"); + const objectType = typeAnnotation?.children().find((c: SgNode) => c.kind() === "object_type"); + if (!objectType) return; + + for (const child of objectType.children()) { + if (child.kind() !== "property_signature") continue; + const name = child.field("name"); + if (name?.text() === "user") edits.push(name.replace("invoker")); } - return firstParam.field("pattern"); } function transformPrincipalCallbackParam(fn: SgNode, edits: Edit[]): void { - const pattern = getFirstFunctionParamPattern(fn); + const param = getFirstFunctionParam(fn); + if (!param) return; + const pattern = getFunctionParamPattern(param); const body = fn.field("body"); if (!pattern || !body) return; @@ -614,20 +636,30 @@ function transformPrincipalCallbackParam(fn: SgNode, edits: Edit[]): void { if (pattern.kind() !== "object_pattern") return; + let hasUserParamProperty = false; let renamesBinding = false; for (const child of pattern.children()) { const kind = child.kind(); if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { + hasUserParamProperty = true; renamesBinding = true; + } else if (kind === "pair_pattern") { + const key = child.field("key"); + if (key?.text() === "user") hasUserParamProperty = true; } else if (kind === "object_assignment_pattern") { const inner = child .children() .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); - if (inner?.text() === "user") renamesBinding = true; + if (inner?.text() === "user") { + hasUserParamProperty = true; + renamesBinding = true; + } } } + if (!hasUserParamProperty) return; if (renamesBinding && patternBindsName(pattern, "invoker")) return; + transformPrincipalCallbackParamType(param, edits); const aliasRenamedUser = renamesBinding && collectAllShadowRanges(body, "invoker").length > 0; @@ -965,6 +997,41 @@ function directBodyInvokerValue( return isInsideAnyRange(pos, shadowRanges) ? text : "null"; } +function directBodyExpressionValue( + value: SgNode, + unauthenticatedLocalNames: Set, + root: SgNode, +): string { + const text = value.text(); + const replacements: Array<[number, number, string]> = []; + const base = value.range().start.index; + + for (const localName of unauthenticatedLocalNames) { + const shadowRanges = collectAllShadowRanges(root, localName); + const ids = value.findAll({ + rule: { + kind: "identifier", + regex: `^${escapeRegex(localName)}$`, + }, + }); + for (const id of ids) { + if (isInsideImportStatement(id)) continue; + if (isMemberExpressionObject(id)) continue; + const range = id.range(); + if (isInsideAnyRange(range.start.index, shadowRanges)) continue; + replacements.push([range.start.index - base, range.end.index - base, "null"]); + } + } + + if (replacements.length === 0) return text; + replacements.sort((a, b) => b[0] - a[0]); + let result = text; + for (const [start, end, replacement] of replacements) { + result = `${result.slice(0, start)}${replacement}${result.slice(end)}`; + } + return result; +} + function canDuplicateDirectBodyValue(value: SgNode): boolean { return value.kind() === "identifier" || value.text() === "null"; } @@ -1025,8 +1092,9 @@ function transformDirectBodyContext( if (!hasInput || !hasEnv || !userNode || !userEntry || !userValue) return; if (!hasInvoker && !canDuplicateDirectBodyValue(userValue)) { + const invokerValue = directBodyExpressionValue(userValue, unauthenticatedLocalNames, root); edits.push( - userEntry.replace(`...((user) => ({ caller: user, invoker: user }))(${userValue.text()})`), + userEntry.replace(`...((user) => ({ caller: user, invoker: user }))(${invokerValue})`), ); return; } diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts index 4684f102a..833b4cad0 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts @@ -29,11 +29,11 @@ export async function runAsCustomUser() { export async function runWithFactory() { return await resolver.body({ input: { id: "user-2" }, - ...((user) => ({ caller: user, invoker: user }))(makeUser()), + ...((user) => ({ caller: user, invoker: user }))(makeUser(null)), env: {}, }); } -function makeUser() { - return customUser; +function makeUser(fallback = customUser) { + return fallback; } diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts index 6211e3803..313ac3393 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts @@ -28,11 +28,11 @@ export async function runAsCustomUser() { export async function runWithFactory() { return await resolver.body({ input: { id: "user-2" }, - user: makeUser(), + user: makeUser(unauthenticatedTailorUser), env: {}, }); } -function makeUser() { - return customUser; +function makeUser(fallback = customUser) { + return fallback; } diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index 1c1347ada..5a986f860 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -1,4 +1,4 @@ -import { db, t } from "@tailor-platform/sdk"; +import { db, t, type TailorPrincipal } from "@tailor-platform/sdk"; const role = db .string() @@ -40,6 +40,7 @@ export const user = db const labels = ["anonymous"].map((invoker) => invoker); return user?.id !== labels[0]; }, + typed: ({ invoker }: { invoker: TailorPrincipal | null }) => invoker?.id !== "", }); export const parsed = t.string().parse({ diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index b6ab1688a..d2e3c8ec8 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -1,4 +1,4 @@ -import { db, t } from "@tailor-platform/sdk"; +import { db, t, type TailorUser } from "@tailor-platform/sdk"; import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; const role = db @@ -41,6 +41,7 @@ export const user = db const labels = ["anonymous"].map((invoker) => invoker); return user?.id !== labels[0]; }, + typed: ({ user }: { user: TailorUser | null }) => user?.id !== "", }); export const parsed = t.string().parse({ From 3a312b5434982c00ce03f51371fe9782b659ea4b Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 20:46:50 +0900 Subject: [PATCH 075/618] fix(codemod): limit principal migration scope --- .../v2/principal-unify/scripts/transform.ts | 195 ------------------ .../tests/body-direct-call/expected.ts | 39 ---- .../tests/body-direct-call/input.ts | 38 ---- .../tests/non-resolver-body-call/input.ts | 12 -- .../tests/quick-filter-body-call/expected.ts | 17 -- .../tests/quick-filter-body-call/input.ts | 16 -- .../expected.ts | 16 -- .../resolver-body-without-sdk-import/input.ts | 15 -- 8 files changed, 348 deletions(-) delete mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/non-resolver-body-call/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/input.ts diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 7a67431b4..4584c81b1 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -16,11 +16,9 @@ const QUICK_FILTER_NEEDLES = [ ".hooks", ".validate", ".parse", - ".body", ]; function quickFilter(source: string): boolean { - if (source.includes(".body")) return true; if (!source.includes("@tailor-platform/sdk")) return false; return QUICK_FILTER_NEEDLES.some((needle) => source.includes(needle)); } @@ -929,191 +927,6 @@ function transformParseArgsObject( } } -function importDefaultName(importStmt: SgNode): string | null { - const importText = importStmt.text(); - if (/^\s*import\s+type\b/.test(importText)) return null; - const match = importText.match(/^\s*import\s+([A-Za-z_$][\w$]*)\s*(?:,|\s+from\b)/); - return match?.[1] ?? null; -} - -function collectResolverBodyObjectNames( - root: SgNode, - createResolverLocalNames: Set, -): Set { - const names = new Set(); - const imports = root.findAll({ rule: { kind: "import_statement" } }); - for (const importStmt of imports) { - const source = extractModuleSource(importStmt.text()); - const defaultName = importDefaultName(importStmt); - const specNames = [...iterateImportSpecs(importStmt)].map(({ localName }) => localName); - const looksLikeResolver = - /resolver/i.test(source) || - (defaultName !== null && /resolver/i.test(defaultName)) || - specNames.some((localName) => /resolver/i.test(localName)); - if (source.startsWith("@tailor-platform/sdk") || !looksLikeResolver) continue; - if (defaultName) names.add(defaultName); - for (const localName of specNames) names.add(localName); - } - - const declarations = root.findAll({ rule: { kind: "variable_declarator" } }); - for (const decl of declarations) { - const name = decl.field("name"); - const value = decl.field("value"); - if (!name || name.kind() !== "identifier" || !value || value.kind() !== "call_expression") { - continue; - } - const callee = value.field("function"); - if (!callee || callee.kind() !== "identifier") continue; - if (!createResolverLocalNames.has(callee.text())) continue; - const pos = callee.range().start.index; - if (isInsideAnyRange(pos, collectAllShadowRanges(root, callee.text()))) continue; - names.add(name.text()); - } - - return names; -} - -function isResolverBodyCall( - call: SgNode, - resolverBodyObjectNames: Set, - root: SgNode, -): boolean { - const object = findMemberCallObject(call); - if (!object || object.kind() !== "identifier") return false; - if (!resolverBodyObjectNames.has(object.text())) return false; - const pos = object.range().start.index; - return !isInsideAnyRange(pos, collectAllShadowRanges(root, object.text())); -} - -function directBodyInvokerValue( - value: SgNode, - unauthenticatedLocalNames: Set, - root: SgNode, -): string { - const text = value.text(); - if (!unauthenticatedLocalNames.has(text)) return text; - const shadowRanges = collectAllShadowRanges(root, text); - const pos = value.range().start.index; - return isInsideAnyRange(pos, shadowRanges) ? text : "null"; -} - -function directBodyExpressionValue( - value: SgNode, - unauthenticatedLocalNames: Set, - root: SgNode, -): string { - const text = value.text(); - const replacements: Array<[number, number, string]> = []; - const base = value.range().start.index; - - for (const localName of unauthenticatedLocalNames) { - const shadowRanges = collectAllShadowRanges(root, localName); - const ids = value.findAll({ - rule: { - kind: "identifier", - regex: `^${escapeRegex(localName)}$`, - }, - }); - for (const id of ids) { - if (isInsideImportStatement(id)) continue; - if (isMemberExpressionObject(id)) continue; - const range = id.range(); - if (isInsideAnyRange(range.start.index, shadowRanges)) continue; - replacements.push([range.start.index - base, range.end.index - base, "null"]); - } - } - - if (replacements.length === 0) return text; - replacements.sort((a, b) => b[0] - a[0]); - let result = text; - for (const [start, end, replacement] of replacements) { - result = `${result.slice(0, start)}${replacement}${result.slice(end)}`; - } - return result; -} - -function canDuplicateDirectBodyValue(value: SgNode): boolean { - return value.kind() === "identifier" || value.text() === "null"; -} - -function transformDirectBodyContext( - call: SgNode, - edits: Edit[], - unauthenticatedLocalNames: Set, - resolverBodyObjectNames: Set, - root: SgNode, -): void { - const memberName = findMemberCallName(call); - if (memberName !== "body") return; - if (!isResolverBodyCall(call, resolverBodyObjectNames, root)) return; - - const args = call.field("arguments"); - const objArg = args?.children().find((c: SgNode) => c.kind() === "object"); - if (!objArg) return; - - let hasInput = false; - let hasEnv = false; - let hasInvoker = false; - let userNode: SgNode | null = null; - let userEntry: SgNode | null = null; - let userValue: SgNode | null = null; - let envNode: SgNode | null = null; - - for (const child of objArg.children()) { - const kind = child.kind(); - if (kind === "pair") { - const key = child.field("key")?.text(); - if (key === "input") hasInput = true; - if (key === "env") { - hasEnv = true; - envNode = child; - } - if (key === "invoker") hasInvoker = true; - if (key === "user") { - userNode = child.field("key") ?? child; - userEntry = child; - userValue = child.field("value"); - } - } else if (kind === "shorthand_property_identifier") { - const name = child.text(); - if (name === "input") hasInput = true; - if (name === "env") { - hasEnv = true; - envNode = child; - } - if (name === "invoker") hasInvoker = true; - if (name === "user") { - userNode = child; - userEntry = child; - userValue = child; - } - } - } - - if (!hasInput || !hasEnv || !userNode || !userEntry || !userValue) return; - if (!hasInvoker && !canDuplicateDirectBodyValue(userValue)) { - const invokerValue = directBodyExpressionValue(userValue, unauthenticatedLocalNames, root); - edits.push( - userEntry.replace(`...((user) => ({ caller: user, invoker: user }))(${invokerValue})`), - ); - return; - } - - edits.push( - userNode.kind() === "shorthand_property_identifier" - ? userNode.replace("caller: user") - : userNode.replace("caller"), - ); - if (hasInvoker || !envNode) return; - const indent = " ".repeat(envNode.range().start.column); - const invokerValue = directBodyInvokerValue(userValue, unauthenticatedLocalNames, root); - edits.push( - envNode.kind() === "shorthand_property_identifier" - ? envNode.replace(`invoker: ${invokerValue},\n${indent}env`) - : envNode.replace(`invoker: ${invokerValue},\n${indent}${envNode.text()}`), - ); -} - /** * Migrate user/actor/invoker types and identifiers to the unified TailorPrincipal. * @@ -1242,18 +1055,10 @@ export default function transform(source: string): string | null { } const sdkFieldLocalBindings = collectSdkFieldLocalBindings(tree, sdkFieldRootNames); - const resolverBodyObjectNames = collectResolverBodyObjectNames(tree, createResolverLocalNames); const memberCalls = tree.findAll({ rule: { kind: "call_expression" } }); for (const call of memberCalls) { transformPrincipalCallbacksInCall(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); transformParseArgsObject(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); - transformDirectBodyContext( - call, - edits, - unauthenticatedLocalNames, - resolverBodyObjectNames, - tree, - ); } if (edits.length === 0) return null; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts deleted file mode 100644 index 833b4cad0..000000000 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/expected.ts +++ /dev/null @@ -1,39 +0,0 @@ -import resolver from "./resolver"; - -const customUser = { - id: "user-2", - type: "user", - workspaceId: "workspace-1", - attributes: {}, - attributeList: [], -}; - -export async function run() { - return await resolver.body({ - input: { id: "user-1" }, - caller: null, - invoker: null, - env: {}, - }); -} - -export async function runAsCustomUser() { - return await resolver.body({ - input: { id: "user-2" }, - caller: customUser, - invoker: customUser, - env: {}, - }); -} - -export async function runWithFactory() { - return await resolver.body({ - input: { id: "user-2" }, - ...((user) => ({ caller: user, invoker: user }))(makeUser(null)), - env: {}, - }); -} - -function makeUser(fallback = customUser) { - return fallback; -} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts deleted file mode 100644 index 313ac3393..000000000 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-direct-call/input.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; -import resolver from "./resolver"; - -const customUser = { - id: "user-2", - type: "user", - workspaceId: "workspace-1", - attributes: {}, - attributeList: [], -}; - -export async function run() { - return await resolver.body({ - input: { id: "user-1" }, - user: unauthenticatedTailorUser, - env: {}, - }); -} - -export async function runAsCustomUser() { - return await resolver.body({ - input: { id: "user-2" }, - user: customUser, - env: {}, - }); -} - -export async function runWithFactory() { - return await resolver.body({ - input: { id: "user-2" }, - user: makeUser(unauthenticatedTailorUser), - env: {}, - }); -} - -function makeUser(fallback = customUser) { - return fallback; -} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/non-resolver-body-call/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/non-resolver-body-call/input.ts deleted file mode 100644 index 05598d5d5..000000000 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/non-resolver-body-call/input.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { TailorPrincipal } from "@tailor-platform/sdk"; - -declare const client: { - body(args: { input: unknown; user: TailorPrincipal | null; env: Record }): unknown; -}; -declare const principal: TailorPrincipal; - -export const result = client.body({ - input: { id: "user-1" }, - user: principal, - env: {}, -}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/expected.ts deleted file mode 100644 index 8af676f63..000000000 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/expected.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { TailorPrincipal } from "@tailor-platform/sdk"; -import resolver from "./resolver"; - -const principal: TailorPrincipal = { - id: "user-1", - type: "user", - workspaceId: "workspace-1", - attributes: {}, - attributeList: [], -}; - -export const result = resolver.body({ - input: { id: "user-1" }, - caller: principal, - invoker: principal, - env: {}, -}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/input.ts deleted file mode 100644 index 0cf5c6a59..000000000 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/quick-filter-body-call/input.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { TailorPrincipal } from "@tailor-platform/sdk"; -import resolver from "./resolver"; - -const principal: TailorPrincipal = { - id: "user-1", - type: "user", - workspaceId: "workspace-1", - attributes: {}, - attributeList: [], -}; - -export const result = resolver.body({ - input: { id: "user-1" }, - user: principal, - env: {}, -}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/expected.ts deleted file mode 100644 index 90b753cb7..000000000 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/expected.ts +++ /dev/null @@ -1,16 +0,0 @@ -import resolver from "./add"; - -const principal = { - id: "user-1", - type: "user", - workspaceId: "workspace-1", - attributes: {}, - attributeList: [], -}; - -export const result = resolver.body({ - input: { id: "user-1" }, - caller: principal, - invoker: principal, - env: {}, -}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/input.ts deleted file mode 100644 index 2bd036313..000000000 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-body-without-sdk-import/input.ts +++ /dev/null @@ -1,15 +0,0 @@ -import resolver from "./add"; - -const principal = { - id: "user-1", - type: "user", - workspaceId: "workspace-1", - attributes: {}, - attributeList: [], -}; - -export const result = resolver.body({ - input: { id: "user-1" }, - user: principal, - env: {}, -}); From b33554cf5ddb32ffa3d63c4b2054a277d2b71d1a Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 21:22:15 +0900 Subject: [PATCH 076/618] test(sdk): align principal fixtures --- .../executor/src/executor/onUserCreated.test.ts | 8 -------- .../resolver/src/resolver/showUserInfo.test.ts | 5 +++-- .../codemods/v2/principal-unify/scripts/transform.ts | 6 ------ packages/sdk/e2e/function-test-run.test.ts | 10 +++++----- 4 files changed, 8 insertions(+), 21 deletions(-) diff --git a/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts b/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts index 93f3f492f..22d1113fc 100644 --- a/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts +++ b/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts @@ -10,14 +10,6 @@ describe("onUserCreated executor", () => { throw new Error("expected function operation"); } await onUserCreated.operation.body({ - workspaceId: "workspace-1", - appNamespace: "app", - env: {}, - actor: null, - invoker: null, - event: "created", - rawEvent: "tailordb.type_record.created", - typeName: "User", newRecord: { id: "user-1", name: "Alice", diff --git a/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts b/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts index 9fa5ee515..991ae9e4f 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts @@ -1,3 +1,4 @@ +import type { TailorPrincipal } from "@tailor-platform/sdk"; import { describe, expect, test } from "vitest"; import resolver from "./showUserInfo"; @@ -21,9 +22,9 @@ describe("showUserInfo resolver", () => { id: "user-123", type: "machine_user" as const, workspaceId: "ws-456", - attributes: {}, + attributes: { role: "admin" }, attributeList: [], - }; + } satisfies TailorPrincipal; const result = await resolver.body({ input: undefined as never, caller: customCaller, diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 4584c81b1..4dba0c173 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -563,12 +563,6 @@ function getFunctionParamPattern(param: SgNode): SgNode | null { return param.field("pattern"); } -function getFirstFunctionParamPattern(fn: SgNode): SgNode | null { - const firstParam = getFirstFunctionParam(fn); - if (!firstParam) return null; - return getFunctionParamPattern(firstParam); -} - function transformPrincipalCallbackParamType(param: SgNode, edits: Edit[]): void { const typeAnnotation = param.field("type"); const objectType = typeAnnotation?.children().find((c: SgNode) => c.kind() === "object_type"); diff --git a/packages/sdk/e2e/function-test-run.test.ts b/packages/sdk/e2e/function-test-run.test.ts index 8bc81eda1..b3c3c2d67 100644 --- a/packages/sdk/e2e/function-test-run.test.ts +++ b/packages/sdk/e2e/function-test-run.test.ts @@ -198,11 +198,11 @@ describe.sequential("E2E: function test-run", () => { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; const nilUuid = "00000000-0000-0000-0000-000000000000"; - expect(parsed.user.type).toBe("machine_user"); - expect(parsed.user.workspaceId).toBe(workspaceId); - expect(parsed.user.role).toBe("MANAGER"); - expect(parsed.user.id).toMatch(uuidRegex); - expect(parsed.user.id).not.toBe(nilUuid); + expect(parsed.caller.type).toBe("machine_user"); + expect(parsed.caller.workspaceId).toBe(workspaceId); + expect(parsed.caller.role).toBe("MANAGER"); + expect(parsed.caller.id).toMatch(uuidRegex); + expect(parsed.caller.id).not.toBe(nilUuid); expect(parsed.invoker.type).toBe("machine_user"); expect(parsed.invoker.workspaceId).toBe(workspaceId); From 49a281cfa74e0542e17b83d5940abba475616979 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 21:40:05 +0900 Subject: [PATCH 077/618] chore(codemod): move principal migration to follow-up --- .../v2/principal-unify/scripts/transform.ts | 433 +----------------- .../tests/shadowed-sdk-field/input.ts | 7 - .../tests/tailordb-callbacks/expected.ts | 64 --- .../tests/tailordb-callbacks/input.ts | 65 --- packages/sdk-codemod/src/registry.ts | 2 +- 5 files changed, 2 insertions(+), 569 deletions(-) delete mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/shadowed-sdk-field/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 4dba0c173..af16cd7f0 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -9,14 +9,7 @@ const TYPE_RENAME_MAP: Record = { const UNAUTHENTICATED = "unauthenticatedTailorUser"; -const QUICK_FILTER_NEEDLES = [ - ...Object.keys(TYPE_RENAME_MAP), - UNAUTHENTICATED, - "createResolver", - ".hooks", - ".validate", - ".parse", -]; +const QUICK_FILTER_NEEDLES = [...Object.keys(TYPE_RENAME_MAP), UNAUTHENTICATED, "createResolver"]; function quickFilter(source: string): boolean { if (!source.includes("@tailor-platform/sdk")) return false; @@ -510,417 +503,6 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { } } -function findMemberCallName(call: SgNode): string | null { - const fn = call.field("function"); - if (!fn || fn.kind() !== "member_expression") return null; - const property = fn.field("property"); - return property?.text() ?? null; -} - -function findMemberCallObject(call: SgNode): SgNode | null { - const fn = call.field("function"); - if (!fn || fn.kind() !== "member_expression") return null; - return fn.field("object"); -} - -function isFunctionNode(node: SgNode): boolean { - return ( - node.kind() === "arrow_function" || - node.kind() === "function_expression" || - node.kind() === "method_definition" - ); -} - -function propertyName(node: SgNode): string | null { - return node.field("key")?.text() ?? node.field("name")?.text() ?? null; -} - -function getFirstFunctionParam(fn: SgNode): SgNode | null { - const params = - fn.field("parameters") ?? - fn.field("parameter") ?? - fn.children().find((c: SgNode) => c.kind() === "formal_parameters"); - if (!params) return null; - if (params.kind() === "object_pattern" || params.kind() === "identifier") { - return params; - } - - return ( - params - .children() - .find( - (c: SgNode) => - c.kind() === "required_parameter" || - c.kind() === "optional_parameter" || - c.kind() === "identifier" || - c.kind() === "object_pattern", - ) ?? null - ); -} - -function getFunctionParamPattern(param: SgNode): SgNode | null { - if (param.kind() === "object_pattern" || param.kind() === "identifier") return param; - return param.field("pattern"); -} - -function transformPrincipalCallbackParamType(param: SgNode, edits: Edit[]): void { - const typeAnnotation = param.field("type"); - const objectType = typeAnnotation?.children().find((c: SgNode) => c.kind() === "object_type"); - if (!objectType) return; - - for (const child of objectType.children()) { - if (child.kind() !== "property_signature") continue; - const name = child.field("name"); - if (name?.text() === "user") edits.push(name.replace("invoker")); - } -} - -function transformPrincipalCallbackParam(fn: SgNode, edits: Edit[]): void { - const param = getFirstFunctionParam(fn); - if (!param) return; - const pattern = getFunctionParamPattern(param); - const body = fn.field("body"); - if (!pattern || !body) return; - - if (pattern.kind() === "identifier") { - const ctxName = pattern.text(); - const ctxShadowRanges = collectCtxShadowRanges(body, ctxName, fn); - const propertyAccesses = body.findAll({ - rule: { kind: "property_identifier", regex: "^user$" }, - }); - for (const propId of propertyAccesses) { - const parent = propId.parent(); - if (!parent || parent.kind() !== "member_expression") continue; - const obj = parent.field("object"); - if (!(obj && obj.kind() === "identifier" && obj.text() === ctxName)) continue; - const pos = obj.range().start.index; - if (isInsideAnyRange(pos, ctxShadowRanges)) continue; - edits.push(propId.replace("invoker")); - } - - const ctxDestructures = body.findAll({ - rule: { - kind: "variable_declarator", - has: { - field: "value", - kind: "identifier", - regex: `^${escapeRegex(ctxName)}$`, - }, - }, - }); - for (const decl of ctxDestructures) { - const pos = decl.range().start.index; - if (isInsideAnyRange(pos, ctxShadowRanges)) continue; - const pat = decl.field("name"); - if (!pat || pat.kind() !== "object_pattern") continue; - for (const child of pat.children()) { - const kind = child.kind(); - if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { - edits.push(child.replace("invoker: user")); - } else if (kind === "pair_pattern") { - const key = child.field("key"); - if (key?.text() === "user") edits.push(key.replace("invoker")); - } - } - } - return; - } - - if (pattern.kind() !== "object_pattern") return; - - let hasUserParamProperty = false; - let renamesBinding = false; - for (const child of pattern.children()) { - const kind = child.kind(); - if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { - hasUserParamProperty = true; - renamesBinding = true; - } else if (kind === "pair_pattern") { - const key = child.field("key"); - if (key?.text() === "user") hasUserParamProperty = true; - } else if (kind === "object_assignment_pattern") { - const inner = child - .children() - .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); - if (inner?.text() === "user") { - hasUserParamProperty = true; - renamesBinding = true; - } - } - } - - if (!hasUserParamProperty) return; - if (renamesBinding && patternBindsName(pattern, "invoker")) return; - transformPrincipalCallbackParamType(param, edits); - - const aliasRenamedUser = renamesBinding && collectAllShadowRanges(body, "invoker").length > 0; - - let renamedShorthandUser = false; - for (const child of pattern.children()) { - const kind = child.kind(); - if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { - if (aliasRenamedUser) { - edits.push(child.replace("invoker: user")); - } else { - edits.push(child.replace("invoker")); - renamedShorthandUser = true; - } - } else if (kind === "pair_pattern") { - const key = child.field("key"); - if (key?.text() === "user") { - edits.push(key.replace("invoker")); - } - } else if (kind === "object_assignment_pattern") { - const inner = child - .children() - .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); - if (inner?.text() === "user") { - if (aliasRenamedUser) { - edits.push(inner.replace("invoker: user")); - } else { - edits.push(inner.replace("invoker")); - renamedShorthandUser = true; - } - } - } - } - - if (!renamedShorthandUser) return; - - const shadowRanges = collectAllShadowRanges(body, "user"); - const refs = body.findAll({ rule: { kind: "identifier", regex: "^user$" } }); - for (const ref of refs) { - const pos = ref.range().start.index; - if (isInsideAnyRange(pos, shadowRanges)) continue; - edits.push(ref.replace("invoker")); - } - - const shortRefs = body.findAll({ - rule: { kind: "shorthand_property_identifier", regex: "^user$" }, - }); - for (const ref of shortRefs) { - const pos = ref.range().start.index; - if (isInsideAnyRange(pos, shadowRanges)) continue; - edits.push(ref.replace("user: invoker")); - } -} - -interface SdkFieldLocalBinding { - name: string; - bindingStart: number; - scope: [number, number]; -} - -function rangeContains([start, end]: [number, number], pos: number): boolean { - return pos >= start && pos < end; -} - -function isShadowedLocalReference( - root: SgNode, - name: string, - pos: number, - bindingStart: number, -): boolean { - const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); - for (const decl of declarators) { - const nameNode = decl.field("name"); - if (!nameNode || !patternBindsName(nameNode, name)) continue; - if (nameNode.range().start.index === bindingStart) continue; - const scope = enclosingScopeRange(decl); - if (scope && rangeContains(scope, pos)) return true; - } - - for (const kind of NESTED_FN_KINDS) { - const fns = root.findAll({ rule: { kind } }); - for (const fn of fns) { - if (!functionRebindsName(fn, name)) continue; - const range = fn.range(); - if (pos >= range.start.index && pos < range.end.index) return true; - } - } - - return false; -} - -function resolvesToSdkFieldLocal( - node: SgNode, - bindings: SdkFieldLocalBinding[], - root: SgNode, -): boolean { - if (node.kind() !== "identifier") return false; - const pos = node.range().start.index; - return bindings.some( - (binding) => - binding.name === node.text() && - rangeContains(binding.scope, pos) && - !isShadowedLocalReference(root, binding.name, pos, binding.bindingStart), - ); -} - -function isSdkFieldExpression( - node: SgNode, - sdkFieldRootNames: Set, - sdkFieldLocalBindings: SdkFieldLocalBinding[], - root: SgNode, -): boolean { - if (node.kind() === "identifier") { - const pos = node.range().start.index; - const isSdkRoot = - sdkFieldRootNames.has(node.text()) && - !isInsideAnyRange(pos, collectAllShadowRanges(root, node.text())); - return isSdkRoot || resolvesToSdkFieldLocal(node, sdkFieldLocalBindings, root); - } - if (node.kind() === "member_expression") { - const object = node.field("object"); - return object - ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalBindings, root) - : false; - } - if (node.kind() === "call_expression") { - const object = findMemberCallObject(node); - return object - ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalBindings, root) - : false; - } - return false; -} - -function collectSdkFieldLocalBindings( - root: SgNode, - sdkFieldRootNames: Set, -): SdkFieldLocalBinding[] { - const bindings: SdkFieldLocalBinding[] = []; - let changed = true; - while (changed) { - changed = false; - const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); - for (const decl of declarators) { - const name = decl.field("name"); - const value = decl.field("value"); - if (!name || name.kind() !== "identifier" || !value) continue; - const bindingStart = name.range().start.index; - if (bindings.some((binding) => binding.bindingStart === bindingStart)) continue; - if (!isSdkFieldExpression(value, sdkFieldRootNames, bindings, root)) continue; - const rootRange = root.range(); - const scope = enclosingScopeRange(decl) ?? [rootRange.start.index, rootRange.end.index]; - bindings.push({ name: name.text(), bindingStart, scope }); - changed = true; - } - } - return bindings; -} - -function isSdkFieldMemberCall( - call: SgNode, - sdkFieldRootNames: Set, - sdkFieldLocalBindings: SdkFieldLocalBinding[], - root: SgNode, -): boolean { - const object = findMemberCallObject(call); - return object - ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalBindings, root) - : false; -} - -function transformHookCallbackObject(node: SgNode, edits: Edit[]): void { - if (node.kind() !== "object") return; - for (const child of node.children()) { - if (child.kind() === "method_definition") { - const key = propertyName(child); - if (key === "create" || key === "update") transformPrincipalCallbackParam(child, edits); - continue; - } - if (child.kind() !== "pair") continue; - const key = child.field("key")?.text(); - const value = child.field("value"); - if (!value) continue; - if ((key === "create" || key === "update") && isFunctionNode(value)) { - transformPrincipalCallbackParam(value, edits); - } else if (value.kind() === "object") { - transformHookCallbackObject(value, edits); - } - } -} - -function transformValidateCallbackNode(node: SgNode, edits: Edit[]): void { - if (isFunctionNode(node)) { - transformPrincipalCallbackParam(node, edits); - return; - } - - if (node.kind() === "array") { - for (const child of node.children()) { - if (isFunctionNode(child)) { - transformPrincipalCallbackParam(child, edits); - } else if (child.kind() === "array") { - transformValidateCallbackNode(child, edits); - } - } - return; - } - - if (node.kind() !== "object") return; - for (const child of node.children()) { - if (child.kind() === "method_definition") { - transformPrincipalCallbackParam(child, edits); - continue; - } - if (child.kind() !== "pair") continue; - const value = child.field("value"); - if (value) transformValidateCallbackNode(value, edits); - } -} - -function transformPrincipalCallbacksInCall( - call: SgNode, - edits: Edit[], - sdkFieldRootNames: Set, - sdkFieldLocalBindings: SdkFieldLocalBinding[], - root: SgNode, -): void { - const memberName = findMemberCallName(call); - if (memberName !== "hooks" && memberName !== "validate") return; - if (!isSdkFieldMemberCall(call, sdkFieldRootNames, sdkFieldLocalBindings, root)) return; - - const args = call.field("arguments"); - if (!args) return; - if (memberName === "hooks") { - const objArg = args.children().find((c: SgNode) => c.kind() === "object"); - if (objArg) transformHookCallbackObject(objArg, edits); - return; - } - - for (const arg of args.children()) { - transformValidateCallbackNode(arg, edits); - } -} - -function transformParseArgsObject( - call: SgNode, - edits: Edit[], - sdkFieldRootNames: Set, - sdkFieldLocalBindings: SdkFieldLocalBinding[], - root: SgNode, -): void { - const memberName = findMemberCallName(call); - if (memberName !== "parse") return; - if (!isSdkFieldMemberCall(call, sdkFieldRootNames, sdkFieldLocalBindings, root)) return; - - const args = call.field("arguments"); - const objArg = args?.children().find((c: SgNode) => c.kind() === "object"); - if (!objArg) return; - - for (const child of objArg.children()) { - const kind = child.kind(); - if (kind === "pair") { - const key = child.field("key"); - if (key?.text() === "user") edits.push(key.replace("invoker")); - } else if (kind === "shorthand_property_identifier" && child.text() === "user") { - edits.push(child.replace("invoker: user")); - } - } -} - /** * Migrate user/actor/invoker types and identifiers to the unified TailorPrincipal. * @@ -934,8 +516,6 @@ function transformParseArgsObject( * handles aliased pairs (`{ user: currentUser }`) by rewriting only the property name, and * rewrites `.user` for non-destructured single-param bodies — respecting variable * shadowing in both directions. - * - Renames TailorDB hook/validator callback `user` parameters to `invoker`, and rewrites - * `.parse({ user: ... })` arguments to `.parse({ invoker: ... })`. * @param source - TypeScript source text. * @returns Transformed source or null when nothing matched. */ @@ -1015,15 +595,11 @@ export default function transform(source: string): string | null { // and unrelated local helpers named `createResolver` (when the SDK import // does not actually bring `createResolver` in) are not. const createResolverLocalNames = new Set(); - const sdkFieldRootNames = new Set(); for (const importStmt of sdkImports) { for (const { importedName, localName } of iterateImportSpecs(importStmt)) { if (importedName === "createResolver") { createResolverLocalNames.add(localName); } - if (importedName === "t" || importedName === "db") { - sdkFieldRootNames.add(localName); - } } } for (const localName of createResolverLocalNames) { @@ -1048,13 +624,6 @@ export default function transform(source: string): string | null { } } - const sdkFieldLocalBindings = collectSdkFieldLocalBindings(tree, sdkFieldRootNames); - const memberCalls = tree.findAll({ rule: { kind: "call_expression" } }); - for (const call of memberCalls) { - transformPrincipalCallbacksInCall(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); - transformParseArgsObject(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); - } - if (edits.length === 0) return null; let result = tree.commitEdits(edits); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadowed-sdk-field/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadowed-sdk-field/input.ts deleted file mode 100644 index 11951b8a7..000000000 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadowed-sdk-field/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { db } from "@tailor-platform/sdk"; - -export function buildField(db: { string(): { hooks(config: unknown): unknown } }) { - return db.string().hooks({ - create: ({ user }: { user: { id: string } }) => user.id, - }); -} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts deleted file mode 100644 index 5a986f860..000000000 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { db, t, type TailorPrincipal } from "@tailor-platform/sdk"; - -const role = db - .string() - .hooks({ - create: ({ value, invoker }) => (invoker?.attributes.role === "ADMIN" ? value : "user"), - update: ctx => ctx.invoker?.id ?? "anonymous", - delete({ user }) { - return user?.id ?? "anonymous"; - }, - }) - .validate([ - [({ invoker }) => invoker?.type === "machine_user", "Machine user required"], - ctx => ctx.invoker?.id !== "", - ]); - -const reviewer = t.string(); -const zodLike = { parse: (arg: unknown) => arg }; - -export const user = db - .type("User", { - role, - note: db.string(), - }) - .hooks({ - note: { - create: ({ invoker: currentUser }) => { - const audit = [{ user: { id: "data-user" } }].map(({ user }) => user.id); - return currentUser?.id ?? audit[0] ?? "anonymous"; - }, - update({ invoker: user }) { - const invoker = user?.id ?? "anonymous"; - return invoker; - }, - }, - }) - .validate({ - note: (ctx) => ctx.invoker?.type !== "machine_user", - fallback: ({ invoker: user = null }) => { - const labels = ["anonymous"].map((invoker) => invoker); - return user?.id !== labels[0]; - }, - typed: ({ invoker }: { invoker: TailorPrincipal | null }) => invoker?.id !== "", - }); - -export const parsed = t.string().parse({ - value: "hello", - data: {}, - invoker: null, -}); - -export const parsedLocal = reviewer.parse({ - value: "hello", - data: {}, - invoker: null, -}); - -export const parsedOther = zodLike.parse({ - user: null, -}); - -export function parseWithShadow(reviewer: { parse(arg: unknown): unknown }, user: unknown) { - return reviewer.parse({ user }); -} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts deleted file mode 100644 index d2e3c8ec8..000000000 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { db, t, type TailorUser } from "@tailor-platform/sdk"; -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; - -const role = db - .string() - .hooks({ - create: ({ value, user }) => (user?.attributes.role === "ADMIN" ? value : "user"), - update: ctx => ctx.user?.id ?? "anonymous", - delete({ user }) { - return user?.id ?? "anonymous"; - }, - }) - .validate([ - [({ user }) => user?.type === "machine_user", "Machine user required"], - ctx => ctx.user?.id !== "", - ]); - -const reviewer = t.string(); -const zodLike = { parse: (arg: unknown) => arg }; - -export const user = db - .type("User", { - role, - note: db.string(), - }) - .hooks({ - note: { - create: ({ user: currentUser }) => { - const audit = [{ user: { id: "data-user" } }].map(({ user }) => user.id); - return currentUser?.id ?? audit[0] ?? "anonymous"; - }, - update({ user }) { - const invoker = user?.id ?? "anonymous"; - return invoker; - }, - }, - }) - .validate({ - note: (ctx) => ctx.user?.type !== "machine_user", - fallback: ({ user = unauthenticatedTailorUser }) => { - const labels = ["anonymous"].map((invoker) => invoker); - return user?.id !== labels[0]; - }, - typed: ({ user }: { user: TailorUser | null }) => user?.id !== "", - }); - -export const parsed = t.string().parse({ - value: "hello", - data: {}, - user: unauthenticatedTailorUser, -}); - -export const parsedLocal = reviewer.parse({ - value: "hello", - data: {}, - user: unauthenticatedTailorUser, -}); - -export const parsedOther = zodLike.parse({ - user: unauthenticatedTailorUser, -}); - -export function parseWithShadow(reviewer: { parse(arg: unknown): unknown }, user: unknown) { - return reviewer.parse({ user }); -} diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 0b2c1d541..bcaae28bf 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -50,7 +50,7 @@ const allCodemods: CodemodPackage[] = [ id: "v2/principal-unify", name: "Unify TailorUser/TailorActor/TailorInvoker → TailorPrincipal", description: - "Rename TailorUser/TailorActor/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, rename resolver body `user` to `caller`, and rename TailorDB callback `user` to `invoker`", + "Rename TailorUser/TailorActor/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, and rename resolver body `user` to `caller`", since: "1.0.0", until: "2.0.0", scriptPath: "v2/principal-unify/scripts/transform.js", From b9f4d80da6f46c0e7d2e51a4a6491fc7dccef007 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 21:43:47 +0900 Subject: [PATCH 078/618] feat(codemod): add principal migration --- .../v2/principal-unify/scripts/transform.ts | 433 +++++++++++++++++- .../tests/shadowed-sdk-field/input.ts | 7 + .../tests/tailordb-callbacks/expected.ts | 64 +++ .../tests/tailordb-callbacks/input.ts | 65 +++ packages/sdk-codemod/src/registry.ts | 2 +- 5 files changed, 569 insertions(+), 2 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/shadowed-sdk-field/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index af16cd7f0..4dba0c173 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -9,7 +9,14 @@ const TYPE_RENAME_MAP: Record = { const UNAUTHENTICATED = "unauthenticatedTailorUser"; -const QUICK_FILTER_NEEDLES = [...Object.keys(TYPE_RENAME_MAP), UNAUTHENTICATED, "createResolver"]; +const QUICK_FILTER_NEEDLES = [ + ...Object.keys(TYPE_RENAME_MAP), + UNAUTHENTICATED, + "createResolver", + ".hooks", + ".validate", + ".parse", +]; function quickFilter(source: string): boolean { if (!source.includes("@tailor-platform/sdk")) return false; @@ -503,6 +510,417 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { } } +function findMemberCallName(call: SgNode): string | null { + const fn = call.field("function"); + if (!fn || fn.kind() !== "member_expression") return null; + const property = fn.field("property"); + return property?.text() ?? null; +} + +function findMemberCallObject(call: SgNode): SgNode | null { + const fn = call.field("function"); + if (!fn || fn.kind() !== "member_expression") return null; + return fn.field("object"); +} + +function isFunctionNode(node: SgNode): boolean { + return ( + node.kind() === "arrow_function" || + node.kind() === "function_expression" || + node.kind() === "method_definition" + ); +} + +function propertyName(node: SgNode): string | null { + return node.field("key")?.text() ?? node.field("name")?.text() ?? null; +} + +function getFirstFunctionParam(fn: SgNode): SgNode | null { + const params = + fn.field("parameters") ?? + fn.field("parameter") ?? + fn.children().find((c: SgNode) => c.kind() === "formal_parameters"); + if (!params) return null; + if (params.kind() === "object_pattern" || params.kind() === "identifier") { + return params; + } + + return ( + params + .children() + .find( + (c: SgNode) => + c.kind() === "required_parameter" || + c.kind() === "optional_parameter" || + c.kind() === "identifier" || + c.kind() === "object_pattern", + ) ?? null + ); +} + +function getFunctionParamPattern(param: SgNode): SgNode | null { + if (param.kind() === "object_pattern" || param.kind() === "identifier") return param; + return param.field("pattern"); +} + +function transformPrincipalCallbackParamType(param: SgNode, edits: Edit[]): void { + const typeAnnotation = param.field("type"); + const objectType = typeAnnotation?.children().find((c: SgNode) => c.kind() === "object_type"); + if (!objectType) return; + + for (const child of objectType.children()) { + if (child.kind() !== "property_signature") continue; + const name = child.field("name"); + if (name?.text() === "user") edits.push(name.replace("invoker")); + } +} + +function transformPrincipalCallbackParam(fn: SgNode, edits: Edit[]): void { + const param = getFirstFunctionParam(fn); + if (!param) return; + const pattern = getFunctionParamPattern(param); + const body = fn.field("body"); + if (!pattern || !body) return; + + if (pattern.kind() === "identifier") { + const ctxName = pattern.text(); + const ctxShadowRanges = collectCtxShadowRanges(body, ctxName, fn); + const propertyAccesses = body.findAll({ + rule: { kind: "property_identifier", regex: "^user$" }, + }); + for (const propId of propertyAccesses) { + const parent = propId.parent(); + if (!parent || parent.kind() !== "member_expression") continue; + const obj = parent.field("object"); + if (!(obj && obj.kind() === "identifier" && obj.text() === ctxName)) continue; + const pos = obj.range().start.index; + if (isInsideAnyRange(pos, ctxShadowRanges)) continue; + edits.push(propId.replace("invoker")); + } + + const ctxDestructures = body.findAll({ + rule: { + kind: "variable_declarator", + has: { + field: "value", + kind: "identifier", + regex: `^${escapeRegex(ctxName)}$`, + }, + }, + }); + for (const decl of ctxDestructures) { + const pos = decl.range().start.index; + if (isInsideAnyRange(pos, ctxShadowRanges)) continue; + const pat = decl.field("name"); + if (!pat || pat.kind() !== "object_pattern") continue; + for (const child of pat.children()) { + const kind = child.kind(); + if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { + edits.push(child.replace("invoker: user")); + } else if (kind === "pair_pattern") { + const key = child.field("key"); + if (key?.text() === "user") edits.push(key.replace("invoker")); + } + } + } + return; + } + + if (pattern.kind() !== "object_pattern") return; + + let hasUserParamProperty = false; + let renamesBinding = false; + for (const child of pattern.children()) { + const kind = child.kind(); + if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { + hasUserParamProperty = true; + renamesBinding = true; + } else if (kind === "pair_pattern") { + const key = child.field("key"); + if (key?.text() === "user") hasUserParamProperty = true; + } else if (kind === "object_assignment_pattern") { + const inner = child + .children() + .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); + if (inner?.text() === "user") { + hasUserParamProperty = true; + renamesBinding = true; + } + } + } + + if (!hasUserParamProperty) return; + if (renamesBinding && patternBindsName(pattern, "invoker")) return; + transformPrincipalCallbackParamType(param, edits); + + const aliasRenamedUser = renamesBinding && collectAllShadowRanges(body, "invoker").length > 0; + + let renamedShorthandUser = false; + for (const child of pattern.children()) { + const kind = child.kind(); + if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { + if (aliasRenamedUser) { + edits.push(child.replace("invoker: user")); + } else { + edits.push(child.replace("invoker")); + renamedShorthandUser = true; + } + } else if (kind === "pair_pattern") { + const key = child.field("key"); + if (key?.text() === "user") { + edits.push(key.replace("invoker")); + } + } else if (kind === "object_assignment_pattern") { + const inner = child + .children() + .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); + if (inner?.text() === "user") { + if (aliasRenamedUser) { + edits.push(inner.replace("invoker: user")); + } else { + edits.push(inner.replace("invoker")); + renamedShorthandUser = true; + } + } + } + } + + if (!renamedShorthandUser) return; + + const shadowRanges = collectAllShadowRanges(body, "user"); + const refs = body.findAll({ rule: { kind: "identifier", regex: "^user$" } }); + for (const ref of refs) { + const pos = ref.range().start.index; + if (isInsideAnyRange(pos, shadowRanges)) continue; + edits.push(ref.replace("invoker")); + } + + const shortRefs = body.findAll({ + rule: { kind: "shorthand_property_identifier", regex: "^user$" }, + }); + for (const ref of shortRefs) { + const pos = ref.range().start.index; + if (isInsideAnyRange(pos, shadowRanges)) continue; + edits.push(ref.replace("user: invoker")); + } +} + +interface SdkFieldLocalBinding { + name: string; + bindingStart: number; + scope: [number, number]; +} + +function rangeContains([start, end]: [number, number], pos: number): boolean { + return pos >= start && pos < end; +} + +function isShadowedLocalReference( + root: SgNode, + name: string, + pos: number, + bindingStart: number, +): boolean { + const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarators) { + const nameNode = decl.field("name"); + if (!nameNode || !patternBindsName(nameNode, name)) continue; + if (nameNode.range().start.index === bindingStart) continue; + const scope = enclosingScopeRange(decl); + if (scope && rangeContains(scope, pos)) return true; + } + + for (const kind of NESTED_FN_KINDS) { + const fns = root.findAll({ rule: { kind } }); + for (const fn of fns) { + if (!functionRebindsName(fn, name)) continue; + const range = fn.range(); + if (pos >= range.start.index && pos < range.end.index) return true; + } + } + + return false; +} + +function resolvesToSdkFieldLocal( + node: SgNode, + bindings: SdkFieldLocalBinding[], + root: SgNode, +): boolean { + if (node.kind() !== "identifier") return false; + const pos = node.range().start.index; + return bindings.some( + (binding) => + binding.name === node.text() && + rangeContains(binding.scope, pos) && + !isShadowedLocalReference(root, binding.name, pos, binding.bindingStart), + ); +} + +function isSdkFieldExpression( + node: SgNode, + sdkFieldRootNames: Set, + sdkFieldLocalBindings: SdkFieldLocalBinding[], + root: SgNode, +): boolean { + if (node.kind() === "identifier") { + const pos = node.range().start.index; + const isSdkRoot = + sdkFieldRootNames.has(node.text()) && + !isInsideAnyRange(pos, collectAllShadowRanges(root, node.text())); + return isSdkRoot || resolvesToSdkFieldLocal(node, sdkFieldLocalBindings, root); + } + if (node.kind() === "member_expression") { + const object = node.field("object"); + return object + ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalBindings, root) + : false; + } + if (node.kind() === "call_expression") { + const object = findMemberCallObject(node); + return object + ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalBindings, root) + : false; + } + return false; +} + +function collectSdkFieldLocalBindings( + root: SgNode, + sdkFieldRootNames: Set, +): SdkFieldLocalBinding[] { + const bindings: SdkFieldLocalBinding[] = []; + let changed = true; + while (changed) { + changed = false; + const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarators) { + const name = decl.field("name"); + const value = decl.field("value"); + if (!name || name.kind() !== "identifier" || !value) continue; + const bindingStart = name.range().start.index; + if (bindings.some((binding) => binding.bindingStart === bindingStart)) continue; + if (!isSdkFieldExpression(value, sdkFieldRootNames, bindings, root)) continue; + const rootRange = root.range(); + const scope = enclosingScopeRange(decl) ?? [rootRange.start.index, rootRange.end.index]; + bindings.push({ name: name.text(), bindingStart, scope }); + changed = true; + } + } + return bindings; +} + +function isSdkFieldMemberCall( + call: SgNode, + sdkFieldRootNames: Set, + sdkFieldLocalBindings: SdkFieldLocalBinding[], + root: SgNode, +): boolean { + const object = findMemberCallObject(call); + return object + ? isSdkFieldExpression(object, sdkFieldRootNames, sdkFieldLocalBindings, root) + : false; +} + +function transformHookCallbackObject(node: SgNode, edits: Edit[]): void { + if (node.kind() !== "object") return; + for (const child of node.children()) { + if (child.kind() === "method_definition") { + const key = propertyName(child); + if (key === "create" || key === "update") transformPrincipalCallbackParam(child, edits); + continue; + } + if (child.kind() !== "pair") continue; + const key = child.field("key")?.text(); + const value = child.field("value"); + if (!value) continue; + if ((key === "create" || key === "update") && isFunctionNode(value)) { + transformPrincipalCallbackParam(value, edits); + } else if (value.kind() === "object") { + transformHookCallbackObject(value, edits); + } + } +} + +function transformValidateCallbackNode(node: SgNode, edits: Edit[]): void { + if (isFunctionNode(node)) { + transformPrincipalCallbackParam(node, edits); + return; + } + + if (node.kind() === "array") { + for (const child of node.children()) { + if (isFunctionNode(child)) { + transformPrincipalCallbackParam(child, edits); + } else if (child.kind() === "array") { + transformValidateCallbackNode(child, edits); + } + } + return; + } + + if (node.kind() !== "object") return; + for (const child of node.children()) { + if (child.kind() === "method_definition") { + transformPrincipalCallbackParam(child, edits); + continue; + } + if (child.kind() !== "pair") continue; + const value = child.field("value"); + if (value) transformValidateCallbackNode(value, edits); + } +} + +function transformPrincipalCallbacksInCall( + call: SgNode, + edits: Edit[], + sdkFieldRootNames: Set, + sdkFieldLocalBindings: SdkFieldLocalBinding[], + root: SgNode, +): void { + const memberName = findMemberCallName(call); + if (memberName !== "hooks" && memberName !== "validate") return; + if (!isSdkFieldMemberCall(call, sdkFieldRootNames, sdkFieldLocalBindings, root)) return; + + const args = call.field("arguments"); + if (!args) return; + if (memberName === "hooks") { + const objArg = args.children().find((c: SgNode) => c.kind() === "object"); + if (objArg) transformHookCallbackObject(objArg, edits); + return; + } + + for (const arg of args.children()) { + transformValidateCallbackNode(arg, edits); + } +} + +function transformParseArgsObject( + call: SgNode, + edits: Edit[], + sdkFieldRootNames: Set, + sdkFieldLocalBindings: SdkFieldLocalBinding[], + root: SgNode, +): void { + const memberName = findMemberCallName(call); + if (memberName !== "parse") return; + if (!isSdkFieldMemberCall(call, sdkFieldRootNames, sdkFieldLocalBindings, root)) return; + + const args = call.field("arguments"); + const objArg = args?.children().find((c: SgNode) => c.kind() === "object"); + if (!objArg) return; + + for (const child of objArg.children()) { + const kind = child.kind(); + if (kind === "pair") { + const key = child.field("key"); + if (key?.text() === "user") edits.push(key.replace("invoker")); + } else if (kind === "shorthand_property_identifier" && child.text() === "user") { + edits.push(child.replace("invoker: user")); + } + } +} + /** * Migrate user/actor/invoker types and identifiers to the unified TailorPrincipal. * @@ -516,6 +934,8 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { * handles aliased pairs (`{ user: currentUser }`) by rewriting only the property name, and * rewrites `.user` for non-destructured single-param bodies — respecting variable * shadowing in both directions. + * - Renames TailorDB hook/validator callback `user` parameters to `invoker`, and rewrites + * `.parse({ user: ... })` arguments to `.parse({ invoker: ... })`. * @param source - TypeScript source text. * @returns Transformed source or null when nothing matched. */ @@ -595,11 +1015,15 @@ export default function transform(source: string): string | null { // and unrelated local helpers named `createResolver` (when the SDK import // does not actually bring `createResolver` in) are not. const createResolverLocalNames = new Set(); + const sdkFieldRootNames = new Set(); for (const importStmt of sdkImports) { for (const { importedName, localName } of iterateImportSpecs(importStmt)) { if (importedName === "createResolver") { createResolverLocalNames.add(localName); } + if (importedName === "t" || importedName === "db") { + sdkFieldRootNames.add(localName); + } } } for (const localName of createResolverLocalNames) { @@ -624,6 +1048,13 @@ export default function transform(source: string): string | null { } } + const sdkFieldLocalBindings = collectSdkFieldLocalBindings(tree, sdkFieldRootNames); + const memberCalls = tree.findAll({ rule: { kind: "call_expression" } }); + for (const call of memberCalls) { + transformPrincipalCallbacksInCall(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); + transformParseArgsObject(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); + } + if (edits.length === 0) return null; let result = tree.commitEdits(edits); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadowed-sdk-field/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadowed-sdk-field/input.ts new file mode 100644 index 000000000..11951b8a7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadowed-sdk-field/input.ts @@ -0,0 +1,7 @@ +import { db } from "@tailor-platform/sdk"; + +export function buildField(db: { string(): { hooks(config: unknown): unknown } }) { + return db.string().hooks({ + create: ({ user }: { user: { id: string } }) => user.id, + }); +} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts new file mode 100644 index 000000000..5a986f860 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -0,0 +1,64 @@ +import { db, t, type TailorPrincipal } from "@tailor-platform/sdk"; + +const role = db + .string() + .hooks({ + create: ({ value, invoker }) => (invoker?.attributes.role === "ADMIN" ? value : "user"), + update: ctx => ctx.invoker?.id ?? "anonymous", + delete({ user }) { + return user?.id ?? "anonymous"; + }, + }) + .validate([ + [({ invoker }) => invoker?.type === "machine_user", "Machine user required"], + ctx => ctx.invoker?.id !== "", + ]); + +const reviewer = t.string(); +const zodLike = { parse: (arg: unknown) => arg }; + +export const user = db + .type("User", { + role, + note: db.string(), + }) + .hooks({ + note: { + create: ({ invoker: currentUser }) => { + const audit = [{ user: { id: "data-user" } }].map(({ user }) => user.id); + return currentUser?.id ?? audit[0] ?? "anonymous"; + }, + update({ invoker: user }) { + const invoker = user?.id ?? "anonymous"; + return invoker; + }, + }, + }) + .validate({ + note: (ctx) => ctx.invoker?.type !== "machine_user", + fallback: ({ invoker: user = null }) => { + const labels = ["anonymous"].map((invoker) => invoker); + return user?.id !== labels[0]; + }, + typed: ({ invoker }: { invoker: TailorPrincipal | null }) => invoker?.id !== "", + }); + +export const parsed = t.string().parse({ + value: "hello", + data: {}, + invoker: null, +}); + +export const parsedLocal = reviewer.parse({ + value: "hello", + data: {}, + invoker: null, +}); + +export const parsedOther = zodLike.parse({ + user: null, +}); + +export function parseWithShadow(reviewer: { parse(arg: unknown): unknown }, user: unknown) { + return reviewer.parse({ user }); +} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts new file mode 100644 index 000000000..d2e3c8ec8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -0,0 +1,65 @@ +import { db, t, type TailorUser } from "@tailor-platform/sdk"; +import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; + +const role = db + .string() + .hooks({ + create: ({ value, user }) => (user?.attributes.role === "ADMIN" ? value : "user"), + update: ctx => ctx.user?.id ?? "anonymous", + delete({ user }) { + return user?.id ?? "anonymous"; + }, + }) + .validate([ + [({ user }) => user?.type === "machine_user", "Machine user required"], + ctx => ctx.user?.id !== "", + ]); + +const reviewer = t.string(); +const zodLike = { parse: (arg: unknown) => arg }; + +export const user = db + .type("User", { + role, + note: db.string(), + }) + .hooks({ + note: { + create: ({ user: currentUser }) => { + const audit = [{ user: { id: "data-user" } }].map(({ user }) => user.id); + return currentUser?.id ?? audit[0] ?? "anonymous"; + }, + update({ user }) { + const invoker = user?.id ?? "anonymous"; + return invoker; + }, + }, + }) + .validate({ + note: (ctx) => ctx.user?.type !== "machine_user", + fallback: ({ user = unauthenticatedTailorUser }) => { + const labels = ["anonymous"].map((invoker) => invoker); + return user?.id !== labels[0]; + }, + typed: ({ user }: { user: TailorUser | null }) => user?.id !== "", + }); + +export const parsed = t.string().parse({ + value: "hello", + data: {}, + user: unauthenticatedTailorUser, +}); + +export const parsedLocal = reviewer.parse({ + value: "hello", + data: {}, + user: unauthenticatedTailorUser, +}); + +export const parsedOther = zodLike.parse({ + user: unauthenticatedTailorUser, +}); + +export function parseWithShadow(reviewer: { parse(arg: unknown): unknown }, user: unknown) { + return reviewer.parse({ user }); +} diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index bcaae28bf..0b2c1d541 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -50,7 +50,7 @@ const allCodemods: CodemodPackage[] = [ id: "v2/principal-unify", name: "Unify TailorUser/TailorActor/TailorInvoker → TailorPrincipal", description: - "Rename TailorUser/TailorActor/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, and rename resolver body `user` to `caller`", + "Rename TailorUser/TailorActor/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, rename resolver body `user` to `caller`, and rename TailorDB callback `user` to `invoker`", since: "1.0.0", until: "2.0.0", scriptPath: "v2/principal-unify/scripts/transform.js", From 3ee872b2dfdbc31a98a635c96bb5bce70c31c8a4 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 21:48:04 +0900 Subject: [PATCH 079/618] refactor(sdk): remove legacy generator pipeline --- AGENTS.md | 2 +- packages/sdk/src/cli/commands/api.test.ts | 6 - .../src/cli/commands/generate/service.test.ts | 757 +++--------------- .../sdk/src/cli/commands/generate/service.ts | 359 +-------- .../src/cli/commands/generate/types.test.ts | 155 ---- .../sdk/src/cli/commands/generate/types.ts | 323 -------- packages/sdk/src/cli/lib.ts | 20 +- packages/sdk/src/cli/shared/config-loader.ts | 8 +- .../src/types/generator-config.generated.ts | 41 - packages/sdk/src/types/generator-config.ts | 8 - 10 files changed, 120 insertions(+), 1559 deletions(-) delete mode 100644 packages/sdk/src/cli/commands/generate/types.test.ts delete mode 100644 packages/sdk/src/cli/commands/generate/types.ts delete mode 100644 packages/sdk/src/types/generator-config.generated.ts diff --git a/AGENTS.md b/AGENTS.md index 606b9f5df..5c9a17eea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -108,7 +108,7 @@ Args include `event` (short name like `"created"`) and `rawEvent` (full event ty ### Plugins -`definePlugins()` takes plugin instances as rest arguments (see `example/tailor.config.ts`). The `kyselyTypePlugin` from `@tailor-platform/sdk/plugin/kysely-type` is required for `getDB()` in resolvers/executors/workflows. `defineGenerators()` is deprecated — use `definePlugins()` instead. +`definePlugins()` takes plugin instances as rest arguments (see `example/tailor.config.ts`). The `kyselyTypePlugin` from `@tailor-platform/sdk/plugin/kysely-type` is required for `getDB()` in resolvers/executors/workflows. ### Configuration diff --git a/packages/sdk/src/cli/commands/api.test.ts b/packages/sdk/src/cli/commands/api.test.ts index e9c4ed541..5423a4cb9 100644 --- a/packages/sdk/src/cli/commands/api.test.ts +++ b/packages/sdk/src/cli/commands/api.test.ts @@ -96,7 +96,6 @@ describe("api command body auto-injection", () => { auth: { name: "my-auth" }, path: "/fake", }, - generators: [], plugins: [], } as never); @@ -116,7 +115,6 @@ describe("api command body auto-injection", () => { db: { "my-db": { files: [] } }, path: "/fake", }, - generators: [], plugins: [], } as never); @@ -135,7 +133,6 @@ describe("api command body auto-injection", () => { db: { "db-1": { files: [] }, "db-2": { files: [] } }, path: "/fake", }, - generators: [], plugins: [], } as never); @@ -154,7 +151,6 @@ describe("api command body auto-injection", () => { idp: [{ name: "my-idp" }], path: "/fake", }, - generators: [], plugins: [], } as never); @@ -173,7 +169,6 @@ describe("api command body auto-injection", () => { resolver: { "my-pipeline": { files: [] } }, path: "/fake", }, - generators: [], plugins: [], } as never); @@ -192,7 +187,6 @@ describe("api command body auto-injection", () => { auth: { name: "my-auth" }, path: "/fake", }, - generators: [], plugins: [], } as never); diff --git a/packages/sdk/src/cli/commands/generate/service.test.ts b/packages/sdk/src/cli/commands/generate/service.test.ts index b99222f08..cc7a37336 100644 --- a/packages/sdk/src/cli/commands/generate/service.test.ts +++ b/packages/sdk/src/cli/commands/generate/service.test.ts @@ -3,19 +3,14 @@ import * as os from "node:os"; import * as path from "pathe"; import { describe, expect, test, beforeEach, afterEach, vi, afterAll } from "vitest"; import { defineApplication } from "@/cli/services/application"; -import { createResolver } from "@/configure/services/resolver/resolver"; -import { db } from "@/configure/services/tailordb/schema"; -import { t } from "@/configure/types"; -import { parseTypes } from "@/parser/service/tailordb"; -import { toSchemaOutputs } from "@/utils/test/internal"; +import { PluginManager } from "@/plugin/manager"; import { createGenerationManager } from "./service"; import type { Application } from "@/cli/services/application"; import type { TailorDBService } from "@/cli/services/tailordb/service"; -import type { LoadedConfig, Generator } from "@/cli/shared/config-loader"; -import type { TailorDBType } from "@/configure/services/tailordb/schema"; -import type { Resolver } from "@/types/resolver.generated"; +import type { LoadedConfig } from "@/cli/shared/config-loader"; +import type { Plugin } from "@/types/plugin"; +import type { TailorDBType } from "@/types/tailordb"; -// ESM-safe explicit mock for Node's fs vi.mock("node:fs", () => { return { writeFile: vi.fn((_, _2, callback) => { @@ -51,55 +46,16 @@ vi.mock("@/cli/shared/logger", async (importOriginal) => { }; }); -class TestGenerator { - readonly id = "test-generator"; - readonly description = "Test generator for unit tests"; - readonly dependencies = ["tailordb", "resolver", "executor"] as const; - - async processType(args: { - type: TailorDBType; - namespace: string; - source: { filePath: string; exportName: string }; - }) { - return { name: args.type.name, processed: true, source: args.source }; - } - - async processResolver(args: { resolver: Resolver; namespace: string }) { - return { name: args.resolver.name, processed: true }; - } - - async processExecutor(executor: { name: T }) { - return { name: executor.name, processed: true }; - } - - async processTailorDBNamespace(args: { namespace: string; types: Record }) { - return { processed: true, count: Object.keys(args.types).length }; - } - - async processResolverNamespace(args: { namespace: string; resolvers: Record }) { - return { processed: true, count: Object.keys(args.resolvers).length }; - } - - async aggregate(args: { input: object; baseDir: string }) { - return { - files: [ - { - path: path.join(args.baseDir, "test-output.txt"), - content: `Input: ${JSON.stringify(args.input)}`, - }, - ], - }; - } -} - function loadedTailorDBService(namespace: string, typeNames: string[]): TailorDBService { - const types = Object.fromEntries(typeNames.map((typeName) => [typeName, {}])); + const types = Object.fromEntries( + typeNames.map((typeName) => [typeName, { name: typeName } as TailorDBType]), + ); const typeSourceInfo = Object.fromEntries( typeNames.map((typeName) => [ typeName, { filePath: `${namespace}/${typeName}.ts`, - exportName: typeName.toLowerCase(), + exportName: typeName, }, ]), ); @@ -127,9 +83,6 @@ function applicationWithTailorDBServices( describe("GenerationManager", () => { let tempDir: string; - // For test-only access to private members - // oxlint-disable-next-line no-explicit-any - let manager: any; let mockConfig: LoadedConfig; afterAll(() => { @@ -145,81 +98,96 @@ describe("GenerationManager", () => { db: { main: { files: ["src/types/*.ts"] } }, resolver: { main: { files: ["src/resolvers/*.ts"] } }, }; - - // for minimal mock - const application = defineApplication({ config: mockConfig }); - manager = createGenerationManager({ - application, - config: mockConfig, - // oxlint-disable-next-line no-explicit-any - generators: [new TestGenerator()] as any, - }); }); afterEach(() => { + vi.mocked(fs.writeFile).mockClear(); + vi.mocked(fs.mkdirSync).mockClear(); if (fs.existsSync(tempDir)) { fs.rmSync(tempDir, { recursive: true, force: true }); } }); describe("constructor", () => { - test("initializes correctly", () => { - expect(manager.application).toBeDefined(); + test("initializes without legacy generator API", () => { + const application = defineApplication({ config: mockConfig }); + const manager = createGenerationManager({ + application, + config: mockConfig, + }); + + expect(manager.application).toBe(application); expect(manager.baseDir).toContain("generated"); + expect(manager.services).toEqual({ tailordb: {}, resolver: {}, executor: {} }); + expect("generators" in manager).toBe(false); + expect("generatorResults" in manager).toBe(false); + expect("processGenerator" in manager).toBe(false); }); - test("base directory is created", () => { + test("creates base directory", () => { + const application = defineApplication({ config: mockConfig }); + createGenerationManager({ + application, + config: mockConfig, + }); + expect(fs.mkdirSync).toHaveBeenCalledWith(expect.stringContaining("generated"), { recursive: true, }); }); }); - describe("generators", () => { - test("generators are passed correctly", () => { - expect(manager.generators.length).toBeGreaterThan(0); - }); - - test("receives custom generator", () => { - const customApp = defineApplication({ config: mockConfig }); - // For test-only - TestGenerator doesn't have brand symbol - // oxlint-disable-next-line no-explicit-any - const managerWithCustom: any = createGenerationManager({ - application: customApp, + describe("generate", () => { + test("executes complete generation process", async () => { + const application = defineApplication({ config: mockConfig }); + const manager = createGenerationManager({ + application, config: mockConfig, - generators: [new TestGenerator()] as unknown as Generator[], }); - expect( - managerWithCustom.generators.some((gen: { id: string }) => gen.id === "test-generator"), - ).toBe(true); - }); - }); - describe("generate", () => { - test("executes complete generation process", async () => { await manager.generate(false); - // Generators are configured but may be 0 if actual type files do not exist - expect(manager.generators.length).toBeGreaterThan(0); - // services will be empty if actual files do not exist expect(manager.services).toBeDefined(); }); - test("processes single application", async () => { - const singleAppConfig = { - ...mockConfig, - name: "single-app", + test("runs plugin generation hooks after TailorDB load", async () => { + const onTailorDBReady = vi.fn().mockResolvedValue({ + files: [{ path: path.join(tempDir, "generated.txt"), content: "generated" }], + }); + const plugin: Plugin = { + id: "test-plugin", + description: "Test plugin", + onTailorDBReady, }; - // For test-only access to private members - const singleApp = defineApplication({ config: singleAppConfig }); - // oxlint-disable-next-line no-explicit-any - const singleAppManager: any = createGenerationManager({ - application: singleApp, - config: singleAppConfig, + const application = applicationWithTailorDBServices(mockConfig, [ + loadedTailorDBService("main", ["User"]), + ]); + const pluginManager = new PluginManager([plugin]); + const manager = createGenerationManager({ + application, + config: mockConfig, + pluginManager, }); - await singleAppManager.generate(false); - expect(singleAppManager.services).toBeDefined(); + await manager.generate(false); + + expect(onTailorDBReady).toHaveBeenCalledWith( + expect.objectContaining({ + tailordb: [ + expect.objectContaining({ + namespace: "main", + types: expect.objectContaining({ User: expect.objectContaining({ name: "User" }) }), + }), + ], + baseDir: expect.stringContaining("test-plugin"), + configPath: "tailor.config.ts", + }), + ); + expect(fs.writeFile).toHaveBeenCalledWith( + path.join(tempDir, "generated.txt"), + "generated", + expect.any(Function), + ); }); test("rejects duplicate TailorDB type names between namespaces", async () => { @@ -251,584 +219,41 @@ describe("GenerationManager", () => { }); }); - describe("runGenerators (via generate)", () => { - beforeEach(async () => { - const types = { - testType: db.type("TestType", {}), - }; - const parsedTypes = parseTypes( - toSchemaOutputs({ TestType: types.testType }), - "test-namespace", - {}, - ); - - manager.services = { - tailordb: { - "test-namespace": { - types: parsedTypes, - sourceInfo: {}, - }, - }, - resolver: { - "test-namespace": { - testResolver: createResolver({ - name: "testResolver", - operation: "query", - // input removed - body: () => ({ string: "" }), - output: t.object({ string: t.string() }), - }), - }, - }, - executor: {}, - }; - }); - - test("processes all generators through generate method", async () => { - // Spy on the generator's aggregate method to verify it was called - const testGenerator = manager.generators[0]; - using aggregateSpy = vi.spyOn(testGenerator, "aggregate"); - - // Use generate method which orchestrates all generator processing - await manager.generate(false); - - // Should process the generator by calling its aggregate method - expect(aggregateSpy).toHaveBeenCalled(); - }); - - test("errors in generator processing do not affect others", async () => { - const errorGenerator = { - id: "error-generator", - description: "Error generator", - dependencies: ["tailordb", "resolver", "executor"] as const, - processType: vi - .fn() - .mockImplementation(() => Promise.reject(new Error("Type processing error"))), - processResolver: vi - .fn() - .mockImplementation(() => Promise.reject(new Error("Resolver processing error"))), - processExecutor: vi - .fn() - .mockImplementation(() => Promise.reject(new Error("Executor processing error"))), - aggregate: vi.fn().mockImplementation(() => Promise.resolve({ files: [] })), - }; - - manager.generators.push(errorGenerator); - - // Verify that processing continues even if an error occurs - // Use generate method to trigger processing - await manager.generate(false); - - // After generate runs, the error generator's methods should have been called - // The test validates that errors don't prevent the generate from completing - expect(errorGenerator.aggregate).toHaveBeenCalled(); - }); - }); - - describe("processGenerator", () => { - let testGenerator: TestGenerator; - - beforeEach(() => { - testGenerator = new TestGenerator(); - manager.generators = [testGenerator]; - - const types = { - testType: db.type("TestType", {}), - }; - const parsedTypes = parseTypes( - toSchemaOutputs({ TestType: types.testType }), - "test-namespace", - {}, - ); - - // Modify existing object instead of reassigning (closure pattern) - manager.services.tailordb["test-namespace"] = { - types: parsedTypes, - sourceInfo: {}, - pluginAttachments: new Map(), - }; - manager.services.resolver["test-namespace"] = { - testResolver: createResolver({ - name: "testResolver", - operation: "query", - // input removed - body: () => ({ string: "" }), - output: t.object({ string: t.string() }), - }), - }; - }); - - test("complete processing of single generator", async () => { - // Clear existing generatorResults (closure pattern - must not reassign) - Object.keys(manager.generatorResults).forEach((key) => { - delete manager.generatorResults[key]; - }); - - // Spy on the generator's methods to verify they were called - using processTypeSpy = vi.spyOn(testGenerator, "processType"); - using processResolverSpy = vi.spyOn(testGenerator, "processResolver"); - using aggregateSpy = vi.spyOn(testGenerator, "aggregate"); - - await manager.processGenerator(testGenerator); - - // Verify generator methods were called during processing - expect(processTypeSpy).toHaveBeenCalled(); - expect(processResolverSpy).toHaveBeenCalled(); - expect(aggregateSpy).toHaveBeenCalled(); - }); - - test("types and resolvers are processed in parallel", async () => { - // Clear existing generatorResults (closure pattern - must not reassign) - Object.keys(manager.generatorResults).forEach((key) => { - delete manager.generatorResults[key]; - }); - - const start = Date.now(); - await manager.processGenerator(testGenerator); - const duration = Date.now() - start; - - expect(duration).toBeLessThan(1000); - }); - }); - - describe("processTailorDBNamespace", () => { - let testGenerator: TestGenerator; - - beforeEach(() => { - testGenerator = new TestGenerator(); - // Modify the existing object instead of reassigning (closure pattern) - manager.generatorResults[testGenerator.id] = { - tailordbResults: {}, - resolverResults: {}, - tailordbNamespaceResults: {}, - resolverNamespaceResults: {}, - executorResults: {}, - }; - }); - - test("processes all types", async () => { - using processTypeSpy = vi.spyOn(testGenerator, "processType"); - const types = { - type1: db.type("Type1", {}), - type2: db.type("Type2", {}), - type3: db.type("Type3", {}), - }; - - const parsedTypes = parseTypes( - toSchemaOutputs({ Type1: types.type1, Type2: types.type2, Type3: types.type3 }), - "test-namespace", - {}, - ); - - // Initialize generatorResults - manager.generatorResults[testGenerator.id] = { - tailordbResults: {}, - resolverResults: {}, - tailordbNamespaceResults: {}, - resolverNamespaceResults: {}, - executorResults: {}, - }; - - await manager.processTailorDBNamespace(testGenerator, "test-namespace", { - types: parsedTypes, - sourceInfo: {}, - pluginAttachments: new Map(), - }); - - expect(processTypeSpy).toHaveBeenCalledTimes(3); - expect( - manager.generatorResults[testGenerator.id].tailordbResults["test-namespace"], - ).toBeDefined(); - expect( - Object.keys(manager.generatorResults[testGenerator.id].tailordbResults["test-namespace"]), - ).toHaveLength(3); - }); - - test("does not error with empty types", async () => { - // Initialize generatorResults - manager.generatorResults[testGenerator.id] = { - tailordbResults: {}, - resolverResults: {}, - tailordbNamespaceResults: {}, - resolverNamespaceResults: {}, - executorResults: {}, - }; - - await expect( - manager.processTailorDBNamespace(testGenerator, "test-namespace", { - types: {}, - sourceInfo: {}, - pluginAttachments: new Map(), - }), - ).resolves.not.toThrow(); - }); - - test("sourceInfo is correctly passed to processType", async () => { - using processTypeSpy = vi.spyOn(testGenerator, "processType"); - const types = { - TestType: db.type("TestType", {}), - }; - - const sourceInfo = { - TestType: { - filePath: "test.ts", - exportName: "TestType", - }, - }; - const parsedTypes = parseTypes( - toSchemaOutputs({ TestType: types.TestType }), - "test-namespace", - sourceInfo, - ); - - // Initialize generatorResults - manager.generatorResults[testGenerator.id] = { - tailordbResults: {}, - resolverResults: {}, - tailordbNamespaceResults: {}, - resolverNamespaceResults: {}, - executorResults: {}, - }; - - await manager.processTailorDBNamespace(testGenerator, "test-namespace", { - types: parsedTypes, - sourceInfo, - pluginAttachments: new Map(), - }); - - expect(processTypeSpy).toHaveBeenCalledWith( - expect.objectContaining({ - type: expect.any(Object), - namespace: "test-namespace", - source: expect.objectContaining({ - filePath: "test.ts", - exportName: "TestType", - }), - plugins: [], - }), - ); - }); - }); - - describe("processResolverNamespace", () => { - let testGenerator: TestGenerator; - - beforeEach(() => { - testGenerator = new TestGenerator(); - // Modify the existing object instead of reassigning (closure pattern) - manager.generatorResults[testGenerator.id] = { - tailordbResults: {}, - resolverResults: {}, - tailordbNamespaceResults: {}, - resolverNamespaceResults: {}, - executorResults: {}, - }; - }); - - test("processes all resolvers", async () => { - using processResolverSpy = vi.spyOn(testGenerator, "processResolver"); - const resolvers = { - resolver1: createResolver({ - name: "resolver1", - operation: "query", - // input removed - body: () => ({ string: "" }), - output: t.object({ string: t.string() }), - }), - resolver2: createResolver({ - name: "resolver2", - operation: "query", - // input removed - body: () => ({ string: "" }), - output: t.object({ string: t.string() }), - }), - }; - - await manager.processResolverNamespace(testGenerator, "test-namespace", resolvers); - - expect(processResolverSpy).toHaveBeenCalledTimes(2); - expect( - manager.generatorResults[testGenerator.id].resolverResults["test-namespace"], - ).toBeDefined(); - expect( - Object.keys(manager.generatorResults[testGenerator.id].resolverResults["test-namespace"]), - ).toHaveLength(2); - }); - }); - - describe("aggregate", () => { - let testGenerator: TestGenerator; - - beforeEach(() => { - testGenerator = new TestGenerator(); - // Modify the existing object instead of reassigning (closure pattern) - manager.generatorResults[testGenerator.id] = { - tailordbResults: {}, - resolverResults: {}, - tailordbNamespaceResults: { - "test-namespace": { types: "processed" }, - }, - resolverNamespaceResults: { - "test-namespace": { resolvers: "processed" }, - }, - executorResults: {}, - }; - }); - - test("calls generator aggregate method", async () => { - using aggregateSpy = vi.spyOn(testGenerator, "aggregate"); - - await manager.aggregate(testGenerator); - - expect(aggregateSpy).toHaveBeenCalledWith({ - input: { - tailordb: [ - { - namespace: "test-namespace", - types: { types: "processed" }, - }, - ], - resolver: [ - { - namespace: "test-namespace", - resolvers: { resolvers: "processed" }, - }, - ], - executor: [], - auth: undefined, - }, - baseDir: expect.stringContaining(testGenerator.id), - configPath: expect.any(String), - }); - }); - - test("writes files correctly", async () => { - await manager.aggregate(testGenerator); - - expect(fs.writeFile).toHaveBeenCalled(); - expect(fs.mkdirSync).toHaveBeenCalled(); - }); - - test("parallel writing of multiple files", async () => { - // Clear previous calls - vi.mocked(fs.writeFile).mockClear(); - - const multiFileGenerator = { - id: testGenerator.id, - description: testGenerator.description, - dependencies: testGenerator.dependencies, - aggregate: vi.fn().mockResolvedValue({ - files: [ - { path: "/test/file1.txt", content: "content1" }, - { path: "/test/file2.txt", content: "content2" }, - { path: "/test/file3.txt", content: "content3" }, - ], - }), - }; - - // Modify existing object instead of reassigning (closure pattern) - manager.generatorResults[multiFileGenerator.id] = { - tailordbResults: {}, - resolverResults: {}, - tailordbNamespaceResults: {}, - resolverNamespaceResults: {}, - executorResults: {}, - }; - - await manager.aggregate(multiFileGenerator); - - expect(fs.writeFile).toHaveBeenCalledTimes(3); - }); - - test("handles file write errors", async () => { - const writeFileError = new Error("Write permission denied"); - vi.mocked(fs.writeFile).mockImplementationOnce((_path, _content, callback) => { - callback(writeFileError); - }); - - const errorGenerator = { - id: testGenerator.id, - description: testGenerator.description, - dependencies: testGenerator.dependencies, - aggregate: vi.fn().mockResolvedValue({ - files: [{ path: "/test/error.txt", content: "content" }], - }), - }; - - // Modify existing object instead of reassigning (closure pattern) - manager.generatorResults[errorGenerator.id] = { - tailordbResults: {}, - resolverResults: {}, - tailordbNamespaceResults: {}, - resolverNamespaceResults: {}, - executorResults: {}, - }; - - await expect(manager.aggregate(errorGenerator)).rejects.toThrow("Write permission denied"); - }); - }); - describe("watch", () => { test("watch method exists", () => { + const application = defineApplication({ config: mockConfig }); + const manager = createGenerationManager({ + application, + config: mockConfig, + }); + expect(typeof manager.watch).toBe("function"); }); test("application has tailorDBServices for watch", () => { + const application = defineApplication({ config: mockConfig }); + const manager = createGenerationManager({ + application, + config: mockConfig, + }); + expect(manager.application.tailorDBServices).toBeDefined(); expect(manager.application.tailorDBServices.length).toBeGreaterThan(0); - expect(manager.application.tailorDBServices[0].namespace).toBe("main"); - expect(manager.application.tailorDBServices[0].config.files).toEqual(["src/types/*.ts"]); + expect(manager.application.tailorDBServices[0]?.namespace).toBe("main"); + expect(manager.application.tailorDBServices[0]?.config.files).toEqual(["src/types/*.ts"]); }); test("application has resolverServices for watch", () => { - expect(manager.application.resolverServices).toBeDefined(); - expect(manager.application.resolverServices.length).toBeGreaterThan(0); - expect(manager.application.resolverServices[0].namespace).toBe("main"); - expect(manager.application.resolverServices[0].config.files).toEqual(["src/resolvers/*.ts"]); - }); - }); -}); - -describe("generate function", () => { - let mockConfig: LoadedConfig; - - beforeEach(() => { - mockConfig = { - name: "test-workspace", - path: "tailor.config.ts", - }; - }); - - test("generate does not automatically call watch", async () => { - const app = defineApplication({ config: mockConfig }); - const manager = createGenerationManager({ application: app, config: mockConfig }); - await expect(manager.generate(false)).resolves.not.toThrow(); - expect(manager.application).toBeDefined(); - }); -}); - -describe("Integration Tests", () => { - let tempDir: string; - let fullConfig: LoadedConfig; - - beforeEach(async () => { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "integration-test-")); - - fullConfig = { - name: "testApp", - path: "tailor.config.ts", - db: { - main: { - files: [path.join(tempDir, "types/*.ts")], - }, - }, - resolver: { - main: { - files: [path.join(tempDir, "resolvers/*.ts")], - }, - }, - }; - }); - - afterEach(() => { - if (fs.existsSync(tempDir)) { - fs.rmSync(tempDir, { recursive: true, force: true }); - } - }); - - test("complete integration test with multiple generators", async () => { - const gen1 = new TestGenerator(); - const gen2 = new TestGenerator(); - // For test-only access to private members - // oxlint-disable-next-line no-explicit-any - const generators: any[] = [gen1, gen2]; - const integrationApp = defineApplication({ config: fullConfig }); - // oxlint-disable-next-line no-explicit-any - const manager: any = createGenerationManager({ - application: integrationApp, - config: fullConfig, - generators, - }); - - await expect(manager.generate(false)).resolves.not.toThrow(); - - expect(manager.generators.length).toBe(2); - expect(manager.generators.every((g: unknown) => g instanceof TestGenerator)).toBe(true); - }); - - test("integration test for error recovery and performance", async () => { - const errorApp = defineApplication({ config: fullConfig }); - const manager = createGenerationManager({ application: errorApp, config: fullConfig }); - - const start = Date.now(); - await manager.generate(false); - const duration = Date.now() - start; - - expect(duration).toBeLessThan(5000); - }); - - describe("Memory Management", () => { - test("no memory leak with large data processing", async () => { - // For test-only - TestGenerator doesn't have brand symbol - // oxlint-disable-next-line no-explicit-any - const largeGenerators: any[] = Array(10) - .fill(0) - .map(() => new TestGenerator()); - - // For test-only access to private members - const memApp = defineApplication({ config: fullConfig }); - // oxlint-disable-next-line no-explicit-any - const manager: any = createGenerationManager({ - application: memApp, - config: fullConfig, - generators: largeGenerators, + const application = defineApplication({ config: mockConfig }); + const manager = createGenerationManager({ + application, + config: mockConfig, }); - // Create large application data structure - manager.services = { - tailordb: {}, - resolver: {}, - executor: {}, - }; - - // Add multiple namespaces - Array(10) - .fill(0) - .forEach((_, nsIdx) => { - const namespace = `namespace-${nsIdx}`; - - // Add types to namespace - const types: Record = {}; - Array(50) - .fill(0) - .forEach((_, typeIdx) => { - types[`Type${nsIdx}_${typeIdx}`] = db.type(`Type${nsIdx}_${typeIdx}`, {}); - }); - - const parsedTypes = parseTypes(toSchemaOutputs(types), namespace, {}); - - manager.services.tailordb[namespace] = { - types: parsedTypes, - sourceInfo: {}, - }; - - // Add resolvers to namespace - manager.services.resolver[namespace] = {}; - Array(10) - .fill(0) - .forEach((_, resolverIdx) => { - manager.services.resolver[namespace][`resolver${nsIdx}_${resolverIdx}`] = - createResolver({ - name: `resolver${nsIdx}_${resolverIdx}`, - operation: "query", - // input removed - body: () => ({ string: "" }), - output: t.object({ string: t.string() }), - }); - }); - }); - - await expect(manager.generate(false)).resolves.not.toThrow(); + expect(manager.application.resolverServices).toBeDefined(); + expect(manager.application.resolverServices.length).toBeGreaterThan(0); + expect(manager.application.resolverServices[0]?.namespace).toBe("main"); + expect(manager.application.resolverServices[0]?.config.files).toEqual(["src/resolvers/*.ts"]); }); }); }); diff --git a/packages/sdk/src/cli/commands/generate/service.ts b/packages/sdk/src/cli/commands/generate/service.ts index dafc720bb..bbd07e438 100644 --- a/packages/sdk/src/cli/commands/generate/service.ts +++ b/packages/sdk/src/cli/commands/generate/service.ts @@ -1,15 +1,6 @@ import { spawn } from "node:child_process"; import * as fs from "node:fs"; import * as path from "pathe"; -import { - type AnyCodeGenerator, - type TailorDBNamespaceResult, - type ResolverNamespaceResult, - type GeneratorAuthInput, - type GeneratorResult, - type DependencyKind, - hasDependency, -} from "@/cli/commands/generate/types"; import { defineApplication, generatePluginFilesIfNeeded, @@ -17,22 +8,25 @@ import { } from "@/cli/services/application"; import { createExecutorService } from "@/cli/services/executor/service"; import { assertUniqueLocalTailorDBTypeNames } from "@/cli/services/tailordb/type-name-validation"; -import { loadConfig, type LoadedConfig, type Generator } from "@/cli/shared/config-loader"; +import { loadConfig, type LoadedConfig } from "@/cli/shared/config-loader"; import { getDistDir } from "@/cli/shared/dist-dir"; import { logger, styles } from "@/cli/shared/logger"; import { generateUserTypes } from "@/cli/shared/type-generator"; import { withSpan } from "@/cli/telemetry"; import { PluginManager } from "@/plugin/manager"; -import { type TailorDBNamespaceData, type ResolverNamespaceData } from "@/types/plugin-generation"; +import { + type GeneratorAuthInput, + type GeneratorResult, + type TailorDBNamespaceData, + type ResolverNamespaceData, +} from "@/types/plugin-generation"; import { assertDefined } from "@/utils/assert"; import { createDependencyWatcher, type DependencyWatcher } from "./watch"; import type { GenerateOptions } from "./options"; import type { Executor } from "@/types/executor.generated"; import type { Plugin, PluginAttachment } from "@/types/plugin"; import type { Resolver } from "@/types/resolver.generated"; -import type { TypeSourceInfo, TypeSourceInfoEntry, TailorDBType } from "@/types/tailordb"; - -export type { CodeGenerator } from "@/cli/commands/generate/types"; +import type { TypeSourceInfo, TailorDBType } from "@/types/tailordb"; type TypeInfo = { types: Record; @@ -46,57 +40,29 @@ type TypeInfo = { export type GenerationManager = { readonly application: Application; readonly baseDir: string; - readonly generators: Generator[]; readonly services: { tailordb: Record; resolver: Record>; executor: Record; }; - readonly generatorResults: GeneratorResults; - processGenerator: (gen: AnyCodeGenerator) => Promise; - processTailorDBNamespace: ( - gen: AnyCodeGenerator, - namespace: string, - typeInfo: TypeInfo, - ) => Promise; - processResolverNamespace: ( - gen: AnyCodeGenerator, - namespace: string, - resolvers: Record, - ) => Promise; - processExecutors: (gen: AnyCodeGenerator) => Promise; - aggregate: (gen: AnyCodeGenerator) => Promise; generate: (watch: boolean) => Promise; watch: () => Promise; }; -type GeneratorResults = Record< - /* generator */ string, - { - tailordbResults: Record>; - resolverResults: Record>; - tailordbNamespaceResults: Record; - resolverNamespaceResults: Record; - executorResults: Record; - } ->; - /** * Creates a generation manager. * @param params - Parameters for creating the generation manager * @param params.application - Application instance to generate code for * @param params.config - Loaded configuration - * @param params.generators - Code generators to run * @param params.pluginManager - Plugin manager for processing plugins * @returns GenerationManager instance */ export function createGenerationManager(params: { application: Application; config: LoadedConfig; - generators?: Generator[]; pluginManager?: PluginManager; }): GenerationManager { - const { application, config, generators = [], pluginManager } = params; + const { application, config, pluginManager } = params; const baseDir = path.join(getDistDir(), "generated"); fs.mkdirSync(baseDir, { recursive: true }); @@ -107,16 +73,10 @@ export function createGenerationManager(params: { } = { tailordb: {}, resolver: {}, executor: {} }; let watcher: DependencyWatcher | null = null; - const generatorResults: GeneratorResults = {}; // Get plugins that have generation hooks const generationPlugins = pluginManager?.getPluginsWithGenerationHooks() ?? []; - // Returns generators that subscribe to the given dependency phase - function getReadyGenerators(dep: DependencyKind): Generator[] { - return generators.filter((g) => (g as AnyCodeGenerator).dependencies.includes(dep)); - } - function getAuthInput(): GeneratorAuthInput | undefined { const authService = application.authService; if (!authService) return undefined; @@ -138,199 +98,6 @@ export function createGenerationManager(params: { }; } - // ========================================================================= - // Generator processing (unchanged - per-type/perNS/aggregate pipeline) - // ========================================================================= - - async function processTailorDBNamespace( - gen: AnyCodeGenerator, - namespace: string, - typeInfo: TypeInfo, - ): Promise { - const results = assertDefined( - generatorResults[gen.id], - `generator result not initialized for ${gen.id}`, - ); - results.tailordbResults[namespace] = {}; - - // Check if generator has processType method - if (!gen.processType) { - return; - } - - const processType = gen.processType; - await Promise.allSettled( - Object.entries(typeInfo.types).map(async ([typeName, type]) => { - try { - assertDefined( - results.tailordbResults[namespace], - `tailordb results not initialized for namespace ${namespace}`, - )[typeName] = await processType({ - type, - namespace, - source: typeInfo.sourceInfo[typeName] as TypeSourceInfoEntry, - plugins: typeInfo.pluginAttachments.get(typeName) ?? [], - }); - } catch (error) { - logger.error( - `Error processing type ${styles.bold(typeName)} in ${namespace} with generator ${gen.id}`, - ); - logger.error(String(error)); - } - }), - ); - - // Process namespace summary if available - if ("processTailorDBNamespace" in gen && typeof gen.processTailorDBNamespace === "function") { - try { - results.tailordbNamespaceResults[namespace] = await gen.processTailorDBNamespace({ - namespace, - types: results.tailordbResults[namespace], - }); - } catch (error) { - logger.error( - `Error processing TailorDB namespace ${styles.bold(namespace)} with generator ${gen.id}`, - ); - logger.error(String(error)); - } - } else { - results.tailordbNamespaceResults[namespace] = results.tailordbResults[namespace]; - } - } - - async function processResolverNamespace( - gen: AnyCodeGenerator, - namespace: string, - resolvers: Record, - ): Promise { - const results = assertDefined( - generatorResults[gen.id], - `generator result not initialized for ${gen.id}`, - ); - results.resolverResults[namespace] = {}; - - // Check if generator has processResolver method - if (!gen.processResolver) { - return; - } - - const processResolver = gen.processResolver; - // Process individual resolvers - await Promise.allSettled( - Object.entries(resolvers).map(async ([resolverName, resolver]) => { - try { - assertDefined( - results.resolverResults[namespace], - `resolver results not initialized for namespace ${namespace}`, - )[resolverName] = await processResolver({ - resolver, - namespace, - }); - } catch (error) { - logger.error( - `Error processing resolver ${styles.bold(resolverName)} in ${namespace} with generator ${gen.id}`, - ); - logger.error(String(error)); - } - }), - ); - - // Process namespace summary if available - if ("processResolverNamespace" in gen && typeof gen.processResolverNamespace === "function") { - try { - results.resolverNamespaceResults[namespace] = await gen.processResolverNamespace({ - namespace, - resolvers: results.resolverResults[namespace], - }); - } catch (error) { - logger.error( - `Error processing Resolver namespace ${styles.bold(namespace)} with generator ${gen.id}`, - ); - logger.error(String(error)); - } - } else { - results.resolverNamespaceResults[namespace] = results.resolverResults[namespace]; - } - } - - async function processExecutors(gen: AnyCodeGenerator): Promise { - const results = assertDefined( - generatorResults[gen.id], - `generator result not initialized for ${gen.id}`, - ); - - // Check if generator has processExecutor method - if (!gen.processExecutor) { - return; - } - - const processExecutor = gen.processExecutor; - // Process individual executors - await Promise.allSettled( - Object.entries(services.executor).map(async ([executorId, executor]) => { - try { - results.executorResults[executorId] = await processExecutor(executor); - } catch (error) { - logger.error( - `Error processing executor ${styles.bold(executor.name)} with generator ${gen.id}`, - ); - logger.error(String(error)); - } - }), - ); - } - - async function aggregate(gen: AnyCodeGenerator): Promise { - const results = assertDefined( - generatorResults[gen.id], - `generator result not initialized for ${gen.id}`, - ); - - const tailordbResults: TailorDBNamespaceResult[] = []; - const resolverResults: ResolverNamespaceResult[] = []; - - // Collect TailorDB namespace results - for (const [namespace, types] of Object.entries(results.tailordbNamespaceResults)) { - tailordbResults.push({ - namespace, - types, - }); - } - - // Collect Resolver namespace results - for (const [namespace, resolvers] of Object.entries(results.resolverNamespaceResults)) { - resolverResults.push({ - namespace, - resolvers, - }); - } - - // Build input based on generator dependencies - const input: Record = { - auth: getAuthInput(), - }; - - if (hasDependency(gen, "tailordb")) { - input.tailordb = tailordbResults; - } - if (hasDependency(gen, "resolver")) { - input.resolver = resolverResults; - } - if (hasDependency(gen, "executor")) { - input.executor = Object.values(results.executorResults); - } - - // Call generator's aggregate method - const result = await gen.aggregate({ - input: input as Parameters[0]["input"], - baseDir: path.join(baseDir, gen.id), - configPath: config.path, - }); - - // Write generated files - await writeGeneratedFiles(gen.id, result); - } - // ========================================================================= // Plugin phase-complete hook runner // ========================================================================= @@ -463,8 +230,8 @@ export function createGenerationManager(params: { /** * Write generated files to disk. - * @param sourceId - Generator or plugin ID for logging - * @param result - Generator result containing files to write + * @param sourceId - Plugin ID for logging + * @param result - Generation result containing files to write */ async function writeGeneratedFiles(sourceId: string, result: GeneratorResult): Promise { await Promise.all( @@ -510,66 +277,6 @@ export function createGenerationManager(params: { ); } - // ========================================================================= - // Generator orchestration - // ========================================================================= - - async function processGenerator(gen: AnyCodeGenerator): Promise { - generatorResults[gen.id] = { - tailordbResults: {}, - resolverResults: {}, - tailordbNamespaceResults: {}, - resolverNamespaceResults: {}, - executorResults: {}, - }; - - // Process TailorDB if generator has tailordb dependency - if (hasDependency(gen, "tailordb")) { - for (const [namespace, types] of Object.entries(services.tailordb)) { - await processTailorDBNamespace(gen, namespace, types); - } - } - - // Process Resolver if generator has resolver dependency - if (hasDependency(gen, "resolver")) { - for (const [namespace, resolvers] of Object.entries(services.resolver)) { - await processResolverNamespace(gen, namespace, resolvers); - } - } - - // Process Executors if generator has executor dependency - if (hasDependency(gen, "executor")) { - await processExecutors(gen); - } - - // Aggregate all results - await aggregate(gen); - } - - async function runGenerators(gens: Generator[], watch: boolean): Promise { - const results = await Promise.allSettled( - gens.map(async (gen) => { - await withSpan(`generate.generator.${gen.id}`, async () => { - try { - await processGenerator(gen as AnyCodeGenerator); - } catch (error) { - logger.error(`Error processing generator ${styles.bold(gen.id)}`); - logger.error(String(error)); - if (!watch) { - throw error; - } - } - }); - }), - ); - if (!watch) { - const failures = results.filter((r): r is PromiseRejectedResult => r.status === "rejected"); - if (failures.length > 0) { - throw new AggregateError(failures.map((f) => f.reason)); - } - } - } - async function restartWatchProcess(): Promise { logger.newline(); logger.info("Restarting watch process to clear module cache...", { @@ -620,14 +327,7 @@ export function createGenerationManager(params: { return { application, baseDir, - generators, services, - generatorResults, - processGenerator, - processTailorDBNamespace, - processResolverNamespace, - processExecutors, - aggregate, async generate(watch: boolean): Promise { logger.newline(); @@ -707,15 +407,11 @@ export function createGenerationManager(params: { logger.newline(); } - // Run generators + plugin hooks for onTailorDBReady - const readyAfterTailorDB = getReadyGenerators("tailordb"); + // Run plugin hooks for onTailorDBReady const hasOnTailorDBReady = generationPlugins.some((p) => p.onTailorDBReady != null); - if (readyAfterTailorDB.length > 0 || hasOnTailorDBReady) { + if (hasOnTailorDBReady) { await withSpan("generate.onTailorDBReady", async () => { - await Promise.all([ - runGenerators(readyAfterTailorDB, watch), - runPluginHook("onTailorDBReady", watch), - ]); + await runPluginHook("onTailorDBReady", watch); }); logger.newline(); } @@ -745,15 +441,11 @@ export function createGenerationManager(params: { } }); - // Run generators + plugin hooks for onResolverReady - const readyAfterResolvers = getReadyGenerators("resolver"); + // Run plugin hooks for onResolverReady const hasOnResolverReady = generationPlugins.some((p) => p.onResolverReady != null); - if (readyAfterResolvers.length > 0 || hasOnResolverReady) { + if (hasOnResolverReady) { await withSpan("generate.onResolversReady", async () => { - await Promise.all([ - runGenerators(readyAfterResolvers, watch), - runPluginHook("onResolverReady", watch), - ]); + await runPluginHook("onResolverReady", watch); }); logger.newline(); } @@ -774,15 +466,11 @@ export function createGenerationManager(params: { }); }); - // Run generators + plugin hooks for onExecutorReady - const readyAfterExecutors = getReadyGenerators("executor"); + // Run plugin hooks for onExecutorReady const hasOnExecutorReady = generationPlugins.some((p) => p.onExecutorReady != null); - if (readyAfterExecutors.length > 0 || hasOnExecutorReady) { + if (hasOnExecutorReady) { await withSpan("generate.onExecutorsReady", async () => { - await Promise.all([ - runGenerators(readyAfterExecutors, watch), - runPluginHook("onExecutorReady", watch), - ]); + await runPluginHook("onExecutorReady", watch); }); logger.newline(); } @@ -824,20 +512,19 @@ export function createGenerationManager(params: { } /** - * Run code generation using the Tailor configuration and generators. + * Run code generation using the Tailor configuration. * @param options - Generation options * @returns Promise that resolves when generation (and watch, if enabled) completes */ export async function generate(options?: GenerateOptions) { return withSpan("generate", async (rootSpan) => { // Load and validate options - const { config, generators, plugins } = await withSpan("generate.loadConfig", async () => { + const { config, plugins } = await withSpan("generate.loadConfig", async () => { return loadConfig(options?.configPath); }); const watch = options?.watch ?? false; rootSpan.setAttribute("generate.watch", watch); - rootSpan.setAttribute("generate.generators.count", generators.length); // Generate user types from loaded config await withSpan("generate.generateUserTypes", async () => @@ -855,7 +542,7 @@ export async function generate(options?: GenerateOptions) { rootSpan.setAttribute("app.name", application.config.name); - const manager = createGenerationManager({ application, config, generators, pluginManager }); + const manager = createGenerationManager({ application, config, pluginManager }); await manager.generate(watch); if (watch) { await manager.watch(); diff --git a/packages/sdk/src/cli/commands/generate/types.test.ts b/packages/sdk/src/cli/commands/generate/types.test.ts deleted file mode 100644 index b7fa74085..000000000 --- a/packages/sdk/src/cli/commands/generate/types.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { describe, expect, test, expectTypeOf } from "vitest"; -import { - hasDependency, - type CodeGenerator, - type TailorDBGenerator, - type ResolverGenerator, - type ExecutorGenerator, - type TailorDBResolverGenerator, - type FullCodeGenerator, - type AnyCodeGenerator, - type DependencyKind, -} from "@/cli/commands/generate/types"; -import type { CodeGeneratorBase } from "@/types/generator-config"; - -describe("Generator type compatibility", () => { - describe("TailorDBGenerator", () => { - test("should have tailordb dependency", () => { - expectTypeOf().toEqualTypeOf(); - }); - - test("should have processType method", () => { - expectTypeOf().toBeFunction(); - }); - - test("should have aggregate method", () => { - expectTypeOf().toBeFunction(); - }); - - test("should not have processResolver or processExecutor", () => { - type Keys = keyof TailorDBGenerator; - expectTypeOf<"processResolver">().not.toEqualTypeOf(); - expectTypeOf<"processExecutor">().not.toEqualTypeOf(); - }); - }); - - describe("ResolverGenerator", () => { - test("should have resolver dependency", () => { - expectTypeOf().toEqualTypeOf(); - }); - - test("should have processResolver method", () => { - expectTypeOf().toBeFunction(); - }); - - test("should have aggregate method", () => { - expectTypeOf().toBeFunction(); - }); - - test("should not have processType or processExecutor", () => { - type Keys = keyof ResolverGenerator; - expectTypeOf<"processType">().not.toEqualTypeOf(); - expectTypeOf<"processExecutor">().not.toEqualTypeOf(); - }); - }); - - describe("ExecutorGenerator", () => { - test("should have executor dependency", () => { - expectTypeOf().toEqualTypeOf(); - }); - - test("should have processExecutor method", () => { - expectTypeOf().toBeFunction(); - }); - - test("should have aggregate method", () => { - expectTypeOf().toBeFunction(); - }); - - test("should not have processType or processResolver", () => { - type Keys = keyof ExecutorGenerator; - expectTypeOf<"processType">().not.toEqualTypeOf(); - expectTypeOf<"processResolver">().not.toEqualTypeOf(); - }); - }); - - describe("TailorDBResolverGenerator", () => { - test("should have tailordb and resolver dependencies", () => { - expectTypeOf().toEqualTypeOf< - readonly ["tailordb", "resolver"] - >(); - }); - - test("should have both processType and processResolver methods", () => { - expectTypeOf().toBeFunction(); - expectTypeOf().toBeFunction(); - }); - - test("should not have processExecutor", () => { - type Keys = keyof TailorDBResolverGenerator; - expectTypeOf<"processExecutor">().not.toEqualTypeOf(); - }); - }); - - describe("FullCodeGenerator", () => { - test("should have all dependencies", () => { - expectTypeOf().toEqualTypeOf< - readonly ["tailordb", "resolver", "executor"] - >(); - }); - - test("should have all process methods", () => { - expectTypeOf().toBeFunction(); - expectTypeOf().toBeFunction(); - expectTypeOf().toBeFunction(); - }); - - test("should have aggregate method", () => { - expectTypeOf().toBeFunction(); - }); - }); - - describe("AnyCodeGenerator", () => { - test("should have optional process methods", () => { - type ProcessType = AnyCodeGenerator["processType"]; - type ProcessResolver = AnyCodeGenerator["processResolver"]; - type ProcessExecutor = AnyCodeGenerator["processExecutor"]; - - // These methods are optional (can be undefined or a function) - expectTypeOf().toExtend(); - expectTypeOf().toExtend(); - expectTypeOf().toExtend(); - }); - - test("should be assignable to CodeGeneratorBase", () => { - expectTypeOf().toExtend(); - }); - }); - - describe("hasDependency runtime utility", () => { - test("should return true when dependency exists", () => { - const gen = { dependencies: ["tailordb", "resolver"] as const }; - expect(hasDependency(gen, "tailordb")).toBe(true); - expect(hasDependency(gen, "resolver")).toBe(true); - }); - - test("should return false when dependency does not exist", () => { - const gen = { dependencies: ["tailordb"] as const }; - expect(hasDependency(gen, "resolver")).toBe(false); - expect(hasDependency(gen, "executor")).toBe(false); - }); - }); - - describe("CodeGenerator generic type", () => { - test("should correctly infer dependencies from type parameter", () => { - type TestGen = CodeGenerator; - expectTypeOf().toEqualTypeOf(); - }); - - test("should be compatible with readonly dependency arrays", () => { - type ReadonlyDeps = readonly DependencyKind[]; - const deps: ReadonlyDeps = ["tailordb", "resolver"]; - expect(deps).toContain("tailordb"); - }); - }); -}); diff --git a/packages/sdk/src/cli/commands/generate/types.ts b/packages/sdk/src/cli/commands/generate/types.ts deleted file mode 100644 index f4f2c3735..000000000 --- a/packages/sdk/src/cli/commands/generate/types.ts +++ /dev/null @@ -1,323 +0,0 @@ -import type { IdProvider as IdProviderConfig, OAuth2Client } from "@/types/auth.generated"; -import type { Executor } from "@/types/executor.generated"; -import type { PluginAttachment } from "@/types/plugin"; -import type { Resolver } from "@/types/resolver.generated"; -import type { TailorDBType, TypeSourceInfoEntry } from "@/types/tailordb"; - -export type { PluginAttachment } from "@/types/plugin"; - -// ======================================== -// Basic types -// ======================================== - -interface GeneratedFile { - path: string; - content: string; - skipIfExists?: boolean; // default: false - executable?: boolean; // default: false - if true, sets chmod +x -} - -export interface GeneratorResult { - files: GeneratedFile[]; - errors?: string[]; -} - -// Namespace results for TailorDB -export interface TailorDBNamespaceResult { - namespace: string; - types: T; -} - -// Namespace results for Resolver -export interface ResolverNamespaceResult { - namespace: string; - resolvers: R; -} - -// Auth configuration for generators -export interface GeneratorAuthInput { - name: string; - userProfile?: { - typeName: string; - namespace: string; - usernameField: string; - }; - machineUsers?: Record }>; - oauth2Clients?: Record; - idProvider?: IdProviderConfig; -} - -// ======================================== -// Dependency types -// ======================================== - -export type DependencyKind = "tailordb" | "resolver" | "executor"; - -// Check if array includes a specific element -type ArrayIncludes = T extends readonly [ - infer First, - ...infer Rest, -] - ? First extends U - ? true - : ArrayIncludes - : false; - -// Check if dependencies array includes a specific dependency -export type HasDependency< - Deps extends readonly DependencyKind[], - D extends DependencyKind, -> = ArrayIncludes; - -// ======================================== -// Source info type for TailorDB types -// Re-exported from parser module -// ======================================== - -export type { - UserDefinedTypeSource, - PluginGeneratedTypeSource, - TypeSourceInfoEntry, -} from "@/types/tailordb"; - -// ======================================== -// Method interfaces for each dependency -// ======================================== - -export interface TailorDBProcessMethods { - processType(args: { - type: TailorDBType; - namespace: string; - source: TypeSourceInfoEntry; - /** Plugin attachments configured on this type via .plugin() method */ - plugins: readonly PluginAttachment[]; - }): T | Promise; - - processTailorDBNamespace?(args: { - namespace: string; - types: Record; - }): Ts | Promise; -} - -export interface ResolverProcessMethods { - processResolver(args: { resolver: Resolver; namespace: string }): R | Promise; - - processResolverNamespace?(args: { - namespace: string; - resolvers: Record; - }): Rs | Promise; -} - -export interface ExecutorProcessMethods { - processExecutor(executor: Executor): E | Promise; -} - -// ======================================== -// Conditional method selection -// ======================================== - -type SelectMethods = (HasDependency< - Deps, - "tailordb" -> extends true - ? TailorDBProcessMethods - : object) & - (HasDependency extends true ? ResolverProcessMethods : object) & - (HasDependency extends true ? ExecutorProcessMethods : object); - -// ======================================== -// Conditional input selection for aggregate -// ======================================== - -interface TailorDBInputPart { - tailordb: TailorDBNamespaceResult[]; -} - -interface ResolverInputPart { - resolver: ResolverNamespaceResult[]; -} - -interface ExecutorInputPart { - executor: E[]; -} - -// Auth is always available (resolved after TailorDB, before generators) -interface AuthPart { - auth?: GeneratorAuthInput; -} - -type SelectInput = (HasDependency< - Deps, - "tailordb" -> extends true - ? TailorDBInputPart - : object) & - (HasDependency extends true ? ResolverInputPart : object) & - (HasDependency extends true ? ExecutorInputPart : object) & - AuthPart; - -/** Input type for TailorDB-only generators */ -export type TailorDBInput = TailorDBInputPart & AuthPart; - -/** Input type for Resolver-only generators */ -export type ResolverInput = ResolverInputPart & AuthPart; - -/** Input type for Executor-only generators */ -export type ExecutorInput = ExecutorInputPart & AuthPart; - -/** Input type for full generators (TailorDB + Resolver + Executor) */ -export type FullInput = TailorDBInputPart & - ResolverInputPart & - ExecutorInputPart & - AuthPart; - -/** Arguments type for aggregate method */ -export interface AggregateArgs { - input: Input; - baseDir: string; - configPath: string; -} - -// ======================================== -// CodeGenerator type definition -// ======================================== - -interface CodeGeneratorCore { - readonly id: string; - readonly description: string; -} - -/** - * Generator interface with dependencies-based conditional methods. - * @template Deps - Dependencies array (e.g., ['tailordb'], ['tailordb', 'resolver']) - * @template T - Return type of processType - * @template R - Return type of processResolver - * @template E - Return type of processExecutor - * @template Ts - Return type of processTailorDBNamespace (default: Record) - * @template Rs - Return type of processResolverNamespace (default: Record) - */ -export type CodeGenerator< - Deps extends readonly DependencyKind[], - T = unknown, - R = unknown, - E = unknown, - Ts = Record, - Rs = Record, -> = CodeGeneratorCore & - SelectMethods & { - readonly dependencies: Deps; - - aggregate(args: { - input: SelectInput; - baseDir: string; - configPath: string; - }): GeneratorResult | Promise; - }; - -// ======================================== -// Helper types for common generator patterns -// ======================================== - -/** TailorDB-only generator */ -export type TailorDBGenerator> = CodeGenerator< - readonly ["tailordb"], - T, - never, - never, - Ts, - never ->; - -/** Resolver-only generator */ -export type ResolverGenerator> = CodeGenerator< - readonly ["resolver"], - never, - R, - never, - never, - Rs ->; - -/** Executor-only generator */ -export type ExecutorGenerator = CodeGenerator< - readonly ["executor"], - never, - never, - E, - never, - never ->; - -/** TailorDB + Resolver generator */ -export type TailorDBResolverGenerator< - T = unknown, - R = unknown, - Ts = Record, - Rs = Record, -> = CodeGenerator; - -/** Full generator (all dependencies) */ -export type FullCodeGenerator< - T = unknown, - R = unknown, - E = unknown, - Ts = Record, - Rs = Record, -> = CodeGenerator; - -// ======================================== -// Runtime utility -// ======================================== - -interface DependencyCarrier { - dependencies: readonly DependencyKind[]; -} - -/** - * Type guard to check if a generator has a specific dependency. - * @template D - * @param generator - Code generator instance - * @param dependency - Dependency kind to check - * @returns True if the generator has the dependency - */ -export function hasDependency( - generator: DependencyCarrier, - dependency: D, -): boolean { - return generator.dependencies.includes(dependency); -} - -// Type for any generator (used in GenerationManager) -// This is a more permissive type that includes all possible methods -export interface AnyCodeGenerator { - readonly id: string; - readonly description: string; - readonly dependencies: readonly DependencyKind[]; - - processType?(args: { - type: TailorDBType; - namespace: string; - source: TypeSourceInfoEntry; - plugins: readonly PluginAttachment[]; - }): unknown | Promise; - - processTailorDBNamespace?(args: { - namespace: string; - types: Record; - }): unknown | Promise; - - processResolver?(args: { resolver: Resolver; namespace: string }): unknown | Promise; - - processResolverNamespace?(args: { - namespace: string; - resolvers: Record; - }): unknown | Promise; - - processExecutor?(executor: Executor): unknown | Promise; - - aggregate(args: { - input: Record; - baseDir: string; - configPath: string; - }): GeneratorResult | Promise; -} diff --git a/packages/sdk/src/cli/lib.ts b/packages/sdk/src/cli/lib.ts index 818707706..4d0679fe3 100644 --- a/packages/sdk/src/cli/lib.ts +++ b/packages/sdk/src/cli/lib.ts @@ -17,24 +17,10 @@ export { generate } from "./commands/generate/service"; export type { GenerateOptions } from "./commands/generate/options"; export { loadConfig, type LoadedConfig } from "./shared/config-loader"; export { generateUserTypes } from "./shared/type-generator"; -export type { - CodeGenerator, - TailorDBGenerator, - ResolverGenerator, - ExecutorGenerator, - TailorDBResolverGenerator, - FullCodeGenerator, - TailorDBInput, - ResolverInput, - ExecutorInput, - FullInput, - AggregateArgs, - GeneratorResult, - DependencyKind, - PluginAttachment, - TypeSourceInfoEntry, -} from "./commands/generate/types"; +export type { GeneratorResult } from "@/types/plugin-generation"; +export type { PluginAttachment } from "@/types/plugin"; export type { TailorDBType } from "@/types/tailordb"; +export type { TypeSourceInfoEntry } from "@/types/tailordb"; export type { Resolver } from "@/types/resolver.generated"; export type { Executor } from "@/types/executor.generated"; diff --git a/packages/sdk/src/cli/shared/config-loader.ts b/packages/sdk/src/cli/shared/config-loader.ts index 534e32aed..b75672533 100644 --- a/packages/sdk/src/cli/shared/config-loader.ts +++ b/packages/sdk/src/cli/shared/config-loader.ts @@ -5,7 +5,6 @@ import { AppConfigSchema } from "@/parser/app-config/schema"; import { PluginConfigSchema } from "@/parser/plugin-config"; import { loadConfigPath } from "./context"; import { installCliTailordbStub } from "./mock"; -import type { AnyCodeGenerator } from "@/cli/commands/generate/types"; import type { AppConfig } from "@/types/app-config"; import type { Plugin } from "@/types/plugin"; @@ -19,18 +18,16 @@ export interface LoadConfigOptions { importNonce?: string; } -export type Generator = AnyCodeGenerator; - /** * Load Tailor configuration file and associated plugins. * @param configPath - Optional explicit config path * @param options - Optional module import behavior. - * @returns Loaded config, generators, plugins, and config path + * @returns Loaded config, plugins, and config path */ export async function loadConfig( configPath?: string, options: LoadConfigOptions = {}, -): Promise<{ config: LoadedConfig; generators: Generator[]; plugins: Plugin[] }> { +): Promise<{ config: LoadedConfig; plugins: Plugin[] }> { installCliTailordbStub(); const foundPath = loadConfigPath(configPath); if (!foundPath) { @@ -88,7 +85,6 @@ export async function loadConfig( return { config: { ...configModule.default, path: resolvedPath } as LoadedConfig, - generators: [], plugins: allPlugins, }; } diff --git a/packages/sdk/src/types/generator-config.generated.ts b/packages/sdk/src/types/generator-config.generated.ts deleted file mode 100644 index ba3ac8cca..000000000 --- a/packages/sdk/src/types/generator-config.generated.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Generated by zinfer - Do not edit manually - -export type CodeGenerator = { - id: string; - description: string; - dependencies: ("tailordb" | "resolver" | "executor")[]; - aggregate: Function; - processType?: Function | undefined; - processResolver?: Function | undefined; - processExecutor?: Function | undefined; - processTailorDBNamespace?: Function | undefined; - processResolverNamespace?: Function | undefined; -}; -export type CodeGeneratorInput = CodeGenerator; - -export type BaseGeneratorConfig = - | ["@tailor-platform/kysely-type", { distPath: string }] - | [ - "@tailor-platform/seed", - { - distPath: string; - machineUserName?: string | undefined; - disableIdpUserSync?: - | { userToIdp?: boolean | undefined; idpToUser?: boolean | undefined } - | undefined; - }, - ] - | ["@tailor-platform/enum-constants", { distPath: string }] - | ["@tailor-platform/file-utils", { distPath: string }] - | { - id: string; - description: string; - dependencies: ("tailordb" | "resolver" | "executor")[]; - aggregate: Function; - processType?: Function | undefined; - processResolver?: Function | undefined; - processExecutor?: Function | undefined; - processTailorDBNamespace?: Function | undefined; - processResolverNamespace?: Function | undefined; - }; -export type BaseGeneratorConfigInput = BaseGeneratorConfig; diff --git a/packages/sdk/src/types/generator-config.ts b/packages/sdk/src/types/generator-config.ts index ab62c3062..14c4d1591 100644 --- a/packages/sdk/src/types/generator-config.ts +++ b/packages/sdk/src/types/generator-config.ts @@ -1,9 +1 @@ -import type { BaseGeneratorConfigInput, CodeGeneratorInput } from "./generator-config.generated"; - export type DependencyKind = "tailordb" | "resolver" | "executor"; - -export type GeneratorConfig = BaseGeneratorConfigInput; - -export type CodeGeneratorBase = Omit & { - dependencies: readonly DependencyKind[]; -}; From 7ddf3c716adf85a66a75d554da7730b5406f84b1 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 22:02:07 +0900 Subject: [PATCH 080/618] chore(codemod): add principal migration changeset --- .changeset/principal-unify-codemod.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/principal-unify-codemod.md diff --git a/.changeset/principal-unify-codemod.md b/.changeset/principal-unify-codemod.md new file mode 100644 index 000000000..c2542022b --- /dev/null +++ b/.changeset/principal-unify-codemod.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": minor +--- + +Add the `v2/principal-unify` codemod so `tailor-sdk upgrade` can migrate SDK principal APIs to `TailorPrincipal`. From d92dcccadc4a39f7afa3bea7da5fa1964a3c8b0b Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 22:41:35 +0900 Subject: [PATCH 081/618] test(example): retry e2e machine user token setup --- example/e2e/globalSetup.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/example/e2e/globalSetup.ts b/example/e2e/globalSetup.ts index 2dfb8582f..87fbf8165 100644 --- a/example/e2e/globalSetup.ts +++ b/example/e2e/globalSetup.ts @@ -1,3 +1,4 @@ +import { setTimeout as delay } from "node:timers/promises"; import { loadAccessToken, loadWorkspaceId, @@ -6,6 +7,8 @@ import { } from "@tailor-platform/sdk/cli"; import type { TestProject } from "vitest/node"; +const machineUserTokenRetryDelays = [5_000, 10_000, 15_000, 20_000, 25_000]; + declare module "vitest" { export interface ProvidedContext { url: string; @@ -18,9 +21,7 @@ declare module "vitest" { export async function setup(project: TestProject) { const app = await show(); - const tokens = await getMachineUserToken({ - name: "manager-machine-user", - }); + const tokens = await getManagerMachineUserToken(); const workspaceId = await loadWorkspaceId(); const platformToken = await loadAccessToken(); @@ -30,3 +31,26 @@ export async function setup(project: TestProject) { project.provide("platformToken", platformToken); project.provide("appName", app.name); } + +async function getManagerMachineUserToken() { + for (let attempt = 0; ; attempt++) { + try { + return await getMachineUserToken({ + name: "manager-machine-user", + }); + } catch (error) { + const retryDelay = machineUserTokenRetryDelays[attempt]; + if (retryDelay === undefined) { + throw error; + } + + const message = error instanceof Error ? error.message : String(error); + console.warn( + `Failed to fetch manager-machine-user token: ${message}. Retrying in ${ + retryDelay / 1000 + }s.`, + ); + await delay(retryDelay); + } + } +} From e94c940594f79ce9536475117394e6d063b9f1ce Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 22:54:37 +0900 Subject: [PATCH 082/618] refactor(cli): inline resolver arg JSON parsing --- packages/sdk/e2e/function-test-run.test.ts | 3 +-- packages/sdk/src/cli/commands/function/test-run.ts | 11 +---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/sdk/e2e/function-test-run.test.ts b/packages/sdk/e2e/function-test-run.test.ts index af1fa4dd5..a4d8f89ed 100644 --- a/packages/sdk/e2e/function-test-run.test.ts +++ b/packages/sdk/e2e/function-test-run.test.ts @@ -26,7 +26,6 @@ import { AuthInvokerSchema, type AuthInvoker } from "@tailor-proto/tailor/v1/aut import { describe, test, expect, beforeAll } from "vitest"; import { bundleForTestRun, type ResolvedMachineUser } from "../src/cli/commands/function/bundle"; import { detectFunctionType } from "../src/cli/commands/function/detect"; -import { assertResolverArgJson } from "../src/cli/commands/function/test-run"; import { initOperatorClient, type OperatorClient } from "../src/cli/shared/client"; import { loadAccessToken } from "../src/cli/shared/context"; import { executeScript, type ScriptExecutionResult } from "../src/cli/shared/script-executor"; @@ -92,7 +91,7 @@ async function runTestRun( if (!detected.hasInput) { resolvedArg = undefined; } else if (detected.inputSchema) { - assertResolverArgJson(resolvedArg); + JSON.parse(resolvedArg); } } diff --git a/packages/sdk/src/cli/commands/function/test-run.ts b/packages/sdk/src/cli/commands/function/test-run.ts index f1092ab6d..f69e05a67 100644 --- a/packages/sdk/src/cli/commands/function/test-run.ts +++ b/packages/sdk/src/cli/commands/function/test-run.ts @@ -136,7 +136,7 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file ); args.arg = undefined; } else if (detected.inputSchema) { - assertResolverArgJson(args.arg); + JSON.parse(args.arg); } } @@ -315,12 +315,3 @@ async function resolveMachineUser( return { name: machineUserName, id, attributes, attributeList }; } - -/** - * Assert that the resolver `--arg` value is valid JSON, failing fast before the - * server round-trip. Schema validation is left to the server. - * @param argStr - JSON string of the arg - */ -export function assertResolverArgJson(argStr: string): void { - JSON.parse(argStr); -} From d86172c3f166e0e7a4f996506fa4ca10d9d24aa0 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 16 Jun 2026 23:03:37 +0900 Subject: [PATCH 083/618] fix(cli): keep email as user-facing identifier --- packages/sdk/docs/cli/user.md | 6 +- packages/sdk/docs/cli/workspace.md | 6 +- .../sdk/src/cli/commands/profile/create.ts | 2 +- .../sdk/src/cli/commands/profile/update.ts | 2 +- .../sdk/src/cli/commands/user/switch.test.ts | 69 +++++++++++++++++++ packages/sdk/src/cli/commands/user/switch.ts | 11 ++- .../sdk/src/cli/commands/workspace/create.ts | 3 +- packages/sdk/src/cli/shared/context.test.ts | 62 +++++++++++++++++ packages/sdk/src/cli/shared/context.ts | 35 +++++++--- 9 files changed, 172 insertions(+), 24 deletions(-) create mode 100644 packages/sdk/src/cli/commands/user/switch.test.ts diff --git a/packages/sdk/docs/cli/user.md b/packages/sdk/docs/cli/user.md index a7c389297..e2dae100b 100644 --- a/packages/sdk/docs/cli/user.md +++ b/packages/sdk/docs/cli/user.md @@ -201,9 +201,9 @@ tailor-sdk user switch **Arguments** -| Argument | Description | Required | -| -------- | ----------- | -------- | -| `user` | User ID | Yes | +| Argument | Description | Required | +| -------- | -------------------------------------------- | -------- | +| `user` | User email address or machine user client ID | Yes | diff --git a/packages/sdk/docs/cli/workspace.md b/packages/sdk/docs/cli/workspace.md index cc4dd5a57..6ea955012 100644 --- a/packages/sdk/docs/cli/workspace.md +++ b/packages/sdk/docs/cli/workspace.md @@ -79,7 +79,7 @@ tailor-sdk workspace create [options] | `--organization-id ` | `-o` | Organization ID to workspace associate with | No | - | `TAILOR_PLATFORM_ORGANIZATION_ID` | | `--folder-id ` | `-f` | Folder ID to workspace associate with | No | - | `TAILOR_PLATFORM_FOLDER_ID` | | `--profile-name ` | `-p` | Profile name to create | No | - | - | -| `--profile-user ` | - | User ID for the profile (defaults to current user) | No | - | - | +| `--profile-user ` | - | User email address or machine user client ID for the profile (defaults to current user) | No | - | - | | `--permission ` | - | Profile permission (requires --profile-name). 'read' blocks all write commands while the profile is active. | No | `"write"` | - | @@ -243,7 +243,7 @@ tailor-sdk profile create [options] | Option | Alias | Description | Required | Default | | ------------------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------- | -| `--user ` | `-u` | User ID | Yes | - | +| `--user ` | `-u` | User email address or machine user client ID | Yes | - | | `--workspace-id ` | `-w` | Workspace ID | Yes | - | | `--permission ` | - | Profile permission. 'read' blocks all write commands while the profile is active. | No | `"write"` | | `--machine-user ` | `-m` | Default machine user name for application-data commands (query, workflow start, function test-run, machineuser token). | No | - | @@ -324,7 +324,7 @@ tailor-sdk profile update [options] | Option | Alias | Description | Required | Default | | ------------------------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------- | -| `--user ` | `-u` | New user ID | No | - | +| `--user ` | `-u` | New user email address or machine user client ID | No | - | | `--workspace-id ` | `-w` | New workspace ID | No | - | | `--permission ` | - | Profile permission. 'read' blocks all write commands; 'write' lifts the restriction. | No | - | | `--machine-user ` | `-m` | Default machine user name for application-data commands (query, workflow start, function test-run, machineuser token). Pass an empty string to clear. | No | - | diff --git a/packages/sdk/src/cli/commands/profile/create.ts b/packages/sdk/src/cli/commands/profile/create.ts index 13dd632eb..34e71ff7e 100644 --- a/packages/sdk/src/cli/commands/profile/create.ts +++ b/packages/sdk/src/cli/commands/profile/create.ts @@ -17,7 +17,7 @@ export const createCommand = defineAppCommand({ }), user: arg(z.string(), { alias: "u", - description: "User ID", + description: "User email address or machine user client ID", }), "workspace-id": arg(z.string(), { alias: "w", diff --git a/packages/sdk/src/cli/commands/profile/update.ts b/packages/sdk/src/cli/commands/profile/update.ts index 2f7838372..2762399c7 100644 --- a/packages/sdk/src/cli/commands/profile/update.ts +++ b/packages/sdk/src/cli/commands/profile/update.ts @@ -17,7 +17,7 @@ export const updateCommand = defineAppCommand({ }), user: arg(z.string().optional(), { alias: "u", - description: "New user ID", + description: "New user email address or machine user client ID", }), "workspace-id": arg(z.string().optional(), { alias: "w", diff --git a/packages/sdk/src/cli/commands/user/switch.test.ts b/packages/sdk/src/cli/commands/user/switch.test.ts new file mode 100644 index 000000000..bdb0307f3 --- /dev/null +++ b/packages/sdk/src/cli/commands/user/switch.test.ts @@ -0,0 +1,69 @@ +import * as fs from "node:fs"; +import * as path from "pathe"; +import { runCommand } from "politty"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import { readPlatformConfig, writePlatformConfig } from "@/cli/shared/context"; +import { captureStderr } from "@/cli/shared/test-helpers/capture-output"; +import { resetKeyringState } from "@/cli/shared/token-store"; +import { switchCommand } from "./switch"; + +const xdgTempDir = vi.hoisted(() => `/tmp/tailor-user-switch-${Date.now()}-${Math.random()}`); + +vi.mock("xdg-basedir", () => ({ + xdgConfig: xdgTempDir, +})); + +vi.mock("@napi-rs/keyring", () => ({ + Entry: class { + setPassword() {} + getPassword(): string | null { + return null; + } + deletePassword() {} + }, +})); + +beforeAll(() => { + fs.mkdirSync(xdgTempDir, { recursive: true }); +}); + +afterAll(() => { + fs.rmSync(xdgTempDir, { recursive: true, force: true }); +}); + +describe("user switch", () => { + beforeEach(() => { + vi.clearAllMocks(); + resetKeyringState(); + }); + + afterEach(() => { + const configPath = path.join(xdgTempDir, "tailor-platform", "config.yaml"); + if (fs.existsSync(configPath)) fs.rmSync(configPath); + }); + + test("stores the subject-keyed user when switching by email metadata", async () => { + writePlatformConfig({ + version: 3, + min_sdk_version: "2.0.0", + users: { + "platform-user-sub": { + storage: "file", + access_token: "token", + refresh_token: "refresh", + token_expires_at: "2999-01-01T00:00:00.000Z", + email: "user@example.com", + }, + }, + profiles: {}, + current_user: null, + }); + + using _stderr = captureStderr(); + + await runCommand(switchCommand, ["user@example.com"]); + + const config = await readPlatformConfig(); + expect(config.current_user).toBe("platform-user-sub"); + }); +}); diff --git a/packages/sdk/src/cli/commands/user/switch.ts b/packages/sdk/src/cli/commands/user/switch.ts index 73778ce5d..fc120ea14 100644 --- a/packages/sdk/src/cli/commands/user/switch.ts +++ b/packages/sdk/src/cli/commands/user/switch.ts @@ -1,7 +1,7 @@ import { arg } from "politty"; import { z } from "zod"; import { defineAppCommand } from "@/cli/shared/command"; -import { readPlatformConfig, writePlatformConfig } from "@/cli/shared/context"; +import { findConfigUserKey, readPlatformConfig, writePlatformConfig } from "@/cli/shared/context"; import { logger } from "@/cli/shared/logger"; import ml from "@/utils/multiline"; @@ -12,23 +12,22 @@ export const switchCommand = defineAppCommand({ .object({ user: arg(z.string(), { positional: true, - description: "User ID", + description: "User email address or machine user client ID", }), }) .strict(), run: async (args) => { const config = await readPlatformConfig(); - // Check if user exists - if (!config.users[args.user]) { + const user = findConfigUserKey(config, args.user); + if (!user) { throw new Error(ml` User "${args.user}" not found. Please login first using 'tailor-sdk login' command to register this user. `); } - // Set current user - config.current_user = args.user; + config.current_user = user; writePlatformConfig(config); logger.success(`Current user set to "${args.user}" successfully.`); diff --git a/packages/sdk/src/cli/commands/workspace/create.ts b/packages/sdk/src/cli/commands/workspace/create.ts index 59b363520..130d45176 100644 --- a/packages/sdk/src/cli/commands/workspace/create.ts +++ b/packages/sdk/src/cli/commands/workspace/create.ts @@ -109,7 +109,8 @@ export const createCommand = defineAppCommand({ description: "Profile name to create", }), "profile-user": arg(z.string().optional(), { - description: "User ID for the profile (defaults to current user)", + description: + "User email address or machine user client ID for the profile (defaults to current user)", }), permission: arg(z.enum(["write", "read"]).default("write"), { description: diff --git a/packages/sdk/src/cli/shared/context.test.ts b/packages/sdk/src/cli/shared/context.test.ts index 0cf78bf43..15f4ffa2d 100644 --- a/packages/sdk/src/cli/shared/context.test.ts +++ b/packages/sdk/src/cli/shared/context.test.ts @@ -4,6 +4,7 @@ import { parseYAML } from "confbox"; import * as path from "pathe"; import { describe, expect, test, vi, beforeEach, afterEach, afterAll, beforeAll } from "vitest"; import { + fetchLatestToken, loadAccessToken, loadConfigPath, loadMachineUserName, @@ -690,6 +691,30 @@ describe("loadAccessToken", () => { expect(result).toBe(validToken); }); + test("fetchLatestToken resolves a subject-keyed user by email metadata", async () => { + writePlatformConfig({ + version: 3, + min_sdk_version: "2.0.0", + users: { + "platform-user-sub": { + storage: "file", + access_token: validToken, + refresh_token: "refresh", + token_expires_at: futureDate, + email: "user@example.com", + }, + }, + profiles: {}, + current_user: "platform-user-sub", + }); + + const config = await readPlatformConfig(); + await expect(fetchLatestToken(config, "user@example.com")).resolves.toEqual({ + accessToken: validToken, + user: "platform-user-sub", + }); + }); + test("refreshes a legacy email-key user into a subject-key V3 config", async () => { clientMocks.refreshToken.mockResolvedValue({ accessToken: "new-access-token", @@ -737,6 +762,43 @@ describe("loadAccessToken", () => { expect(config.profiles.default?.user).toBe("platform-user-sub"); }); + test("logs when refresh updates the stored user email", async () => { + clientMocks.refreshToken.mockResolvedValue({ + accessToken: "new-access-token", + refreshToken: "new-refresh-token", + expiresAt: Date.now() + 3600 * 1000, + }); + clientMocks.fetchUserInfo.mockResolvedValue({ + sub: "platform-user-sub", + email: "new@example.com", + }); + writePlatformConfig({ + version: 3, + min_sdk_version: "2.0.0", + users: { + "platform-user-sub": { + access_token: "expired-access-token", + refresh_token: "refresh", + token_expires_at: pastDate, + storage: "file", + email: "old@example.com", + }, + }, + profiles: {}, + current_user: "platform-user-sub", + }); + + const config = await readPlatformConfig(); + using infoSpy = vi.spyOn(logger, "info").mockImplementation(() => {}); + + await fetchLatestToken(config, "platform-user-sub"); + + expect(infoSpy).toHaveBeenCalledWith( + 'Updated local user email from "old@example.com" to "new@example.com".', + ); + expect(config.users["platform-user-sub"]?.email).toBe("new@example.com"); + }); + test("keeps the legacy email key when subject resolution fails on refresh", async () => { clientMocks.refreshToken.mockResolvedValue({ accessToken: "new-access-token", diff --git a/packages/sdk/src/cli/shared/context.ts b/packages/sdk/src/cli/shared/context.ts index 3cbcac9e0..7fc49c19e 100644 --- a/packages/sdk/src/cli/shared/context.ts +++ b/packages/sdk/src/cli/shared/context.ts @@ -155,6 +155,11 @@ function inferEmailFromUserId(user: string): string | undefined { return z.email().safeParse(user).success ? user : undefined; } +export function findConfigUserKey(config: PfConfig, user: string): string | undefined { + if (config.users[user]) return user; + return Object.entries(config.users).find(([, entry]) => entry?.email === user)?.[0]; +} + function migrateV2ToV3(v2Config: PfConfigV2): PfConfig { const users: PfConfig["users"] = {}; @@ -628,16 +633,24 @@ function shouldResolveSubjectOnRefresh(user: string, userEntry: PfUser): boolean /** * Fetch the latest access token, refreshing if necessary. * @param config - Platform config - * @param user - User ID + * @param user - User identifier * @returns Latest access token and the canonical user ID it is stored under * (the resolved subject when a legacy email key was migrated during refresh, - * otherwise the input user) + * otherwise the matching config user) */ export async function fetchLatestToken( config: PfConfig, user: string, ): Promise<{ accessToken: string; user: string }> { - const userEntry = config.users[user]; + const storedUser = findConfigUserKey(config, user); + if (!storedUser) { + throw new Error(ml` + User "${user}" not found. + Please verify your user name and login using 'tailor-sdk login' command. + `); + } + + const userEntry = config.users[storedUser]; if (!userEntry) { throw new Error(ml` User "${user}" not found. @@ -645,10 +658,10 @@ export async function fetchLatestToken( `); } - const tokens = await resolveTokens(userEntry, user); + const tokens = await resolveTokens(userEntry, storedUser); if (new Date(userEntry.token_expires_at) > new Date()) { - return { accessToken: tokens.accessToken, user }; + return { accessToken: tokens.accessToken, user: storedUser }; } if (!tokens.refreshToken) { @@ -677,9 +690,10 @@ export async function fetchLatestToken( assertDefined(resp.expiresAt, "token refresh response missing expiresAt"), ).toISOString(); - let resolvedUser = user; - let email = userEntry.email; - if (shouldResolveSubjectOnRefresh(user, userEntry)) { + let resolvedUser = storedUser; + const previousEmail = userEntry.email ?? inferEmailFromUserId(storedUser); + let email = previousEmail; + if (shouldResolveSubjectOnRefresh(storedUser, userEntry)) { try { const userInfo = await fetchUserInfo(resp.accessToken); resolvedUser = userInfo.sub; @@ -699,7 +713,10 @@ export async function fetchLatestToken( newExpiresAt, { email }, ); - await removeLegacyUserAlias(config, user, resolvedUser); + await removeLegacyUserAlias(config, storedUser, resolvedUser); + if (previousEmail && email && previousEmail !== email) { + logger.info(`Updated local user email from "${previousEmail}" to "${email}".`); + } writePlatformConfig(config); return { accessToken: resp.accessToken, user: resolvedUser }; } From fa83075f5e0e91085c0ef0cb44b7058a28a79ec3 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 16 Jun 2026 23:24:17 +0900 Subject: [PATCH 084/618] refactor(cli)!: accept JSON value for executeScript arg executeScript previously required a pre-serialized JSON string for its arg. It now accepts a JSON-serializable value (generic, defaulting to unknown) and serializes internally, so callers pass the value directly. Update the test-run, query, and seed callers accordingly. --- .changeset/execute-script-json-arg.md | 5 +++++ example/seed/exec.mjs | 5 ++--- .../sdk/src/cli/commands/function/test-run.ts | 16 ++++++---------- packages/sdk/src/cli/query/index.test.ts | 8 ++++---- packages/sdk/src/cli/query/index.ts | 8 ++++---- .../sdk/src/cli/shared/script-executor.test.ts | 2 +- packages/sdk/src/cli/shared/script-executor.ts | 12 ++++++------ packages/sdk/src/plugin/builtin/seed/index.ts | 5 ++--- 8 files changed, 30 insertions(+), 31 deletions(-) create mode 100644 .changeset/execute-script-json-arg.md diff --git a/.changeset/execute-script-json-arg.md b/.changeset/execute-script-json-arg.md new file mode 100644 index 000000000..cb57790d9 --- /dev/null +++ b/.changeset/execute-script-json-arg.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +`executeScript` now takes its `arg` as a JSON-serializable value instead of a pre-serialized JSON string. Pass the value directly (e.g. `arg: { a: 1 }`) instead of `arg: JSON.stringify({ a: 1 })`. diff --git a/example/seed/exec.mjs b/example/seed/exec.mjs index 203be8544..534e5424f 100644 --- a/example/seed/exec.mjs +++ b/example/seed/exec.mjs @@ -307,7 +307,6 @@ const operatorClient = await initOperatorClient(accessToken); workspaceId, name: "truncate-idp-user.ts", code: idpTruncateCode, - arg: JSON.stringify({}), invoker: { namespace: authNamespace, machineUserName, @@ -501,7 +500,7 @@ const seedViaTestExecScript = async (namespace, typesToSeed, deps, selfRefTypes) workspaceId, name: `seed-${namespace}.ts`, code: bundled.bundledCode, - arg: JSON.stringify({ data: chunk.data, order: chunk.order, selfRefTypes }), + arg: { data: chunk.data, order: chunk.order, selfRefTypes }, invoker: { namespace: authNamespace, machineUserName, @@ -596,7 +595,7 @@ const seedViaTestExecScript = async (namespace, typesToSeed, deps, selfRefTypes) workspaceId, name: "seed-idp-user.ts", code: idpSeedCode, - arg: JSON.stringify({ users: rows }), + arg: { users: rows }, invoker: { namespace: authNamespace, machineUserName, diff --git a/packages/sdk/src/cli/commands/function/test-run.ts b/packages/sdk/src/cli/commands/function/test-run.ts index 465a88e16..716db358e 100644 --- a/packages/sdk/src/cli/commands/function/test-run.ts +++ b/packages/sdk/src/cli/commands/function/test-run.ts @@ -134,15 +134,11 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file functionName = detected.name; logger.info(`Detected: ${styles.bold(detected.type)} ${styles.info(`"${detected.name}"`)}`); - if (detected.type === "resolver" && args.arg) { - if (!detected.hasInput) { - logger.warn( - '--arg is ignored because this resolver has no input schema. Define "input" in your resolver to use --arg.', - ); - args.arg = undefined; - } else if (detected.inputSchema) { - JSON.parse(args.arg); - } + if (detected.type === "resolver" && args.arg && !detected.hasInput) { + logger.warn( + '--arg is ignored because this resolver has no input schema. Define "input" in your resolver to use --arg.', + ); + args.arg = undefined; } logger.info("Bundling..."); @@ -171,7 +167,7 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file workspaceId, name: scriptName, code: bundledCode, - arg: args.arg, + arg: args.arg === undefined ? undefined : JSON.parse(args.arg), invoker: authInvoker, }); diff --git a/packages/sdk/src/cli/query/index.test.ts b/packages/sdk/src/cli/query/index.test.ts index 939e3d75d..50c6e7ca9 100644 --- a/packages/sdk/src/cli/query/index.test.ts +++ b/packages/sdk/src/cli/query/index.test.ts @@ -276,7 +276,7 @@ describe("query", () => { expect(executeScript).toHaveBeenCalled(); const call = vi.mocked(executeScript).mock.calls[0]![0]!; - const arg = JSON.parse(call.arg ?? "{}"); + const arg = call.arg as { queries: string[] }; expect(arg.queries).toEqual(["SELECT 1; ", "SELECT 2"]); }); @@ -292,7 +292,7 @@ describe("query", () => { }); const call = vi.mocked(executeScript).mock.calls[0]![0]!; - const arg = JSON.parse(call.arg ?? "{}"); + const arg = call.arg as { queries: string[] }; expect(arg.queries).toHaveLength(1); }); @@ -322,11 +322,11 @@ describe("query", () => { expect(executeScript).toHaveBeenCalledWith( expect.objectContaining({ name: "query-gql.js", - arg: JSON.stringify({ + arg: { endpoint: "https://app.example.com/query", accessToken: "mu-token", query: "{ viewer { id } }", - }), + }, }), ); diff --git a/packages/sdk/src/cli/query/index.ts b/packages/sdk/src/cli/query/index.ts index 8b0c35b43..2f89d3eaa 100644 --- a/packages/sdk/src/cli/query/index.ts +++ b/packages/sdk/src/cli/query/index.ts @@ -210,10 +210,10 @@ async function sqlQuery( workspaceId: args.workspaceId, name: `query-sql-${args.namespace}.js`, code: args.bundledCode, - arg: JSON.stringify({ + arg: { namespace: args.namespace, queries, - }), + }, invoker, }); @@ -251,11 +251,11 @@ async function gqlQuery( workspaceId: args.workspaceId, name: `query-gql.js`, code: args.bundledCode, - arg: JSON.stringify({ + arg: { endpoint: `${application.url}/query`, accessToken, query: args.query, - }), + }, invoker, }); diff --git a/packages/sdk/src/cli/shared/script-executor.test.ts b/packages/sdk/src/cli/shared/script-executor.test.ts index 862330fea..8f012251f 100644 --- a/packages/sdk/src/cli/shared/script-executor.test.ts +++ b/packages/sdk/src/cli/shared/script-executor.test.ts @@ -193,7 +193,7 @@ describe("executeScript", () => { workspaceId: "workspace-1", name: "test-script.js", code: "export function main() { return { success: true }; }", - arg: '{"input":"value"}', + arg: { input: "value" }, invoker: mockAuthInvoker, }); diff --git a/packages/sdk/src/cli/shared/script-executor.ts b/packages/sdk/src/cli/shared/script-executor.ts index 0ffe8caa6..79a1885f5 100644 --- a/packages/sdk/src/cli/shared/script-executor.ts +++ b/packages/sdk/src/cli/shared/script-executor.ts @@ -17,7 +17,7 @@ export const DEFAULT_POLL_INTERVAL = 1000; /** * Options for script execution */ -export interface ScriptExecutionOptions { +export interface ScriptExecutionOptions { /** Operator client instance */ client: OperatorClient; /** Workspace ID */ @@ -26,8 +26,8 @@ export interface ScriptExecutionOptions { name: string; /** Bundled script code to execute */ code: string; - /** Optional JSON string argument to pass to the script */ - arg?: string; + /** Optional JSON-serializable argument to pass to the script */ + arg?: T; /** Auth invoker for script execution */ invoker: AuthInvoker; /** Polling interval in milliseconds (default: 1000ms) */ @@ -117,8 +117,8 @@ export async function waitForExecution( * @param {ScriptExecutionOptions} options - Execution options * @returns {Promise} Execution result */ -export async function executeScript( - options: ScriptExecutionOptions, +export async function executeScript( + options: ScriptExecutionOptions, ): Promise { const { client, workspaceId, name, code, arg, invoker, pollInterval } = options; @@ -127,7 +127,7 @@ export async function executeScript( workspaceId, name, code, - arg: arg ?? JSON.stringify({}), + arg: JSON.stringify(arg ?? {}), invoker, }); const executionId = response.executionId; diff --git a/packages/sdk/src/plugin/builtin/seed/index.ts b/packages/sdk/src/plugin/builtin/seed/index.ts index a805dad93..d36865ac9 100644 --- a/packages/sdk/src/plugin/builtin/seed/index.ts +++ b/packages/sdk/src/plugin/builtin/seed/index.ts @@ -100,7 +100,7 @@ function generateIdpUserSeedFunction(hasIdpUser: boolean, idpNamespace: string | workspaceId, name: "seed-idp-user.ts", code: idpSeedCode, - arg: JSON.stringify({ users: rows }), + arg: { users: rows }, invoker: { namespace: authNamespace, machineUserName, @@ -186,7 +186,6 @@ function generateIdpUserTruncateFunction(hasIdpUser: boolean, idpNamespace: stri workspaceId, name: "truncate-idp-user.ts", code: idpTruncateCode, - arg: JSON.stringify({}), invoker: { namespace: authNamespace, machineUserName, @@ -674,7 +673,7 @@ ${namespaceSelfRefEntries} workspaceId, name: \`seed-\${namespace}.ts\`, code: bundled.bundledCode, - arg: JSON.stringify({ data: chunk.data, order: chunk.order, selfRefTypes }), + arg: { data: chunk.data, order: chunk.order, selfRefTypes }, invoker: { namespace: authNamespace, machineUserName, From c2a8df520a1249b406394fcc41e409401be5bd92 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 16 Jun 2026 23:47:09 +0900 Subject: [PATCH 085/618] fix(cli): update remaining executeScript callers to JSON value arg Follow-up to the executeScript arg API change: update the callers and generated artifacts that still passed a pre-serialized JSON string, which double-encoded under the new internal serialization. - migration e2e and the function test-run e2e helper now pass the arg value directly (parsing the CLI string once), fixing the seed and resolver e2e failures. - Regenerate the create-sdk generators seed template and the example seed generator expected fixture. --- example/tests/fixtures/expected/seed/exec.mjs | 5 ++--- example/tests/scripts/migration_e2e.ts | 2 +- .../templates/generators/src/seed/exec.mjs | 2 +- packages/sdk/e2e/function-test-run.test.ts | 12 ++++-------- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/example/tests/fixtures/expected/seed/exec.mjs b/example/tests/fixtures/expected/seed/exec.mjs index 0f47f91d9..b69257e4f 100644 --- a/example/tests/fixtures/expected/seed/exec.mjs +++ b/example/tests/fixtures/expected/seed/exec.mjs @@ -307,7 +307,6 @@ const operatorClient = await initOperatorClient(accessToken); workspaceId, name: "truncate-idp-user.ts", code: idpTruncateCode, - arg: JSON.stringify({}), invoker: { namespace: authNamespace, machineUserName, @@ -501,7 +500,7 @@ const seedViaTestExecScript = async (namespace, typesToSeed, deps, selfRefTypes) workspaceId, name: `seed-${namespace}.ts`, code: bundled.bundledCode, - arg: JSON.stringify({ data: chunk.data, order: chunk.order, selfRefTypes }), + arg: { data: chunk.data, order: chunk.order, selfRefTypes }, invoker: { namespace: authNamespace, machineUserName, @@ -596,7 +595,7 @@ const seedViaTestExecScript = async (namespace, typesToSeed, deps, selfRefTypes) workspaceId, name: "seed-idp-user.ts", code: idpSeedCode, - arg: JSON.stringify({ users: rows }), + arg: { users: rows }, invoker: { namespace: authNamespace, machineUserName, diff --git a/example/tests/scripts/migration_e2e.ts b/example/tests/scripts/migration_e2e.ts index 0b98c2226..886e1bca3 100644 --- a/example/tests/scripts/migration_e2e.ts +++ b/example/tests/scripts/migration_e2e.ts @@ -270,7 +270,7 @@ const seedData = async ( workspaceId, name: `${label}.js`, code: bundled.bundledCode, - arg: JSON.stringify({ data, order }), + arg: { data, order }, invoker, }); if (!result.success) { diff --git a/packages/create-sdk/templates/generators/src/seed/exec.mjs b/packages/create-sdk/templates/generators/src/seed/exec.mjs index 865ed0666..0097ec9d9 100644 --- a/packages/create-sdk/templates/generators/src/seed/exec.mjs +++ b/packages/create-sdk/templates/generators/src/seed/exec.mjs @@ -388,7 +388,7 @@ const seedViaTestExecScript = async (namespace, typesToSeed, deps, selfRefTypes) workspaceId, name: `seed-${namespace}.ts`, code: bundled.bundledCode, - arg: JSON.stringify({ data: chunk.data, order: chunk.order, selfRefTypes }), + arg: { data: chunk.data, order: chunk.order, selfRefTypes }, invoker: { namespace: authNamespace, machineUserName, diff --git a/packages/sdk/e2e/function-test-run.test.ts b/packages/sdk/e2e/function-test-run.test.ts index 8322aefa0..3bc038986 100644 --- a/packages/sdk/e2e/function-test-run.test.ts +++ b/packages/sdk/e2e/function-test-run.test.ts @@ -75,7 +75,7 @@ async function runTestRun( workspaceId, name: scriptName, code, - arg: options?.arg, + arg: options?.arg === undefined ? undefined : JSON.parse(options.arg), invoker: authInvoker, }); return { ...result, scriptName }; @@ -87,12 +87,8 @@ async function runTestRun( }); let resolvedArg = options?.arg; - if (detected.type === "resolver" && resolvedArg) { - if (!detected.hasInput) { - resolvedArg = undefined; - } else if (detected.inputSchema) { - JSON.parse(resolvedArg); - } + if (detected.type === "resolver" && resolvedArg && !detected.hasInput) { + resolvedArg = undefined; } const { bundledCode, scriptName } = await bundleForTestRun({ @@ -108,7 +104,7 @@ async function runTestRun( workspaceId, name: scriptName, code: bundledCode, - arg: resolvedArg, + arg: resolvedArg === undefined ? undefined : JSON.parse(resolvedArg), invoker: authInvoker, }); From a4df486766ac8253f98b1a80eeeb52d794209a7d Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 16 Jun 2026 23:53:04 +0900 Subject: [PATCH 086/618] test(cli): pass JSON value arg in pre-bundled e2e case The pre-bundled .js branch of the function test-run e2e called executeScript with a JSON string arg, which double-encoded under the new internal serialization. Pass the value directly. --- packages/sdk/e2e/function-test-run.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/e2e/function-test-run.test.ts b/packages/sdk/e2e/function-test-run.test.ts index 3bc038986..399250368 100644 --- a/packages/sdk/e2e/function-test-run.test.ts +++ b/packages/sdk/e2e/function-test-run.test.ts @@ -308,7 +308,7 @@ describe.sequential("E2E: function test-run", () => { workspaceId, name: "add.js", code, - arg: '{"a":5,"b":7}', + arg: { a: 5, b: 7 }, invoker: authInvoker, }); From e0cbd27dc0091052bcf4c0c500b851e73e560e9d Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 09:01:03 +0900 Subject: [PATCH 087/618] refactor(cli): tighten executeScript arg typing and error handling Address review feedback on the executeScript arg API: - Constrain the arg generic with JsonCompatible so non-JSON values (functions, bigint, toJSON-bearing objects) are rejected statically. - Default the wire arg only when arg is undefined, so an explicit null round-trips instead of collapsing to {}. - Wrap the function test-run --arg JSON.parse in a try/catch that rethrows with --arg-specific context (preserving the original cause). --- example/tests/scripts/migration_e2e.ts | 4 +++- packages/sdk/src/cli/commands/function/test-run.ts | 14 +++++++++++++- packages/sdk/src/cli/query/index.test.ts | 4 ++-- packages/sdk/src/cli/shared/script-executor.ts | 5 +++-- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/example/tests/scripts/migration_e2e.ts b/example/tests/scripts/migration_e2e.ts index 886e1bca3..8e99aa387 100644 --- a/example/tests/scripts/migration_e2e.ts +++ b/example/tests/scripts/migration_e2e.ts @@ -14,6 +14,8 @@ import { } from "@tailor-platform/sdk/cli"; import { AuthInvokerSchema } from "@tailor-platform/tailor-proto/auth_resource_pb"; +type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; + const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const exampleDir = path.resolve(scriptDir, "..", ".."); const fixtureRoot = path.resolve(exampleDir, "tests", "migration-fixtures"); @@ -270,7 +272,7 @@ const seedData = async ( workspaceId, name: `${label}.js`, code: bundled.bundledCode, - arg: { data, order }, + arg: { data, order } as unknown as JsonValue, invoker, }); if (!result.success) { diff --git a/packages/sdk/src/cli/commands/function/test-run.ts b/packages/sdk/src/cli/commands/function/test-run.ts index 716db358e..d300e35e9 100644 --- a/packages/sdk/src/cli/commands/function/test-run.ts +++ b/packages/sdk/src/cli/commands/function/test-run.ts @@ -22,6 +22,7 @@ import { formatErrorWithSourcemap } from "@/cli/shared/stack-trace"; import { assertDefined } from "@/utils/assert"; import { bundleForTestRun, type ResolvedMachineUser } from "./bundle"; import { detectFunctionType } from "./detect"; +import type { JsonValue } from "@/types/helpers"; export const testRunCommand = defineAppCommand({ name: "test-run", @@ -162,12 +163,23 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file logger.info(`Executing on workspace ${styles.dim(workspaceId)}...`); + let parsedArg: JsonValue | undefined; + if (args.arg !== undefined) { + try { + parsedArg = JSON.parse(args.arg); + } catch (error) { + throw new Error(`Invalid --arg JSON: ${error instanceof Error ? error.message : error}`, { + cause: error, + }); + } + } + const result = await executeScript({ client, workspaceId, name: scriptName, code: bundledCode, - arg: args.arg === undefined ? undefined : JSON.parse(args.arg), + arg: parsedArg, invoker: authInvoker, }); diff --git a/packages/sdk/src/cli/query/index.test.ts b/packages/sdk/src/cli/query/index.test.ts index 50c6e7ca9..437a66d15 100644 --- a/packages/sdk/src/cli/query/index.test.ts +++ b/packages/sdk/src/cli/query/index.test.ts @@ -276,7 +276,7 @@ describe("query", () => { expect(executeScript).toHaveBeenCalled(); const call = vi.mocked(executeScript).mock.calls[0]![0]!; - const arg = call.arg as { queries: string[] }; + const arg = call.arg as unknown as { queries: string[] }; expect(arg.queries).toEqual(["SELECT 1; ", "SELECT 2"]); }); @@ -292,7 +292,7 @@ describe("query", () => { }); const call = vi.mocked(executeScript).mock.calls[0]![0]!; - const arg = call.arg as { queries: string[] }; + const arg = call.arg as unknown as { queries: string[] }; expect(arg.queries).toHaveLength(1); }); diff --git a/packages/sdk/src/cli/shared/script-executor.ts b/packages/sdk/src/cli/shared/script-executor.ts index 79a1885f5..f726b741b 100644 --- a/packages/sdk/src/cli/shared/script-executor.ts +++ b/packages/sdk/src/cli/shared/script-executor.ts @@ -7,6 +7,7 @@ import { FunctionExecution_Status } from "@tailor-proto/tailor/v1/function_resource_pb"; import type { OperatorClient } from "@/cli/shared/client"; +import type { JsonCompatible } from "@/types/helpers"; import type { AuthInvoker } from "@tailor-proto/tailor/v1/auth_resource_pb"; /** @@ -118,7 +119,7 @@ export async function waitForExecution( * @returns {Promise} Execution result */ export async function executeScript( - options: ScriptExecutionOptions, + options: ScriptExecutionOptions & { arg?: JsonCompatible }, ): Promise { const { client, workspaceId, name, code, arg, invoker, pollInterval } = options; @@ -127,7 +128,7 @@ export async function executeScript( workspaceId, name, code, - arg: JSON.stringify(arg ?? {}), + arg: JSON.stringify(arg === undefined ? {} : arg), invoker, }); const executionId = response.executionId; From a12cee7d4342d699ee1f293f354ce324b0aaa468 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 09:04:20 +0900 Subject: [PATCH 088/618] fix(sdk): restore check scripts and dependency bumps dropped in main merge --- packages/sdk-codemod/package.json | 2 +- packages/sdk/package.json | 14 +- pnpm-lock.yaml | 255 +++--------------------------- 3 files changed, 32 insertions(+), 239 deletions(-) diff --git a/packages/sdk-codemod/package.json b/packages/sdk-codemod/package.json index ebdcea7bc..74e823117 100644 --- a/packages/sdk-codemod/package.json +++ b/packages/sdk-codemod/package.json @@ -33,7 +33,7 @@ "picomatch": "4.0.4", "pkg-types": "2.3.1", "politty": "0.6.0", - "semver": "7.8.3", + "semver": "7.8.4", "zod": "4.4.3" }, "devDependencies": { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 6f9ee3261..c2ca1d961 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -143,6 +143,8 @@ "build": "tsdown && politty generate-worker --bin dist/cli/index.mjs --program tailor-sdk --shell zsh --verify", "lint": "oxlint --type-aware .", "check:public-api-jsdoc": "tsx scripts/check-public-api-jsdoc.ts", + "check:zod-isolation": "tsx scripts/check-zod-isolation.ts", + "check:import-cycles": "tsx scripts/check-import-cycles.ts", "lint:fix": "oxlint --type-aware . --fix", "typecheck": "tsc --noEmit", "typecheck:go": "tsgo", @@ -160,8 +162,8 @@ "@badgateway/oauth2-client": "3.3.1", "@bufbuild/protobuf": "2.12.0", "@bufbuild/protovalidate": "1.2.0", - "@connectrpc/connect": "2.1.1", - "@connectrpc/connect-node": "2.1.1", + "@connectrpc/connect": "2.1.2", + "@connectrpc/connect-node": "2.1.2", "@inquirer/core": "11.2.1", "@inquirer/prompts": "8.5.2", "@jridgewell/trace-mapping": "0.3.31", @@ -181,10 +183,10 @@ "chokidar": "5.0.0", "confbox": "0.2.4", "date-fns": "4.4.0", - "es-toolkit": "1.47.0", + "es-toolkit": "1.47.1", "find-up-simple": "1.0.1", "globals": "17.6.0", - "graphql": "16.14.1", + "graphql": "16.14.2", "inflection": "3.0.2", "kysely": "0.29.2", "madge": "8.0.0", @@ -196,8 +198,8 @@ "pgsql-ast-parser": "12.0.2", "pkg-types": "2.3.1", "politty": "0.6.0", - "rolldown": "1.1.0", - "semver": "7.8.3", + "rolldown": "1.1.1", + "semver": "7.8.4", "sql-highlight": "6.1.0", "std-env": "4.1.0", "table": "6.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c785a0e1..a1de9f5f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -373,7 +373,7 @@ importers: dependencies: '@0no-co/graphql.web': specifier: 1.2.0 - version: 1.2.0(graphql@16.14.1) + version: 1.2.0(graphql@16.14.2) '@badgateway/oauth2-client': specifier: 3.3.1 version: 3.3.1 @@ -384,11 +384,11 @@ importers: specifier: 1.2.0 version: 1.2.0(@bufbuild/protobuf@2.12.0) '@connectrpc/connect': - specifier: 2.1.1 - version: 2.1.1(@bufbuild/protobuf@2.12.0) + specifier: 2.1.2 + version: 2.1.2(@bufbuild/protobuf@2.12.0) '@connectrpc/connect-node': - specifier: 2.1.1 - version: 2.1.1(@bufbuild/protobuf@2.12.0)(@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.12.0)) + specifier: 2.1.2 + version: 2.1.2(@bufbuild/protobuf@2.12.0)(@connectrpc/connect@2.1.2(@bufbuild/protobuf@2.12.0)) '@inquirer/core': specifier: 11.2.1 version: 11.2.1(@types/node@24.13.2) @@ -433,7 +433,7 @@ importers: version: 0.4.1 '@urql/core': specifier: 6.0.1 - version: 6.0.1(graphql@16.14.1) + version: 6.0.1(graphql@16.14.2) chalk: specifier: 5.6.2 version: 5.6.2 @@ -447,8 +447,8 @@ importers: specifier: 4.4.0 version: 4.4.0 es-toolkit: - specifier: 1.47.0 - version: 1.47.0 + specifier: 1.47.1 + version: 1.47.1 find-up-simple: specifier: 1.0.1 version: 1.0.1 @@ -456,8 +456,8 @@ importers: specifier: 17.6.0 version: 17.6.0 graphql: - specifier: 16.14.1 - version: 16.14.1 + specifier: 16.14.2 + version: 16.14.2 inflection: specifier: 3.0.2 version: 3.0.2 @@ -492,11 +492,11 @@ importers: specifier: 0.6.0 version: 0.6.0(@clack/prompts@1.5.1)(@inquirer/prompts@8.5.2(@types/node@24.13.2))(zod@4.4.3) rolldown: - specifier: 1.1.0 - version: 1.1.0 + specifier: 1.1.1 + version: 1.1.1 semver: - specifier: 7.8.3 - version: 7.8.3 + specifier: 7.8.4 + version: 7.8.4 sql-highlight: specifier: 6.1.0 version: 6.1.0 @@ -598,8 +598,8 @@ importers: specifier: 0.6.0 version: 0.6.0(@clack/prompts@1.5.1)(@inquirer/prompts@8.5.2(@types/node@24.13.2))(zod@4.4.3) semver: - specifier: 7.8.3 - version: 7.8.3 + specifier: 7.8.4 + version: 7.8.4 zod: specifier: 4.4.3 version: 4.4.3 @@ -848,13 +848,6 @@ packages: resolution: {integrity: sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==} engines: {node: '>= 20.12.0'} - '@connectrpc/connect-node@2.1.1': - resolution: {integrity: sha512-s3TfsI1XF+n+1z6MBS9rTnFsxxR4Rw5wmdEnkQINli81ESGxcsfaEet8duzq8LVuuCupmhUsgpRo0Nv9pZkufg==} - engines: {node: '>=20'} - peerDependencies: - '@bufbuild/protobuf': ^2.7.0 - '@connectrpc/connect': 2.1.1 - '@connectrpc/connect-node@2.1.2': resolution: {integrity: sha512-+i/aAOpsI8sIx1mbYp6d99zvxaUSF6t/jP9Ux9maAmjsZPgmIQ3JuIeYi0zJIP9zlCnBlJjkpPosshCgdRuThQ==} engines: {node: '>=20'} @@ -862,11 +855,6 @@ packages: '@bufbuild/protobuf': ^2.7.0 '@connectrpc/connect': 2.1.2 - '@connectrpc/connect@2.1.1': - resolution: {integrity: sha512-JzhkaTvM73m2K1URT6tv53k2RwngSmCXLZJgK580qNQOXRzZRR/BCMfZw3h+90JpnG6XksP5bYT+cz0rpUzUWQ==} - peerDependencies: - '@bufbuild/protobuf': ^2.7.0 - '@connectrpc/connect@2.1.2': resolution: {integrity: sha512-MXkBijtcX09R10Eb6sFeIetc6w6746eio6xtfuyVOH7oQAacT1X0GzMIQFux6Qy8cq3W/T5qX5Bei8YbFtmRGA==} peerDependencies: @@ -1637,9 +1625,6 @@ packages: '@oxc-project/types@0.133.0': resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} - '@oxc-project/types@0.134.0': - resolution: {integrity: sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ==} - '@oxc-project/types@0.135.0': resolution: {integrity: sha512-wR+xRdFkUBMvcAjBJ2q2kcZM6d+DKu2NgoOyxZgYwZdLhmiv6+rnO8PZ/P68kMiZtIKm+pW7zyEJ4kSOs0vo+Q==} @@ -2033,12 +2018,6 @@ packages: cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.1.0': - resolution: {integrity: sha512-gCYzGOSkYY6Z034suzd20euvds7lPzMEEla62DJGE/ZAlR4OMBnNbvnBSsIGUCAr52gaWMsloGxP4tVGtN5aCA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - '@rolldown/binding-android-arm64@1.1.1': resolution: {integrity: sha512-BLf9Wak/gfwVb7NQTQW4wBgL3oAfPy7ArEkhwV543OVw/uY6B47z5xYsqPSZ9PDOorvURPinws6ThaFuNgGLgA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2051,12 +2030,6 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.1.0': - resolution: {integrity: sha512-JQBD77MNgu+4Z6RAyg69acugdrhhVoWesr3l47zohYZ2YV2fwkWMArkN/2p4l6Ei+Sno7W5q+UsKdVWq5Ens0w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - '@rolldown/binding-darwin-arm64@1.1.1': resolution: {integrity: sha512-rRZRPy/Ynb+Mxu0O6tfPldHeDgAn0sRij+IOUy6sFdUlv3hArGW/DloE3GfAxtqpOJuRNgF74Nr5gM4xBeU2jQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2069,12 +2042,6 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.0': - resolution: {integrity: sha512-p/8cXUTK4Sob604e+xxPhVSbDFf29E6J0l/xESM9rdCfn3aDai3nEs6TnMHUsdD5aNlFz0+gDbiGlozLKGa2YA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - '@rolldown/binding-darwin-x64@1.1.1': resolution: {integrity: sha512-/MtefPxhKPyWWFM8L45OWiEqRf+eSU2Qv9ZAyTaoZOoGcoPKxbbhjTJO2/U2IThv0uDZ4NWHc3/oTsR6IEOtww==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2087,12 +2054,6 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.1.0': - resolution: {integrity: sha512-KbtOSlVv6fElujiZWMcC3aQYhEwLVVf073RcwlSmpGQvIsKZFUqc0ef4sjUuurRwfbiI6JJXji9DQn+86hawmQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - '@rolldown/binding-freebsd-x64@1.1.1': resolution: {integrity: sha512-202K+cpIi1kx/Zn7AtxBi4LTXSY67Aszb2K9rNsuW7FeBeh0nqoNmYLOSZidV0p88VPBzMmTZcHAdPNo3kRYzQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2105,12 +2066,6 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.1.0': - resolution: {integrity: sha512-9fZ9i0o0/MQaw7om6Z6TsT7tfCk0jtbEFtC+aPqZL5RNsGWNcHvn6EHgL3dAprjq+AZzPTAQjg2JtpJaMt+6pg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.1.1': resolution: {integrity: sha512-wl9NfeXNUwrXtUc063tddmZFUI6qiNs1CNOwni0OL4vC7MqVSYugra3ZgtDmtVy8e0DluJTENmzIv2BwqLzT4Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2124,13 +2079,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.1.0': - resolution: {integrity: sha512-+tog7T66i+yFyIuuAnjL6xmW182W/qTBOUt6BtQ6lBIM1Eikh/fSMz4HGgvuCp5uU0zuIVWng7kDYthjCMOHcg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.1.1': resolution: {integrity: sha512-at2EO4o7D/PJLC4Xik16bU4CcjQE2tSv1LfqMA0TRYQYQihRm3gZeDB8xaX28A9SFedibcAk5DeMCKt4REKG0A==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2145,13 +2093,6 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.1.0': - resolution: {integrity: sha512-4b7yruLIIj/oZ3GpcLOvxcLCLDMraohn3IhQfN2hBP4w9UekG0DTIajWguJosRGfySf/+h/NwRUiMKoCpxCrqQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.1.1': resolution: {integrity: sha512-5PUjZx366h9tkJTPJF5eibxOlK3sGoeRiBJLLjjEB5/kLDuhr6qB3LkhqLz1smXNgsX+pBhnbcJBrPE30HznAA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2166,13 +2107,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.1.0': - resolution: {integrity: sha512-QRDOVZd0bhQ5jLsUsCC3dUxDWdTSVY9WMznowZgCGOrZfLLgctWpelhUASEiBwsXfat/JwYnVd1EaxMhqyT+UQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.1.1': resolution: {integrity: sha512-1WK84XPeio3tjP1sM/TMXiC0G1i1iq1qGZ71KfNQjEFLU1kwD+Cv5T8nGySg/JUFwLbaScu6ve9DmeXlmqpkFA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2187,13 +2121,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.0': - resolution: {integrity: sha512-ypxT+Hq76NFG7woFbNbySnGEajFuYuIXeKz/jfCU+lXUoxfi3zLE6OG/ZQNeK3RpZSYJlAe2bokpsQ046CaieQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.1': resolution: {integrity: sha512-1nS1X5z1uMJ369RU25hTpKCFvUwXZp12dIzlzk4S+UxCTcSVGsAE6tzkOSufv/7jnmAtK0ZlrsJxh2fGmsnVSw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2208,13 +2135,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.0': - resolution: {integrity: sha512-IdovCmfROFmpTLahdecTDFL74aLERVYN68F/mLZjfVh6LfoplPfI6deyHNMTcVujbokDV5k05XrFO22zfv+qjg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.1': resolution: {integrity: sha512-NwX/wspnq4vYyMFsqbYvzums3ki/Tk8FZbMzMAovPDp3OfLeYKby/D+9osokadXuYEV3OvpeHlwnr/bG8QMixA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2229,13 +2149,6 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-musl@1.1.0': - resolution: {integrity: sha512-pcA8xlFp2tyk9T2R6Fi/rPe3bQ1MA+sSMDNUU5Ogu80GHOatkE4P8YCreGAvZErm5Ho2YRXnyvNrWiRncfVysQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - '@rolldown/binding-linux-x64-musl@1.1.1': resolution: {integrity: sha512-+n46LhDrJFQM+229y4oXtVpj1G50U/+XuHMlpnisFTEXhrg9f/YIjp/HymX+PVJjBEr7XHRs3CFLelV464pqwA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2249,12 +2162,6 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.1.0': - resolution: {integrity: sha512-4+fexHayrLCWpriPh4c6dNvL4an34DEZCG7zOM/FD5QNF6h8DT+bDXzyB/kfC8lDJbaFb7jKShtnjDQFXVQEjg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.1.1': resolution: {integrity: sha512-qGwEu47zOWYo7LdRHhCWTNhzwGtxXpdY6CERs8QEOqC0PXGGics/e3vHnyEUKt8xK6YkbZXFUCeklrpB6js8ag==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2266,11 +2173,6 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.1.0': - resolution: {integrity: sha512-SbL++MNmOw6QamrwIGDMSSfM4ceTzFr+RjbOExJSLLBinScU4WI5OdA413h1qwPw2yH7lVF1+H4svQ+6mSXKTQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.1.1': resolution: {integrity: sha512-qczfgEH8u0wHGGOXtA7UMAybNKuQjjEXairyQaw4WzjiMztfbgatG1h4OKays/smhtwbWltpKCRGtVhU6h40Sg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2282,12 +2184,6 @@ packages: cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.1.0': - resolution: {integrity: sha512-+xTE6XC7wBgk0VKRXGG+QAnyW5S9b8vfsFpiMjf0waQTmSQSU8onsH/beyZ8X4aXVveJnotiy7VDjLOaW8bTrg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.1.1': resolution: {integrity: sha512-4psXSh63mSbwJF+mB8/9yfUUEzBiHYcUjxa32EO9ZwKy0Ypwjcg4F10D8SvVXgd+isy2UUUjF9HJJnDu1T/4Gg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2300,12 +2196,6 @@ packages: cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.0': - resolution: {integrity: sha512-Ogji1TQNqH3ACLnYr+1Ns1nyrJ0CO2P585u9Hsh02pXvtFiFpgtgT2b3P4PnCOU86VVCvqtAeCN4OftMT8KU4w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.1': resolution: {integrity: sha512-MUvC/HLXVjzkQkWiExdVTEEWf0py+GfWm8WKSZsekG3ih6a21iy0BHPF07X3JIf3ifoklZXTIaHTLPBgH1C3dw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2799,8 +2689,8 @@ packages: es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - es-toolkit@1.47.0: - resolution: {integrity: sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==} + es-toolkit@1.47.1: + resolution: {integrity: sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==} esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} @@ -2942,10 +2832,6 @@ packages: peerDependencies: graphql: 14 - 16 - graphql@16.14.1: - resolution: {integrity: sha512-cQOsSMS/IrDz82PVyRDvf/Q1F/bRbBVjJlh+xYOkI1qw2bWRvWGiWc+m2O0d6l4Bt1fyY+8kzJ8JFWGJqNeDBg==} - engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - graphql@16.14.2: resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -3577,11 +3463,6 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.1.0: - resolution: {integrity: sha512-zpMvlJhs5PkXRTtKc0CaLBVI9AR/VDiJFpM+kx//hgToEca7FgMlGjaRIisXBcb19T76LswgmKECSQ96hjWr5A==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - rolldown@1.1.1: resolution: {integrity: sha512-IN750c0p+s3jqJIsFLRZrQazmbAB1kkQDTtQjSt/gbS2ywLhlv4R5Shazer0FZKmuo/BsO3/w2UoYnUjuOZqHg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3606,11 +3487,6 @@ packages: engines: {node: '>=18'} hasBin: true - semver@7.8.3: - resolution: {integrity: sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==} - engines: {node: '>=10'} - hasBin: true - semver@7.8.4: resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} engines: {node: '>=10'} @@ -3995,9 +3871,9 @@ packages: snapshots: - '@0no-co/graphql.web@1.2.0(graphql@16.14.1)': + '@0no-co/graphql.web@1.2.0(graphql@16.14.2)': optionalDependencies: - graphql: 16.14.1 + graphql: 16.14.2 '@ast-grep/napi-darwin-arm64@0.43.0': optional: true @@ -4216,20 +4092,11 @@ snapshots: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@connectrpc/connect-node@2.1.1(@bufbuild/protobuf@2.12.0)(@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.12.0))': - dependencies: - '@bufbuild/protobuf': 2.12.0 - '@connectrpc/connect': 2.1.1(@bufbuild/protobuf@2.12.0) - '@connectrpc/connect-node@2.1.2(@bufbuild/protobuf@2.12.0)(@connectrpc/connect@2.1.2(@bufbuild/protobuf@2.12.0))': dependencies: '@bufbuild/protobuf': 2.12.0 '@connectrpc/connect': 2.1.2(@bufbuild/protobuf@2.12.0) - '@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.12.0)': - dependencies: - '@bufbuild/protobuf': 2.12.0 - '@connectrpc/connect@2.1.2(@bufbuild/protobuf@2.12.0)': dependencies: '@bufbuild/protobuf': 2.12.0 @@ -4779,8 +4646,6 @@ snapshots: '@oxc-project/types@0.133.0': {} - '@oxc-project/types@0.134.0': {} - '@oxc-project/types@0.135.0': {} '@oxc-resolver/binding-android-arm-eabi@11.20.0': @@ -4985,108 +4850,72 @@ snapshots: '@rolldown/binding-android-arm64@1.0.3': optional: true - '@rolldown/binding-android-arm64@1.1.0': - optional: true - '@rolldown/binding-android-arm64@1.1.1': optional: true '@rolldown/binding-darwin-arm64@1.0.3': optional: true - '@rolldown/binding-darwin-arm64@1.1.0': - optional: true - '@rolldown/binding-darwin-arm64@1.1.1': optional: true '@rolldown/binding-darwin-x64@1.0.3': optional: true - '@rolldown/binding-darwin-x64@1.1.0': - optional: true - '@rolldown/binding-darwin-x64@1.1.1': optional: true '@rolldown/binding-freebsd-x64@1.0.3': optional: true - '@rolldown/binding-freebsd-x64@1.1.0': - optional: true - '@rolldown/binding-freebsd-x64@1.1.1': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.0': - optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.1': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.0': - optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.1': optional: true '@rolldown/binding-linux-arm64-musl@1.0.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.0': - optional: true - '@rolldown/binding-linux-arm64-musl@1.1.1': optional: true '@rolldown/binding-linux-ppc64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.0': - optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.1': optional: true '@rolldown/binding-linux-s390x-gnu@1.0.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.0': - optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.1': optional: true '@rolldown/binding-linux-x64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.0': - optional: true - '@rolldown/binding-linux-x64-gnu@1.1.1': optional: true '@rolldown/binding-linux-x64-musl@1.0.3': optional: true - '@rolldown/binding-linux-x64-musl@1.1.0': - optional: true - '@rolldown/binding-linux-x64-musl@1.1.1': optional: true '@rolldown/binding-openharmony-arm64@1.0.3': optional: true - '@rolldown/binding-openharmony-arm64@1.1.0': - optional: true - '@rolldown/binding-openharmony-arm64@1.1.1': optional: true @@ -5097,13 +4926,6 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-wasm32-wasi@1.1.0': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - optional: true - '@rolldown/binding-wasm32-wasi@1.1.1': dependencies: '@emnapi/core': 1.11.0 @@ -5114,18 +4936,12 @@ snapshots: '@rolldown/binding-win32-arm64-msvc@1.0.3': optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.0': - optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.1': optional: true '@rolldown/binding-win32-x64-msvc@1.0.3': optional: true - '@rolldown/binding-win32-x64-msvc@1.1.0': - optional: true - '@rolldown/binding-win32-x64-msvc@1.1.1': optional: true @@ -5266,9 +5082,9 @@ snapshots: '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260612.1 '@typescript/native-preview-win32-x64': 7.0.0-dev.20260612.1 - '@urql/core@6.0.1(graphql@16.14.1)': + '@urql/core@6.0.1(graphql@16.14.2)': dependencies: - '@0no-co/graphql.web': 1.2.0(graphql@16.14.1) + '@0no-co/graphql.web': 1.2.0(graphql@16.14.2) wonka: 6.3.6 transitivePeerDependencies: - graphql @@ -5599,7 +5415,7 @@ snapshots: es-module-lexer@2.1.0: {} - es-toolkit@1.47.0: {} + es-toolkit@1.47.1: {} esbuild@0.28.1: optionalDependencies: @@ -5763,8 +5579,6 @@ snapshots: '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2) graphql: 16.14.2 - graphql@16.14.1: {} - graphql@16.14.2: {} has-flag@4.0.0: {} @@ -6422,27 +6236,6 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.3 '@rolldown/binding-win32-x64-msvc': 1.0.3 - rolldown@1.1.0: - dependencies: - '@oxc-project/types': 0.134.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.0 - '@rolldown/binding-darwin-arm64': 1.1.0 - '@rolldown/binding-darwin-x64': 1.1.0 - '@rolldown/binding-freebsd-x64': 1.1.0 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.0 - '@rolldown/binding-linux-arm64-gnu': 1.1.0 - '@rolldown/binding-linux-arm64-musl': 1.1.0 - '@rolldown/binding-linux-ppc64-gnu': 1.1.0 - '@rolldown/binding-linux-s390x-gnu': 1.1.0 - '@rolldown/binding-linux-x64-gnu': 1.1.0 - '@rolldown/binding-linux-x64-musl': 1.1.0 - '@rolldown/binding-openharmony-arm64': 1.1.0 - '@rolldown/binding-wasm32-wasi': 1.1.0 - '@rolldown/binding-win32-arm64-msvc': 1.1.0 - '@rolldown/binding-win32-x64-msvc': 1.1.0 - rolldown@1.1.1: dependencies: '@oxc-project/types': 0.135.0 @@ -6479,8 +6272,6 @@ snapshots: commander: 12.1.0 enhanced-resolve: 5.24.0 - semver@7.8.3: {} - semver@7.8.4: {} shebang-command@2.0.0: From a9d4fe4f3a48458ac551644cacb7f02cc1b03559 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 09:57:55 +0900 Subject: [PATCH 089/618] feat(codemod): add v2/execute-script-arg codemod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate user code for the executeScript arg API change: unwrap `JSON.stringify(...)` passed as the `executeScript` `arg` option (`arg: JSON.stringify(X)` → `arg: X`), scoped to the top-level `arg` property of the object passed directly to `executeScript`. Indirect forms (a stringified value held in a variable, multi-arg `JSON.stringify`, etc.) are intentionally left untouched. To flag those, extend `legacyPatterns` to support AND-groups (`string[]` = all substrings must co-occur) and register `["executeScript", "JSON.stringify"]` so unmigrated files surface a manual-migration warning without warning on every executeScript usage. --- .changeset/execute-script-json-arg.md | 3 + .../v2/execute-script-arg/codemod.yaml | 7 ++ .../execute-script-arg/scripts/transform.ts | 72 +++++++++++++++++++ .../tests/basic/expected.ts | 8 +++ .../execute-script-arg/tests/basic/input.ts | 8 +++ .../tests/identifier-arg/expected.ts | 1 + .../tests/identifier-arg/input.ts | 1 + .../tests/indirect-variable/input.ts | 2 + .../tests/multi-arg-stringify/input.ts | 1 + .../tests/non-arg-key/input.ts | 1 + packages/sdk-codemod/src/registry.ts | 11 +++ packages/sdk-codemod/src/runner.test.ts | 37 +++++++++- packages/sdk-codemod/src/runner.ts | 14 +++- packages/sdk-codemod/src/transform.test.ts | 4 ++ packages/sdk-codemod/src/types.ts | 10 ++- packages/sdk-codemod/tsdown.config.ts | 2 + 16 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/execute-script-arg/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/execute-script-arg/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/execute-script-arg/tests/basic/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/execute-script-arg/tests/basic/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/execute-script-arg/tests/identifier-arg/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/execute-script-arg/tests/identifier-arg/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/execute-script-arg/tests/indirect-variable/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/execute-script-arg/tests/multi-arg-stringify/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/execute-script-arg/tests/non-arg-key/input.ts diff --git a/.changeset/execute-script-json-arg.md b/.changeset/execute-script-json-arg.md index cb57790d9..101f71ab3 100644 --- a/.changeset/execute-script-json-arg.md +++ b/.changeset/execute-script-json-arg.md @@ -1,5 +1,8 @@ --- "@tailor-platform/sdk": major +"@tailor-platform/sdk-codemod": patch --- `executeScript` now takes its `arg` as a JSON-serializable value instead of a pre-serialized JSON string. Pass the value directly (e.g. `arg: { a: 1 }`) instead of `arg: JSON.stringify({ a: 1 })`. + +Add the `v2/execute-script-arg` codemod, which unwraps `JSON.stringify(...)` passed as the `executeScript` `arg` option. Indirect forms (a stringified value held in a variable, etc.) are left for manual migration and surfaced as a warning. diff --git a/packages/sdk-codemod/codemods/v2/execute-script-arg/codemod.yaml b/packages/sdk-codemod/codemods/v2/execute-script-arg/codemod.yaml new file mode 100644 index 000000000..68eb43f1c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/execute-script-arg/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/execute-script-arg" +version: "1.0.0" +description: "Unwrap JSON.stringify(...) passed as the executeScript `arg` option; v2 takes a JSON-serializable value directly" +engine: jssg +language: typescript +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/execute-script-arg/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/execute-script-arg/scripts/transform.ts new file mode 100644 index 000000000..84d9bc033 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/execute-script-arg/scripts/transform.ts @@ -0,0 +1,72 @@ +import { parse, Lang } from "@ast-grep/napi"; +import type { Edit, SgNode } from "@ast-grep/napi"; + +const NEEDLE = "executeScript"; + +function quickFilter(source: string): boolean { + return source.includes(NEEDLE) && source.includes("JSON.stringify"); +} + +function pairKeyText(pair: SgNode): string | null { + const key = pair.children()[0]; + if (!key) return null; + return key.text().replace(/^['"]|['"]$/g, ""); +} + +/** + * True when `stringifyCall` is the value of a top-level `arg:` property in the + * object literal passed directly to `executeScript(...)`. The chain checked is + * `JSON.stringify(...)` → pair (`arg:`) → object → arguments → `executeScript` + * call, so a nested `arg:` (e.g. `executeScript({ opts: { arg: ... } })`) or an + * unrelated `JSON.stringify` is left untouched. + */ +function isExecuteScriptArg(stringifyCall: SgNode): boolean { + const pair = stringifyCall.parent(); + if (!pair || pair.kind() !== "pair") return false; + if (pairKeyText(pair) !== "arg") return false; + + const obj = pair.parent(); + if (!obj || obj.kind() !== "object") return false; + + const args = obj.parent(); + if (!args || args.kind() !== "arguments") return false; + + const call = args.parent(); + if (!call || call.kind() !== "call_expression") return false; + + const callee = call.children()[0]; + return !!callee && callee.text() === NEEDLE; +} + +/** + * Rewrite `executeScript({ ..., arg: JSON.stringify(X), ... })` to + * `executeScript({ ..., arg: X, ... })`. + * + * In v2 the `executeScript` `arg` option takes a JSON-serializable value and + * serializes it internally, so a pre-stringified argument double-encodes. Only + * the literal `arg: JSON.stringify()` form is rewritten; indirect + * forms (a stringified value held in a variable, `JSON.stringify(x, null, 2)`, + * etc.) are left for manual migration. + * @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 (!quickFilter(source)) return null; + + const lang = source.includes("") ? Lang.Tsx : Lang.TypeScript; + const root = parse(lang, source).root(); + + const edits: Edit[] = []; + for (const match of root.findAll({ rule: { pattern: "JSON.stringify($X)" } })) { + if (!isExecuteScriptArg(match)) continue; + const inner = match.getMatch("X"); + if (!inner) continue; + edits.push(match.replace(inner.text())); + } + + if (edits.length === 0) return null; + + const result = root.commitEdits(edits); + return result === source ? null : result; +} diff --git a/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/basic/expected.ts b/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/basic/expected.ts new file mode 100644 index 000000000..800d5103f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/basic/expected.ts @@ -0,0 +1,8 @@ +const result = await executeScript({ + client, + workspaceId, + name: "seed.ts", + code: bundledCode, + arg: { users: rows }, + invoker, +}); diff --git a/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/basic/input.ts b/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/basic/input.ts new file mode 100644 index 000000000..7d02ccb76 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/basic/input.ts @@ -0,0 +1,8 @@ +const result = await executeScript({ + client, + workspaceId, + name: "seed.ts", + code: bundledCode, + arg: JSON.stringify({ users: rows }), + invoker, +}); diff --git a/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/identifier-arg/expected.ts b/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/identifier-arg/expected.ts new file mode 100644 index 000000000..03a361817 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/identifier-arg/expected.ts @@ -0,0 +1 @@ +await executeScript({ client, workspaceId, code, arg: payload, invoker }); diff --git a/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/identifier-arg/input.ts b/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/identifier-arg/input.ts new file mode 100644 index 000000000..665cfdccf --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/identifier-arg/input.ts @@ -0,0 +1 @@ +await executeScript({ client, workspaceId, code, arg: JSON.stringify(payload), invoker }); diff --git a/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/indirect-variable/input.ts b/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/indirect-variable/input.ts new file mode 100644 index 000000000..40efb78e5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/indirect-variable/input.ts @@ -0,0 +1,2 @@ +const serialized = JSON.stringify({ users: rows }); +await executeScript({ client, workspaceId, code, arg: serialized, invoker }); diff --git a/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/multi-arg-stringify/input.ts b/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/multi-arg-stringify/input.ts new file mode 100644 index 000000000..b049d1c7d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/multi-arg-stringify/input.ts @@ -0,0 +1 @@ +await executeScript({ client, workspaceId, code, arg: JSON.stringify(payload, null, 2), invoker }); diff --git a/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/non-arg-key/input.ts b/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/non-arg-key/input.ts new file mode 100644 index 000000000..e85e7287f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/execute-script-arg/tests/non-arg-key/input.ts @@ -0,0 +1 @@ +await executeScript({ client, workspaceId, code: JSON.stringify(meta), invoker }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index bcaae28bf..bdc361f2a 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -97,6 +97,17 @@ const allCodemods: CodemodPackage[] = [ scriptPath: "v2/tailordb-namespace/scripts/transform.js", legacyPatterns: ["Tailordb."], }, + { + id: "v2/execute-script-arg", + name: "executeScript arg JSON.stringify → value", + description: + "Unwrap `JSON.stringify(...)` passed as the `executeScript` `arg` option. In v2 `arg` takes a JSON-serializable value and is serialized internally, so a pre-stringified argument double-encodes.", + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/execute-script-arg/scripts/transform.js", + filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"], + legacyPatterns: [["executeScript", "JSON.stringify"]], + }, ]; /** diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 8d38ee48b..1eab6329d 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -25,7 +25,7 @@ function makeCodemod( id: string, scriptPath: string, filePatterns?: string[], - legacyPatterns?: string[], + legacyPatterns?: Array, ): CodemodPackage { return { id, @@ -360,5 +360,40 @@ describe("runCodemods", () => { "README.md: contains --machineuser but was not migrated automatically (rule: test/partial). Manual migration may be needed.", ]); }); + + test("AND-group legacy pattern warns only when every substring co-occurs", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-and-group-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "both.ts"), + "executeScript({ arg: payload });\nconst s = JSON.stringify(x);\n", + "utf-8", + ); + await fs.promises.writeFile( + path.join(dir, "only-stringify.ts"), + "const s = JSON.stringify(x);\n", + "utf-8", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod( + "test/and-group", + partialTransformPath, + ["**/*.ts"], + [["executeScript", "JSON.stringify"]], + ), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toEqual([ + "both.ts: contains executeScript + JSON.stringify but was not migrated automatically (rule: test/and-group). Manual migration may be needed.", + ]); + }); }); }); diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index b249cdb2b..77fed4f13 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -123,7 +123,15 @@ interface LoadedTransform { id: string; transform: TransformFn; matches: (relativePath: string) => boolean; - legacyPatterns: string[]; + legacyPatterns: Array; +} + +/** Resolve a legacy pattern against content, returning its label when matched. */ +function matchLegacyPattern(content: string, pattern: string | string[]): string | null { + if (typeof pattern === "string") { + return content.includes(pattern) ? pattern : null; + } + return pattern.every((p) => content.includes(p)) ? pattern.join(" + ") : null; } function legacyPatternWarnings( @@ -132,7 +140,9 @@ function legacyPatternWarnings( transforms: LoadedTransform[], ): string[] { return transforms.flatMap((lt) => { - const found = lt.legacyPatterns.filter((p) => content.includes(p)); + const found = lt.legacyPatterns + .map((p) => matchLegacyPattern(content, p)) + .filter((label): label is string => label !== null); if (found.length === 0) return []; return [ `${relative}: contains ${found.join(", ")} but was not migrated automatically (rule: ${lt.id}). Manual migration may be needed.`, diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index eda87d836..5ab9121fa 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -94,4 +94,8 @@ describe("codemod transforms", () => { test("v2/tailordb-namespace transforms correctly", async () => { await expect(runFixtureCases("v2/tailordb-namespace")).resolves.toBeUndefined(); }); + + test("v2/execute-script-arg transforms correctly", async () => { + await expect(runFixtureCases("v2/execute-script-arg")).resolves.toBeUndefined(); + }); }); diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index f4bcd3245..c21e0a399 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -18,8 +18,14 @@ export interface CodemodPackage { language?: string; /** Custom file glob patterns. Defaults to TypeScript patterns when omitted. */ filePatterns?: string[]; - /** Legacy patterns to detect in unmodified files for manual migration warnings. */ - legacyPatterns?: string[]; + /** + * Patterns to detect in post-transform file content for manual migration + * warnings. A plain string warns when that substring is present; a + * `string[]` group warns only when every substring in the group is present + * (AND), letting a rule target a co-occurrence such as `executeScript` used + * together with `JSON.stringify`. + */ + legacyPatterns?: Array; } /** diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index 1eb5e6c31..aa8ede142 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -29,6 +29,8 @@ export default defineConfig([ "codemods/v2/auth-invoker-unwrap/scripts/transform.ts", "v2/tailordb-namespace/scripts/transform": "codemods/v2/tailordb-namespace/scripts/transform.ts", + "v2/execute-script-arg/scripts/transform": + "codemods/v2/execute-script-arg/scripts/transform.ts", }, format: ["esm"], target: "node18", From 8b5870e85db1efec7647acb98226f8161e3d1583 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 10:25:40 +0900 Subject: [PATCH 090/618] feat(codemod): add LLM-assisted review for codemod-uncatchable cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some migrations cannot be completed by a deterministic AST transform (for example a value reached through a variable). A codemod can now declare `suspiciousPatterns` plus an inline `prompt`: after running, files whose post-transform content still matches a suspicious pattern are reported as `llmReviews` (in the JSON output and on stderr) together with the prompt, so the remaining cases can be finished with an LLM. Unlike `legacyPatterns`, suspicious patterns need not be exhaustive — a broad signal such as the API name is enough to point an LLM at the right files. The `auth.invoker(...)` codemod adopts this for its non-literal-argument cases. --- .changeset/codemod-llm-review.md | 5 +++ packages/sdk-codemod/src/index.ts | 22 +++++++++++ packages/sdk-codemod/src/registry.ts | 19 ++++++++- packages/sdk-codemod/src/runner.test.ts | 52 +++++++++++++++++++++++++ packages/sdk-codemod/src/runner.ts | 28 ++++++++++++- packages/sdk-codemod/src/types.ts | 27 +++++++++++++ 6 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 .changeset/codemod-llm-review.md diff --git a/.changeset/codemod-llm-review.md b/.changeset/codemod-llm-review.md new file mode 100644 index 000000000..dafb3bbab --- /dev/null +++ b/.changeset/codemod-llm-review.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Add LLM-assisted review support to the codemod runner. A codemod can declare `suspiciousPatterns` plus a `prompt`; after running, files whose post-transform content still matches a suspicious pattern are reported as `llmReviews` (in the JSON output and on stderr) together with the codemod's migration prompt. This surfaces the cases a deterministic transform cannot safely complete (e.g. a value reached through a variable) so they can be finished with an LLM. The `auth.invoker(...)` codemod adopts this for its non-literal-argument cases. diff --git a/packages/sdk-codemod/src/index.ts b/packages/sdk-codemod/src/index.ts index b79af31f4..0a5712d63 100644 --- a/packages/sdk-codemod/src/index.ts +++ b/packages/sdk-codemod/src/index.ts @@ -11,6 +11,22 @@ import type { RunOutput } from "./types"; const packageJson = await readPackageJSON(path.dirname(fileURLToPath(import.meta.url)) + "/.."); +/** + * Print an LLM-assisted review task to stderr: the flagged files plus the + * codemod's migration prompt, ready to hand to an LLM for the cases the + * deterministic transform could not complete on its own. + * @param review - The review task (codemod id, prompt, files) + */ +function printLlmReview(review: { codemodId: string; prompt: string; files: string[] }): void { + process.stderr.write( + `\n🤖 LLM-assisted review suggested (${review.codemodId}) — the codemod cannot safely migrate these automatically:\n`, + ); + for (const file of review.files) { + process.stderr.write(` - ${file}\n`); + } + process.stderr.write(`\nPrompt for an LLM:\n${review.prompt.trim()}\n`); +} + const main = defineCommand({ name: packageJson.name ?? "sdk-codemod", description: packageJson.description ?? "Codemod runner for Tailor Platform SDK upgrades", @@ -43,6 +59,7 @@ const main = defineCommand({ filesModified: [], warnings: [], errors: [], + llmReviews: [], }; if (codemods.length === 0) { @@ -67,12 +84,17 @@ const main = defineCommand({ output.codemodsSkipped = codemods.length - result.appliedCodemodIds.size; output.filesModified = result.filesModified; output.warnings = result.warnings; + output.llmReviews = result.llmReviews; if (result.changed) { process.stderr.write(` ${result.filesModified.length} file(s) modified\n`); } else { process.stderr.write(" No changes needed\n"); } + + for (const review of output.llmReviews) { + printLlmReview(review); + } } catch (error) { const message = error instanceof Error ? error.message : String(error); output.errors.push({ codemodId: "pipeline", message }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index bcaae28bf..311ec3121 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -85,7 +85,24 @@ const allCodemods: CodemodPackage[] = [ since: "1.0.0", until: "2.0.0", scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js", - legacyPatterns: ["auth.invoker"], + suspiciousPatterns: ["auth.invoker"], + prompt: [ + "In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the", + "machine user name passed directly as a string. The codemod already rewrote the", + 'string-literal form auth.invoker("name") to "name". These files still contain', + "auth.invoker(...) because the argument is not a plain string literal (a variable,", + "template literal, function call, or property access).", + "", + "For each remaining auth.invoker() call:", + "1. Replace the whole call with as-is (e.g. auth.invoker(name) becomes name).", + "2. Make sure evaluates to the machine user name (a string); adjust it if it", + " resolves to an object or an auth config value instead.", + "3. After removing every auth.invoker usage in a file, delete the now-unused auth", + " import (keeping it pulls Node-only config modules into runtime bundles); leave", + " the import if auth is still referenced elsewhere.", + "", + "Do not change behavior beyond removing the auth.invoker() indirection.", + ].join("\n"), }, { id: "v2/tailordb-namespace", diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 8d38ee48b..6cd0be476 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -26,6 +26,7 @@ function makeCodemod( scriptPath: string, filePatterns?: string[], legacyPatterns?: string[], + extra?: Pick, ): CodemodPackage { return { id, @@ -36,6 +37,7 @@ function makeCodemod( scriptPath, filePatterns, legacyPatterns, + ...extra, }; } @@ -360,5 +362,55 @@ describe("runCodemods", () => { "README.md: contains --machineuser but was not migrated automatically (rule: test/partial). Manual migration may be needed.", ]); }); + + test("flags files matching a suspicious pattern for LLM review", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-test-")); + tmpDir = dir; + await fs.promises.writeFile(path.join(dir, "a.ts"), "executeScript({ arg: payload });\n"); + await fs.promises.writeFile(path.join(dir, "b.ts"), "const x = 1;\n"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/llm", partialTransformPath, ["**/*.ts"], undefined, { + suspiciousPatterns: ["executeScript"], + prompt: "Rewrite remaining executeScript usages by hand.", + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.llmReviews).toEqual([ + { + codemodId: "test/llm", + prompt: "Rewrite remaining executeScript usages by hand.", + files: ["a.ts"], + }, + ]); + }); + + test("does not flag for LLM review without a prompt", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-noprompt-test-")); + tmpDir = dir; + await fs.promises.writeFile(path.join(dir, "a.ts"), "executeScript({ arg: payload });\n"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/llm", partialTransformPath, ["**/*.ts"], undefined, { + suspiciousPatterns: ["executeScript"], + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.llmReviews).toEqual([]); + }); }); }); diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index b249cdb2b..25805a9a3 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -4,7 +4,7 @@ import chalk from "chalk"; import { structuredPatch } from "diff"; import * as path from "pathe"; import picomatch from "picomatch"; -import type { CodemodPackage } from "./types"; +import type { CodemodPackage, LlmReview } from "./types"; /** * A transform function that receives source text and file path, @@ -40,6 +40,8 @@ export interface CodemodRunResult { warnings: string[]; /** IDs of codemods that actually produced changes in at least one file. */ appliedCodemodIds: Set; + /** Files flagged for LLM-assisted review, grouped by codemod. */ + llmReviews: LlmReview[]; } /** Default file patterns for TypeScript files. */ @@ -124,6 +126,8 @@ interface LoadedTransform { transform: TransformFn; matches: (relativePath: string) => boolean; legacyPatterns: string[]; + suspiciousPatterns: string[]; + prompt?: string; } function legacyPatternWarnings( @@ -165,6 +169,8 @@ export async function runCodemods( transform: await loadTransform(scriptPath), matches: picomatch(patterns, { dot: true }), legacyPatterns: codemod.legacyPatterns ?? [], + suspiciousPatterns: codemod.suspiciousPatterns ?? [], + prompt: codemod.prompt, }); } @@ -172,6 +178,8 @@ export async function runCodemods( const warnings: string[] = []; const appliedCodemodIds = new Set(); const seen = new Set(); + // codemod id -> files flagged for LLM-assisted review + const suspiciousByCodemod = new Map(); for await (const relative of walkFiles(targetPath)) { const absolute = path.resolve(targetPath, relative); @@ -207,12 +215,30 @@ export async function runCodemods( } warnings.push(...legacyPatternWarnings(relative, current, matchedTransforms)); + + for (const lt of matchedTransforms) { + if (!lt.prompt || lt.suspiciousPatterns.length === 0) continue; + if (lt.suspiciousPatterns.some((p) => current.includes(p))) { + const files = suspiciousByCodemod.get(lt.id) ?? []; + files.push(relative); + suspiciousByCodemod.set(lt.id, files); + } + } } + const llmReviews: LlmReview[] = loaded + .filter((lt) => lt.prompt && suspiciousByCodemod.has(lt.id)) + .map((lt) => ({ + codemodId: lt.id, + prompt: lt.prompt as string, + files: suspiciousByCodemod.get(lt.id) as string[], + })); + return { changed: filesModified.length > 0, filesModified, warnings, appliedCodemodIds, + llmReviews, }; } diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index f4bcd3245..5014ff70f 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -20,6 +20,31 @@ export interface CodemodPackage { filePatterns?: string[]; /** Legacy patterns to detect in unmodified files for manual migration warnings. */ legacyPatterns?: string[]; + /** + * Substrings that, when present in a file's post-transform content, mark it + * as a candidate for LLM-assisted review. Use this for migrations the + * deterministic transform cannot safely complete on its own (e.g. a value + * reached through a variable or a dynamic expression). Unlike + * `legacyPatterns`, these do not need to be exhaustive: a broad signal such + * as the API name is enough to point an LLM at the right files. Requires + * `prompt`. + */ + suspiciousPatterns?: string[]; + /** + * Prompt that instructs an LLM how to finish the migration for files matched + * by `suspiciousPatterns`. + */ + prompt?: string; +} + +/** A batch of files an LLM should review for one codemod, with its prompt. */ +export interface LlmReview { + /** Codemod id that flagged these files. */ + codemodId: string; + /** Prompt describing the migration for an LLM. */ + prompt: string; + /** Files (relative to the target) that matched a suspicious pattern. */ + files: string[]; } /** @@ -31,4 +56,6 @@ export interface RunOutput { filesModified: string[]; warnings: string[]; errors: Array<{ codemodId: string; message: string }>; + /** Files flagged for LLM-assisted review, grouped by codemod. */ + llmReviews: LlmReview[]; } From e2c84a559990add2f10cafb7f1a93f06857e5a63 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 10:43:13 +0900 Subject: [PATCH 091/618] feat(codemod): add LLM-assisted review prompt to execute-script-arg Adopt the suspiciousPatterns + prompt mechanism for the executeScript arg codemod, mirroring auth-invoker-unwrap. The deterministic transform only rewrites the direct arg: JSON.stringify(X) form; flag every file containing executeScript (a broad signal, no need to enumerate every indirect form) and attach a prompt telling an LLM to finish the indirect cases (a stringified value held in a variable, multi-argument JSON.stringify, dynamically built options). Replaces the previous legacy-pattern warning for this codemod. --- .changeset/execute-script-json-arg.md | 2 +- packages/sdk-codemod/src/registry.ts | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.changeset/execute-script-json-arg.md b/.changeset/execute-script-json-arg.md index 101f71ab3..5342c615d 100644 --- a/.changeset/execute-script-json-arg.md +++ b/.changeset/execute-script-json-arg.md @@ -5,4 +5,4 @@ `executeScript` now takes its `arg` as a JSON-serializable value instead of a pre-serialized JSON string. Pass the value directly (e.g. `arg: { a: 1 }`) instead of `arg: JSON.stringify({ a: 1 })`. -Add the `v2/execute-script-arg` codemod, which unwraps `JSON.stringify(...)` passed as the `executeScript` `arg` option. Indirect forms (a stringified value held in a variable, etc.) are left for manual migration and surfaced as a warning. +Add the `v2/execute-script-arg` codemod, which unwraps `JSON.stringify(...)` passed as the `executeScript` `arg` option. Indirect forms (a stringified value held in a variable, etc.) cannot be rewritten automatically and are surfaced as an LLM-assisted review task with a migration prompt. diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index b7e13af8a..0cf6728a7 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -123,7 +123,21 @@ const allCodemods: CodemodPackage[] = [ until: "2.0.0", scriptPath: "v2/execute-script-arg/scripts/transform.js", filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"], - legacyPatterns: [["executeScript", "JSON.stringify"]], + suspiciousPatterns: ["executeScript"], + prompt: [ + "In Tailor SDK v2 the executeScript() arg option takes a JSON-serializable value", + "and is serialized internally, so a pre-stringified argument double-encodes. The", + "codemod already rewrote the direct form arg: JSON.stringify(X) to arg: X. Review", + "the executeScript calls in these files for cases it could not rewrite — where the", + "arg value is reached indirectly, for example:", + "- a variable holding a JSON.stringify(...) result (const s = JSON.stringify(x); ... arg: s)", + "- JSON.stringify(x, null, 2) or another multi-argument form", + "- an options object built or spread dynamically", + "", + "For each such call, pass the underlying value directly as arg (drop the", + "JSON.stringify wrapper) so executeScript serializes it once. Leave calls that", + "already pass a plain value unchanged.", + ].join("\n"), }, ]; From 886087ee77788fd8b41c97856ab45e9609c5efb3 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 11:55:14 +0900 Subject: [PATCH 092/618] docs(workflow): note local runner replays the body per trigger runWorkflowLocally() re-runs the orchestrator body once per .trigger() call, mirroring the platform runtime, so flag that body-internal side effects fire on every pass and belong in the triggered jobs. --- packages/sdk/docs/testing.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index e594f0981..d7dbd4255 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -729,6 +729,8 @@ describe("order-fulfillment workflow", () => { Pass `{ env }` as the third argument when job bodies need configuration values during the local run. +Like the platform runtime, the local runner re-runs the orchestrator body once per `.trigger()` call (N triggers means N+1 passes), so any side effects outside the trigger results fire on every pass. Keep the body deterministic and move repeatable side effects into the triggered jobs. + This helper is still a local runner. Use E2E tests when you need to verify deployed workflow scheduling, suspension, or replay behavior. **Use when:** you want to verify orchestration end to end without the cost of a real deployment. From 1030597d3292675eb50ee9dfc0a6c2a7d2146267 Mon Sep 17 00:00:00 2001 From: "tailor-platform-pr-trigger[bot]" <247949890+tailor-platform-pr-trigger[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 03:02:23 +0000 Subject: [PATCH 093/618] Version Packages (next) --- .changeset/pre.json | 20 +++++++--- packages/create-sdk/CHANGELOG.md | 2 + packages/create-sdk/package.json | 2 +- packages/sdk-codemod/CHANGELOG.md | 14 +++++++ packages/sdk-codemod/package.json | 2 +- packages/sdk/CHANGELOG.md | 64 +++++++++++++++++++++++++++++++ packages/sdk/package.json | 2 +- 7 files changed, 97 insertions(+), 9 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index bdbcec2fe..f9321bcdd 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -7,12 +7,20 @@ "@tailor-platform/sdk-codemod": "0.2.7" }, "changesets": [ - "docs-idp-user-auth-policy", - "fix-lockfile-audit-vulns", - "migration-rollback-on-failure", - "plugin-cli-import-codemod", - "proto-init-shape-optionality", + "remove-auth-invoker-helper", + "remove-define-generators", + "remove-function-test-run-input-wrapper", + "remove-runtime-globals-compatibility", + "remove-tailor-sdk-skills-shim", + "remove-v2-cli-aliases", + "renovate-1425", + "renovate-1429", + "renovate-1433", + "renovate-1448", + "reorganize-types-zod-isolation", + "require-function-log-content-hash", + "store-cli-users-by-subject", "v2-baseline", - "validate-migrate-json" + "workflow-trigger-dispatch" ] } diff --git a/packages/create-sdk/CHANGELOG.md b/packages/create-sdk/CHANGELOG.md index 023002ced..0f4fb50b7 100644 --- a/packages/create-sdk/CHANGELOG.md +++ b/packages/create-sdk/CHANGELOG.md @@ -1,5 +1,7 @@ # @tailor-platform/create-sdk +## 2.0.0-next.1 + ## 2.0.0-next.0 ## 1.66.0 diff --git a/packages/create-sdk/package.json b/packages/create-sdk/package.json index 3c52d8ddf..27d5f1828 100644 --- a/packages/create-sdk/package.json +++ b/packages/create-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/create-sdk", - "version": "2.0.0-next.0", + "version": "2.0.0-next.1", "description": "A CLI tool to quickly create a new Tailor Platform SDK project", "license": "MIT", "repository": { diff --git a/packages/sdk-codemod/CHANGELOG.md b/packages/sdk-codemod/CHANGELOG.md index 04c95dd5f..37127d7b8 100644 --- a/packages/sdk-codemod/CHANGELOG.md +++ b/packages/sdk-codemod/CHANGELOG.md @@ -1,5 +1,19 @@ # @tailor-platform/sdk-codemod +## 0.3.0-next.1 +### Patch Changes + + + +- [#1460](https://github.com/tailor-platform/sdk/pull/1460) [`f49c6d1`](https://github.com/tailor-platform/sdk/commit/f49c6d1b5a856969cb4e04ae7d3a87ed34aa020f) Thanks [@dqn](https://github.com/dqn)! - Remove the v1 runtime globals compatibility layer. Importing from `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` declarations; opt into globals with `@tailor-platform/sdk/runtime/globals` or use the typed wrappers from `@tailor-platform/sdk/runtime`. + + The capital-cased `Tailordb.*` namespace is removed. If your project still references `Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, or `typeof Tailordb.Client`, migrate before upgrading: run `pnpm dlx @tailor-platform/sdk-codemod v2/tailordb-namespace` to rewrite them to lowercase `tailordb.*`, then add `import "@tailor-platform/sdk/runtime/globals"` so the rewritten references resolve. + + +- [#1457](https://github.com/tailor-platform/sdk/pull/1457) [`84325f8`](https://github.com/tailor-platform/sdk/commit/84325f8602a5631b7c323c997b1425235509920e) Thanks [@dqn](https://github.com/dqn)! - Remove deprecated CLI aliases for the v2 command surface. Use `tailor-sdk deploy` instead of `tailor-sdk apply`, `tailor-sdk crashreport` instead of `tailor-sdk crash-report`, and the hyphenated `--machine-user` option instead of the hidden `--machineuser` alias. + + Fix the v2 CLI rename codemod to migrate the hidden `--machineuser` option to `--machine-user`. + ## 0.3.0-next.0 ### Minor Changes diff --git a/packages/sdk-codemod/package.json b/packages/sdk-codemod/package.json index 74e823117..beee89c36 100644 --- a/packages/sdk-codemod/package.json +++ b/packages/sdk-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk-codemod", - "version": "0.3.0-next.0", + "version": "0.3.0-next.1", "description": "Codemod runner for Tailor Platform SDK upgrades", "license": "MIT", "repository": { diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index d433d455a..a2a0a7e03 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,5 +1,69 @@ # @tailor-platform/sdk +## 2.0.0-next.1 +### Major Changes + + + +- [#1442](https://github.com/tailor-platform/sdk/pull/1442) [`07cc256`](https://github.com/tailor-platform/sdk/commit/07cc256b9f1d695694d67438e1b0cb6df096ece8) Thanks [@dqn](https://github.com/dqn)! - Remove the deprecated `auth.invoker("")` helper. Pass machine user names directly as `authInvoker` strings in resolver, executor, and workflow APIs. + + + +- [#1440](https://github.com/tailor-platform/sdk/pull/1440) [`f0895f4`](https://github.com/tailor-platform/sdk/commit/f0895f4231578e54229004d3aa5bac6bd24361e3) Thanks [@dqn](https://github.com/dqn)! - Remove `defineGenerators()` and legacy `generators` config support. Use `definePlugins()` with the built-in plugin packages for code generation. + + + +- [#1441](https://github.com/tailor-platform/sdk/pull/1441) [`7604ad5`](https://github.com/tailor-platform/sdk/commit/7604ad5fdf27b791e8f1db880faed156cd6554d5) Thanks [@dqn](https://github.com/dqn)! - Remove support for wrapping `tailor-sdk function test-run --arg` resolver input in an `input` object. Pass resolver input fields directly as the `--arg` JSON value. + + + +- [#1460](https://github.com/tailor-platform/sdk/pull/1460) [`f49c6d1`](https://github.com/tailor-platform/sdk/commit/f49c6d1b5a856969cb4e04ae7d3a87ed34aa020f) Thanks [@dqn](https://github.com/dqn)! - Remove the v1 runtime globals compatibility layer. Importing from `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` declarations; opt into globals with `@tailor-platform/sdk/runtime/globals` or use the typed wrappers from `@tailor-platform/sdk/runtime`. + + The capital-cased `Tailordb.*` namespace is removed. If your project still references `Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, or `typeof Tailordb.Client`, migrate before upgrading: run `pnpm dlx @tailor-platform/sdk-codemod v2/tailordb-namespace` to rewrite them to lowercase `tailordb.*`, then add `import "@tailor-platform/sdk/runtime/globals"` so the rewritten references resolve. + + +- [#1459](https://github.com/tailor-platform/sdk/pull/1459) [`2c3d7ad`](https://github.com/tailor-platform/sdk/commit/2c3d7add213b171df2959b8a14e8dc2e3c3a7ec7) Thanks [@dqn](https://github.com/dqn)! - Remove the deprecated `tailor-sdk-skills` binary shim. Use `tailor-sdk skills install` to install the bundled Tailor SDK agent skill. + + + +- [#1457](https://github.com/tailor-platform/sdk/pull/1457) [`84325f8`](https://github.com/tailor-platform/sdk/commit/84325f8602a5631b7c323c997b1425235509920e) Thanks [@dqn](https://github.com/dqn)! - Remove deprecated CLI aliases for the v2 command surface. Use `tailor-sdk deploy` instead of `tailor-sdk apply`, `tailor-sdk crashreport` instead of `tailor-sdk crash-report`, and the hyphenated `--machine-user` option instead of the hidden `--machineuser` alias. + + Fix the v2 CLI rename codemod to migrate the hidden `--machineuser` option to `--machine-user`. + + +- [#1462](https://github.com/tailor-platform/sdk/pull/1462) [`77e7f45`](https://github.com/tailor-platform/sdk/commit/77e7f4512ddd141a8858ab37a3c48d8ad7e16543) Thanks [@dqn](https://github.com/dqn)! - Require `FunctionExecution.contentHash` for `function logs` stack trace source mapping. Executions without a content hash now show raw stack traces instead of mapping against the current function bundle. + + + +- [#1458](https://github.com/tailor-platform/sdk/pull/1458) [`ad04913`](https://github.com/tailor-platform/sdk/commit/ad049131d61dfc9f96bff8ffe7c5e5e261523b14) Thanks [@dqn](https://github.com/dqn)! - Store CLI human users by stable subject ID in the platform config instead of email, while preserving email for display and migrating legacy email-keyed entries on login or token refresh. + + + +- [#1438](https://github.com/tailor-platform/sdk/pull/1438) [`2c552cd`](https://github.com/tailor-platform/sdk/commit/2c552cd716f9abbe90ec4b22e51d0cde155c2bf9) Thanks [@dqn](https://github.com/dqn)! - Align workflow job `.trigger()` with the platform runtime. Job triggers now require a mocked workflow runtime in tests instead of running job bodies locally, and `trigger()` returns the job result directly instead of a Promise wrapper. Use `mockWorkflow()` to mock trigger results in tests, or `runWorkflowLocally()` for full-chain local workflow tests. + + +### Patch Changes + + + +- [#1425](https://github.com/tailor-platform/sdk/pull/1425) [`644dca8`](https://github.com/tailor-platform/sdk/commit/644dca8ee631ff18550646d3d82bad76fba6bc33) Thanks [@renovate](https://github.com/apps/renovate)! - fix(deps): update dependency graphql to v16.14.2 + + + +- [#1429](https://github.com/tailor-platform/sdk/pull/1429) [`b933f29`](https://github.com/tailor-platform/sdk/commit/b933f291b99efb3668077cd7870abe979dc3b10b) Thanks [@renovate](https://github.com/apps/renovate)! - fix(deps): update rolldown to v1.1.1 + + + +- [#1433](https://github.com/tailor-platform/sdk/pull/1433) [`4d07f2f`](https://github.com/tailor-platform/sdk/commit/4d07f2fd814bda5886c8f0c9546f21128dcce74b) Thanks [@renovate](https://github.com/apps/renovate)! - fix(deps): update dependency es-toolkit to v1.47.1 + + + +- [#1448](https://github.com/tailor-platform/sdk/pull/1448) [`4ce01dd`](https://github.com/tailor-platform/sdk/commit/4ce01dd851dae7754103a918ba89afe073f942c6) Thanks [@renovate](https://github.com/apps/renovate)! - fix(deps): update @connectrpc to v2.1.2 + + + +- [#1390](https://github.com/tailor-platform/sdk/pull/1390) [`388f3d6`](https://github.com/tailor-platform/sdk/commit/388f3d69b722589cd5cca7bbbc3667a849ecaa3b) Thanks [@toiroakr](https://github.com/toiroakr)! - Guarantee that importing the SDK never loads zod in user projects — neither zod runtime code in bundled functions nor zod type computation in tsc. Internal type definitions are reorganized into per-layer pure type modules, and new CI checks verify every user-facing entry point stays zod-free at both the type and runtime level and that the internal module graph has no import cycles. + ## 2.0.0-next.0 ### Major Changes diff --git a/packages/sdk/package.json b/packages/sdk/package.json index c2ca1d961..ab577c344 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk", - "version": "2.0.0-next.0", + "version": "2.0.0-next.1", "description": "Tailor Platform SDK - The SDK to work with Tailor Platform", "license": "MIT", "repository": { From a376dc8cd053d20744c90104e8b44ed2729ffe8c Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 13:04:36 +0900 Subject: [PATCH 094/618] refactor(sdk): remove openDownloadStream file API --- .changeset/remove-open-download-stream.md | 8 ++ example/generated/files.ts | 8 +- example/tests/fixtures/expected/files.ts | 8 +- .../generators/src/generated/files.ts | 8 +- packages/sdk/docs/runtime.md | 2 +- packages/sdk/docs/testing.md | 22 ----- .../builtin/file-utils/generate-file-utils.ts | 14 +-- .../plugin/builtin/file-utils/index.test.ts | 6 ++ packages/sdk/src/runtime/file.test.ts | 24 +----- packages/sdk/src/runtime/file.ts | 44 ---------- packages/sdk/src/vitest/mock.test.ts | 45 +--------- packages/sdk/src/vitest/mock.ts | 85 ------------------- 12 files changed, 39 insertions(+), 235 deletions(-) create mode 100644 .changeset/remove-open-download-stream.md diff --git a/.changeset/remove-open-download-stream.md b/.changeset/remove-open-download-stream.md new file mode 100644 index 000000000..e40b446c0 --- /dev/null +++ b/.changeset/remove-open-download-stream.md @@ -0,0 +1,8 @@ +--- +"@tailor-platform/sdk": major +"@tailor-platform/create-sdk": patch +--- + +Remove the deprecated `openDownloadStream` file streaming API. Use `downloadStream` for streamed file downloads. + +The generated file utilities now emit `downloadFileStream`, which calls `downloadStream` and returns `FileDownloadStreamResponse`, instead of the removed `openFileDownloadStream` helper. diff --git a/example/generated/files.ts b/example/generated/files.ts index 0389b9acd..b7e393b3d 100644 --- a/example/generated/files.ts +++ b/example/generated/files.ts @@ -3,7 +3,7 @@ import type { FileUploadOptions, FileUploadResponse, FileMetadata, - FileStreamIterator, + FileDownloadStreamResponse, } from "@tailor-platform/sdk/runtime/file"; export interface TypeWithFiles { @@ -58,10 +58,10 @@ export async function getFileMetadata( return await file.getMetadata(namespaces[type], type, field, recordId); } -export async function openFileDownloadStream( +export async function downloadFileStream( type: T, field: TypeWithFiles[T]["fields"], recordId: string, -): Promise { - return await file.openDownloadStream(namespaces[type], type, field, recordId); +): Promise { + return await file.downloadStream(namespaces[type], type, field, recordId); } diff --git a/example/tests/fixtures/expected/files.ts b/example/tests/fixtures/expected/files.ts index 0389b9acd..b7e393b3d 100644 --- a/example/tests/fixtures/expected/files.ts +++ b/example/tests/fixtures/expected/files.ts @@ -3,7 +3,7 @@ import type { FileUploadOptions, FileUploadResponse, FileMetadata, - FileStreamIterator, + FileDownloadStreamResponse, } from "@tailor-platform/sdk/runtime/file"; export interface TypeWithFiles { @@ -58,10 +58,10 @@ export async function getFileMetadata( return await file.getMetadata(namespaces[type], type, field, recordId); } -export async function openFileDownloadStream( +export async function downloadFileStream( type: T, field: TypeWithFiles[T]["fields"], recordId: string, -): Promise { - return await file.openDownloadStream(namespaces[type], type, field, recordId); +): Promise { + return await file.downloadStream(namespaces[type], type, field, recordId); } diff --git a/packages/create-sdk/templates/generators/src/generated/files.ts b/packages/create-sdk/templates/generators/src/generated/files.ts index e7498a317..a18d7ada1 100644 --- a/packages/create-sdk/templates/generators/src/generated/files.ts +++ b/packages/create-sdk/templates/generators/src/generated/files.ts @@ -3,7 +3,7 @@ import type { FileUploadOptions, FileUploadResponse, FileMetadata, - FileStreamIterator, + FileDownloadStreamResponse, } from "@tailor-platform/sdk/runtime/file"; export interface TypeWithFiles { @@ -50,10 +50,10 @@ export async function getFileMetadata( return await file.getMetadata(namespaces[type], type, field, recordId); } -export async function openFileDownloadStream( +export async function downloadFileStream( type: T, field: TypeWithFiles[T]["fields"], recordId: string, -): Promise { - return await file.openDownloadStream(namespaces[type], type, field, recordId); +): Promise { + return await file.downloadStream(namespaces[type], type, field, recordId); } diff --git a/packages/sdk/docs/runtime.md b/packages/sdk/docs/runtime.md index 32c84d23d..c617e66bc 100644 --- a/packages/sdk/docs/runtime.md +++ b/packages/sdk/docs/runtime.md @@ -78,7 +78,7 @@ The runtime entry re-exports the following namespaces. Detailed signatures, para - `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`, `downloadStream`, `uploadStream`, `openDownloadStream` _(deprecated)_) +- `file` — `tailordb.file` BLOB API (`upload`, `download`, `downloadAsBase64`, `delete`, `getMetadata`, `downloadStream`, `uploadStream`) ## Testing diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index d7dbd4255..9fa9f2e05 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -265,28 +265,6 @@ test("mock file download stream", async () => { }); ``` -For the deprecated `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 (deprecated openDownloadStream)", async () => { - using file = mockFile(); - file.enqueueResult([ - { - type: "metadata", - 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 }, - { 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/plugin/builtin/file-utils/generate-file-utils.ts b/packages/sdk/src/plugin/builtin/file-utils/generate-file-utils.ts index 4d5ea1be8..d0673f218 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 @@ -43,7 +43,7 @@ export function generateUnifiedFileUtils( FileUploadOptions, FileUploadResponse, FileMetadata, - FileStreamIterator, + FileDownloadStreamResponse, } from "@tailor-platform/sdk/runtime/file"; ` + "\n"; @@ -116,15 +116,15 @@ export function generateUnifiedFileUtils( } ` + "\n"; - // Generate openFileDownloadStream helper function - const openDownloadStreamFunction = + // Generate downloadFileStream helper function + const downloadStreamFunction = multiline /* ts */ ` - export async function openFileDownloadStream( + export async function downloadFileStream( type: T, field: TypeWithFiles[T]["fields"], recordId: string, - ): Promise { - return await file.openDownloadStream(namespaces[type], type, field, recordId); + ): Promise { + return await file.downloadStream(namespaces[type], type, field, recordId); } ` + "\n"; @@ -136,6 +136,6 @@ export function generateUnifiedFileUtils( uploadFunction, deleteFunction, getMetadataFunction, - openDownloadStreamFunction, + downloadStreamFunction, ].join("\n"); } diff --git a/packages/sdk/src/plugin/builtin/file-utils/index.test.ts b/packages/sdk/src/plugin/builtin/file-utils/index.test.ts index feafecc5d..f99a1fdcf 100644 --- a/packages/sdk/src/plugin/builtin/file-utils/index.test.ts +++ b/packages/sdk/src/plugin/builtin/file-utils/index.test.ts @@ -106,6 +106,12 @@ describe("FileUtilsPlugin", () => { expect(result).toContain('"receipt" | "form"'); expect(result).toContain('User: "tailordb"'); expect(result).toContain('SalesOrder: "tailordb"'); + expect(result).toContain("FileDownloadStreamResponse"); + expect(result).toContain("export async function downloadFileStream"); + expect(result).toContain("return await file.downloadStream"); + expect(result).not.toContain("FileStreamIterator"); + expect(result).not.toContain("openFileDownloadStream"); + expect(result).not.toContain("openDownloadStream"); }); test("should merge types from multiple namespaces", () => { diff --git a/packages/sdk/src/runtime/file.test.ts b/packages/sdk/src/runtime/file.test.ts index 36a160c1f..93ab6c40a 100644 --- a/packages/sdk/src/runtime/file.test.ts +++ b/packages/sdk/src/runtime/file.test.ts @@ -98,28 +98,8 @@ describe("@tailor-platform/sdk/runtime/file", () => { ]); }); - test("openDownloadStream forwards and yields StreamValue chunks", async () => { - using fileM = mockFile(); - 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" }, - ]; - fileM.enqueueResult(sequence); - - const stream = await file.openDownloadStream("ns", "Doc", "blob", "rec-1"); - - const chunks: file.StreamValue[] = []; - for await (const chunk of stream) { - chunks.push(chunk); - } - - expect(chunks).toEqual(sequence); - expect(fileM.calls[0]?.method).toBe("openDownloadStream"); + test("does not export the removed openDownloadStream wrapper", () => { + expect("openDownloadStream" in file).toBe(false); }); test("downloadStream forwards and returns body with metadata", async () => { diff --git a/packages/sdk/src/runtime/file.ts b/packages/sdk/src/runtime/file.ts index d062b038a..1ea2b0e31 100644 --- a/packages/sdk/src/runtime/file.ts +++ b/packages/sdk/src/runtime/file.ts @@ -39,13 +39,6 @@ export interface FileMetadata { 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; @@ -80,18 +73,6 @@ export interface FileDownloadStreamResponse { 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" @@ -206,22 +187,6 @@ export interface TailorDBFileAPI { recordId: string, ): Promise; - /** - * Open a download stream for large files. - * @deprecated Use {@link downloadStream} instead. - * @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, - fieldName: string, - recordId: string, - ): Promise; - /** * Download a file as a ReadableStream. * @param namespace - TailorDB namespace @@ -296,15 +261,6 @@ export const deleteFile: TailorDBFileAPI["delete"] = (...args) => api().delete(. */ export const getMetadata: TailorDBFileAPI["getMetadata"] = (...args) => api().getMetadata(...args); -/** - * See {@link TailorDBFileAPI.openDownloadStream}. - * @deprecated Use {@link downloadStream} instead. - * @param args - Forwarded to {@link TailorDBFileAPI.openDownloadStream} - * @returns Async iterator yielding file chunks; call `close()` to release resources - */ -export const openDownloadStream: TailorDBFileAPI["openDownloadStream"] = (...args) => - api().openDownloadStream(...args); - /** * See {@link TailorDBFileAPI.downloadStream}. * @param args - Forwarded to {@link TailorDBFileAPI.downloadStream} diff --git a/packages/sdk/src/vitest/mock.test.ts b/packages/sdk/src/vitest/mock.test.ts index 0a680c4f3..d1e109f5f 100644 --- a/packages/sdk/src/vitest/mock.test.ts +++ b/packages/sdk/src/vitest/mock.test.ts @@ -712,48 +712,9 @@ describe("mock", () => { expect(file.calls).toHaveLength(0); }); - test("openDownloadStream rejects raw bytes to guide callers to structured chunks", async () => { - using file = mockFile(); - file.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 rejects non-StreamValue elements yielded by the iterable", async () => { - using file = mockFile(); - // Uint8Array[] is iterable but its elements aren't StreamValue items. - file.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 () => { - using file = mockFile(); - 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 }, - ]; - file.enqueueResult(sequence); - 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).toEqual(sequence); + test("does not install the removed openDownloadStream file mock", () => { + using _file = mockFile(); + expect("openDownloadStream" in (globalThis as any).tailordb.file).toBe(false); }); 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 e97a39832..938c3dc70 100644 --- a/packages/sdk/src/vitest/mock.ts +++ b/packages/sdk/src/vitest/mock.ts @@ -918,83 +918,6 @@ const FILE_DEFAULTS: Record = { uploadStream: { metadata: { fileSize: 0, sha256sum: "" } }, }; -type FileStream = AsyncIterableIterator & { close(): Promise }; - -function toFileStream(value: unknown): FileStream { - if ( - value !== null && - typeof value === "object" && - Symbol.asyncIterator in value && - typeof (value as { close?: unknown }).close === "function" - ) { - return value as FileStream; - } - if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) { - throw new TypeError( - "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.", - ); - } - if ( - value !== null && - typeof value === "object" && - (Symbol.iterator in value || Symbol.asyncIterator in value) - ) { - const source = value as Iterable | AsyncIterable; - const inner = - Symbol.asyncIterator in source - ? (source as AsyncIterable)[Symbol.asyncIterator]() - : (source as Iterable)[Symbol.iterator](); - 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() {}, - [Symbol.asyncIterator]() { - return stream; - }, - }; - return stream; - } - const empty: FileStream = { - async next() { - return { done: true as const, value: undefined }; - }, - async close() {}, - [Symbol.asyncIterator]() { - return empty; - }, - }; - return empty; -} - -function assertStreamValue(v: unknown): void { - if (v === null || typeof v !== "object") { - throw new TypeError( - 'openDownloadStream expected a StreamValue item ({ type: "metadata" | "chunk" | "complete", ... }); ' + - `got ${typeof v === "object" ? "null" : typeof v}.`, - ); - } - if (v instanceof ArrayBuffer || ArrayBuffer.isView(v)) { - throw new TypeError( - "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( - 'openDownloadStream expected a StreamValue item with type "metadata" | "chunk" | "complete"; ' + - `got ${JSON.stringify(type)}.`, - ); - } -} - /** * Acquire a disposable mock for `tailordb.file`. Restored on dispose. * @returns Disposable File mock control object @@ -1054,14 +977,6 @@ export function mockFile() { async getMetadata(namespace: string, typeName: string, fieldName: string, recordId: string) { return handle("getMetadata", namespace, typeName, fieldName, recordId); }, - async openDownloadStream( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ) { - return toFileStream(handle("openDownloadStream", namespace, typeName, fieldName, recordId)); - }, async downloadStream(namespace: string, typeName: string, fieldName: string, recordId: string) { const resolved = handle("downloadStream", namespace, typeName, fieldName, recordId); if (resolved != null) return resolved; From 067987974205a6a9c993580e91a1d8c26cff353c Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 13:10:04 +0900 Subject: [PATCH 095/618] refactor(sdk): unify principal expression generation --- .../sdk/src/cli/shared/runtime-exprs.test.ts | 120 +++++++++++++++++- packages/sdk/src/cli/shared/runtime-exprs.ts | 62 ++++----- .../sdk/src/parser/service/tailordb/field.ts | 101 ++++++++++++--- .../sdk/src/parser/service/tailordb/index.ts | 2 +- 4 files changed, 238 insertions(+), 47 deletions(-) diff --git a/packages/sdk/src/cli/shared/runtime-exprs.test.ts b/packages/sdk/src/cli/shared/runtime-exprs.test.ts index d587de1f6..c127955d7 100644 --- a/packages/sdk/src/cli/shared/runtime-exprs.test.ts +++ b/packages/sdk/src/cli/shared/runtime-exprs.test.ts @@ -1,5 +1,33 @@ import { describe, test, expect } from "vitest"; -import { buildExecutorArgsExpr, buildResolverOperationHookExpr } from "./runtime-exprs"; +import { + INVOKER_EXPR, + buildExecutorArgsExpr, + buildResolverOperationHookExpr, +} from "./runtime-exprs"; + +const runInvokerExpr = (invoker: unknown): unknown => + Function( + "tailor", + `return ${INVOKER_EXPR};`, + )({ + context: { getInvoker: () => invoker }, + }); + +const runExecutorArgsExpr = (args: Record): Record => + Function("args", `return ${buildExecutorArgsExpr("schedule", {})};`)(args) as Record< + string, + unknown + >; + +const runResolverOperationHookExpr = ( + user: unknown, + context = { pipeline: {}, args: {} }, +): Record => + Function( + "context", + "user", + `return ${buildResolverOperationHookExpr({})}`, + )(context, user) as Record; describe("buildExecutorArgsExpr", () => { const env = { API_URL: "https://example.com", DEBUG: true }; @@ -34,6 +62,41 @@ describe("buildExecutorArgsExpr", () => { const expr = buildExecutorArgsExpr("schedule", env); expect(expr).not.toContain("event:"); }); + + test("maps actor payloads to TailorPrincipal shape", () => { + expect( + runExecutorArgsExpr({ + namespaceName: "app", + actor: { + userType: "USER_TYPE_USER", + userId: "user-1", + workspaceId: "workspace-1", + attributeMap: { role: "admin" }, + attributes: ["role"], + }, + }).actor, + ).toEqual({ + id: "user-1", + type: "user", + workspaceId: "workspace-1", + attributes: { role: "admin" }, + attributeList: ["role"], + }); + }); + + test("maps absent actor payloads to null", () => { + const nilUuid = "00000000-0000-0000-0000-000000000000"; + const actors = [ + null, + undefined, + { userType: "USER_TYPE_UNSPECIFIED", userId: "user-1" }, + { userType: "USER_TYPE_USER", userId: nilUuid }, + { userType: "USER_TYPE_USER" }, + ]; + for (const actor of actors) { + expect(runExecutorArgsExpr({ namespaceName: "app", actor }).actor).toBeNull(); + } + }); }); describe("resolverExecuted trigger", () => { @@ -123,4 +186,59 @@ describe("buildResolverOperationHookExpr", () => { const expr = buildResolverOperationHookExpr({}); expect(expr).toContain("env: {}"); }); + + test("maps caller payloads to TailorPrincipal shape", () => { + expect( + runResolverOperationHookExpr({ + type: "USER_TYPE_MACHINE_USER", + id: "machine-1", + workspace_id: "workspace-1", + attribute_map: { team: "ops" }, + attributes: ["team"], + }).caller, + ).toEqual({ + id: "machine-1", + type: "machine_user", + workspaceId: "workspace-1", + attributes: { team: "ops" }, + attributeList: ["team"], + }); + }); + + test("maps absent caller payloads to null", () => { + const nilUuid = "00000000-0000-0000-0000-000000000000"; + const users = [ + null, + undefined, + { type: "USER_TYPE_UNSPECIFIED", id: "user-1" }, + { type: "USER_TYPE_USER", id: nilUuid }, + ]; + for (const user of users) { + expect(runResolverOperationHookExpr(user).caller).toBeNull(); + } + }); +}); + +describe("INVOKER_EXPR", () => { + test("maps invoker payloads to TailorPrincipal shape", () => { + expect( + runInvokerExpr({ + id: "user-1", + type: "user", + workspaceId: "workspace-1", + attributeMap: { role: "member" }, + attributes: ["role"], + }), + ).toEqual({ + id: "user-1", + type: "user", + workspaceId: "workspace-1", + attributes: { role: "member" }, + attributeList: ["role"], + }); + }); + + test("maps anonymous invokers to null", () => { + expect(runInvokerExpr(null)).toBeNull(); + }); }); diff --git a/packages/sdk/src/cli/shared/runtime-exprs.ts b/packages/sdk/src/cli/shared/runtime-exprs.ts index d6ea43fad..69221c0ff 100644 --- a/packages/sdk/src/cli/shared/runtime-exprs.ts +++ b/packages/sdk/src/cli/shared/runtime-exprs.ts @@ -7,10 +7,13 @@ * - Bundle inline: interpolated into the generated `.entry.js` wrapper and * evaluated inside the bundled script at function entry. * - * The principal field mapping (server → SDK) shared across services is defined - * in `@/parser/service/tailordb` as `tailorPrincipalMap`. + * The principal field mapping (server → SDK) shared across services is built by + * `makePrincipalExpr` from `@/parser/service/tailordb`; `tailorPrincipalMap` + * (the `caller` mapping) is one of its outputs. `INVOKER_EXPR` and + * `ACTOR_TRANSFORM_EXPR` below come from the same factory so the three stay in + * sync. */ -import { tailorPrincipalMap } from "@/parser/service/tailordb"; +import { makePrincipalExpr, tailorPrincipalMap } from "@/parser/service/tailordb"; import type { Trigger } from "@/types/executor.generated"; // --------------------------------------------------------------------------- @@ -21,16 +24,21 @@ import type { Trigger } from "@/types/executor.generated"; * `invoker` value expression, inlined into bundler entry wrappers. * * Calls `tailor.context.getInvoker()` at function entry and maps the server - * shape to `TailorPrincipal | null`. Anonymous callers (`null`) pass through - * as `null`. + * shape to `TailorPrincipal | null`. The payload is already in SDK type shape, + * so no `USER_TYPE_*` normalization is needed; anonymous callers (`null`) pass + * through as `null`. */ -export const INVOKER_EXPR = `(($raw) => $raw ? ({ - id: $raw.id, - type: $raw.type, - workspaceId: $raw.workspaceId, - attributes: $raw.attributeMap, - attributeList: $raw.attributes, -}) : null)(tailor.context.getInvoker())`; +export const INVOKER_EXPR = makePrincipalExpr({ + source: "tailor.context.getInvoker()", + normalize: false, + fields: { + type: { raw: "$raw.type" }, + id: "$raw.id", + workspaceId: "$raw.workspaceId", + attributes: "$raw.attributeMap", + attributeList: "$raw.attributes", + }, +}); // --------------------------------------------------------------------------- // Executor @@ -41,24 +49,18 @@ export const INVOKER_EXPR = `(($raw) => $raw ? ({ * * Transforms the server's actor object to match `TailorPrincipal | null`. */ -const ACTOR_TRANSFORM_EXPR = `actor: (($raw) => { - const type = $raw?.userType === "USER_TYPE_USER" - ? "user" - : $raw?.userType === "USER_TYPE_MACHINE_USER" - ? "machine_user" - : $raw?.type; - const id = $raw?.userId ?? $raw?.id; - if (!$raw || !id || !type || type === "USER_TYPE_UNSPECIFIED" || id === "00000000-0000-0000-0000-000000000000") { - return null; - } - return { - id, - type, - workspaceId: $raw.workspaceId, - attributes: $raw.attributeMap ?? {}, - attributeList: $raw.attributes ?? [], - }; -})(args.actor)`; +const ACTOR_TRANSFORM_EXPR = `actor: ${makePrincipalExpr({ + source: "args.actor", + normalize: true, + requireId: true, + fields: { + type: { raw: "$raw?.userType", fallback: "$raw?.type" }, + id: "$raw?.userId ?? $raw?.id", + workspaceId: "$raw.workspaceId", + attributes: "$raw.attributeMap ?? {}", + attributeList: "$raw.attributes ?? []", + }, +})}`; /** * Build the JavaScript expression that transforms server-format executor event diff --git a/packages/sdk/src/parser/service/tailordb/field.ts b/packages/sdk/src/parser/service/tailordb/field.ts index 217c244a4..1048b1ce4 100644 --- a/packages/sdk/src/parser/service/tailordb/field.ts +++ b/packages/sdk/src/parser/service/tailordb/field.ts @@ -9,25 +9,96 @@ import type { TailorDBTypeRaw as TailorDBTypeSchemaOutput } from "@/types/tailor const NIL_UUID = "00000000-0000-0000-0000-000000000000"; -// Since there's naming difference between platform and SDK, use this mapping in -// all scripts to provide variables that match `TailorPrincipal | null`. -export const tailorPrincipalMap = /* js */ `(($raw) => { - const type = $raw?.type === "USER_TYPE_USER" +/** + * Per-source field expressions for {@link makePrincipalExpr}. Each value is a JS + * snippet evaluated against the raw server payload bound to `$raw`. + */ +interface PrincipalFieldExprs { + /** + * Raw type accessor. `raw` is matched against `USER_TYPE_*` when normalizing; + * `fallback` (defaulting to `raw`) supplies the type for non-matching values, + * which lets sources whose primary field is empty fall back to a secondary. + */ + type: { raw: string; fallback?: string }; + /** Raw id accessor. */ + id: string; + workspaceId: string; + attributes: string; + attributeList: string; +} + +interface MakePrincipalExprOptions { + source: string; + fields: PrincipalFieldExprs; + normalize: boolean; + requireId?: boolean; +} + +/** + * Build the server→SDK principal mapping expression shared across services. + * + * All principal-bearing call sites (`caller`, `actor`, `invoker`) must agree on + * the `TailorPrincipal | null` shape, so they are generated here rather than + * hand-written per service. + * @param options - Mapping source and field expressions + * @param options.source - Expression yielding the raw server payload (e.g. `user`) + * @param options.fields - Per-source accessors for each principal field + * @param options.normalize - When true, map `USER_TYPE_*` to SDK type literals and + * return `null` for unspecified types or the nil-UUID id; when false, the + * payload is already in SDK shape and only `null` pass-through is applied + * @param options.requireId - When true, missing ids are also mapped to `null` + * @returns A JS expression string evaluating to `TailorPrincipal | null` + */ +export function makePrincipalExpr(options: MakePrincipalExprOptions): string { + const { source, fields, normalize, requireId = false } = options; + const body = /* js */ `{ + id, + type, + workspaceId: ${fields.workspaceId}, + attributes: ${fields.attributes}, + attributeList: ${fields.attributeList}, + }`; + if (!normalize) { + return /* js */ `(($raw) => { + if (!$raw) { + return null; + } + const id = ${fields.id}; + const type = ${fields.type.raw}; + return ${body}; +})(${source})`; + } + const missingIdGuard = requireId ? " || !id" : ""; + return /* js */ `(($raw) => { + if (!$raw) { + return null; + } + const type = ${fields.type.raw} === "USER_TYPE_USER" ? "user" - : $raw?.type === "USER_TYPE_MACHINE_USER" + : ${fields.type.raw} === "USER_TYPE_MACHINE_USER" ? "machine_user" - : $raw?.type; - if (!$raw || !type || type === "USER_TYPE_UNSPECIFIED" || $raw.id === "${NIL_UUID}") { + : ${fields.type.fallback ?? fields.type.raw}; + const id = ${fields.id}; + if (!type || type === "USER_TYPE_UNSPECIFIED" || id === "${NIL_UUID}"${missingIdGuard}) { return null; } - return { - id: $raw.id, - type, - workspaceId: $raw.workspace_id ?? $raw.workspaceId, - attributes: $raw.attribute_map ?? $raw.attributeMap ?? {}, - attributeList: $raw.attributes ?? [], - }; -})(user)`; + return ${body}; +})(${source})`; +} + +// Since there's naming difference between platform and SDK, use this mapping in +// all scripts to provide variables that match `TailorPrincipal | null`. +export const tailorPrincipalMap = makePrincipalExpr({ + source: "user", + normalize: true, + fields: { + type: { raw: "$raw?.type" }, + id: "$raw.id", + workspaceId: "$raw.workspace_id ?? $raw.workspaceId", + attributes: "$raw.attribute_map ?? $raw.attributeMap ?? {}", + attributeList: "$raw.attributes ?? []", + }, +}); /** * Convert a function to a string representation. diff --git a/packages/sdk/src/parser/service/tailordb/index.ts b/packages/sdk/src/parser/service/tailordb/index.ts index 53451acf5..2a5692b98 100644 --- a/packages/sdk/src/parser/service/tailordb/index.ts +++ b/packages/sdk/src/parser/service/tailordb/index.ts @@ -1,3 +1,3 @@ -export { stringifyFunction, tailorPrincipalMap } from "./field"; +export { makePrincipalExpr, stringifyFunction, tailorPrincipalMap } from "./field"; export { parseTypes } from "./type-parser"; export { TailorDBServiceConfigSchema, TailorDBTypeSchema } from "./schema"; From 2af228d93bb652d9fdba20baec2d525ae4554e86 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 13:14:56 +0900 Subject: [PATCH 096/618] refactor(codemod): tighten llmReviews construction and docs Address review feedback on the LLM-review support: - Build llmReviews with a typed loop (no `as` casts) and sort each file list so output ordering is deterministic across filesystems. - Type printLlmReview with the shared LlmReview type instead of an inline shape, keeping the CLI and JSON output in lockstep. - Clarify the suspiciousPatterns JSDoc: it has no effect unless prompt is also set (both fields are optional). --- packages/sdk-codemod/src/index.ts | 4 ++-- packages/sdk-codemod/src/runner.ts | 14 +++++++------- packages/sdk-codemod/src/types.ts | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/sdk-codemod/src/index.ts b/packages/sdk-codemod/src/index.ts index 0a5712d63..ec94516a8 100644 --- a/packages/sdk-codemod/src/index.ts +++ b/packages/sdk-codemod/src/index.ts @@ -7,7 +7,7 @@ import { arg, defineCommand, runMain } from "politty"; import { z } from "zod"; import { getApplicableCodemods, resolveCodemodScript } from "./registry"; import { runCodemods } from "./runner"; -import type { RunOutput } from "./types"; +import type { LlmReview, RunOutput } from "./types"; const packageJson = await readPackageJSON(path.dirname(fileURLToPath(import.meta.url)) + "/.."); @@ -17,7 +17,7 @@ const packageJson = await readPackageJSON(path.dirname(fileURLToPath(import.meta * deterministic transform could not complete on its own. * @param review - The review task (codemod id, prompt, files) */ -function printLlmReview(review: { codemodId: string; prompt: string; files: string[] }): void { +function printLlmReview(review: LlmReview): void { process.stderr.write( `\n🤖 LLM-assisted review suggested (${review.codemodId}) — the codemod cannot safely migrate these automatically:\n`, ); diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 25805a9a3..7d134042a 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -226,13 +226,13 @@ export async function runCodemods( } } - const llmReviews: LlmReview[] = loaded - .filter((lt) => lt.prompt && suspiciousByCodemod.has(lt.id)) - .map((lt) => ({ - codemodId: lt.id, - prompt: lt.prompt as string, - files: suspiciousByCodemod.get(lt.id) as string[], - })); + const llmReviews: LlmReview[] = []; + for (const lt of loaded) { + const files = suspiciousByCodemod.get(lt.id); + if (!lt.prompt || !files) continue; + // Sort for deterministic output regardless of filesystem traversal order. + llmReviews.push({ codemodId: lt.id, prompt: lt.prompt, files: files.toSorted() }); + } return { changed: filesModified.length > 0, diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index 5014ff70f..48a4e739d 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -26,8 +26,8 @@ export interface CodemodPackage { * deterministic transform cannot safely complete on its own (e.g. a value * reached through a variable or a dynamic expression). Unlike * `legacyPatterns`, these do not need to be exhaustive: a broad signal such - * as the API name is enough to point an LLM at the right files. Requires - * `prompt`. + * as the API name is enough to point an LLM at the right files. Has no effect + * unless `prompt` is also set. */ suspiciousPatterns?: string[]; /** From 7892a3b412abeeedf35d9e06d661270c1f34d54d Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 13:22:33 +0900 Subject: [PATCH 097/618] docs(sdk): align TailorDB validation principal docs --- packages/sdk/docs/services/tailordb.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index 3d74f862e..39442b7bc 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -323,7 +323,7 @@ Add validation rules to fields. Validators receive three arguments (executed aft - `value`: Field value after hook transformation - `data`: Entire record data after hook transformations (for accessing other field values) -- `user`: User performing the operation +- `invoker`: Principal performing the operation Validators return `true` for success, `false` for failure. Use array form `[validator, errorMessage]` for custom error messages. From f0940be46702075c7d69dce72bc9e65fa5f4893f Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 13:31:38 +0900 Subject: [PATCH 098/618] refactor(cli): default executeScript arg generic to JsonValue Default the ScriptExecutionOptions / executeScript type parameter to JsonValue instead of unknown. With unknown, factoring the options into a variable typed as the bare exported ScriptExecutionOptions fixed T to unknown, collapsing JsonCompatible to never and failing the executeScript(opts) call (TS2345). JsonValue keeps the companion type usable on its own while still rejecting non-JSON values. --- .../src/cli/shared/script-executor.test.ts | 30 ++++++++++++++++++- .../sdk/src/cli/shared/script-executor.ts | 6 ++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/sdk/src/cli/shared/script-executor.test.ts b/packages/sdk/src/cli/shared/script-executor.test.ts index 8f012251f..5144e48dc 100644 --- a/packages/sdk/src/cli/shared/script-executor.test.ts +++ b/packages/sdk/src/cli/shared/script-executor.test.ts @@ -1,6 +1,11 @@ import { FunctionExecution_Status } from "@tailor-proto/tailor/v1/function_resource_pb"; import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"; -import { waitForExecution, executeScript, DEFAULT_POLL_INTERVAL } from "./script-executor"; +import { + waitForExecution, + executeScript, + DEFAULT_POLL_INTERVAL, + type ScriptExecutionOptions, +} from "./script-executor"; import type { OperatorClient } from "@/cli/shared/client"; import type { AuthInvoker } from "@tailor-proto/tailor/v1/auth_resource_pb"; @@ -242,6 +247,29 @@ describe("executeScript", () => { ); }); + test("accepts options typed as the bare ScriptExecutionOptions", async () => { + const client = createMockClient({ + testExecScript: vi.fn().mockResolvedValue({ executionId: "exec-123" }), + getFunctionExecution: vi.fn().mockResolvedValue({ + execution: { status: FunctionExecution_Status.SUCCESS, logs: "", result: "" }, + }), + }); + + // Regression: a typed-out options object (no explicit type argument, so T + // defaults to JsonValue) must stay assignable to executeScript's parameter. + const options: ScriptExecutionOptions = { + client, + workspaceId: "workspace-1", + name: "test-script.js", + code: "code", + arg: { a: 1 }, + invoker: mockAuthInvoker, + }; + + const result = await executeScript(options); + expect(result.success).toBe(true); + }); + test("returns failure result when script fails", async () => { const client = createMockClient({ testExecScript: vi.fn().mockResolvedValue({ diff --git a/packages/sdk/src/cli/shared/script-executor.ts b/packages/sdk/src/cli/shared/script-executor.ts index f726b741b..93d806af8 100644 --- a/packages/sdk/src/cli/shared/script-executor.ts +++ b/packages/sdk/src/cli/shared/script-executor.ts @@ -7,7 +7,7 @@ import { FunctionExecution_Status } from "@tailor-proto/tailor/v1/function_resource_pb"; import type { OperatorClient } from "@/cli/shared/client"; -import type { JsonCompatible } from "@/types/helpers"; +import type { JsonCompatible, JsonValue } from "@/types/helpers"; import type { AuthInvoker } from "@tailor-proto/tailor/v1/auth_resource_pb"; /** @@ -18,7 +18,7 @@ export const DEFAULT_POLL_INTERVAL = 1000; /** * Options for script execution */ -export interface ScriptExecutionOptions { +export interface ScriptExecutionOptions { /** Operator client instance */ client: OperatorClient; /** Workspace ID */ @@ -118,7 +118,7 @@ export async function waitForExecution( * @param {ScriptExecutionOptions} options - Execution options * @returns {Promise} Execution result */ -export async function executeScript( +export async function executeScript( options: ScriptExecutionOptions & { arg?: JsonCompatible }, ): Promise { const { client, workspaceId, name, code, arg, invoker, pollInterval } = options; From c2c7028542af38b4ad28a92886a86271cd633821 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 13:38:56 +0900 Subject: [PATCH 099/618] fix(sdk-codemod): rewrite typed principal callback contexts --- .../codemods/v2/principal-unify/scripts/transform.ts | 1 + .../v2/principal-unify/tests/tailordb-callbacks/expected.ts | 2 +- .../v2/principal-unify/tests/tailordb-callbacks/input.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 4dba0c173..edefab7ce 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -623,6 +623,7 @@ function transformPrincipalCallbackParam(fn: SgNode, edits: Edit[]): void { } } } + transformPrincipalCallbackParamType(param, edits); return; } diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index 5a986f860..2d8d5f7a5 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -4,7 +4,7 @@ const role = db .string() .hooks({ create: ({ value, invoker }) => (invoker?.attributes.role === "ADMIN" ? value : "user"), - update: ctx => ctx.invoker?.id ?? "anonymous", + update: (ctx: { invoker: TailorPrincipal | null }) => ctx.invoker?.id ?? "anonymous", delete({ user }) { return user?.id ?? "anonymous"; }, diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index d2e3c8ec8..66067a76e 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -5,7 +5,7 @@ const role = db .string() .hooks({ create: ({ value, user }) => (user?.attributes.role === "ADMIN" ? value : "user"), - update: ctx => ctx.user?.id ?? "anonymous", + update: (ctx: { user: TailorUser | null }) => ctx.user?.id ?? "anonymous", delete({ user }) { return user?.id ?? "anonymous"; }, From c28c7965ba0e0dd257b1efeb199d3b497756c2a4 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 13:40:38 +0900 Subject: [PATCH 100/618] chore(example): refresh principal migration diff --- example/migrations/0002/diff.json | 50 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/example/migrations/0002/diff.json b/example/migrations/0002/diff.json index 99742e58a..e3abafca4 100644 --- a/example/migrations/0002/diff.json +++ b/example/migrations/0002/diff.json @@ -1,7 +1,7 @@ { "version": 1, "namespace": "tailordb", - "createdAt": "2026-06-16T09:57:56.775Z", + "createdAt": "2026-06-17T04:37:34.736Z", "changes": [ { "kind": "field_modified", @@ -25,7 +25,7 @@ "validate": [ { "script": { - "expr": "(({value})=>value.length>5)({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({value})=>value.length>5)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "errorMessage": "Name must be longer than 5 characters" } @@ -60,13 +60,13 @@ "validate": [ { "script": { - "expr": "(({value})=>value?value.length>1:true)({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({value})=>value?value.length>1:true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "errorMessage": "failed by `({value})=>value?value.length>1:true`" }, { "script": { - "expr": "(({value})=>value?value.length<100:true)({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({value})=>value?value.length<100:true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "errorMessage": "failed by `({value})=>value?value.length<100:true`" } @@ -94,10 +94,10 @@ "required": true, "hooks": { "create": { - "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "update": { - "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -122,7 +122,7 @@ "description": "Record creation timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -147,7 +147,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -172,7 +172,7 @@ "description": "Record creation timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -197,7 +197,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -222,7 +222,7 @@ "description": "Record creation timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -247,7 +247,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -314,7 +314,7 @@ "validate": [ { "script": { - "expr": "(({value})=>value>0)({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({value})=>value>0)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "errorMessage": "failed by `({value})=>value>0`" } @@ -355,7 +355,7 @@ "description": "Record creation timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -380,7 +380,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -405,7 +405,7 @@ "description": "Record creation timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -430,7 +430,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -455,7 +455,7 @@ "description": "Record creation timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -480,7 +480,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -505,7 +505,7 @@ "description": "Record creation timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -530,7 +530,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -555,7 +555,7 @@ "description": "Record creation timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -580,7 +580,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -605,7 +605,7 @@ "description": "Record creation timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -630,7 +630,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n if (!$raw || !type || type === \"USER_TYPE_UNSPECIFIED\" || $raw.id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id: $raw.id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } From 60efe017b2fc1663b03278b5ab64948dd8bb8e7c Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 13:57:10 +0900 Subject: [PATCH 101/618] ci: sync docs consistency workflow --- .github/workflows/docs-consistency-check.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs-consistency-check.yml b/.github/workflows/docs-consistency-check.yml index e0e89fb03..116b3d55e 100644 --- a/.github/workflows/docs-consistency-check.yml +++ b/.github/workflows/docs-consistency-check.yml @@ -10,18 +10,24 @@ on: - "AGENTS.md" - "CLAUDE.md" -permissions: - contents: read - pull-requests: write - id-token: write +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} jobs: docs-consistency: + name: Docs consistency if: > github.event.action == 'ready_for_review' || (github.event.action == 'labeled' && github.event.label.name == 'docs-check') runs-on: ubuntu-slim timeout-minutes: 10 + permissions: + contents: read # checkout the PR branch + pull-requests: write # upsert the docs review on the PR + id-token: write # OIDC for the Claude Code action env: PR_NUMBER: ${{ github.event.number }} REVIEW_BODY_FILE: ${{ github.workspace }}/docs-consistency-review.md @@ -237,4 +243,5 @@ jobs: if: always() && github.event.action == 'labeled' && github.event.label.name == 'docs-check' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --remove-label docs-check + REPO: ${{ github.repository }} + run: gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label docs-check From f6ce786f0a6d6bf23b8a10f2b6b177f5d900b9b9 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 14:00:59 +0900 Subject: [PATCH 102/618] fix(sdk-codemod): rewrite referenced TailorDB callbacks --- .../v2/principal-unify/scripts/transform.ts | 165 ++++++++++++++++-- .../tests/tailordb-callbacks/expected.ts | 16 +- .../tests/tailordb-callbacks/input.ts | 16 +- 3 files changed, 175 insertions(+), 22 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index edefab7ce..89c25780d 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -526,6 +526,7 @@ function findMemberCallObject(call: SgNode): SgNode | null { function isFunctionNode(node: SgNode): boolean { return ( node.kind() === "arrow_function" || + node.kind() === "function_declaration" || node.kind() === "function_expression" || node.kind() === "method_definition" ); @@ -731,6 +732,15 @@ function isShadowedLocalReference( if (scope && rangeContains(scope, pos)) return true; } + const functionDecls = root.findAll({ rule: { kind: "function_declaration" } }); + for (const fn of functionDecls) { + const nameNode = fn.field("name"); + if (!nameNode || nameNode.text() !== name) continue; + if (nameNode.range().start.index === bindingStart) continue; + const scope = enclosingScopeRange(fn); + if (scope && rangeContains(scope, pos)) return true; + } + for (const kind of NESTED_FN_KINDS) { const fns = root.findAll({ rule: { kind } }); for (const fn of fns) { @@ -823,7 +833,93 @@ function isSdkFieldMemberCall( : false; } -function transformHookCallbackObject(node: SgNode, edits: Edit[]): void { +interface LocalCallbackBinding { + name: string; + fn: SgNode; + bindingStart: number; + scope: [number, number]; +} + +function collectLocalCallbackBindings(root: SgNode): LocalCallbackBinding[] { + const rootRange = root.range(); + const rootScope: [number, number] = [rootRange.start.index, rootRange.end.index]; + const bindings: LocalCallbackBinding[] = []; + + const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarators) { + const name = decl.field("name"); + const value = decl.field("value"); + if (!name || name.kind() !== "identifier" || !value || !isFunctionNode(value)) continue; + bindings.push({ + name: name.text(), + fn: value, + bindingStart: name.range().start.index, + scope: enclosingScopeRange(decl) ?? rootScope, + }); + } + + const functionDecls = root.findAll({ rule: { kind: "function_declaration" } }); + for (const fn of functionDecls) { + const name = fn.field("name"); + if (!name || name.kind() !== "identifier") continue; + bindings.push({ + name: name.text(), + fn, + bindingStart: name.range().start.index, + scope: enclosingScopeRange(fn) ?? rootScope, + }); + } + + return bindings; +} + +function resolveLocalCallbackBinding( + node: SgNode, + bindings: LocalCallbackBinding[], + root: SgNode, +): LocalCallbackBinding | null { + if (node.kind() !== "identifier") return null; + const pos = node.range().start.index; + return ( + bindings.find( + (binding) => + binding.name === node.text() && + rangeContains(binding.scope, pos) && + !isShadowedLocalReference(root, binding.name, pos, binding.bindingStart), + ) ?? null + ); +} + +function transformPrincipalCallbackNode( + node: SgNode, + edits: Edit[], + callbackBindings: LocalCallbackBinding[], + transformedCallbackStarts: Set, + root: SgNode, +): boolean { + if (isFunctionNode(node)) { + transformPrincipalCallbackParam(node, edits); + return true; + } + + const binding = resolveLocalCallbackBinding(node, callbackBindings, root); + if (!binding) return false; + + const start = binding.fn.range().start.index; + if (!transformedCallbackStarts.has(start)) { + transformedCallbackStarts.add(start); + transformPrincipalCallbackParam(binding.fn, edits); + } + return true; +} + +function transformHookCallbackObject( + node: SgNode, + edits: Edit[], + callbackBindings: LocalCallbackBinding[], + transformedCallbackStarts: Set, + root: SgNode, +): void { if (node.kind() !== "object") return; for (const child of node.children()) { if (child.kind() === "method_definition") { @@ -835,27 +931,42 @@ function transformHookCallbackObject(node: SgNode, edits: Edit[]): void { const key = child.field("key")?.text(); const value = child.field("value"); if (!value) continue; - if ((key === "create" || key === "update") && isFunctionNode(value)) { - transformPrincipalCallbackParam(value, edits); + if (key === "create" || key === "update") { + transformPrincipalCallbackNode( + value, + edits, + callbackBindings, + transformedCallbackStarts, + root, + ); } else if (value.kind() === "object") { - transformHookCallbackObject(value, edits); + transformHookCallbackObject(value, edits, callbackBindings, transformedCallbackStarts, root); } } } -function transformValidateCallbackNode(node: SgNode, edits: Edit[]): void { - if (isFunctionNode(node)) { - transformPrincipalCallbackParam(node, edits); +function transformValidateCallbackNode( + node: SgNode, + edits: Edit[], + callbackBindings: LocalCallbackBinding[], + transformedCallbackStarts: Set, + root: SgNode, +): void { + if ( + transformPrincipalCallbackNode(node, edits, callbackBindings, transformedCallbackStarts, root) + ) { return; } if (node.kind() === "array") { for (const child of node.children()) { - if (isFunctionNode(child)) { - transformPrincipalCallbackParam(child, edits); - } else if (child.kind() === "array") { - transformValidateCallbackNode(child, edits); - } + transformValidateCallbackNode( + child, + edits, + callbackBindings, + transformedCallbackStarts, + root, + ); } return; } @@ -868,7 +979,15 @@ function transformValidateCallbackNode(node: SgNode, edits: Edit[]): void { } if (child.kind() !== "pair") continue; const value = child.field("value"); - if (value) transformValidateCallbackNode(value, edits); + if (value) { + transformValidateCallbackNode( + value, + edits, + callbackBindings, + transformedCallbackStarts, + root, + ); + } } } @@ -877,6 +996,8 @@ function transformPrincipalCallbacksInCall( edits: Edit[], sdkFieldRootNames: Set, sdkFieldLocalBindings: SdkFieldLocalBinding[], + callbackBindings: LocalCallbackBinding[], + transformedCallbackStarts: Set, root: SgNode, ): void { const memberName = findMemberCallName(call); @@ -887,12 +1008,14 @@ function transformPrincipalCallbacksInCall( if (!args) return; if (memberName === "hooks") { const objArg = args.children().find((c: SgNode) => c.kind() === "object"); - if (objArg) transformHookCallbackObject(objArg, edits); + if (objArg) { + transformHookCallbackObject(objArg, edits, callbackBindings, transformedCallbackStarts, root); + } return; } for (const arg of args.children()) { - transformValidateCallbackNode(arg, edits); + transformValidateCallbackNode(arg, edits, callbackBindings, transformedCallbackStarts, root); } } @@ -1050,9 +1173,19 @@ export default function transform(source: string): string | null { } const sdkFieldLocalBindings = collectSdkFieldLocalBindings(tree, sdkFieldRootNames); + const callbackBindings = collectLocalCallbackBindings(tree); + const transformedCallbackStarts = new Set(); const memberCalls = tree.findAll({ rule: { kind: "call_expression" } }); for (const call of memberCalls) { - transformPrincipalCallbacksInCall(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); + transformPrincipalCallbacksInCall( + call, + edits, + sdkFieldRootNames, + sdkFieldLocalBindings, + callbackBindings, + transformedCallbackStarts, + tree, + ); transformParseArgsObject(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); } diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index 2d8d5f7a5..9299d3216 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -1,18 +1,28 @@ import { db, t, type TailorPrincipal } from "@tailor-platform/sdk"; +const roleCreate = ({ value, invoker }: { value: string; invoker: TailorPrincipal | null }) => + invoker?.attributes.role === "ADMIN" ? value : "user"; + +function hasMachineUser({ invoker }: { invoker: TailorPrincipal | null }) { + return invoker?.type === "machine_user"; +} + +const hasInvoker = (ctx: { invoker: TailorPrincipal | null }) => ctx.invoker?.id !== ""; + const role = db .string() .hooks({ - create: ({ value, invoker }) => (invoker?.attributes.role === "ADMIN" ? value : "user"), + create: roleCreate, update: (ctx: { invoker: TailorPrincipal | null }) => ctx.invoker?.id ?? "anonymous", delete({ user }) { return user?.id ?? "anonymous"; }, }) .validate([ - [({ invoker }) => invoker?.type === "machine_user", "Machine user required"], + [hasMachineUser, "Machine user required"], ctx => ctx.invoker?.id !== "", - ]); + ]) + .validate(hasInvoker); const reviewer = t.string(); const zodLike = { parse: (arg: unknown) => arg }; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index 66067a76e..e8d1c31ca 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -1,19 +1,29 @@ import { db, t, type TailorUser } from "@tailor-platform/sdk"; import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; +const roleCreate = ({ value, user }: { value: string; user: TailorUser | null }) => + user?.attributes.role === "ADMIN" ? value : "user"; + +function hasMachineUser({ user }: { user: TailorUser | null }) { + return user?.type === "machine_user"; +} + +const hasInvoker = (ctx: { user: TailorUser | null }) => ctx.user?.id !== ""; + const role = db .string() .hooks({ - create: ({ value, user }) => (user?.attributes.role === "ADMIN" ? value : "user"), + create: roleCreate, update: (ctx: { user: TailorUser | null }) => ctx.user?.id ?? "anonymous", delete({ user }) { return user?.id ?? "anonymous"; }, }) .validate([ - [({ user }) => user?.type === "machine_user", "Machine user required"], + [hasMachineUser, "Machine user required"], ctx => ctx.user?.id !== "", - ]); + ]) + .validate(hasInvoker); const reviewer = t.string(); const zodLike = { parse: (arg: unknown) => arg }; From 60ee55dc7dd48a5251a625b9d7ff6af16f4154a3 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 14:07:29 +0900 Subject: [PATCH 103/618] refactor(cli): type executeScript arg with Jsonifiable Use type-fest`s Jsonifiable for the executeScript arg type (`ScriptExecutionOptions`, `arg?: T`) instead of the JsonCompatible/JsonValue combination. This matches the existing `workflow start` arg typing, accepts interface-typed objects (which JsonValue rejects via its index signature), keeps the generic for callers that want a precise arg type, and stays usable on the bare exported type while still rejecting non-JSON values. --- packages/sdk/src/cli/commands/function/test-run.ts | 4 ++-- packages/sdk/src/cli/shared/script-executor.test.ts | 4 ++-- packages/sdk/src/cli/shared/script-executor.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/sdk/src/cli/commands/function/test-run.ts b/packages/sdk/src/cli/commands/function/test-run.ts index d300e35e9..2ac4293c4 100644 --- a/packages/sdk/src/cli/commands/function/test-run.ts +++ b/packages/sdk/src/cli/commands/function/test-run.ts @@ -22,7 +22,7 @@ import { formatErrorWithSourcemap } from "@/cli/shared/stack-trace"; import { assertDefined } from "@/utils/assert"; import { bundleForTestRun, type ResolvedMachineUser } from "./bundle"; import { detectFunctionType } from "./detect"; -import type { JsonValue } from "@/types/helpers"; +import type { Jsonifiable } from "type-fest"; export const testRunCommand = defineAppCommand({ name: "test-run", @@ -163,7 +163,7 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file logger.info(`Executing on workspace ${styles.dim(workspaceId)}...`); - let parsedArg: JsonValue | undefined; + let parsedArg: Jsonifiable | undefined; if (args.arg !== undefined) { try { parsedArg = JSON.parse(args.arg); diff --git a/packages/sdk/src/cli/shared/script-executor.test.ts b/packages/sdk/src/cli/shared/script-executor.test.ts index 5144e48dc..4fcca0c4a 100644 --- a/packages/sdk/src/cli/shared/script-executor.test.ts +++ b/packages/sdk/src/cli/shared/script-executor.test.ts @@ -255,8 +255,8 @@ describe("executeScript", () => { }), }); - // Regression: a typed-out options object (no explicit type argument, so T - // defaults to JsonValue) must stay assignable to executeScript's parameter. + // Regression: a typed-out options object must stay assignable to + // executeScript's parameter (arg is Jsonifiable, usable on its own). const options: ScriptExecutionOptions = { client, workspaceId: "workspace-1", diff --git a/packages/sdk/src/cli/shared/script-executor.ts b/packages/sdk/src/cli/shared/script-executor.ts index 93d806af8..a08516b8b 100644 --- a/packages/sdk/src/cli/shared/script-executor.ts +++ b/packages/sdk/src/cli/shared/script-executor.ts @@ -7,8 +7,8 @@ import { FunctionExecution_Status } from "@tailor-proto/tailor/v1/function_resource_pb"; import type { OperatorClient } from "@/cli/shared/client"; -import type { JsonCompatible, JsonValue } from "@/types/helpers"; import type { AuthInvoker } from "@tailor-proto/tailor/v1/auth_resource_pb"; +import type { Jsonifiable } from "type-fest"; /** * Default polling interval for script execution status in milliseconds (1 second) @@ -18,7 +18,7 @@ export const DEFAULT_POLL_INTERVAL = 1000; /** * Options for script execution */ -export interface ScriptExecutionOptions { +export interface ScriptExecutionOptions { /** Operator client instance */ client: OperatorClient; /** Workspace ID */ @@ -118,8 +118,8 @@ export async function waitForExecution( * @param {ScriptExecutionOptions} options - Execution options * @returns {Promise} Execution result */ -export async function executeScript( - options: ScriptExecutionOptions & { arg?: JsonCompatible }, +export async function executeScript( + options: ScriptExecutionOptions, ): Promise { const { client, workspaceId, name, code, arg, invoker, pollInterval } = options; From dd178256f98685ba8c79b8bff561f6740c42e368 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 14:19:38 +0900 Subject: [PATCH 104/618] fix(sdk-codemod): migrate local TailorDB hook configs --- .../v2/principal-unify/scripts/transform.ts | 117 +++++++++++++++++- .../tests/tailordb-callbacks/expected.ts | 19 +++ .../tests/tailordb-callbacks/input.ts | 19 +++ 3 files changed, 149 insertions(+), 6 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 89c25780d..204afc06b 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -840,9 +840,20 @@ interface LocalCallbackBinding { scope: [number, number]; } -function collectLocalCallbackBindings(root: SgNode): LocalCallbackBinding[] { +interface LocalObjectBinding { + name: string; + object: SgNode; + bindingStart: number; + scope: [number, number]; +} + +function localBindingRootScope(root: SgNode): [number, number] { const rootRange = root.range(); - const rootScope: [number, number] = [rootRange.start.index, rootRange.end.index]; + return [rootRange.start.index, rootRange.end.index]; +} + +function collectLocalCallbackBindings(root: SgNode): LocalCallbackBinding[] { + const rootScope = localBindingRootScope(root); const bindings: LocalCallbackBinding[] = []; const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); @@ -873,6 +884,26 @@ function collectLocalCallbackBindings(root: SgNode): LocalCallbackBinding[] { return bindings; } +function collectLocalObjectBindings(root: SgNode): LocalObjectBinding[] { + const rootScope = localBindingRootScope(root); + const bindings: LocalObjectBinding[] = []; + + const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarators) { + const name = decl.field("name"); + const value = decl.field("value"); + if (!name || name.kind() !== "identifier" || value?.kind() !== "object") continue; + bindings.push({ + name: name.text(), + object: value, + bindingStart: name.range().start.index, + scope: enclosingScopeRange(decl) ?? rootScope, + }); + } + + return bindings; +} + function resolveLocalCallbackBinding( node: SgNode, bindings: LocalCallbackBinding[], @@ -890,6 +921,23 @@ function resolveLocalCallbackBinding( ); } +function resolveLocalObjectBinding( + node: SgNode, + bindings: LocalObjectBinding[], + root: SgNode, +): LocalObjectBinding | null { + if (node.kind() !== "identifier") return null; + const pos = node.range().start.index; + return ( + bindings.find( + (binding) => + binding.name === node.text() && + rangeContains(binding.scope, pos) && + !isShadowedLocalReference(root, binding.name, pos, binding.bindingStart), + ) ?? null + ); +} + function transformPrincipalCallbackNode( node: SgNode, edits: Edit[], @@ -932,19 +980,59 @@ function transformHookCallbackObject( const value = child.field("value"); if (!value) continue; if (key === "create" || key === "update") { - transformPrincipalCallbackNode( + const transformed = transformPrincipalCallbackNode( value, edits, callbackBindings, transformedCallbackStarts, root, ); + if (!transformed && value.kind() === "object") { + transformHookCallbackObject( + value, + edits, + callbackBindings, + transformedCallbackStarts, + root, + ); + } } else if (value.kind() === "object") { transformHookCallbackObject(value, edits, callbackBindings, transformedCallbackStarts, root); } } } +function transformHookCallbackConfigNode( + node: SgNode, + edits: Edit[], + callbackBindings: LocalCallbackBinding[], + objectBindings: LocalObjectBinding[], + transformedCallbackStarts: Set, + transformedObjectStarts: Set, + root: SgNode, +): boolean { + if (node.kind() === "object") { + transformHookCallbackObject(node, edits, callbackBindings, transformedCallbackStarts, root); + return true; + } + + const binding = resolveLocalObjectBinding(node, objectBindings, root); + if (!binding) return false; + + const start = binding.object.range().start.index; + if (!transformedObjectStarts.has(start)) { + transformedObjectStarts.add(start); + transformHookCallbackObject( + binding.object, + edits, + callbackBindings, + transformedCallbackStarts, + root, + ); + } + return true; +} + function transformValidateCallbackNode( node: SgNode, edits: Edit[], @@ -997,7 +1085,9 @@ function transformPrincipalCallbacksInCall( sdkFieldRootNames: Set, sdkFieldLocalBindings: SdkFieldLocalBinding[], callbackBindings: LocalCallbackBinding[], + objectBindings: LocalObjectBinding[], transformedCallbackStarts: Set, + transformedObjectStarts: Set, root: SgNode, ): void { const memberName = findMemberCallName(call); @@ -1007,9 +1097,20 @@ function transformPrincipalCallbacksInCall( const args = call.field("arguments"); if (!args) return; if (memberName === "hooks") { - const objArg = args.children().find((c: SgNode) => c.kind() === "object"); - if (objArg) { - transformHookCallbackObject(objArg, edits, callbackBindings, transformedCallbackStarts, root); + for (const arg of args.children()) { + if ( + transformHookCallbackConfigNode( + arg, + edits, + callbackBindings, + objectBindings, + transformedCallbackStarts, + transformedObjectStarts, + root, + ) + ) { + break; + } } return; } @@ -1174,7 +1275,9 @@ export default function transform(source: string): string | null { const sdkFieldLocalBindings = collectSdkFieldLocalBindings(tree, sdkFieldRootNames); const callbackBindings = collectLocalCallbackBindings(tree); + const objectBindings = collectLocalObjectBindings(tree); const transformedCallbackStarts = new Set(); + const transformedObjectStarts = new Set(); const memberCalls = tree.findAll({ rule: { kind: "call_expression" } }); for (const call of memberCalls) { transformPrincipalCallbacksInCall( @@ -1183,7 +1286,9 @@ export default function transform(source: string): string | null { sdkFieldRootNames, sdkFieldLocalBindings, callbackBindings, + objectBindings, transformedCallbackStarts, + transformedObjectStarts, tree, ); transformParseArgsObject(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index 9299d3216..4ffd03e15 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -9,6 +9,11 @@ function hasMachineUser({ invoker }: { invoker: TailorPrincipal | null }) { const hasInvoker = (ctx: { invoker: TailorPrincipal | null }) => ctx.invoker?.id !== ""; +const sharedHooks = { + create: ({ invoker }: { invoker: TailorPrincipal | null }) => invoker?.id ?? "anonymous", + update: (ctx: { invoker: TailorPrincipal | null }) => ctx.invoker?.id ?? "anonymous", +}; + const role = db .string() .hooks({ @@ -24,6 +29,8 @@ const role = db ]) .validate(hasInvoker); +const localHookedRole = db.string().hooks(sharedHooks); + const reviewer = t.string(); const zodLike = { parse: (arg: unknown) => arg }; @@ -53,6 +60,18 @@ export const user = db typed: ({ invoker }: { invoker: TailorPrincipal | null }) => invoker?.id !== "", }); +export const audit = db + .type("Audit", { + create: db.string(), + update: db.string(), + }) + .hooks({ + create: { + create: ({ invoker }: { invoker: TailorPrincipal | null }) => invoker?.id ?? "anonymous", + update: (ctx: { invoker: TailorPrincipal | null }) => ctx.invoker?.id ?? "anonymous", + }, + }); + export const parsed = t.string().parse({ value: "hello", data: {}, diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index e8d1c31ca..65b24f53b 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -10,6 +10,11 @@ function hasMachineUser({ user }: { user: TailorUser | null }) { const hasInvoker = (ctx: { user: TailorUser | null }) => ctx.user?.id !== ""; +const sharedHooks = { + create: ({ user }: { user: TailorUser | null }) => user?.id ?? "anonymous", + update: (ctx: { user: TailorUser | null }) => ctx.user?.id ?? "anonymous", +}; + const role = db .string() .hooks({ @@ -25,6 +30,8 @@ const role = db ]) .validate(hasInvoker); +const localHookedRole = db.string().hooks(sharedHooks); + const reviewer = t.string(); const zodLike = { parse: (arg: unknown) => arg }; @@ -54,6 +61,18 @@ export const user = db typed: ({ user }: { user: TailorUser | null }) => user?.id !== "", }); +export const audit = db + .type("Audit", { + create: db.string(), + update: db.string(), + }) + .hooks({ + create: { + create: ({ user }: { user: TailorUser | null }) => user?.id ?? "anonymous", + update: (ctx: { user: TailorUser | null }) => ctx.user?.id ?? "anonymous", + }, + }); + export const parsed = t.string().parse({ value: "hello", data: {}, From 7b2cb11f7f9411d039713de764aefd993b7d6411 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 14:36:57 +0900 Subject: [PATCH 105/618] fix(sdk-codemod): rewrite named TailorDB callback arg types --- .../v2/principal-unify/scripts/transform.ts | 187 ++++++++++++++++-- .../tests/tailordb-callbacks/expected.ts | 17 +- .../tests/tailordb-callbacks/input.ts | 17 +- 3 files changed, 202 insertions(+), 19 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 204afc06b..c5994b4d7 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -564,11 +564,20 @@ function getFunctionParamPattern(param: SgNode): SgNode | null { return param.field("pattern"); } -function transformPrincipalCallbackParamType(param: SgNode, edits: Edit[]): void { - const typeAnnotation = param.field("type"); - const objectType = typeAnnotation?.children().find((c: SgNode) => c.kind() === "object_type"); - if (!objectType) return; +interface LocalCallbackTypeBinding { + name: string; + declaration: SgNode; + bindingStart: number; + scope: [number, number]; +} + +interface CallbackTypeContext { + bindings: LocalCallbackTypeBinding[]; + transformedTypeStarts: Set; + root: SgNode; +} +function transformObjectTypeUserProperty(objectType: SgNode, edits: Edit[]): void { for (const child of objectType.children()) { if (child.kind() !== "property_signature") continue; const name = child.field("name"); @@ -576,7 +585,108 @@ function transformPrincipalCallbackParamType(param: SgNode, edits: Edit[]): void } } -function transformPrincipalCallbackParam(fn: SgNode, edits: Edit[]): void { +function localCallbackTypeObject(declaration: SgNode): SgNode | null { + return ( + declaration + .children() + .find((c: SgNode) => c.kind() === "object_type" || c.kind() === "interface_body") ?? null + ); +} + +function collectLocalCallbackTypeBindings(root: SgNode): LocalCallbackTypeBinding[] { + const rootScope = localBindingRootScope(root); + const bindings: LocalCallbackTypeBinding[] = []; + + for (const kind of ["type_alias_declaration", "interface_declaration"]) { + const declarations = root.findAll({ rule: { kind } }); + for (const declaration of declarations) { + if (!localCallbackTypeObject(declaration)) continue; + const name = declaration.field("name"); + if (!name || (name.kind() !== "type_identifier" && name.kind() !== "identifier")) continue; + bindings.push({ + name: name.text(), + declaration, + bindingStart: name.range().start.index, + scope: enclosingScopeRange(declaration) ?? rootScope, + }); + } + } + + return bindings; +} + +function isShadowedLocalTypeReference( + root: SgNode, + name: string, + pos: number, + bindingStart: number, +): boolean { + for (const kind of ["type_alias_declaration", "interface_declaration"]) { + const declarations = root.findAll({ rule: { kind } }); + for (const declaration of declarations) { + const nameNode = declaration.field("name"); + if (!nameNode || nameNode.text() !== name) continue; + if (nameNode.range().start.index === bindingStart) continue; + const scope = enclosingScopeRange(declaration); + if (scope && rangeContains(scope, pos)) return true; + } + } + return false; +} + +function resolveLocalCallbackTypeBinding( + node: SgNode, + context: CallbackTypeContext, +): LocalCallbackTypeBinding | null { + const pos = node.range().start.index; + return ( + context.bindings.find( + (binding) => + binding.name === node.text() && + rangeContains(binding.scope, pos) && + !isShadowedLocalTypeReference(context.root, binding.name, pos, binding.bindingStart), + ) ?? null + ); +} + +function transformNamedPrincipalCallbackType( + typeName: SgNode, + edits: Edit[], + context: CallbackTypeContext, +): void { + const binding = resolveLocalCallbackTypeBinding(typeName, context); + if (!binding) return; + + const start = binding.declaration.range().start.index; + if (context.transformedTypeStarts.has(start)) return; + + const objectType = localCallbackTypeObject(binding.declaration); + if (!objectType) return; + context.transformedTypeStarts.add(start); + transformObjectTypeUserProperty(objectType, edits); +} + +function transformPrincipalCallbackParamType( + param: SgNode, + edits: Edit[], + typeContext?: CallbackTypeContext, +): void { + const typeAnnotation = param.field("type"); + const objectType = typeAnnotation?.children().find((c: SgNode) => c.kind() === "object_type"); + if (objectType) { + transformObjectTypeUserProperty(objectType, edits); + return; + } + + const typeName = typeAnnotation?.children().find((c: SgNode) => c.kind() === "type_identifier"); + if (typeName && typeContext) transformNamedPrincipalCallbackType(typeName, edits, typeContext); +} + +function transformPrincipalCallbackParam( + fn: SgNode, + edits: Edit[], + typeContext?: CallbackTypeContext, +): void { const param = getFirstFunctionParam(fn); if (!param) return; const pattern = getFunctionParamPattern(param); @@ -624,7 +734,7 @@ function transformPrincipalCallbackParam(fn: SgNode, edits: Edit[]): void { } } } - transformPrincipalCallbackParamType(param, edits); + transformPrincipalCallbackParamType(param, edits, typeContext); return; } @@ -653,7 +763,7 @@ function transformPrincipalCallbackParam(fn: SgNode, edits: Edit[]): void { if (!hasUserParamProperty) return; if (renamesBinding && patternBindsName(pattern, "invoker")) return; - transformPrincipalCallbackParamType(param, edits); + transformPrincipalCallbackParamType(param, edits, typeContext); const aliasRenamedUser = renamesBinding && collectAllShadowRanges(body, "invoker").length > 0; @@ -943,10 +1053,11 @@ function transformPrincipalCallbackNode( edits: Edit[], callbackBindings: LocalCallbackBinding[], transformedCallbackStarts: Set, + typeContext: CallbackTypeContext, root: SgNode, ): boolean { if (isFunctionNode(node)) { - transformPrincipalCallbackParam(node, edits); + transformPrincipalCallbackParam(node, edits, typeContext); return true; } @@ -956,7 +1067,7 @@ function transformPrincipalCallbackNode( const start = binding.fn.range().start.index; if (!transformedCallbackStarts.has(start)) { transformedCallbackStarts.add(start); - transformPrincipalCallbackParam(binding.fn, edits); + transformPrincipalCallbackParam(binding.fn, edits, typeContext); } return true; } @@ -966,13 +1077,16 @@ function transformHookCallbackObject( edits: Edit[], callbackBindings: LocalCallbackBinding[], transformedCallbackStarts: Set, + typeContext: CallbackTypeContext, root: SgNode, ): void { if (node.kind() !== "object") return; for (const child of node.children()) { if (child.kind() === "method_definition") { const key = propertyName(child); - if (key === "create" || key === "update") transformPrincipalCallbackParam(child, edits); + if (key === "create" || key === "update") { + transformPrincipalCallbackParam(child, edits, typeContext); + } continue; } if (child.kind() !== "pair") continue; @@ -985,6 +1099,7 @@ function transformHookCallbackObject( edits, callbackBindings, transformedCallbackStarts, + typeContext, root, ); if (!transformed && value.kind() === "object") { @@ -993,11 +1108,19 @@ function transformHookCallbackObject( edits, callbackBindings, transformedCallbackStarts, + typeContext, root, ); } } else if (value.kind() === "object") { - transformHookCallbackObject(value, edits, callbackBindings, transformedCallbackStarts, root); + transformHookCallbackObject( + value, + edits, + callbackBindings, + transformedCallbackStarts, + typeContext, + root, + ); } } } @@ -1009,10 +1132,18 @@ function transformHookCallbackConfigNode( objectBindings: LocalObjectBinding[], transformedCallbackStarts: Set, transformedObjectStarts: Set, + typeContext: CallbackTypeContext, root: SgNode, ): boolean { if (node.kind() === "object") { - transformHookCallbackObject(node, edits, callbackBindings, transformedCallbackStarts, root); + transformHookCallbackObject( + node, + edits, + callbackBindings, + transformedCallbackStarts, + typeContext, + root, + ); return true; } @@ -1027,6 +1158,7 @@ function transformHookCallbackConfigNode( edits, callbackBindings, transformedCallbackStarts, + typeContext, root, ); } @@ -1038,10 +1170,18 @@ function transformValidateCallbackNode( edits: Edit[], callbackBindings: LocalCallbackBinding[], transformedCallbackStarts: Set, + typeContext: CallbackTypeContext, root: SgNode, ): void { if ( - transformPrincipalCallbackNode(node, edits, callbackBindings, transformedCallbackStarts, root) + transformPrincipalCallbackNode( + node, + edits, + callbackBindings, + transformedCallbackStarts, + typeContext, + root, + ) ) { return; } @@ -1053,6 +1193,7 @@ function transformValidateCallbackNode( edits, callbackBindings, transformedCallbackStarts, + typeContext, root, ); } @@ -1062,7 +1203,7 @@ function transformValidateCallbackNode( if (node.kind() !== "object") return; for (const child of node.children()) { if (child.kind() === "method_definition") { - transformPrincipalCallbackParam(child, edits); + transformPrincipalCallbackParam(child, edits, typeContext); continue; } if (child.kind() !== "pair") continue; @@ -1073,6 +1214,7 @@ function transformValidateCallbackNode( edits, callbackBindings, transformedCallbackStarts, + typeContext, root, ); } @@ -1088,6 +1230,7 @@ function transformPrincipalCallbacksInCall( objectBindings: LocalObjectBinding[], transformedCallbackStarts: Set, transformedObjectStarts: Set, + typeContext: CallbackTypeContext, root: SgNode, ): void { const memberName = findMemberCallName(call); @@ -1106,6 +1249,7 @@ function transformPrincipalCallbacksInCall( objectBindings, transformedCallbackStarts, transformedObjectStarts, + typeContext, root, ) ) { @@ -1116,7 +1260,14 @@ function transformPrincipalCallbacksInCall( } for (const arg of args.children()) { - transformValidateCallbackNode(arg, edits, callbackBindings, transformedCallbackStarts, root); + transformValidateCallbackNode( + arg, + edits, + callbackBindings, + transformedCallbackStarts, + typeContext, + root, + ); } } @@ -1276,6 +1427,11 @@ export default function transform(source: string): string | null { const sdkFieldLocalBindings = collectSdkFieldLocalBindings(tree, sdkFieldRootNames); const callbackBindings = collectLocalCallbackBindings(tree); const objectBindings = collectLocalObjectBindings(tree); + const typeContext: CallbackTypeContext = { + bindings: collectLocalCallbackTypeBindings(tree), + transformedTypeStarts: new Set(), + root: tree, + }; const transformedCallbackStarts = new Set(); const transformedObjectStarts = new Set(); const memberCalls = tree.findAll({ rule: { kind: "call_expression" } }); @@ -1289,6 +1445,7 @@ export default function transform(source: string): string | null { objectBindings, transformedCallbackStarts, transformedObjectStarts, + typeContext, tree, ); transformParseArgsObject(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index 4ffd03e15..c0d648300 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -9,9 +9,21 @@ function hasMachineUser({ invoker }: { invoker: TailorPrincipal | null }) { const hasInvoker = (ctx: { invoker: TailorPrincipal | null }) => ctx.invoker?.id !== ""; +type HookArgs = { + value: string; + invoker: TailorPrincipal | null; +}; + +interface ValidatorArgs { + invoker: TailorPrincipal | null; +} + +const namedTypeHook = ({ invoker }: HookArgs) => invoker?.id ?? "anonymous"; +const namedTypeValidator = ({ invoker }: ValidatorArgs) => invoker?.id !== ""; + const sharedHooks = { create: ({ invoker }: { invoker: TailorPrincipal | null }) => invoker?.id ?? "anonymous", - update: (ctx: { invoker: TailorPrincipal | null }) => ctx.invoker?.id ?? "anonymous", + update: namedTypeHook, }; const role = db @@ -27,7 +39,8 @@ const role = db [hasMachineUser, "Machine user required"], ctx => ctx.invoker?.id !== "", ]) - .validate(hasInvoker); + .validate(hasInvoker) + .validate(namedTypeValidator); const localHookedRole = db.string().hooks(sharedHooks); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index 65b24f53b..2de2d382d 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -10,9 +10,21 @@ function hasMachineUser({ user }: { user: TailorUser | null }) { const hasInvoker = (ctx: { user: TailorUser | null }) => ctx.user?.id !== ""; +type HookArgs = { + value: string; + user: TailorUser | null; +}; + +interface ValidatorArgs { + user: TailorUser | null; +} + +const namedTypeHook = ({ user }: HookArgs) => user?.id ?? "anonymous"; +const namedTypeValidator = ({ user }: ValidatorArgs) => user?.id !== ""; + const sharedHooks = { create: ({ user }: { user: TailorUser | null }) => user?.id ?? "anonymous", - update: (ctx: { user: TailorUser | null }) => ctx.user?.id ?? "anonymous", + update: namedTypeHook, }; const role = db @@ -28,7 +40,8 @@ const role = db [hasMachineUser, "Machine user required"], ctx => ctx.user?.id !== "", ]) - .validate(hasInvoker); + .validate(hasInvoker) + .validate(namedTypeValidator); const localHookedRole = db.string().hooks(sharedHooks); From d62262adfaae080046c03bff5de8047aaa2318ff Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 15:00:41 +0900 Subject: [PATCH 106/618] fix(sdk-codemod): preserve nullable principal migrations --- .../v2/principal-unify/scripts/transform.ts | 121 ++++++++++++++++-- .../tests/aliased-create-resolver/expected.ts | 2 +- .../tests/aliased-destructure/expected.ts | 2 +- .../principal-unify/tests/basic/expected.ts | 2 +- .../tests/body-with-derived-const/expected.ts | 2 +- .../tests/context-arg/expected.ts | 4 +- .../tests/defaulted-destructure/expected.ts | 2 +- .../tests/nested-ctx-shadow/expected.ts | 2 +- .../nested-destructure-shadow/expected.ts | 2 +- .../principal-unify/tests/shadow/expected.ts | 2 +- .../tests/tailordb-callbacks/expected.ts | 5 + .../tests/tailordb-callbacks/input.ts | 5 + .../tests/type-imports/expected.ts | 4 +- .../tests/type-imports/input.ts | 2 + 14 files changed, 138 insertions(+), 19 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index c5994b4d7..2db263347 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -42,6 +42,27 @@ function isMemberExpressionObject(node: SgNode): boolean { return r.start.index === or.start.index && r.end.index === or.end.index; } +function isOptionalizableMemberObject(node: SgNode): boolean { + if (!isMemberExpressionObject(node)) return false; + const parent = node.parent(); + if (parent?.text().startsWith(`${node.text()}?.`)) return false; + return parent?.field("property")?.kind() === "property_identifier"; +} + +function principalIdentifierReplacement(node: SgNode, name: string): string { + return isOptionalizableMemberObject(node) ? `${name}?` : name; +} + +function principalPropertyReplacement(node: SgNode, name: string): string { + const parent = node.parent(); + return parent && isOptionalizableMemberObject(parent) ? `${name}?` : name; +} + +function renamedTypeIdentifierText(name: string): string | null { + if (name === "TailorInvoker") return "(TailorPrincipal | null)"; + return TYPE_RENAME_MAP[name] ?? null; +} + interface ImportRewriteResult { newText: string; touched: boolean; @@ -306,6 +327,27 @@ function collectAllShadowRanges(root: SgNode, name: string): Array<[number, numb return ranges; } +interface PrincipalLocalBinding { + name: string; + bindingStart: number; +} + +function guardPrincipalMemberAccesses( + root: SgNode, + binding: PrincipalLocalBinding, + edits: Edit[], +): void { + const refs = root.findAll({ + rule: { kind: "identifier", regex: `^${escapeRegex(binding.name)}$` }, + }); + for (const ref of refs) { + if (!isOptionalizableMemberObject(ref)) continue; + const pos = ref.range().start.index; + if (isShadowedLocalReference(root, binding.name, pos, binding.bindingStart)) continue; + edits.push(ref.replace(`${binding.name}?`)); + } +} + function findResolverBodyArrow(call: SgNode): SgNode | null { const args = call.field("arguments"); if (!args) return null; @@ -407,6 +449,7 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { if (hasCallerBindingConflict(pattern, body)) return; let renamedShorthandUser = false; + const principalAliasBindings: PrincipalLocalBinding[] = []; // Only iterate top-level pattern children so nested destructures like // `({ input: { user } })` are not mistaken for the resolver context user. for (const child of pattern.children()) { @@ -418,6 +461,13 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { const key = child.field("key"); if (key && key.text() === "user") { edits.push(key.replace("caller")); + const value = child.field("value"); + if (value?.kind() === "identifier") { + principalAliasBindings.push({ + name: value.text(), + bindingStart: value.range().start.index, + }); + } } } else if (kind === "object_assignment_pattern") { // `{ user = fallback }` — the inner shorthand is the binding; default @@ -441,7 +491,7 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { for (const ref of refs) { const pos = ref.range().start.index; if (isInsideAnyRange(pos, shadowRanges)) continue; - edits.push(ref.replace("caller")); + edits.push(ref.replace(principalIdentifierReplacement(ref, "caller"))); } // Object literal shorthand (kind: `shorthand_property_identifier`, no // `_pattern` suffix) is both the key and the value. Rewriting it to @@ -458,6 +508,9 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { edits.push(ref.replace("user: caller")); } } + for (const binding of principalAliasBindings) { + guardPrincipalMemberAccesses(body, binding, edits); + } return; } @@ -475,7 +528,7 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { if (!(obj && obj.kind() === "identifier" && obj.text() === ctxName)) continue; const pos = obj.range().start.index; if (isInsideAnyRange(pos, ctxShadowRanges)) continue; - edits.push(propId.replace("caller")); + edits.push(propId.replace(principalPropertyReplacement(propId, "caller"))); } // Also rewrite destructures of the context, e.g. `const { user } = ctx;` → @@ -491,6 +544,7 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { }, }, }); + const principalAliasBindings: PrincipalLocalBinding[] = []; for (const decl of ctxDestructures) { const pos = decl.range().start.index; if (isInsideAnyRange(pos, ctxShadowRanges)) continue; @@ -500,14 +554,28 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { const k = child.kind(); if (k === "shorthand_property_identifier_pattern" && child.text() === "user") { edits.push(child.replace("caller: user")); + principalAliasBindings.push({ + name: "user", + bindingStart: child.range().start.index, + }); } else if (k === "pair_pattern") { const key = child.field("key"); if (key && key.text() === "user") { edits.push(key.replace("caller")); + const value = child.field("value"); + if (value?.kind() === "identifier") { + principalAliasBindings.push({ + name: value.text(), + bindingStart: value.range().start.index, + }); + } } } } } + for (const binding of principalAliasBindings) { + guardPrincipalMemberAccesses(body, binding, edits); + } } function findMemberCallName(call: SgNode): string | null { @@ -706,7 +774,7 @@ function transformPrincipalCallbackParam( if (!(obj && obj.kind() === "identifier" && obj.text() === ctxName)) continue; const pos = obj.range().start.index; if (isInsideAnyRange(pos, ctxShadowRanges)) continue; - edits.push(propId.replace("invoker")); + edits.push(propId.replace(principalPropertyReplacement(propId, "invoker"))); } const ctxDestructures = body.findAll({ @@ -719,6 +787,7 @@ function transformPrincipalCallbackParam( }, }, }); + const principalAliasBindings: PrincipalLocalBinding[] = []; for (const decl of ctxDestructures) { const pos = decl.range().start.index; if (isInsideAnyRange(pos, ctxShadowRanges)) continue; @@ -728,12 +797,28 @@ function transformPrincipalCallbackParam( const kind = child.kind(); if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { edits.push(child.replace("invoker: user")); + principalAliasBindings.push({ + name: "user", + bindingStart: child.range().start.index, + }); } else if (kind === "pair_pattern") { const key = child.field("key"); - if (key?.text() === "user") edits.push(key.replace("invoker")); + if (key?.text() === "user") { + edits.push(key.replace("invoker")); + const value = child.field("value"); + if (value?.kind() === "identifier") { + principalAliasBindings.push({ + name: value.text(), + bindingStart: value.range().start.index, + }); + } + } } } } + for (const binding of principalAliasBindings) { + guardPrincipalMemberAccesses(body, binding, edits); + } transformPrincipalCallbackParamType(param, edits, typeContext); return; } @@ -742,6 +827,7 @@ function transformPrincipalCallbackParam( let hasUserParamProperty = false; let renamesBinding = false; + const principalAliasBindings: PrincipalLocalBinding[] = []; for (const child of pattern.children()) { const kind = child.kind(); if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { @@ -749,7 +835,16 @@ function transformPrincipalCallbackParam( renamesBinding = true; } else if (kind === "pair_pattern") { const key = child.field("key"); - if (key?.text() === "user") hasUserParamProperty = true; + if (key?.text() === "user") { + hasUserParamProperty = true; + const value = child.field("value"); + if (value?.kind() === "identifier") { + principalAliasBindings.push({ + name: value.text(), + bindingStart: value.range().start.index, + }); + } + } } else if (kind === "object_assignment_pattern") { const inner = child .children() @@ -797,14 +892,19 @@ function transformPrincipalCallbackParam( } } - if (!renamedShorthandUser) return; + if (!renamedShorthandUser) { + for (const binding of principalAliasBindings) { + guardPrincipalMemberAccesses(body, binding, edits); + } + return; + } const shadowRanges = collectAllShadowRanges(body, "user"); const refs = body.findAll({ rule: { kind: "identifier", regex: "^user$" } }); for (const ref of refs) { const pos = ref.range().start.index; if (isInsideAnyRange(pos, shadowRanges)) continue; - edits.push(ref.replace("invoker")); + edits.push(ref.replace(principalIdentifierReplacement(ref, "invoker"))); } const shortRefs = body.findAll({ @@ -815,6 +915,10 @@ function transformPrincipalCallbackParam( if (isInsideAnyRange(pos, shadowRanges)) continue; edits.push(ref.replace("user: invoker")); } + + for (const binding of principalAliasBindings) { + guardPrincipalMemberAccesses(body, binding, edits); + } } interface SdkFieldLocalBinding { @@ -1348,7 +1452,8 @@ export default function transform(source: string): string | null { }); for (const id of typeIdents) { if (!sdkRenameSourceNames.has(id.text())) continue; - const newName = TYPE_RENAME_MAP[id.text()]!; + const newName = renamedTypeIdentifierText(id.text()); + if (!newName) continue; edits.push(id.replace(newName)); } diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-create-resolver/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-create-resolver/expected.ts index 696045d1a..fc945343b 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-create-resolver/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-create-resolver/expected.ts @@ -4,5 +4,5 @@ export default makeResolver({ name: "n", operation: "query", output: t.string(), - body: ({ caller }) => caller.id, + body: ({ caller }) => caller?.id, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-destructure/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-destructure/expected.ts index 3aa4c354f..f0b3a14d7 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-destructure/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-destructure/expected.ts @@ -4,5 +4,5 @@ export default createResolver({ name: "n", operation: "query", output: t.string(), - body: ({ caller: currentUser }) => currentUser.id, + body: ({ caller: currentUser }) => currentUser?.id, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts index 5c1553ae6..25357e5a6 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts @@ -6,7 +6,7 @@ export default createResolver({ input: t.object({ id: t.string() }), output: t.object({ id: t.string() }), body: ({ input, caller }) => { - return { id: caller.id }; + return { id: caller?.id }; }, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-with-derived-const/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-with-derived-const/expected.ts index a0c293c7e..4d1855042 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-with-derived-const/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-with-derived-const/expected.ts @@ -5,7 +5,7 @@ export default createResolver({ operation: "query", output: t.string(), body: ({ caller }) => { - const userId = caller.id; + const userId = caller?.id; return userId; }, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/context-arg/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/context-arg/expected.ts index 997a81ed6..ecec00dba 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/context-arg/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/context-arg/expected.ts @@ -5,7 +5,7 @@ export default createResolver({ operation: "query", output: t.object({ id: t.string(), type: t.string() }), body: (ctx) => ({ - id: ctx.caller.id, - type: ctx.caller.type, + id: ctx.caller?.id, + type: ctx.caller?.type, }), }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/defaulted-destructure/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/defaulted-destructure/expected.ts index 5f726a55a..3dc125322 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/defaulted-destructure/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/defaulted-destructure/expected.ts @@ -6,5 +6,5 @@ export default createResolver({ name: "n", operation: "query", output: t.string(), - body: ({ caller = fallback }) => caller.id, + body: ({ caller = fallback }) => caller?.id, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-ctx-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-ctx-shadow/expected.ts index a645dde3d..28475e634 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-ctx-shadow/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-ctx-shadow/expected.ts @@ -7,7 +7,7 @@ export default createResolver({ operation: "query", output: t.array(t.string()), body: (ctx) => ({ - me: ctx.caller.id, + me: ctx.caller?.id, others: items.map((ctx) => ctx.user.id), }), }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure-shadow/expected.ts index bd3efce01..a8706f56a 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure-shadow/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure-shadow/expected.ts @@ -6,5 +6,5 @@ export default createResolver({ name: "n", operation: "query", output: t.array(t.string()), - body: ({ caller }) => items.map(({ user }) => user.id).concat(caller.id), + body: ({ caller }) => items.map(({ user }) => user.id).concat(caller?.id), }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadow/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadow/expected.ts index bf9246993..d987710ec 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadow/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadow/expected.ts @@ -10,6 +10,6 @@ export default createResolver({ const user = { id: "fake" }; return { id: user.id }; } - return { id: caller.id }; + return { id: caller?.id }; }, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index c0d648300..619dd5297 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -44,6 +44,11 @@ const role = db const localHookedRole = db.string().hooks(sharedHooks); +const directHookedRole = db.string().hooks({ + create: ({ invoker }) => invoker?.id, + update: (ctx) => ctx.invoker?.id, +}); + const reviewer = t.string(); const zodLike = { parse: (arg: unknown) => arg }; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index 2de2d382d..1c295ecd2 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -45,6 +45,11 @@ const role = db const localHookedRole = db.string().hooks(sharedHooks); +const directHookedRole = db.string().hooks({ + create: ({ user }) => user.id, + update: (ctx) => ctx.user.id, +}); + const reviewer = t.string(); const zodLike = { parse: (arg: unknown) => arg }; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts index ee7750963..3b5bfcc8c 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts @@ -3,5 +3,7 @@ import type { TailorPrincipal } from "@tailor-platform/sdk"; export type Props = { user: TailorPrincipal; actor: TailorPrincipal; - invoker: TailorPrincipal; + invoker: (TailorPrincipal | null); + nullableInvoker: (TailorPrincipal | null) | null; + invokers: (TailorPrincipal | null)[]; }; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts index d42dfc898..82fbac821 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts @@ -4,4 +4,6 @@ export type Props = { user: TailorUser; actor: TailorActor; invoker: TailorInvoker; + nullableInvoker: TailorInvoker | null; + invokers: TailorInvoker[]; }; From 26dc32f386e95a2d8792acf360a1dd99080dc24c Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 15:30:14 +0900 Subject: [PATCH 107/618] fix(sdk-codemod): preserve aliased invoker nullability --- .../v2/principal-unify/scripts/transform.ts | 27 ++++++++++++++----- .../tests/aliased-type-import/expected.ts | 3 ++- .../tests/aliased-type-import/input.ts | 3 ++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 2db263347..0ea759baf 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -110,6 +110,7 @@ function rebuildImportStatement( importStmt: SgNode, globalEmittedRenamed: Set, unauthenticatedLocalNames: Set, + nullableInvokerAliasLocalNames: Set, ): ImportRewriteResult { const importText = importStmt.text(); const isImportType = /^\s*import\s+type\b/.test(importText); @@ -127,16 +128,20 @@ function rebuildImportStatement( const renamed = TYPE_RENAME_MAP[importedName]; if (renamed) { touched = true; - const finalLocal = aliasNode?.text() ?? renamed; + const dropsAliasForNullableInvoker = importedName === "TailorInvoker" && !!aliasNode; + if (dropsAliasForNullableInvoker) nullableInvokerAliasLocalNames.add(localName); + const finalLocal = dropsAliasForNullableInvoker ? renamed : (aliasNode?.text() ?? renamed); if (seenLocal.has(finalLocal)) continue; // Cross-statement dedupe for non-aliased renames so a file with // `import { TailorUser } from "@tailor-platform/sdk"` and // `import { TailorActor } from "@tailor-platform/sdk"` does not collapse to // two duplicate `import { TailorPrincipal } ...` lines. - if (!aliasNode && globalEmittedRenamed.has(renamed)) continue; + if ((!aliasNode || dropsAliasForNullableInvoker) && globalEmittedRenamed.has(renamed)) { + continue; + } seenLocal.add(finalLocal); - if (!aliasNode) globalEmittedRenamed.add(renamed); - const asPart = aliasNode ? ` as ${aliasNode.text()}` : ""; + if (!aliasNode || dropsAliasForNullableInvoker) globalEmittedRenamed.add(renamed); + const asPart = aliasNode && !dropsAliasForNullableInvoker ? ` as ${aliasNode.text()}` : ""; newSpecTexts.push(`${isTypeOnly ? "type " : ""}${renamed}${asPart}`); } else if (importedName === UNAUTHENTICATED) { touched = true; @@ -1436,11 +1441,15 @@ export default function transform(source: string): string | null { // alias. A local `import type { TailorUser } from './domain'` must stay alone // even when the file also imports something else from the SDK. const sdkRenameSourceNames = new Set(); + const nullableInvokerAliasLocalNames = new Set(); for (const importStmt of sdkImports) { - for (const { importedName, aliasNode } of iterateImportSpecs(importStmt)) { + for (const { importedName, aliasNode, localName } of iterateImportSpecs(importStmt)) { if (TYPE_RENAME_MAP[importedName] && !aliasNode) { sdkRenameSourceNames.add(importedName); } + if (importedName === "TailorInvoker" && aliasNode) { + nullableInvokerAliasLocalNames.add(localName); + } } } @@ -1451,8 +1460,11 @@ export default function transform(source: string): string | null { }, }); for (const id of typeIdents) { - if (!sdkRenameSourceNames.has(id.text())) continue; - const newName = renamedTypeIdentifierText(id.text()); + const newName = sdkRenameSourceNames.has(id.text()) + ? renamedTypeIdentifierText(id.text()) + : nullableInvokerAliasLocalNames.has(id.text()) + ? renamedTypeIdentifierText("TailorInvoker") + : null; if (!newName) continue; edits.push(id.replace(newName)); } @@ -1468,6 +1480,7 @@ export default function transform(source: string): string | null { importStmt, globalEmittedRenamed, unauthenticatedLocalNames, + nullableInvokerAliasLocalNames, ); if (!touched) continue; edits.push(importStmt.replace(newText)); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/expected.ts index d3498e2ca..3d76c81ce 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/expected.ts @@ -1,5 +1,6 @@ -import { type TailorPrincipal as MyUser } from "@tailor-platform/sdk"; +import { type TailorPrincipal as MyUser, type TailorPrincipal } from "@tailor-platform/sdk"; export type Props = { caller: MyUser; + invoker: (TailorPrincipal | null); }; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/input.ts index d3611f901..c995a2e74 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/input.ts @@ -1,5 +1,6 @@ -import { type TailorUser as MyUser } from "@tailor-platform/sdk"; +import { type TailorUser as MyUser, type TailorInvoker as MyInvoker } from "@tailor-platform/sdk"; export type Props = { caller: MyUser; + invoker: MyInvoker; }; From 27c190d9aa987e435d30106af968ae2f7220b5d9 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 16:41:45 +0900 Subject: [PATCH 108/618] docs(codemod): document output and LLM reviews in CLI help Add notes and examples to the sdk-codemod command help explaining the stdout JSON summary (filesModified / warnings / llmReviews) and how to use the llmReviews prompts for changes the codemods cannot fully migrate on their own. --- packages/sdk-codemod/src/index.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/sdk-codemod/src/index.ts b/packages/sdk-codemod/src/index.ts index ec94516a8..c2b7f452e 100644 --- a/packages/sdk-codemod/src/index.ts +++ b/packages/sdk-codemod/src/index.ts @@ -30,6 +30,27 @@ function printLlmReview(review: LlmReview): void { const main = defineCommand({ name: packageJson.name ?? "sdk-codemod", description: packageJson.description ?? "Codemod runner for Tailor Platform SDK upgrades", + notes: `Applies the codemods matching the \`--from\`/\`--to\` version range to the +\`--target\` directory, then writes a JSON summary to \`stdout\`: + +- \`filesModified\`: files a codemod changed +- \`warnings\`: files that may still need manual migration +- \`llmReviews\`: changes the codemods could not fully migrate on their own. Each + entry has the affected \`files\` and a \`prompt\` — hand the prompt and files to + an LLM (or follow it yourself) to finish those cases. + +Progress, warnings, and the LLM-review prompts are also printed to \`stderr\` in +human-readable form, so \`stdout\` stays pure JSON for piping.`, + examples: [ + { + cmd: "--from 1.64.0 --to 2.0.0", + desc: "Apply every codemod for the 1.64.0 -> 2.0.0 upgrade to the current project", + }, + { + cmd: "--from 1.64.0 --to 2.0.0 --dry-run", + desc: "Preview the changes and any LLM-review prompts without writing files", + }, + ], args: z .object({ from: arg(z.string(), { From f23d0af1bf93dfe39541f1288d39009dc9ea805b Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 15:56:10 +0900 Subject: [PATCH 109/618] fix(sdk-codemod): migrate actor principal property access --- .../codemods/v2/principal-unify/codemod.yaml | 2 +- .../v2/principal-unify/scripts/transform.ts | 308 +++++++++++++++++- .../tests/actor-member-access-local/input.ts | 18 + .../tests/actor-member-access/expected.ts | 16 + .../tests/actor-member-access/input.ts | 16 + .../tests/type-imports/expected.ts | 8 + .../tests/type-imports/input.ts | 10 +- packages/sdk-codemod/src/registry.ts | 12 +- 8 files changed, 380 insertions(+), 10 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/input.ts diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/codemod.yaml b/packages/sdk-codemod/codemods/v2/principal-unify/codemod.yaml index 1dd94b1ac..b6d95370e 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/codemod.yaml +++ b/packages/sdk-codemod/codemods/v2/principal-unify/codemod.yaml @@ -1,6 +1,6 @@ name: "@tailor-platform/principal-unify" version: "1.0.0" -description: "Unify TailorUser/TailorActor/TailorInvoker into TailorPrincipal and rename resolver body `user` → `caller`" +description: "Unify TailorUser/TailorActor/TailorActorType/TailorInvoker into TailorPrincipal and rename resolver body `user` → `caller`" engine: jssg language: typescript since: "1.0.0" diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 0ea759baf..c29e40ab3 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -4,6 +4,7 @@ import type { Edit, SgNode } from "@ast-grep/napi"; const TYPE_RENAME_MAP: Record = { TailorUser: "TailorPrincipal", TailorActor: "TailorPrincipal", + TailorActorType: "TailorPrincipal", TailorInvoker: "TailorPrincipal", }; @@ -12,12 +13,24 @@ const UNAUTHENTICATED = "unauthenticatedTailorUser"; const QUICK_FILTER_NEEDLES = [ ...Object.keys(TYPE_RENAME_MAP), UNAUTHENTICATED, + "userId", + "userType", "createResolver", ".hooks", ".validate", ".parse", ]; +const ACTOR_PROPERTY_RENAME_MAP: Record = { + userId: "id", + userType: "type", +}; + +const ACTOR_TYPE_LITERAL_RENAME_MAP: Record = { + USER_TYPE_USER: "user", + USER_TYPE_MACHINE_USER: "machine_user", +}; + function quickFilter(source: string): boolean { if (!source.includes("@tailor-platform/sdk")) return false; return QUICK_FILTER_NEEDLES.some((needle) => source.includes(needle)); @@ -58,8 +71,166 @@ function principalPropertyReplacement(node: SgNode, name: string): string { return parent && isOptionalizableMemberObject(parent) ? `${name}?` : name; } +function addActorPropertyReplacement( + property: SgNode, + edits: Edit[], + transformedActorPropertyStarts: Set, +): void { + const newName = ACTOR_PROPERTY_RENAME_MAP[property.text()]; + if (!newName) return; + const start = property.range().start.index; + if (transformedActorPropertyStarts.has(start)) return; + transformedActorPropertyStarts.add(start); + edits.push(property.replace(newName)); +} + +function actorTypeLiteralReplacement(literal: SgNode): string | null { + const match = literal + .text() + .match(/^(['"])(USER_TYPE_USER|USER_TYPE_MACHINE_USER|USER_TYPE_UNSPECIFIED)\1$/); + if (!match) return null; + const [, quote, value] = match; + if (value === "USER_TYPE_UNSPECIFIED") return "undefined"; + return `${quote}${ACTOR_TYPE_LITERAL_RENAME_MAP[value]!}${quote}`; +} + +function isTransformedActorTypeMember( + node: SgNode, + transformedActorPropertyStarts: Set, +): boolean { + if (node.kind() !== "member_expression") return false; + const property = node.field("property"); + return ( + property?.text() === "userType" && + transformedActorPropertyStarts.has(property.range().start.index) + ); +} + +function transformActorTypeComparisonLiterals( + root: SgNode, + edits: Edit[], + transformedActorPropertyStarts: Set, +): void { + if (transformedActorPropertyStarts.size === 0) return; + const transformedLiteralStarts = new Set(); + const binaries = root.findAll({ rule: { kind: "binary_expression" } }); + for (const binary of binaries) { + if ( + !binary + .children() + .some((child) => isTransformedActorTypeMember(child, transformedActorPropertyStarts)) + ) { + continue; + } + for (const child of binary.children()) { + if (child.kind() !== "string") continue; + const replacement = actorTypeLiteralReplacement(child); + if (!replacement) continue; + const start = child.range().start.index; + if (transformedLiteralStarts.has(start)) continue; + transformedLiteralStarts.add(start); + edits.push(child.replace(replacement)); + } + } +} + +function transformActorBindingMemberAccesses( + root: SgNode, + binding: PrincipalLocalBinding, + edits: Edit[], + transformedActorPropertyStarts: Set, +): void { + const refs = root.findAll({ + rule: { kind: "identifier", regex: `^${escapeRegex(binding.name)}$` }, + }); + for (const ref of refs) { + const parent = ref.parent(); + if (!parent || parent.kind() !== "member_expression") continue; + const object = parent.field("object"); + if (!object || object.range().start.index !== ref.range().start.index) continue; + const property = parent.field("property"); + if (!property || property.kind() !== "property_identifier") continue; + if (!ACTOR_PROPERTY_RENAME_MAP[property.text()]) continue; + const pos = ref.range().start.index; + if (isShadowedLocalReference(root, binding.name, pos, binding.bindingStart)) continue; + addActorPropertyReplacement(property, edits, transformedActorPropertyStarts); + } +} + +function isTailorActorTypeReference(node: SgNode, actorTypeLocalNames: Set): boolean { + const typeAnnotation = node.field("type"); + if (!typeAnnotation) return false; + const typeIds = typeAnnotation.findAll({ rule: { kind: "type_identifier" } }); + return typeIds.some((id) => actorTypeLocalNames.has(id.text())); +} + +function transformTailorActorTypedMemberAccesses( + root: SgNode, + actorTypeLocalNames: Set, + edits: Edit[], + transformedActorPropertyStarts: Set, +): void { + if (actorTypeLocalNames.size === 0) return; + + for (const kind of NESTED_FN_KINDS) { + const fns = root.findAll({ rule: { kind } }); + for (const fn of fns) { + const param = getFirstFunctionParam(fn); + if (!param || !isTailorActorTypeReference(param, actorTypeLocalNames)) continue; + const pattern = getFunctionParamPattern(param); + const body = fn.field("body"); + if (!pattern || pattern.kind() !== "identifier" || !body) continue; + transformActorBindingMemberAccesses( + body, + { name: pattern.text(), bindingStart: pattern.range().start.index }, + edits, + transformedActorPropertyStarts, + ); + } + } + + const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarators) { + if (!isTailorActorTypeReference(decl, actorTypeLocalNames)) continue; + const name = decl.field("name"); + if (!name || name.kind() !== "identifier") continue; + transformActorBindingMemberAccesses( + root, + { name: name.text(), bindingStart: name.range().start.index }, + edits, + transformedActorPropertyStarts, + ); + } +} + +function transformExecutorCtxActorAccesses( + body: SgNode, + ctxName: string, + ctxShadowRanges: Array<[number, number]>, + edits: Edit[], + transformedActorPropertyStarts: Set, +): void { + const properties = body.findAll({ + rule: { kind: "property_identifier", regex: "^(userId|userType)$" }, + }); + for (const property of properties) { + const parent = property.parent(); + if (!parent || parent.kind() !== "member_expression") continue; + const object = parent.field("object"); + if (!object || object.kind() !== "member_expression") continue; + const actorProperty = object.field("property"); + if (actorProperty?.text() !== "actor") continue; + const ctxObject = object.field("object"); + if (!ctxObject || ctxObject.kind() !== "identifier" || ctxObject.text() !== ctxName) continue; + const pos = ctxObject.range().start.index; + if (isInsideAnyRange(pos, ctxShadowRanges)) continue; + addActorPropertyReplacement(property, edits, transformedActorPropertyStarts); + } +} + function renamedTypeIdentifierText(name: string): string | null { if (name === "TailorInvoker") return "(TailorPrincipal | null)"; + if (name === "TailorActorType") return 'TailorPrincipal["type"]'; return TYPE_RENAME_MAP[name] ?? null; } @@ -111,6 +282,7 @@ function rebuildImportStatement( globalEmittedRenamed: Set, unauthenticatedLocalNames: Set, nullableInvokerAliasLocalNames: Set, + actorTypeAliasLocalNames: Set, ): ImportRewriteResult { const importText = importStmt.text(); const isImportType = /^\s*import\s+type\b/.test(importText); @@ -129,19 +301,22 @@ function rebuildImportStatement( if (renamed) { touched = true; const dropsAliasForNullableInvoker = importedName === "TailorInvoker" && !!aliasNode; + const dropsAliasForActorType = importedName === "TailorActorType" && !!aliasNode; + const dropsAliasForExpandedType = dropsAliasForNullableInvoker || dropsAliasForActorType; if (dropsAliasForNullableInvoker) nullableInvokerAliasLocalNames.add(localName); - const finalLocal = dropsAliasForNullableInvoker ? renamed : (aliasNode?.text() ?? renamed); + if (dropsAliasForActorType) actorTypeAliasLocalNames.add(localName); + const finalLocal = dropsAliasForExpandedType ? renamed : (aliasNode?.text() ?? renamed); if (seenLocal.has(finalLocal)) continue; // Cross-statement dedupe for non-aliased renames so a file with // `import { TailorUser } from "@tailor-platform/sdk"` and // `import { TailorActor } from "@tailor-platform/sdk"` does not collapse to // two duplicate `import { TailorPrincipal } ...` lines. - if ((!aliasNode || dropsAliasForNullableInvoker) && globalEmittedRenamed.has(renamed)) { + if ((!aliasNode || dropsAliasForExpandedType) && globalEmittedRenamed.has(renamed)) { continue; } seenLocal.add(finalLocal); - if (!aliasNode || dropsAliasForNullableInvoker) globalEmittedRenamed.add(renamed); - const asPart = aliasNode && !dropsAliasForNullableInvoker ? ` as ${aliasNode.text()}` : ""; + if (!aliasNode || dropsAliasForExpandedType) globalEmittedRenamed.add(renamed); + const asPart = aliasNode && !dropsAliasForExpandedType ? ` as ${aliasNode.text()}` : ""; newSpecTexts.push(`${isTypeOnly ? "type " : ""}${renamed}${asPart}`); } else if (importedName === UNAUTHENTICATED) { touched = true; @@ -637,6 +812,83 @@ function getFunctionParamPattern(param: SgNode): SgNode | null { return param.field("pattern"); } +function findExecutorBodyFunctions(call: SgNode): SgNode[] { + const args = call.field("arguments"); + const objArg = args?.children().find((c: SgNode) => c.kind() === "object"); + if (!objArg) return []; + + const functions: SgNode[] = []; + const visitObject = (object: SgNode): void => { + for (const child of object.children()) { + if (child.kind() === "method_definition") { + if (propertyName(child) === "body") functions.push(child); + continue; + } + if (child.kind() !== "pair") continue; + + const value = child.field("value"); + if (!value) continue; + if (child.field("key")?.text() === "body" && isFunctionNode(value)) { + functions.push(value); + } else if (value.kind() === "object") { + visitObject(value); + } + } + }; + + visitObject(objArg); + return functions; +} + +function transformExecutorBodyActorAccesses( + fn: SgNode, + edits: Edit[], + transformedActorPropertyStarts: Set, +): void { + const param = getFirstFunctionParam(fn); + const pattern = param ? getFunctionParamPattern(param) : null; + const body = fn.field("body"); + if (!pattern || !body) return; + + if (pattern.kind() === "identifier") { + const ctxName = pattern.text(); + const ctxShadowRanges = collectCtxShadowRanges(body, ctxName, fn); + transformExecutorCtxActorAccesses( + body, + ctxName, + ctxShadowRanges, + edits, + transformedActorPropertyStarts, + ); + return; + } + + if (pattern.kind() !== "object_pattern") return; + + for (const child of pattern.children()) { + const kind = child.kind(); + if (kind === "shorthand_property_identifier_pattern" && child.text() === "actor") { + transformActorBindingMemberAccesses( + body, + { name: "actor", bindingStart: child.range().start.index }, + edits, + transformedActorPropertyStarts, + ); + } else if (kind === "pair_pattern") { + const key = child.field("key"); + const value = child.field("value"); + if (key?.text() === "actor" && value?.kind() === "identifier") { + transformActorBindingMemberAccesses( + body, + { name: value.text(), bindingStart: value.range().start.index }, + edits, + transformedActorPropertyStarts, + ); + } + } + } +} + interface LocalCallbackTypeBinding { name: string; declaration: SgNode; @@ -1415,6 +1667,7 @@ function transformParseArgsObject( * - Replaces standalone references to `unauthenticatedTailorUser` with `null`. Member-access * forms like `unauthenticatedTailorUser.id` are left alone on purpose so the resulting TS * error after the import is removed points the author at the broken access. + * - Rewrites actor member accesses from `userId` / `userType` to `id` / `type`. * - Renames `user` to `caller` for top-level destructured resolver bodies (`{ input, user }`), * handles aliased pairs (`{ user: currentUser }`) by rewriting only the property name, and * rewrites `.user` for non-destructured single-param bodies — respecting variable @@ -1442,14 +1695,22 @@ export default function transform(source: string): string | null { // even when the file also imports something else from the SDK. const sdkRenameSourceNames = new Set(); const nullableInvokerAliasLocalNames = new Set(); + const actorTypeAliasLocalNames = new Set(); + const actorTypeLocalNames = new Set(); for (const importStmt of sdkImports) { for (const { importedName, aliasNode, localName } of iterateImportSpecs(importStmt)) { if (TYPE_RENAME_MAP[importedName] && !aliasNode) { sdkRenameSourceNames.add(importedName); } + if (importedName === "TailorActor") { + actorTypeLocalNames.add(localName); + } if (importedName === "TailorInvoker" && aliasNode) { nullableInvokerAliasLocalNames.add(localName); } + if (importedName === "TailorActorType" && aliasNode) { + actorTypeAliasLocalNames.add(localName); + } } } @@ -1464,10 +1725,19 @@ export default function transform(source: string): string | null { ? renamedTypeIdentifierText(id.text()) : nullableInvokerAliasLocalNames.has(id.text()) ? renamedTypeIdentifierText("TailorInvoker") - : null; + : actorTypeAliasLocalNames.has(id.text()) + ? renamedTypeIdentifierText("TailorActorType") + : null; if (!newName) continue; edits.push(id.replace(newName)); } + const transformedActorPropertyStarts = new Set(); + transformTailorActorTypedMemberAccesses( + tree, + actorTypeLocalNames, + edits, + transformedActorPropertyStarts, + ); let importRemoved = false; const globalEmittedRenamed = new Set(); @@ -1481,6 +1751,7 @@ export default function transform(source: string): string | null { globalEmittedRenamed, unauthenticatedLocalNames, nullableInvokerAliasLocalNames, + actorTypeAliasLocalNames, ); if (!touched) continue; edits.push(importStmt.replace(newText)); @@ -1509,12 +1780,16 @@ export default function transform(source: string): string | null { // and unrelated local helpers named `createResolver` (when the SDK import // does not actually bring `createResolver` in) are not. const createResolverLocalNames = new Set(); + const createExecutorLocalNames = new Set(); const sdkFieldRootNames = new Set(); for (const importStmt of sdkImports) { for (const { importedName, localName } of iterateImportSpecs(importStmt)) { if (importedName === "createResolver") { createResolverLocalNames.add(localName); } + if (importedName === "createExecutor") { + createExecutorLocalNames.add(localName); + } if (importedName === "t" || importedName === "db") { sdkFieldRootNames.add(localName); } @@ -1541,6 +1816,29 @@ export default function transform(source: string): string | null { if (arrow) transformResolverBody(arrow, edits); } } + for (const localName of createExecutorLocalNames) { + const shadowRanges = collectAllShadowRanges(tree, localName); + const calls = tree.findAll({ + rule: { + kind: "call_expression", + has: { + field: "function", + kind: "identifier", + regex: `^${escapeRegex(localName)}$`, + }, + }, + }); + for (const call of calls) { + const callee = call.field("function"); + if (!callee) continue; + const pos = callee.range().start.index; + if (isInsideAnyRange(pos, shadowRanges)) continue; + for (const fn of findExecutorBodyFunctions(call)) { + transformExecutorBodyActorAccesses(fn, edits, transformedActorPropertyStarts); + } + } + } + transformActorTypeComparisonLiterals(tree, edits, transformedActorPropertyStarts); const sdkFieldLocalBindings = collectSdkFieldLocalBindings(tree, sdkFieldRootNames); const callbackBindings = collectLocalCallbackBindings(tree); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access-local/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access-local/input.ts new file mode 100644 index 000000000..18185eaf1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access-local/input.ts @@ -0,0 +1,18 @@ +import { createExecutor } from "@tailor-platform/sdk"; + +const actor = { + userId: "domain-user", + userType: "performer", +}; + +export const domainActor = { + id: actor.userId, + type: actor.userType, +}; + +export const onEvent = createExecutor({ + operation: { + kind: "function", + body: () => domainActor, + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/expected.ts new file mode 100644 index 000000000..b072e0f21 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/expected.ts @@ -0,0 +1,16 @@ +import { createExecutor } from "@tailor-platform/sdk"; + +export const onEvent = createExecutor({ + operation: { + kind: "function", + async body(args) { + return { + id: args.actor?.id, + type: args.actor?.type, + isUser: args.actor?.type === "user", + isMachine: args.actor?.type === "machine_user", + isUnspecified: args.actor?.type === undefined, + }; + }, + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/input.ts new file mode 100644 index 000000000..43a025169 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/input.ts @@ -0,0 +1,16 @@ +import { createExecutor } from "@tailor-platform/sdk"; + +export const onEvent = createExecutor({ + operation: { + kind: "function", + async body(args) { + return { + id: args.actor?.userId, + type: args.actor?.userType, + isUser: args.actor?.userType === "USER_TYPE_USER", + isMachine: args.actor?.userType === "USER_TYPE_MACHINE_USER", + isUnspecified: args.actor?.userType === "USER_TYPE_UNSPECIFIED", + }; + }, + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts index 3b5bfcc8c..6be376b0b 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts @@ -6,4 +6,12 @@ export type Props = { invoker: (TailorPrincipal | null); nullableInvoker: (TailorPrincipal | null) | null; invokers: (TailorPrincipal | null)[]; + actorType: TailorPrincipal["type"]; }; + +export function actorFields(actor: TailorPrincipal | null) { + return { + id: actor?.id, + type: actor?.type, + }; +} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts index 82fbac821..3f69494cf 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts @@ -1,4 +1,4 @@ -import type { TailorUser, TailorActor, TailorInvoker } from "@tailor-platform/sdk"; +import type { TailorUser, TailorActor, TailorInvoker, TailorActorType } from "@tailor-platform/sdk"; export type Props = { user: TailorUser; @@ -6,4 +6,12 @@ export type Props = { invoker: TailorInvoker; nullableInvoker: TailorInvoker | null; invokers: TailorInvoker[]; + actorType: TailorActorType; }; + +export function actorFields(actor: TailorActor | null) { + return { + id: actor?.userId, + type: actor?.userType, + }; +} diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 0b2c1d541..ab78a9317 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -48,13 +48,19 @@ const allCodemods: CodemodPackage[] = [ }, { id: "v2/principal-unify", - name: "Unify TailorUser/TailorActor/TailorInvoker → TailorPrincipal", + name: "Unify TailorUser/TailorActor/TailorActorType/TailorInvoker → TailorPrincipal", description: - "Rename TailorUser/TailorActor/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, rename resolver body `user` to `caller`, and rename TailorDB callback `user` to `invoker`", + "Rename TailorUser/TailorActor/TailorActorType/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, rename resolver body `user` to `caller`, and rename TailorDB callback `user` to `invoker`", since: "1.0.0", until: "2.0.0", scriptPath: "v2/principal-unify/scripts/transform.js", - legacyPatterns: ["TailorUser", "TailorActor", "TailorInvoker", "unauthenticatedTailorUser"], + legacyPatterns: [ + "TailorUser", + "TailorActor", + "TailorActorType", + "TailorInvoker", + "unauthenticatedTailorUser", + ], }, { id: "v2/apply-to-deploy", From 02a0474cd217953c61634f664833280750ee713e Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 16:52:08 +0900 Subject: [PATCH 110/618] fix(sdk): support early Node 22 workflow invoker storage --- .../configure/services/workflow/job.test.ts | 33 +++++++++++++++++++ .../services/workflow/test-env-key.ts | 4 +-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/configure/services/workflow/job.test.ts b/packages/sdk/src/configure/services/workflow/job.test.ts index 4e2c76f80..843f7e499 100644 --- a/packages/sdk/src/configure/services/workflow/job.test.ts +++ b/packages/sdk/src/configure/services/workflow/job.test.ts @@ -60,6 +60,39 @@ describe("WorkflowJob type inference", () => { }); }); + test("direct body calls work when process.getBuiltinModule is unavailable", async () => { + const descriptor = Object.getOwnPropertyDescriptor(process, "getBuiltinModule"); + Object.defineProperty(process, "getBuiltinModule", { + configurable: true, + value: undefined, + }); + try { + const invoker: TailorPrincipal = { + id: "principal-1", + type: "user", + workspaceId: "workspace-1", + attributes: {}, + attributeList: [], + }; + const child = createWorkflowJob({ + name: "capture-child-invoker-without-get-builtin-module", + body: (_input: undefined, context) => context.invoker?.id ?? "anonymous", + }); + const parent = createWorkflowJob({ + name: "propagate-parent-invoker-without-get-builtin-module", + body: async () => await child.trigger(), + }); + + await expect(parent.body(undefined, { env: {}, invoker })).resolves.toBe("principal-1"); + } finally { + if (descriptor) { + Object.defineProperty(process, "getBuiltinModule", descriptor); + } else { + delete (process as { getBuiltinModule?: unknown }).getBuiltinModule; + } + } + }); + test("direct body calls propagate invoker to triggered child jobs", async () => { const invoker: TailorPrincipal = { id: "principal-1", diff --git a/packages/sdk/src/configure/services/workflow/test-env-key.ts b/packages/sdk/src/configure/services/workflow/test-env-key.ts index 145aba3db..8c0f4ffc1 100644 --- a/packages/sdk/src/configure/services/workflow/test-env-key.ts +++ b/packages/sdk/src/configure/services/workflow/test-env-key.ts @@ -9,6 +9,7 @@ * it from nested Vitest configs that do not resolve `@/` aliases. * @internal */ +import { AsyncLocalStorage } from "node:async_hooks"; import type { TailorEnv, TailorPrincipal } from "../../../runtime/types"; const SLOT_KEY = "__tailorWorkflowTestEnv"; @@ -22,9 +23,6 @@ let invokerStorage: AsyncLocalStorageLike | undefined; function workflowInvokerStorage(): AsyncLocalStorageLike { if (!invokerStorage) { - const { AsyncLocalStorage } = process.getBuiltinModule("node:async_hooks") as { - AsyncLocalStorage: new () => AsyncLocalStorageLike; - }; invokerStorage = new AsyncLocalStorage(); } return invokerStorage; From a69542423d050887b35e8c7bdee137de7726dec7 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 17:19:02 +0900 Subject: [PATCH 111/618] fix(sdk-codemod): preserve nullable principal callback migrations --- .../v2/principal-unify/scripts/transform.ts | 126 +++++++++++++++--- .../tests/tailordb-callbacks/expected.ts | 21 ++- .../tests/tailordb-callbacks/input.ts | 21 ++- .../tests/type-imports/expected.ts | 2 + .../tests/type-imports/input.ts | 2 + 5 files changed, 148 insertions(+), 24 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index c29e40ab3..5f2a357f6 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -71,6 +71,28 @@ function principalPropertyReplacement(node: SgNode, name: string): string { return parent && isOptionalizableMemberObject(parent) ? `${name}?` : name; } +function isObjectDestructureInitializer(node: SgNode): boolean { + const parent = node.parent(); + if (!parent || parent.kind() !== "variable_declarator") return false; + const value = parent.field("value"); + if (!value) return false; + const valueRange = value.range(); + const nodeRange = node.range(); + if ( + valueRange.start.index !== nodeRange.start.index || + valueRange.end.index !== nodeRange.end.index + ) { + return false; + } + return parent.field("name")?.kind() === "object_pattern"; +} + +function principalReadReplacement(node: SgNode, name: string): string { + return isObjectDestructureInitializer(node) + ? `${name} ?? {}` + : principalIdentifierReplacement(node, name); +} + function addActorPropertyReplacement( property: SgNode, edits: Edit[], @@ -134,6 +156,27 @@ function transformActorTypeComparisonLiterals( } } +function transformTailorActorTypeInitializerLiterals( + root: SgNode, + actorTypeLocalNames: Set, + edits: Edit[], +): void { + if (actorTypeLocalNames.size === 0) return; + const transformedLiteralStarts = new Set(); + const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarators) { + if (!isTailorActorTypeReference(decl, actorTypeLocalNames)) continue; + const value = decl.field("value"); + if (!value || value.kind() !== "string") continue; + const replacement = actorTypeLiteralReplacement(value); + if (!replacement) continue; + const start = value.range().start.index; + if (transformedLiteralStarts.has(start)) continue; + transformedLiteralStarts.add(start); + edits.push(value.replace(replacement)); + } +} + function transformActorBindingMemberAccesses( root: SgNode, binding: PrincipalLocalBinding, @@ -526,6 +569,16 @@ function guardPrincipalMemberAccesses( if (isShadowedLocalReference(root, binding.name, pos, binding.bindingStart)) continue; edits.push(ref.replace(`${binding.name}?`)); } + + const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarators) { + const value = decl.field("value"); + if (!value || value.kind() !== "identifier" || value.text() !== binding.name) continue; + if (!isObjectDestructureInitializer(value)) continue; + const pos = value.range().start.index; + if (isShadowedLocalReference(root, binding.name, pos, binding.bindingStart)) continue; + edits.push(value.replace(`${binding.name} ?? {}`)); + } } function findResolverBodyArrow(call: SgNode): SgNode | null { @@ -899,14 +952,29 @@ interface LocalCallbackTypeBinding { interface CallbackTypeContext { bindings: LocalCallbackTypeBinding[]; transformedTypeStarts: Set; + transformedPrincipalTypeStarts: Set; root: SgNode; } -function transformObjectTypeUserProperty(objectType: SgNode, edits: Edit[]): void { +function transformObjectTypeUserProperty( + objectType: SgNode, + edits: Edit[], + transformedPrincipalTypeStarts: Set, +): void { for (const child of objectType.children()) { if (child.kind() !== "property_signature") continue; const name = child.field("name"); - if (name?.text() === "user") edits.push(name.replace("invoker")); + if (name?.text() !== "user") continue; + edits.push(name.replace("invoker")); + + const typeAnnotation = child.field("type"); + if (!typeAnnotation || /\bnull\b/.test(typeAnnotation.text())) continue; + const typeIds = typeAnnotation.findAll({ rule: { kind: "type_identifier" } }); + for (const typeId of typeIds) { + if (typeId.text() !== "TailorUser") continue; + transformedPrincipalTypeStarts.add(typeId.range().start.index); + edits.push(typeId.replace("TailorPrincipal | null")); + } } } @@ -988,7 +1056,7 @@ function transformNamedPrincipalCallbackType( const objectType = localCallbackTypeObject(binding.declaration); if (!objectType) return; context.transformedTypeStarts.add(start); - transformObjectTypeUserProperty(objectType, edits); + transformObjectTypeUserProperty(objectType, edits, context.transformedPrincipalTypeStarts); } function transformPrincipalCallbackParamType( @@ -999,7 +1067,13 @@ function transformPrincipalCallbackParamType( const typeAnnotation = param.field("type"); const objectType = typeAnnotation?.children().find((c: SgNode) => c.kind() === "object_type"); if (objectType) { - transformObjectTypeUserProperty(objectType, edits); + if (typeContext) { + transformObjectTypeUserProperty( + objectType, + edits, + typeContext.transformedPrincipalTypeStarts, + ); + } return; } @@ -1161,7 +1235,7 @@ function transformPrincipalCallbackParam( for (const ref of refs) { const pos = ref.range().start.index; if (isInsideAnyRange(pos, shadowRanges)) continue; - edits.push(ref.replace(principalIdentifierReplacement(ref, "invoker"))); + edits.push(ref.replace(principalReadReplacement(ref, "invoker"))); } const shortRefs = body.findAll({ @@ -1697,6 +1771,7 @@ export default function transform(source: string): string | null { const nullableInvokerAliasLocalNames = new Set(); const actorTypeAliasLocalNames = new Set(); const actorTypeLocalNames = new Set(); + const actorTypeValueLocalNames = new Set(); for (const importStmt of sdkImports) { for (const { importedName, aliasNode, localName } of iterateImportSpecs(importStmt)) { if (TYPE_RENAME_MAP[importedName] && !aliasNode) { @@ -1705,6 +1780,9 @@ export default function transform(source: string): string | null { if (importedName === "TailorActor") { actorTypeLocalNames.add(localName); } + if (importedName === "TailorActorType") { + actorTypeValueLocalNames.add(localName); + } if (importedName === "TailorInvoker" && aliasNode) { nullableInvokerAliasLocalNames.add(localName); } @@ -1714,23 +1792,6 @@ export default function transform(source: string): string | null { } } - const typeIdents = tree.findAll({ - rule: { - kind: "type_identifier", - not: { inside: { kind: "import_statement" } }, - }, - }); - for (const id of typeIdents) { - const newName = sdkRenameSourceNames.has(id.text()) - ? renamedTypeIdentifierText(id.text()) - : nullableInvokerAliasLocalNames.has(id.text()) - ? renamedTypeIdentifierText("TailorInvoker") - : actorTypeAliasLocalNames.has(id.text()) - ? renamedTypeIdentifierText("TailorActorType") - : null; - if (!newName) continue; - edits.push(id.replace(newName)); - } const transformedActorPropertyStarts = new Set(); transformTailorActorTypedMemberAccesses( tree, @@ -1738,6 +1799,7 @@ export default function transform(source: string): string | null { edits, transformedActorPropertyStarts, ); + transformTailorActorTypeInitializerLiterals(tree, actorTypeValueLocalNames, edits); let importRemoved = false; const globalEmittedRenamed = new Set(); @@ -1846,6 +1908,7 @@ export default function transform(source: string): string | null { const typeContext: CallbackTypeContext = { bindings: collectLocalCallbackTypeBindings(tree), transformedTypeStarts: new Set(), + transformedPrincipalTypeStarts: new Set(), root: tree, }; const transformedCallbackStarts = new Set(); @@ -1867,6 +1930,25 @@ export default function transform(source: string): string | null { transformParseArgsObject(call, edits, sdkFieldRootNames, sdkFieldLocalBindings, tree); } + const typeIdents = tree.findAll({ + rule: { + kind: "type_identifier", + not: { inside: { kind: "import_statement" } }, + }, + }); + for (const id of typeIdents) { + if (typeContext.transformedPrincipalTypeStarts.has(id.range().start.index)) continue; + const newName = sdkRenameSourceNames.has(id.text()) + ? renamedTypeIdentifierText(id.text()) + : nullableInvokerAliasLocalNames.has(id.text()) + ? renamedTypeIdentifierText("TailorInvoker") + : actorTypeAliasLocalNames.has(id.text()) + ? renamedTypeIdentifierText("TailorActorType") + : null; + if (!newName) continue; + edits.push(id.replace(newName)); + } + if (edits.length === 0) return null; let result = tree.commitEdits(edits); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index 619dd5297..566767923 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -21,6 +21,21 @@ interface ValidatorArgs { const namedTypeHook = ({ invoker }: HookArgs) => invoker?.id ?? "anonymous"; const namedTypeValidator = ({ invoker }: ValidatorArgs) => invoker?.id !== ""; +type StrictHookArgs = { + value: string; + invoker: TailorPrincipal | null; +}; + +const strictHook = ({ invoker }: { invoker: TailorPrincipal | null }) => { + const { id } = invoker ?? {}; + return id; +}; + +const namedStrictHook = ({ invoker }: StrictHookArgs) => { + const { id } = invoker ?? {}; + return id; +}; + const sharedHooks = { create: ({ invoker }: { invoker: TailorPrincipal | null }) => invoker?.id ?? "anonymous", update: namedTypeHook, @@ -43,9 +58,13 @@ const role = db .validate(namedTypeValidator); const localHookedRole = db.string().hooks(sharedHooks); +const strictHookedRole = db.string().hooks({ create: strictHook, update: namedStrictHook }); const directHookedRole = db.string().hooks({ - create: ({ invoker }) => invoker?.id, + create: ({ invoker }) => { + const { id } = invoker ?? {}; + return id; + }, update: (ctx) => ctx.invoker?.id, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index 1c295ecd2..03639b530 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -22,6 +22,21 @@ interface ValidatorArgs { const namedTypeHook = ({ user }: HookArgs) => user?.id ?? "anonymous"; const namedTypeValidator = ({ user }: ValidatorArgs) => user?.id !== ""; +type StrictHookArgs = { + value: string; + user: TailorUser; +}; + +const strictHook = ({ user }: { user: TailorUser }) => { + const { id } = user; + return id; +}; + +const namedStrictHook = ({ user }: StrictHookArgs) => { + const { id } = user; + return id; +}; + const sharedHooks = { create: ({ user }: { user: TailorUser | null }) => user?.id ?? "anonymous", update: namedTypeHook, @@ -44,9 +59,13 @@ const role = db .validate(namedTypeValidator); const localHookedRole = db.string().hooks(sharedHooks); +const strictHookedRole = db.string().hooks({ create: strictHook, update: namedStrictHook }); const directHookedRole = db.string().hooks({ - create: ({ user }) => user.id, + create: ({ user }) => { + const { id } = user; + return id; + }, update: (ctx) => ctx.user.id, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts index 6be376b0b..d73fdc6e0 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts @@ -15,3 +15,5 @@ export function actorFields(actor: TailorPrincipal | null) { type: actor?.type, }; } + +export const actorTypeValue: TailorPrincipal["type"] = "user"; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts index 3f69494cf..457bfc2ce 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts @@ -15,3 +15,5 @@ export function actorFields(actor: TailorActor | null) { type: actor?.userType, }; } + +export const actorTypeValue: TailorActorType = "USER_TYPE_USER"; From 6a384c8666607cb2de20b16ca7dbfcc186025f7a Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 17:41:54 +0900 Subject: [PATCH 112/618] fix(sdk-codemod): migrate principal type aliases safely --- .../v2/principal-unify/scripts/transform.ts | 90 ++++++++++++++++++- .../tests/ctx-destructure/expected.ts | 2 +- .../tests/tailordb-callbacks/expected.ts | 15 +++- .../tests/tailordb-callbacks/input.ts | 15 +++- .../tests/type-imports/expected.ts | 4 + .../tests/type-imports/input.ts | 4 + 6 files changed, 119 insertions(+), 11 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 5f2a357f6..cdb32561c 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -177,6 +177,71 @@ function transformTailorActorTypeInitializerLiterals( } } +function transformActorTypeBindingComparisons( + root: SgNode, + binding: PrincipalLocalBinding, + edits: Edit[], +): void { + const refs = root.findAll({ + rule: { kind: "identifier", regex: `^${escapeRegex(binding.name)}$` }, + }); + const transformedLiteralStarts = new Set(); + for (const ref of refs) { + const binary = ref.parent(); + if (!binary || binary.kind() !== "binary_expression") continue; + if ( + isShadowedLocalReference(root, binding.name, ref.range().start.index, binding.bindingStart) + ) { + continue; + } + for (const child of binary.children()) { + if (child.kind() !== "string") continue; + const replacement = actorTypeLiteralReplacement(child); + if (!replacement) continue; + const start = child.range().start.index; + if (transformedLiteralStarts.has(start)) continue; + transformedLiteralStarts.add(start); + edits.push(child.replace(replacement)); + } + } +} + +function transformTailorActorTypeBindingComparisons( + root: SgNode, + actorTypeLocalNames: Set, + edits: Edit[], +): void { + if (actorTypeLocalNames.size === 0) return; + + for (const kind of NESTED_FN_KINDS) { + const fns = root.findAll({ rule: { kind } }); + for (const fn of fns) { + const param = getFirstFunctionParam(fn); + if (!param || !isTailorActorTypeReference(param, actorTypeLocalNames)) continue; + const pattern = getFunctionParamPattern(param); + const body = fn.field("body"); + if (!pattern || pattern.kind() !== "identifier" || !body) continue; + transformActorTypeBindingComparisons( + body, + { name: pattern.text(), bindingStart: pattern.range().start.index }, + edits, + ); + } + } + + const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarators) { + if (!isTailorActorTypeReference(decl, actorTypeLocalNames)) continue; + const name = decl.field("name"); + if (!name || name.kind() !== "identifier") continue; + transformActorTypeBindingComparisons( + root, + { name: name.text(), bindingStart: name.range().start.index }, + edits, + ); + } +} + function transformActorBindingMemberAccesses( root: SgNode, binding: PrincipalLocalBinding, @@ -953,6 +1018,7 @@ interface CallbackTypeContext { bindings: LocalCallbackTypeBinding[]; transformedTypeStarts: Set; transformedPrincipalTypeStarts: Set; + tailorUserTypeLocalNames: Set; root: SgNode; } @@ -960,6 +1026,7 @@ function transformObjectTypeUserProperty( objectType: SgNode, edits: Edit[], transformedPrincipalTypeStarts: Set, + tailorUserTypeLocalNames: Set, ): void { for (const child of objectType.children()) { if (child.kind() !== "property_signature") continue; @@ -971,9 +1038,11 @@ function transformObjectTypeUserProperty( if (!typeAnnotation || /\bnull\b/.test(typeAnnotation.text())) continue; const typeIds = typeAnnotation.findAll({ rule: { kind: "type_identifier" } }); for (const typeId of typeIds) { - if (typeId.text() !== "TailorUser") continue; + if (!tailorUserTypeLocalNames.has(typeId.text())) continue; transformedPrincipalTypeStarts.add(typeId.range().start.index); - edits.push(typeId.replace("TailorPrincipal | null")); + const replacement = + typeId.text() === "TailorUser" ? "TailorPrincipal | null" : `${typeId.text()} | null`; + edits.push(typeId.replace(replacement)); } } } @@ -1056,7 +1125,12 @@ function transformNamedPrincipalCallbackType( const objectType = localCallbackTypeObject(binding.declaration); if (!objectType) return; context.transformedTypeStarts.add(start); - transformObjectTypeUserProperty(objectType, edits, context.transformedPrincipalTypeStarts); + transformObjectTypeUserProperty( + objectType, + edits, + context.transformedPrincipalTypeStarts, + context.tailorUserTypeLocalNames, + ); } function transformPrincipalCallbackParamType( @@ -1072,6 +1146,7 @@ function transformPrincipalCallbackParamType( objectType, edits, typeContext.transformedPrincipalTypeStarts, + typeContext.tailorUserTypeLocalNames, ); } return; @@ -1272,7 +1347,8 @@ function isShadowedLocalReference( for (const decl of declarators) { const nameNode = decl.field("name"); if (!nameNode || !patternBindsName(nameNode, name)) continue; - if (nameNode.range().start.index === bindingStart) continue; + const nameRange = nameNode.range(); + if (bindingStart >= nameRange.start.index && bindingStart < nameRange.end.index) continue; const scope = enclosingScopeRange(decl); if (scope && rangeContains(scope, pos)) return true; } @@ -1768,6 +1844,7 @@ export default function transform(source: string): string | null { // alias. A local `import type { TailorUser } from './domain'` must stay alone // even when the file also imports something else from the SDK. const sdkRenameSourceNames = new Set(); + const tailorUserTypeLocalNames = new Set(); const nullableInvokerAliasLocalNames = new Set(); const actorTypeAliasLocalNames = new Set(); const actorTypeLocalNames = new Set(); @@ -1777,6 +1854,9 @@ export default function transform(source: string): string | null { if (TYPE_RENAME_MAP[importedName] && !aliasNode) { sdkRenameSourceNames.add(importedName); } + if (importedName === "TailorUser") { + tailorUserTypeLocalNames.add(localName); + } if (importedName === "TailorActor") { actorTypeLocalNames.add(localName); } @@ -1800,6 +1880,7 @@ export default function transform(source: string): string | null { transformedActorPropertyStarts, ); transformTailorActorTypeInitializerLiterals(tree, actorTypeValueLocalNames, edits); + transformTailorActorTypeBindingComparisons(tree, actorTypeValueLocalNames, edits); let importRemoved = false; const globalEmittedRenamed = new Set(); @@ -1909,6 +1990,7 @@ export default function transform(source: string): string | null { bindings: collectLocalCallbackTypeBindings(tree), transformedTypeStarts: new Set(), transformedPrincipalTypeStarts: new Set(), + tailorUserTypeLocalNames, root: tree, }; const transformedCallbackStarts = new Set(); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-destructure/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-destructure/expected.ts index 00575ecc3..fc8e2050f 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-destructure/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-destructure/expected.ts @@ -6,6 +6,6 @@ export default createResolver({ output: t.string(), body: (ctx) => { const { caller: user } = ctx; - return user.id; + return user?.id; }, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index 566767923..702179a61 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -1,4 +1,4 @@ -import { db, t, type TailorPrincipal } from "@tailor-platform/sdk"; +import { db, t, type TailorPrincipal, type TailorPrincipal as MyUser } from "@tailor-platform/sdk"; const roleCreate = ({ value, invoker }: { value: string; invoker: TailorPrincipal | null }) => invoker?.attributes.role === "ADMIN" ? value : "user"; @@ -36,6 +36,8 @@ const namedStrictHook = ({ invoker }: StrictHookArgs) => { return id; }; +const aliasedStrictHook = ({ invoker }: { invoker: MyUser | null }) => invoker?.id; + const sharedHooks = { create: ({ invoker }: { invoker: TailorPrincipal | null }) => invoker?.id ?? "anonymous", update: namedTypeHook, @@ -58,14 +60,21 @@ const role = db .validate(namedTypeValidator); const localHookedRole = db.string().hooks(sharedHooks); -const strictHookedRole = db.string().hooks({ create: strictHook, update: namedStrictHook }); +const strictHookedRole = db + .string() + .hooks({ create: strictHook, update: namedStrictHook }) + .hooks({ create: aliasedStrictHook }); const directHookedRole = db.string().hooks({ create: ({ invoker }) => { const { id } = invoker ?? {}; return id; }, - update: (ctx) => ctx.invoker?.id, + update: (ctx) => { + const { invoker: user } = ctx; + const { invoker: currentUser } = ctx; + return user?.id ?? currentUser?.id; + }, }); const reviewer = t.string(); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index 03639b530..046988994 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -1,4 +1,4 @@ -import { db, t, type TailorUser } from "@tailor-platform/sdk"; +import { db, t, type TailorUser, type TailorUser as MyUser } from "@tailor-platform/sdk"; import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; const roleCreate = ({ value, user }: { value: string; user: TailorUser | null }) => @@ -37,6 +37,8 @@ const namedStrictHook = ({ user }: StrictHookArgs) => { return id; }; +const aliasedStrictHook = ({ user }: { user: MyUser }) => user.id; + const sharedHooks = { create: ({ user }: { user: TailorUser | null }) => user?.id ?? "anonymous", update: namedTypeHook, @@ -59,14 +61,21 @@ const role = db .validate(namedTypeValidator); const localHookedRole = db.string().hooks(sharedHooks); -const strictHookedRole = db.string().hooks({ create: strictHook, update: namedStrictHook }); +const strictHookedRole = db + .string() + .hooks({ create: strictHook, update: namedStrictHook }) + .hooks({ create: aliasedStrictHook }); const directHookedRole = db.string().hooks({ create: ({ user }) => { const { id } = user; return id; }, - update: (ctx) => ctx.user.id, + update: (ctx) => { + const { user } = ctx; + const { user: currentUser } = ctx; + return user.id ?? currentUser.id; + }, }); const reviewer = t.string(); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts index d73fdc6e0..e154c4724 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts @@ -17,3 +17,7 @@ export function actorFields(actor: TailorPrincipal | null) { } export const actorTypeValue: TailorPrincipal["type"] = "user"; + +export function isUserType(type: TailorPrincipal["type"]) { + return type === "user"; +} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts index 457bfc2ce..f01471340 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts @@ -17,3 +17,7 @@ export function actorFields(actor: TailorActor | null) { } export const actorTypeValue: TailorActorType = "USER_TYPE_USER"; + +export function isUserType(type: TailorActorType) { + return type === "USER_TYPE_USER"; +} From 99297806efbbf3946ab799906448173c3f3c504a Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 18:06:28 +0900 Subject: [PATCH 113/618] fix(sdk-codemod): preserve principal parse args --- .../v2/principal-unify/scripts/transform.ts | 16 +++++++++++++--- .../v2/principal-unify/tests/basic/expected.ts | 3 ++- .../v2/principal-unify/tests/basic/input.ts | 3 ++- .../tests/tailordb-callbacks/expected.ts | 3 ++- .../tests/tailordb-callbacks/input.ts | 3 ++- .../tests/type-imports/expected.ts | 7 ++++--- .../principal-unify/tests/type-imports/input.ts | 1 + 7 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index cdb32561c..f1a525c69 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -93,6 +93,16 @@ function principalReadReplacement(node: SgNode, name: string): string { : principalIdentifierReplacement(node, name); } +function isParseArgumentShorthand(node: SgNode): boolean { + if (node.kind() !== "shorthand_property_identifier") return false; + const object = node.parent(); + if (!object || object.kind() !== "object") return false; + const args = object.parent(); + if (!args || args.kind() !== "arguments") return false; + const call = args.parent(); + return !!call && call.kind() === "call_expression" && findMemberCallName(call) === "parse"; +} + function addActorPropertyReplacement( property: SgNode, edits: Edit[], @@ -338,7 +348,7 @@ function transformExecutorCtxActorAccesses( function renamedTypeIdentifierText(name: string): string | null { if (name === "TailorInvoker") return "(TailorPrincipal | null)"; - if (name === "TailorActorType") return 'TailorPrincipal["type"]'; + if (name === "TailorActorType") return '(TailorPrincipal["type"] | undefined)'; return TYPE_RENAME_MAP[name] ?? null; } @@ -803,7 +813,7 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { for (const ref of shortRefs) { const pos = ref.range().start.index; if (isInsideAnyRange(pos, shadowRanges)) continue; - edits.push(ref.replace("user: caller")); + edits.push(ref.replace(isParseArgumentShorthand(ref) ? "invoker: caller" : "user: caller")); } } for (const binding of principalAliasBindings) { @@ -1319,7 +1329,7 @@ function transformPrincipalCallbackParam( for (const ref of shortRefs) { const pos = ref.range().start.index; if (isInsideAnyRange(pos, shadowRanges)) continue; - edits.push(ref.replace("user: invoker")); + edits.push(ref.replace(isParseArgumentShorthand(ref) ? "invoker" : "user: invoker")); } for (const binding of principalAliasBindings) { diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts index 25357e5a6..bbe7d8f6e 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts @@ -6,7 +6,8 @@ export default createResolver({ input: t.object({ id: t.string() }), output: t.object({ id: t.string() }), body: ({ input, caller }) => { - return { id: caller?.id }; + const parsed = t.string().parse({ value: input.id, data: {}, invoker: caller }); + return { id: parsed.value ?? caller?.id }; }, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts index 76bb3630e..205c48b08 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts @@ -6,7 +6,8 @@ export default createResolver({ input: t.object({ id: t.string() }), output: t.object({ id: t.string() }), body: ({ input, user }) => { - return { id: user.id }; + const parsed = t.string().parse({ value: input.id, data: {}, user }); + return { id: parsed.value ?? user.id }; }, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index 702179a61..d0208e1bc 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -67,8 +67,9 @@ const strictHookedRole = db const directHookedRole = db.string().hooks({ create: ({ invoker }) => { + const parsed = t.string().parse({ value: "hello", data: {}, invoker }); const { id } = invoker ?? {}; - return id; + return parsed.value ?? id; }, update: (ctx) => { const { invoker: user } = ctx; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index 046988994..c20b0c507 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -68,8 +68,9 @@ const strictHookedRole = db const directHookedRole = db.string().hooks({ create: ({ user }) => { + const parsed = t.string().parse({ value: "hello", data: {}, user }); const { id } = user; - return id; + return parsed.value ?? id; }, update: (ctx) => { const { user } = ctx; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts index e154c4724..74e3a85a3 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts @@ -6,7 +6,7 @@ export type Props = { invoker: (TailorPrincipal | null); nullableInvoker: (TailorPrincipal | null) | null; invokers: (TailorPrincipal | null)[]; - actorType: TailorPrincipal["type"]; + actorType: (TailorPrincipal["type"] | undefined); }; export function actorFields(actor: TailorPrincipal | null) { @@ -16,8 +16,9 @@ export function actorFields(actor: TailorPrincipal | null) { }; } -export const actorTypeValue: TailorPrincipal["type"] = "user"; +export const actorTypeValue: (TailorPrincipal["type"] | undefined) = "user"; +export const actorTypeMissing: (TailorPrincipal["type"] | undefined) = undefined; -export function isUserType(type: TailorPrincipal["type"]) { +export function isUserType(type: (TailorPrincipal["type"] | undefined)) { return type === "user"; } diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts index f01471340..f78993b6b 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts @@ -17,6 +17,7 @@ export function actorFields(actor: TailorActor | null) { } export const actorTypeValue: TailorActorType = "USER_TYPE_USER"; +export const actorTypeMissing: TailorActorType = "USER_TYPE_UNSPECIFIED"; export function isUserType(type: TailorActorType) { return type === "USER_TYPE_USER"; From 1abbd3b510c46968961446dc11b0dfe4bc05e425 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 18:37:31 +0900 Subject: [PATCH 114/618] fix(sdk-codemod): avoid principal alias shadowing --- .../v2/principal-unify/scripts/transform.ts | 104 ++++++++++++++---- .../tests/caller-free-reference/expected.ts | 13 +++ .../tests/caller-free-reference/input.ts | 13 +++ .../tests/tailordb-callbacks/expected.ts | 9 ++ .../tests/tailordb-callbacks/input.ts | 9 ++ 5 files changed, 127 insertions(+), 21 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-free-reference/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-free-reference/input.ts diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index f1a525c69..a8296ad40 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -625,6 +625,35 @@ function collectAllShadowRanges(root: SgNode, name: string): Array<[number, numb return ranges; } +function hasUnshadowedIdentifierReference(root: SgNode, name: string): boolean { + const shadowRanges = collectAllShadowRanges(root, name); + const refs = root.findAll({ + rule: { kind: "identifier", regex: `^${escapeRegex(name)}$` }, + }); + for (const ref of refs) { + if (!isInsideAnyRange(ref.range().start.index, shadowRanges)) return true; + } + return false; +} + +function rewriteParseArgumentShorthands( + root: SgNode, + localName: string, + propertyName: string, + edits: Edit[], +): void { + const shadowRanges = collectAllShadowRanges(root, localName); + const shortRefs = root.findAll({ + rule: { kind: "shorthand_property_identifier", regex: `^${escapeRegex(localName)}$` }, + }); + for (const ref of shortRefs) { + const pos = ref.range().start.index; + if (isInsideAnyRange(pos, shadowRanges)) continue; + if (!isParseArgumentShorthand(ref)) continue; + edits.push(ref.replace(`${propertyName}: ${localName}`)); + } +} + interface PrincipalLocalBinding { name: string; bindingStart: number; @@ -700,22 +729,15 @@ function hasCallerBindingConflict(pattern: SgNode, body: SgNode): boolean { if (inner && inner.text() === "caller") return true; } } - const decls = body.findAll({ - rule: { - kind: "identifier", - regex: "^caller$", - inside: { kind: "variable_declarator" }, - }, - }); - if (decls.length > 0) return true; - const shortDecls = body.findAll({ - rule: { - kind: "shorthand_property_identifier_pattern", - regex: "^caller$", - inside: { kind: "variable_declarator" }, - }, - }); - if (shortDecls.length > 0) return true; + const decls = body.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of decls) { + const nameNode = decl.field("name"); + if (nameNode && patternBindsName(nameNode, "caller")) return true; + } + const functionDecls = body.findAll({ rule: { kind: "function_declaration" } }); + for (const fn of functionDecls) { + if (fn.field("name")?.text() === "caller") return true; + } for (const k of NESTED_FN_KINDS) { const fns = body.findAll({ rule: { kind: k } }); for (const fn of fns) { @@ -756,6 +778,8 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { if (pattern.kind() === "object_pattern") { if (hasCallerBindingConflict(pattern, body)) return; + const aliasRenamedUser = hasUnshadowedIdentifierReference(body, "caller"); + let aliasedShorthandUser = false; let renamedShorthandUser = false; const principalAliasBindings: PrincipalLocalBinding[] = []; // Only iterate top-level pattern children so nested destructures like @@ -763,8 +787,17 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { for (const child of pattern.children()) { const kind = child.kind(); if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { - edits.push(child.replace("caller")); - renamedShorthandUser = true; + if (aliasRenamedUser) { + edits.push(child.replace("caller: user")); + aliasedShorthandUser = true; + principalAliasBindings.push({ + name: "user", + bindingStart: child.range().start.index, + }); + } else { + edits.push(child.replace("caller")); + renamedShorthandUser = true; + } } else if (kind === "pair_pattern") { const key = child.field("key"); if (key && key.text() === "user") { @@ -784,11 +817,23 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { .children() .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); if (inner && inner.text() === "user") { - edits.push(inner.replace("caller")); - renamedShorthandUser = true; + if (aliasRenamedUser) { + edits.push(inner.replace("caller: user")); + aliasedShorthandUser = true; + principalAliasBindings.push({ + name: "user", + bindingStart: inner.range().start.index, + }); + } else { + edits.push(inner.replace("caller")); + renamedShorthandUser = true; + } } } } + if (aliasedShorthandUser) { + rewriteParseArgumentShorthands(body, "user", "invoker", edits); + } if (renamedShorthandUser) { // Use the broader shadow-range collector here so a nested arrow that // re-binds `user` as a parameter (e.g. `items.map((user) => user.id)`) @@ -1276,14 +1321,23 @@ function transformPrincipalCallbackParam( if (renamesBinding && patternBindsName(pattern, "invoker")) return; transformPrincipalCallbackParamType(param, edits, typeContext); - const aliasRenamedUser = renamesBinding && collectAllShadowRanges(body, "invoker").length > 0; + const aliasRenamedUser = + renamesBinding && + (collectAllShadowRanges(body, "invoker").length > 0 || + hasUnshadowedIdentifierReference(body, "invoker")); + let aliasedShorthandUser = false; let renamedShorthandUser = false; for (const child of pattern.children()) { const kind = child.kind(); if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { if (aliasRenamedUser) { edits.push(child.replace("invoker: user")); + aliasedShorthandUser = true; + principalAliasBindings.push({ + name: "user", + bindingStart: child.range().start.index, + }); } else { edits.push(child.replace("invoker")); renamedShorthandUser = true; @@ -1300,6 +1354,11 @@ function transformPrincipalCallbackParam( if (inner?.text() === "user") { if (aliasRenamedUser) { edits.push(inner.replace("invoker: user")); + aliasedShorthandUser = true; + principalAliasBindings.push({ + name: "user", + bindingStart: inner.range().start.index, + }); } else { edits.push(inner.replace("invoker")); renamedShorthandUser = true; @@ -1309,6 +1368,9 @@ function transformPrincipalCallbackParam( } if (!renamedShorthandUser) { + if (aliasedShorthandUser) { + rewriteParseArgumentShorthands(body, "user", "invoker", edits); + } for (const binding of principalAliasBindings) { guardPrincipalMemberAccesses(body, binding, edits); } diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-free-reference/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-free-reference/expected.ts new file mode 100644 index 000000000..d5fb407d1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-free-reference/expected.ts @@ -0,0 +1,13 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +const caller = { id: "outer-caller" }; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ caller: user }) => { + const parsed = t.string().parse({ value: "hello", data: {}, invoker: user }); + return user?.id ?? caller.id ?? parsed.value; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-free-reference/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-free-reference/input.ts new file mode 100644 index 000000000..278d0d9ad --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-free-reference/input.ts @@ -0,0 +1,13 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +const caller = { id: "outer-caller" }; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ user }) => { + const parsed = t.string().parse({ value: "hello", data: {}, user }); + return user.id ?? caller.id ?? parsed.value; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index d0208e1bc..42dff3501 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -65,6 +65,8 @@ const strictHookedRole = db .hooks({ create: strictHook, update: namedStrictHook }) .hooks({ create: aliasedStrictHook }); +const invoker = { id: "outer-invoker" }; + const directHookedRole = db.string().hooks({ create: ({ invoker }) => { const parsed = t.string().parse({ value: "hello", data: {}, invoker }); @@ -78,6 +80,13 @@ const directHookedRole = db.string().hooks({ }, }); +const externalInvokerHookedRole = db.string().hooks({ + create: ({ invoker: user }) => { + const parsed = t.string().parse({ value: "hello", data: {}, invoker: user }); + return user?.id ?? invoker.id ?? parsed.value; + }, +}); + const reviewer = t.string(); const zodLike = { parse: (arg: unknown) => arg }; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index c20b0c507..bc9099685 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -66,6 +66,8 @@ const strictHookedRole = db .hooks({ create: strictHook, update: namedStrictHook }) .hooks({ create: aliasedStrictHook }); +const invoker = { id: "outer-invoker" }; + const directHookedRole = db.string().hooks({ create: ({ user }) => { const parsed = t.string().parse({ value: "hello", data: {}, user }); @@ -79,6 +81,13 @@ const directHookedRole = db.string().hooks({ }, }); +const externalInvokerHookedRole = db.string().hooks({ + create: ({ user }) => { + const parsed = t.string().parse({ value: "hello", data: {}, user }); + return user.id ?? invoker.id ?? parsed.value; + }, +}); + const reviewer = t.string(); const zodLike = { parse: (arg: unknown) => arg }; From 937fb5e75da461ec28c2007c869e0e71263d4cfa Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 18:55:12 +0900 Subject: [PATCH 115/618] fix(sdk-codemod): guard computed principal reads --- .../v2/principal-unify/scripts/transform.ts | 38 ++++++++++++++----- .../principal-unify/tests/basic/expected.ts | 2 +- .../v2/principal-unify/tests/basic/input.ts | 2 +- .../tests/tailordb-callbacks/expected.ts | 2 +- .../tests/tailordb-callbacks/input.ts | 2 +- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index a8296ad40..9f3360065 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -45,30 +45,48 @@ function isInsideImportStatement(node: SgNode): boolean { return false; } -function isMemberExpressionObject(node: SgNode): boolean { +function memberObjectParent(node: SgNode): SgNode | null { const parent = node.parent(); - if (!parent || parent.kind() !== "member_expression") return false; + if ( + !parent || + (parent.kind() !== "member_expression" && parent.kind() !== "subscript_expression") + ) { + return null; + } const obj = parent.field("object"); - if (!obj) return false; + if (!obj) return null; const r = node.range(); const or = obj.range(); - return r.start.index === or.start.index && r.end.index === or.end.index; + if (r.start.index !== or.start.index || r.end.index !== or.end.index) return null; + return parent; +} + +function isMemberExpressionObject(node: SgNode): boolean { + return memberObjectParent(node) !== null; +} + +function optionalPrincipalReadKind(node: SgNode): "property" | "computed" | null { + const parent = memberObjectParent(node); + if (!parent) return null; + if (parent.text().startsWith(`${node.text()}?.`)) return null; + if (parent.kind() === "subscript_expression") return "computed"; + return parent.field("property")?.kind() === "property_identifier" ? "property" : null; } function isOptionalizableMemberObject(node: SgNode): boolean { - if (!isMemberExpressionObject(node)) return false; - const parent = node.parent(); - if (parent?.text().startsWith(`${node.text()}?.`)) return false; - return parent?.field("property")?.kind() === "property_identifier"; + return optionalPrincipalReadKind(node) !== null; } function principalIdentifierReplacement(node: SgNode, name: string): string { - return isOptionalizableMemberObject(node) ? `${name}?` : name; + const readKind = optionalPrincipalReadKind(node); + if (readKind === "computed") return `${name}?.`; + if (readKind === "property") return `${name}?`; + return name; } function principalPropertyReplacement(node: SgNode, name: string): string { const parent = node.parent(); - return parent && isOptionalizableMemberObject(parent) ? `${name}?` : name; + return parent ? principalIdentifierReplacement(parent, name) : name; } function isObjectDestructureInitializer(node: SgNode): boolean { diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts index bbe7d8f6e..e71562690 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts @@ -7,7 +7,7 @@ export default createResolver({ output: t.object({ id: t.string() }), body: ({ input, caller }) => { const parsed = t.string().parse({ value: input.id, data: {}, invoker: caller }); - return { id: parsed.value ?? caller?.id }; + return { id: parsed.value ?? caller?.["id"] ?? caller?.id }; }, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts index 205c48b08..8f87232e8 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts @@ -7,7 +7,7 @@ export default createResolver({ output: t.object({ id: t.string() }), body: ({ input, user }) => { const parsed = t.string().parse({ value: input.id, data: {}, user }); - return { id: parsed.value ?? user.id }; + return { id: parsed.value ?? user["id"] ?? user.id }; }, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index 42dff3501..4c6caa815 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -71,7 +71,7 @@ const directHookedRole = db.string().hooks({ create: ({ invoker }) => { const parsed = t.string().parse({ value: "hello", data: {}, invoker }); const { id } = invoker ?? {}; - return parsed.value ?? id; + return parsed.value ?? invoker?.["id"] ?? id; }, update: (ctx) => { const { invoker: user } = ctx; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index bc9099685..aa92d1fdd 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -72,7 +72,7 @@ const directHookedRole = db.string().hooks({ create: ({ user }) => { const parsed = t.string().parse({ value: "hello", data: {}, user }); const { id } = user; - return parsed.value ?? id; + return parsed.value ?? user["id"] ?? id; }, update: (ctx) => { const { user } = ctx; From ac30ef15c7d7c5d396a07e64e96368cf21eb8329 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 19:16:14 +0900 Subject: [PATCH 116/618] fix(sdk-codemod): handle namespace principal migrations --- .../v2/principal-unify/scripts/transform.ts | 198 ++++++++++++++++-- .../tests/assignment-target/input.ts | 11 + .../tests/namespace-import/expected.ts | 19 ++ .../tests/namespace-import/input.ts | 19 ++ 4 files changed, 224 insertions(+), 23 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/assignment-target/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/input.ts diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 9f3360065..3558f7745 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -69,6 +69,7 @@ function optionalPrincipalReadKind(node: SgNode): "property" | "computed" | null const parent = memberObjectParent(node); if (!parent) return null; if (parent.text().startsWith(`${node.text()}?.`)) return null; + if (isAssignmentTargetReference(node)) return null; if (parent.kind() === "subscript_expression") return "computed"; return parent.field("property")?.kind() === "property_identifier" ? "property" : null; } @@ -111,6 +112,39 @@ function principalReadReplacement(node: SgNode, name: string): string { : principalIdentifierReplacement(node, name); } +function nodeRangeContains(outer: SgNode, inner: SgNode): boolean { + const outerRange = outer.range(); + const innerRange = inner.range(); + return ( + innerRange.start.index >= outerRange.start.index && innerRange.end.index <= outerRange.end.index + ); +} + +function isAssignmentTargetReference(node: SgNode): boolean { + let current = node; + let parent = current.parent(); + while ( + parent && + (parent.kind() === "member_expression" || parent.kind() === "subscript_expression") + ) { + const object = parent.field("object"); + if (!object || !nodeRangeContains(object, current)) break; + current = parent; + parent = current.parent(); + } + + if (!parent) return false; + if (parent.kind() === "update_expression") return true; + if ( + parent.kind() !== "assignment_expression" && + parent.kind() !== "augmented_assignment_expression" + ) { + return false; + } + const left = parent.field("left"); + return !!left && nodeRangeContains(left, current); +} + function isParseArgumentShorthand(node: SgNode): boolean { if (node.kind() !== "shorthand_property_identifier") return false; const object = node.parent(); @@ -654,6 +688,19 @@ function hasUnshadowedIdentifierReference(root: SgNode, name: string): boolean { return false; } +function hasPrincipalAssignmentTarget(root: SgNode, name: string): boolean { + const shadowRanges = collectAllShadowRanges(root, name); + const refs = root.findAll({ + rule: { kind: "identifier", regex: `^${escapeRegex(name)}$` }, + }); + for (const ref of refs) { + const pos = ref.range().start.index; + if (isInsideAnyRange(pos, shadowRanges)) continue; + if (isAssignmentTargetReference(ref)) return true; + } + return false; +} + function rewriteParseArgumentShorthands( root: SgNode, localName: string, @@ -722,6 +769,41 @@ function findResolverBodyArrow(call: SgNode): SgNode | null { return null; } +function* iterateNamespaceImportLocalNames(importStmt: SgNode): Generator { + const namespaceImports = importStmt.findAll({ rule: { kind: "namespace_import" } }); + for (const namespaceImport of namespaceImports) { + const localName = namespaceImport.children().find((c: SgNode) => c.kind() === "identifier"); + if (localName) yield localName.text(); + } +} + +function isUnshadowedNamespaceObject( + node: SgNode, + namespaceNames: Set, + root: SgNode, +): boolean { + if (node.kind() !== "identifier" || !namespaceNames.has(node.text())) return false; + return !isInsideAnyRange(node.range().start.index, collectAllShadowRanges(root, node.text())); +} + +function isSdkNamespaceQualifiedTypeIdentifier( + typeId: SgNode, + namespaceNames: Set, + root: SgNode, +): boolean { + if (typeId.kind() !== "type_identifier") return false; + const parent = typeId.parent(); + if (!parent || parent.kind() !== "nested_type_identifier") return false; + const namespaceObject = parent.children().find((c: SgNode) => c.kind() === "identifier"); + return !!namespaceObject && isUnshadowedNamespaceObject(namespaceObject, namespaceNames, root); +} + +function renamedQualifiedTypeIdentifierText(name: string): string | null { + if (name === "TailorInvoker") return "TailorPrincipal | null"; + if (name === "TailorActorType") return 'TailorPrincipal["type"] | undefined'; + return TYPE_RENAME_MAP[name] ?? null; +} + /** * Look for any binding named `caller` in the resolver body or pattern. When * one exists, renaming `user` → `caller` would either shadow it, collide with @@ -794,6 +876,7 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { if (!pattern) return; if (pattern.kind() === "object_pattern") { + if (hasPrincipalAssignmentTarget(body, "user")) return; if (hasCallerBindingConflict(pattern, body)) return; const aliasRenamedUser = hasUnshadowedIdentifierReference(body, "caller"); @@ -1092,14 +1175,30 @@ interface CallbackTypeContext { transformedTypeStarts: Set; transformedPrincipalTypeStarts: Set; tailorUserTypeLocalNames: Set; + sdkNamespaceNames: Set; root: SgNode; } +function nullableTailorUserTypeReplacement( + typeId: SgNode, + typeContext: CallbackTypeContext, +): string | null { + if (typeContext.tailorUserTypeLocalNames.has(typeId.text())) { + return typeId.text() === "TailorUser" ? "TailorPrincipal | null" : `${typeId.text()} | null`; + } + return isSdkNamespaceQualifiedTypeIdentifier( + typeId, + typeContext.sdkNamespaceNames, + typeContext.root, + ) + ? "TailorPrincipal | null" + : null; +} + function transformObjectTypeUserProperty( objectType: SgNode, edits: Edit[], - transformedPrincipalTypeStarts: Set, - tailorUserTypeLocalNames: Set, + typeContext: CallbackTypeContext, ): void { for (const child of objectType.children()) { if (child.kind() !== "property_signature") continue; @@ -1111,10 +1210,9 @@ function transformObjectTypeUserProperty( if (!typeAnnotation || /\bnull\b/.test(typeAnnotation.text())) continue; const typeIds = typeAnnotation.findAll({ rule: { kind: "type_identifier" } }); for (const typeId of typeIds) { - if (!tailorUserTypeLocalNames.has(typeId.text())) continue; - transformedPrincipalTypeStarts.add(typeId.range().start.index); - const replacement = - typeId.text() === "TailorUser" ? "TailorPrincipal | null" : `${typeId.text()} | null`; + const replacement = nullableTailorUserTypeReplacement(typeId, typeContext); + if (!replacement) continue; + typeContext.transformedPrincipalTypeStarts.add(typeId.range().start.index); edits.push(typeId.replace(replacement)); } } @@ -1198,12 +1296,7 @@ function transformNamedPrincipalCallbackType( const objectType = localCallbackTypeObject(binding.declaration); if (!objectType) return; context.transformedTypeStarts.add(start); - transformObjectTypeUserProperty( - objectType, - edits, - context.transformedPrincipalTypeStarts, - context.tailorUserTypeLocalNames, - ); + transformObjectTypeUserProperty(objectType, edits, context); } function transformPrincipalCallbackParamType( @@ -1215,12 +1308,7 @@ function transformPrincipalCallbackParamType( const objectType = typeAnnotation?.children().find((c: SgNode) => c.kind() === "object_type"); if (objectType) { if (typeContext) { - transformObjectTypeUserProperty( - objectType, - edits, - typeContext.transformedPrincipalTypeStarts, - typeContext.tailorUserTypeLocalNames, - ); + transformObjectTypeUserProperty(objectType, edits, typeContext); } return; } @@ -1336,6 +1424,7 @@ function transformPrincipalCallbackParam( } if (!hasUserParamProperty) return; + if (renamesBinding && hasPrincipalAssignmentTarget(body, "user")) return; if (renamesBinding && patternBindsName(pattern, "invoker")) return; transformPrincipalCallbackParamType(param, edits, typeContext); @@ -1939,7 +2028,11 @@ export default function transform(source: string): string | null { const actorTypeAliasLocalNames = new Set(); const actorTypeLocalNames = new Set(); const actorTypeValueLocalNames = new Set(); + const sdkNamespaceNames = new Set(); for (const importStmt of sdkImports) { + for (const namespaceName of iterateNamespaceImportLocalNames(importStmt)) { + sdkNamespaceNames.add(namespaceName); + } for (const { importedName, aliasNode, localName } of iterateImportSpecs(importStmt)) { if (TYPE_RENAME_MAP[importedName] && !aliasNode) { sdkRenameSourceNames.add(importedName); @@ -2015,6 +2108,9 @@ export default function transform(source: string): string | null { const createResolverLocalNames = new Set(); const createExecutorLocalNames = new Set(); const sdkFieldRootNames = new Set(); + for (const namespaceName of sdkNamespaceNames) { + sdkFieldRootNames.add(namespaceName); + } for (const importStmt of sdkImports) { for (const { importedName, localName } of iterateImportSpecs(importStmt)) { if (importedName === "createResolver") { @@ -2049,6 +2145,32 @@ export default function transform(source: string): string | null { if (arrow) transformResolverBody(arrow, edits); } } + for (const namespaceName of sdkNamespaceNames) { + const shadowRanges = collectAllShadowRanges(tree, namespaceName); + const calls = tree.findAll({ + rule: { + kind: "call_expression", + has: { + field: "function", + kind: "member_expression", + has: { + field: "property", + kind: "property_identifier", + regex: "^createResolver$", + }, + }, + }, + }); + for (const call of calls) { + const callee = call.field("function"); + const object = callee?.field("object"); + if (!object || object.kind() !== "identifier" || object.text() !== namespaceName) continue; + const pos = object.range().start.index; + if (isInsideAnyRange(pos, shadowRanges)) continue; + const arrow = findResolverBodyArrow(call); + if (arrow) transformResolverBody(arrow, edits); + } + } for (const localName of createExecutorLocalNames) { const shadowRanges = collectAllShadowRanges(tree, localName); const calls = tree.findAll({ @@ -2071,6 +2193,33 @@ export default function transform(source: string): string | null { } } } + for (const namespaceName of sdkNamespaceNames) { + const shadowRanges = collectAllShadowRanges(tree, namespaceName); + const calls = tree.findAll({ + rule: { + kind: "call_expression", + has: { + field: "function", + kind: "member_expression", + has: { + field: "property", + kind: "property_identifier", + regex: "^createExecutor$", + }, + }, + }, + }); + for (const call of calls) { + const callee = call.field("function"); + const object = callee?.field("object"); + if (!object || object.kind() !== "identifier" || object.text() !== namespaceName) continue; + const pos = object.range().start.index; + if (isInsideAnyRange(pos, shadowRanges)) continue; + for (const fn of findExecutorBodyFunctions(call)) { + transformExecutorBodyActorAccesses(fn, edits, transformedActorPropertyStarts); + } + } + } transformActorTypeComparisonLiterals(tree, edits, transformedActorPropertyStarts); const sdkFieldLocalBindings = collectSdkFieldLocalBindings(tree, sdkFieldRootNames); @@ -2081,6 +2230,7 @@ export default function transform(source: string): string | null { transformedTypeStarts: new Set(), transformedPrincipalTypeStarts: new Set(), tailorUserTypeLocalNames, + sdkNamespaceNames, root: tree, }; const transformedCallbackStarts = new Set(); @@ -2112,11 +2262,13 @@ export default function transform(source: string): string | null { if (typeContext.transformedPrincipalTypeStarts.has(id.range().start.index)) continue; const newName = sdkRenameSourceNames.has(id.text()) ? renamedTypeIdentifierText(id.text()) - : nullableInvokerAliasLocalNames.has(id.text()) - ? renamedTypeIdentifierText("TailorInvoker") - : actorTypeAliasLocalNames.has(id.text()) - ? renamedTypeIdentifierText("TailorActorType") - : null; + : isSdkNamespaceQualifiedTypeIdentifier(id, sdkNamespaceNames, tree) + ? renamedQualifiedTypeIdentifierText(id.text()) + : nullableInvokerAliasLocalNames.has(id.text()) + ? renamedTypeIdentifierText("TailorInvoker") + : actorTypeAliasLocalNames.has(id.text()) + ? renamedTypeIdentifierText("TailorActorType") + : null; if (!newName) continue; edits.push(id.replace(newName)); } diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/assignment-target/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/assignment-target/input.ts new file mode 100644 index 000000000..f246ece12 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/assignment-target/input.ts @@ -0,0 +1,11 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ user }) => { + user.attributes.role = "ADMIN"; + return user.id; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/expected.ts new file mode 100644 index 000000000..68ad8eb27 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/expected.ts @@ -0,0 +1,19 @@ +import * as sdk from "@tailor-platform/sdk"; + +type User = sdk.TailorPrincipal; + +const role = sdk.db.string().hooks({ + create: ({ invoker }: { invoker: sdk.TailorPrincipal | null }) => invoker?.id ?? "anonymous", +}); + +export default sdk.createResolver({ + name: "n", + operation: "query", + output: sdk.t.string(), + body: ({ caller }) => { + const parsed = sdk.t.string().parse({ value: "hello", data: {}, invoker: caller }); + return caller?.id ?? parsed.value; + }, +}); + +export const helper = (u: User) => role.parse({ value: u.id, data: {}, invoker: null }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/input.ts new file mode 100644 index 000000000..2511b6a68 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/input.ts @@ -0,0 +1,19 @@ +import * as sdk from "@tailor-platform/sdk"; + +type User = sdk.TailorUser; + +const role = sdk.db.string().hooks({ + create: ({ user }: { user: sdk.TailorUser | null }) => user?.id ?? "anonymous", +}); + +export default sdk.createResolver({ + name: "n", + operation: "query", + output: sdk.t.string(), + body: ({ user }) => { + const parsed = sdk.t.string().parse({ value: "hello", data: {}, user }); + return user.id ?? parsed.value; + }, +}); + +export const helper = (u: User) => role.parse({ value: u.id, data: {}, user: null }); From cad202622aca4bb75e7009144ea9dd2414a64235 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 19:36:44 +0900 Subject: [PATCH 117/618] fix(sdk-codemod): refine principal literal migrations --- .../v2/principal-unify/scripts/transform.ts | 191 ++++++++++++++---- .../tests/actor-member-access/expected.ts | 13 ++ .../tests/actor-member-access/input.ts | 13 ++ .../principal-unify/tests/basic/expected.ts | 1 + .../v2/principal-unify/tests/basic/input.ts | 1 + .../tests/tailordb-callbacks/expected.ts | 1 + .../tests/tailordb-callbacks/input.ts | 1 + .../tests/type-imports/expected.ts | 8 + .../tests/type-imports/input.ts | 8 + 9 files changed, 202 insertions(+), 35 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 3558f7745..1aeee16ae 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -145,14 +145,37 @@ function isAssignmentTargetReference(node: SgNode): boolean { return !!left && nodeRangeContains(left, current); } -function isParseArgumentShorthand(node: SgNode): boolean { - if (node.kind() !== "shorthand_property_identifier") return false; +function parseArgumentCall(node: SgNode): SgNode | null { + if (node.kind() !== "shorthand_property_identifier") return null; const object = node.parent(); - if (!object || object.kind() !== "object") return false; + if (!object || object.kind() !== "object") return null; const args = object.parent(); - if (!args || args.kind() !== "arguments") return false; + if (!args || args.kind() !== "arguments") return null; const call = args.parent(); - return !!call && call.kind() === "call_expression" && findMemberCallName(call) === "parse"; + return call && call.kind() === "call_expression" && findMemberCallName(call) === "parse" + ? call + : null; +} + +interface SdkFieldParseContext { + sdkFieldRootNames: Set; + sdkFieldLocalBindings: SdkFieldLocalBinding[]; + root: SgNode; +} + +function isSdkFieldParseArgumentShorthand( + node: SgNode, + parseContext: SdkFieldParseContext, +): boolean { + const call = parseArgumentCall(node); + return call + ? isSdkFieldMemberCall( + call, + parseContext.sdkFieldRootNames, + parseContext.sdkFieldLocalBindings, + parseContext.root, + ) + : false; } function addActorPropertyReplacement( @@ -178,6 +201,34 @@ function actorTypeLiteralReplacement(literal: SgNode): string | null { return `${quote}${ACTOR_TYPE_LITERAL_RENAME_MAP[value]!}${quote}`; } +function addActorTypeLiteralReplacement( + literal: SgNode, + edits: Edit[], + transformedLiteralStarts: Set, +): void { + if (literal.kind() !== "string") return; + const replacement = actorTypeLiteralReplacement(literal); + if (!replacement) return; + const start = literal.range().start.index; + if (transformedLiteralStarts.has(start)) return; + transformedLiteralStarts.add(start); + edits.push(literal.replace(replacement)); +} + +function transformActorTypeLiteralsInNode( + node: SgNode, + edits: Edit[], + transformedLiteralStarts: Set, +): void { + if (node.kind() === "string") { + addActorTypeLiteralReplacement(node, edits, transformedLiteralStarts); + } + const literals = node.findAll({ rule: { kind: "string" } }); + for (const literal of literals) { + addActorTypeLiteralReplacement(literal, edits, transformedLiteralStarts); + } +} + function isTransformedActorTypeMember( node: SgNode, transformedActorPropertyStarts: Set, @@ -190,6 +241,25 @@ function isTransformedActorTypeMember( ); } +function nodeContainsTransformedActorTypeMember( + node: SgNode, + transformedActorPropertyStarts: Set, +): boolean { + if (isTransformedActorTypeMember(node, transformedActorPropertyStarts)) return true; + const members = node.findAll({ rule: { kind: "member_expression" } }); + return members.some((member) => + isTransformedActorTypeMember(member, transformedActorPropertyStarts), + ); +} + +function switchDiscriminant(node: SgNode): SgNode | null { + return node.children().find((child) => child.kind() === "parenthesized_expression") ?? null; +} + +function switchBody(node: SgNode): SgNode | null { + return node.children().find((child) => child.kind() === "switch_body") ?? null; +} + function transformActorTypeComparisonLiterals( root: SgNode, edits: Edit[], @@ -207,13 +277,26 @@ function transformActorTypeComparisonLiterals( continue; } for (const child of binary.children()) { - if (child.kind() !== "string") continue; - const replacement = actorTypeLiteralReplacement(child); - if (!replacement) continue; - const start = child.range().start.index; - if (transformedLiteralStarts.has(start)) continue; - transformedLiteralStarts.add(start); - edits.push(child.replace(replacement)); + addActorTypeLiteralReplacement(child, edits, transformedLiteralStarts); + } + } + + const switches = root.findAll({ rule: { kind: "switch_statement" } }); + for (const switchNode of switches) { + const discriminant = switchDiscriminant(switchNode); + if ( + !discriminant || + !nodeContainsTransformedActorTypeMember(discriminant, transformedActorPropertyStarts) + ) { + continue; + } + const body = switchBody(switchNode); + if (!body) continue; + const cases = body.findAll({ rule: { kind: "switch_case" } }); + for (const caseNode of cases) { + for (const child of caseNode.children()) { + addActorTypeLiteralReplacement(child, edits, transformedLiteralStarts); + } } } } @@ -229,13 +312,8 @@ function transformTailorActorTypeInitializerLiterals( for (const decl of declarators) { if (!isTailorActorTypeReference(decl, actorTypeLocalNames)) continue; const value = decl.field("value"); - if (!value || value.kind() !== "string") continue; - const replacement = actorTypeLiteralReplacement(value); - if (!replacement) continue; - const start = value.range().start.index; - if (transformedLiteralStarts.has(start)) continue; - transformedLiteralStarts.add(start); - edits.push(value.replace(replacement)); + if (!value) continue; + transformActorTypeLiteralsInNode(value, edits, transformedLiteralStarts); } } @@ -257,13 +335,34 @@ function transformActorTypeBindingComparisons( continue; } for (const child of binary.children()) { - if (child.kind() !== "string") continue; - const replacement = actorTypeLiteralReplacement(child); - if (!replacement) continue; - const start = child.range().start.index; - if (transformedLiteralStarts.has(start)) continue; - transformedLiteralStarts.add(start); - edits.push(child.replace(replacement)); + addActorTypeLiteralReplacement(child, edits, transformedLiteralStarts); + } + } + + const switches = root.findAll({ rule: { kind: "switch_statement" } }); + for (const switchNode of switches) { + const discriminant = switchDiscriminant(switchNode); + if (!discriminant) continue; + const refs = discriminant.findAll({ + rule: { kind: "identifier", regex: `^${escapeRegex(binding.name)}$` }, + }); + const matchesBinding = refs.some( + (ref) => + !isShadowedLocalReference( + root, + binding.name, + ref.range().start.index, + binding.bindingStart, + ), + ); + if (!matchesBinding) continue; + const body = switchBody(switchNode); + if (!body) continue; + const cases = body.findAll({ rule: { kind: "switch_case" } }); + for (const caseNode of cases) { + for (const child of caseNode.children()) { + addActorTypeLiteralReplacement(child, edits, transformedLiteralStarts); + } } } } @@ -705,6 +804,7 @@ function rewriteParseArgumentShorthands( root: SgNode, localName: string, propertyName: string, + parseContext: SdkFieldParseContext, edits: Edit[], ): void { const shadowRanges = collectAllShadowRanges(root, localName); @@ -714,7 +814,7 @@ function rewriteParseArgumentShorthands( for (const ref of shortRefs) { const pos = ref.range().start.index; if (isInsideAnyRange(pos, shadowRanges)) continue; - if (!isParseArgumentShorthand(ref)) continue; + if (!isSdkFieldParseArgumentShorthand(ref, parseContext)) continue; edits.push(ref.replace(`${propertyName}: ${localName}`)); } } @@ -847,7 +947,11 @@ function hasCallerBindingConflict(pattern: SgNode, body: SgNode): boolean { return false; } -function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { +function transformResolverBody( + arrowNode: SgNode, + edits: Edit[], + parseContext: SdkFieldParseContext, +): void { const params = arrowNode.field("parameters") ?? arrowNode.field("parameter") ?? @@ -933,7 +1037,7 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { } } if (aliasedShorthandUser) { - rewriteParseArgumentShorthands(body, "user", "invoker", edits); + rewriteParseArgumentShorthands(body, "user", "invoker", parseContext, edits); } if (renamedShorthandUser) { // Use the broader shadow-range collector here so a nested arrow that @@ -959,7 +1063,13 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { for (const ref of shortRefs) { const pos = ref.range().start.index; if (isInsideAnyRange(pos, shadowRanges)) continue; - edits.push(ref.replace(isParseArgumentShorthand(ref) ? "invoker: caller" : "user: caller")); + edits.push( + ref.replace( + isSdkFieldParseArgumentShorthand(ref, parseContext) + ? "invoker: caller" + : "user: caller", + ), + ); } } for (const binding of principalAliasBindings) { @@ -1176,6 +1286,8 @@ interface CallbackTypeContext { transformedPrincipalTypeStarts: Set; tailorUserTypeLocalNames: Set; sdkNamespaceNames: Set; + sdkFieldRootNames: Set; + sdkFieldLocalBindings: SdkFieldLocalBinding[]; root: SgNode; } @@ -1476,7 +1588,7 @@ function transformPrincipalCallbackParam( if (!renamedShorthandUser) { if (aliasedShorthandUser) { - rewriteParseArgumentShorthands(body, "user", "invoker", edits); + rewriteParseArgumentShorthands(body, "user", "invoker", typeContext, edits); } for (const binding of principalAliasBindings) { guardPrincipalMemberAccesses(body, binding, edits); @@ -1498,7 +1610,9 @@ function transformPrincipalCallbackParam( for (const ref of shortRefs) { const pos = ref.range().start.index; if (isInsideAnyRange(pos, shadowRanges)) continue; - edits.push(ref.replace(isParseArgumentShorthand(ref) ? "invoker" : "user: invoker")); + edits.push( + ref.replace(isSdkFieldParseArgumentShorthand(ref, typeContext) ? "invoker" : "user: invoker"), + ); } for (const binding of principalAliasBindings) { @@ -2124,6 +2238,12 @@ export default function transform(source: string): string | null { } } } + const sdkFieldLocalBindings = collectSdkFieldLocalBindings(tree, sdkFieldRootNames); + const parseContext: SdkFieldParseContext = { + sdkFieldRootNames, + sdkFieldLocalBindings, + root: tree, + }; for (const localName of createResolverLocalNames) { const shadowRanges = collectAllShadowRanges(tree, localName); const calls = tree.findAll({ @@ -2142,7 +2262,7 @@ export default function transform(source: string): string | null { const pos = callee.range().start.index; if (isInsideAnyRange(pos, shadowRanges)) continue; const arrow = findResolverBodyArrow(call); - if (arrow) transformResolverBody(arrow, edits); + if (arrow) transformResolverBody(arrow, edits, parseContext); } } for (const namespaceName of sdkNamespaceNames) { @@ -2168,7 +2288,7 @@ export default function transform(source: string): string | null { const pos = object.range().start.index; if (isInsideAnyRange(pos, shadowRanges)) continue; const arrow = findResolverBodyArrow(call); - if (arrow) transformResolverBody(arrow, edits); + if (arrow) transformResolverBody(arrow, edits, parseContext); } } for (const localName of createExecutorLocalNames) { @@ -2222,7 +2342,6 @@ export default function transform(source: string): string | null { } transformActorTypeComparisonLiterals(tree, edits, transformedActorPropertyStarts); - const sdkFieldLocalBindings = collectSdkFieldLocalBindings(tree, sdkFieldRootNames); const callbackBindings = collectLocalCallbackBindings(tree); const objectBindings = collectLocalObjectBindings(tree); const typeContext: CallbackTypeContext = { @@ -2231,6 +2350,8 @@ export default function transform(source: string): string | null { transformedPrincipalTypeStarts: new Set(), tailorUserTypeLocalNames, sdkNamespaceNames, + sdkFieldRootNames, + sdkFieldLocalBindings, root: tree, }; const transformedCallbackStarts = new Set(); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/expected.ts index b072e0f21..5d549ee20 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/expected.ts @@ -4,9 +4,22 @@ export const onEvent = createExecutor({ operation: { kind: "function", async body(args) { + let label = "unknown"; + switch (args.actor?.type) { + case "user": + label = "user"; + break; + case "machine_user": + label = "machine"; + break; + case undefined: + label = "missing"; + break; + } return { id: args.actor?.id, type: args.actor?.type, + label, isUser: args.actor?.type === "user", isMachine: args.actor?.type === "machine_user", isUnspecified: args.actor?.type === undefined, diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/input.ts index 43a025169..94400e65b 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/actor-member-access/input.ts @@ -4,9 +4,22 @@ export const onEvent = createExecutor({ operation: { kind: "function", async body(args) { + let label = "unknown"; + switch (args.actor?.userType) { + case "USER_TYPE_USER": + label = "user"; + break; + case "USER_TYPE_MACHINE_USER": + label = "machine"; + break; + case "USER_TYPE_UNSPECIFIED": + label = "missing"; + break; + } return { id: args.actor?.userId, type: args.actor?.userType, + label, isUser: args.actor?.userType === "USER_TYPE_USER", isMachine: args.actor?.userType === "USER_TYPE_MACHINE_USER", isUnspecified: args.actor?.userType === "USER_TYPE_UNSPECIFIED", diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts index e71562690..e45ae39a2 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts @@ -7,6 +7,7 @@ export default createResolver({ output: t.object({ id: t.string() }), body: ({ input, caller }) => { const parsed = t.string().parse({ value: input.id, data: {}, invoker: caller }); + const parsedOther = { parse: (arg: unknown) => arg }.parse({ user: caller }); return { id: parsed.value ?? caller?.["id"] ?? caller?.id }; }, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts index 8f87232e8..adb7c5ccb 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts @@ -7,6 +7,7 @@ export default createResolver({ output: t.object({ id: t.string() }), body: ({ input, user }) => { const parsed = t.string().parse({ value: input.id, data: {}, user }); + const parsedOther = { parse: (arg: unknown) => arg }.parse({ user }); return { id: parsed.value ?? user["id"] ?? user.id }; }, }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index 4c6caa815..e8feb308e 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -70,6 +70,7 @@ const invoker = { id: "outer-invoker" }; const directHookedRole = db.string().hooks({ create: ({ invoker }) => { const parsed = t.string().parse({ value: "hello", data: {}, invoker }); + const parsedOther = { parse: (arg: unknown) => arg }.parse({ user: invoker }); const { id } = invoker ?? {}; return parsed.value ?? invoker?.["id"] ?? id; }, diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index aa92d1fdd..8411583a1 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -71,6 +71,7 @@ const invoker = { id: "outer-invoker" }; const directHookedRole = db.string().hooks({ create: ({ user }) => { const parsed = t.string().parse({ value: "hello", data: {}, user }); + const parsedOther = { parse: (arg: unknown) => arg }.parse({ user }); const { id } = user; return parsed.value ?? user["id"] ?? id; }, diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts index 74e3a85a3..ab3398b96 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts @@ -18,6 +18,14 @@ export function actorFields(actor: TailorPrincipal | null) { export const actorTypeValue: (TailorPrincipal["type"] | undefined) = "user"; export const actorTypeMissing: (TailorPrincipal["type"] | undefined) = undefined; +export const allowedActorTypes: (TailorPrincipal["type"] | undefined)[] = [ + "user", + "machine_user", +]; +export const actorTypeConfig: { primary: (TailorPrincipal["type"] | undefined); fallback?: (TailorPrincipal["type"] | undefined) } = { + primary: "user", + fallback: undefined, +}; export function isUserType(type: (TailorPrincipal["type"] | undefined)) { return type === "user"; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts index f78993b6b..e60423d98 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts @@ -18,6 +18,14 @@ export function actorFields(actor: TailorActor | null) { export const actorTypeValue: TailorActorType = "USER_TYPE_USER"; export const actorTypeMissing: TailorActorType = "USER_TYPE_UNSPECIFIED"; +export const allowedActorTypes: TailorActorType[] = [ + "USER_TYPE_USER", + "USER_TYPE_MACHINE_USER", +]; +export const actorTypeConfig: { primary: TailorActorType; fallback?: TailorActorType } = { + primary: "USER_TYPE_USER", + fallback: "USER_TYPE_UNSPECIFIED", +}; export function isUserType(type: TailorActorType) { return type === "USER_TYPE_USER"; From c5b10d2841ded08927285bce538c05220cde5e4c Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 20:40:52 +0900 Subject: [PATCH 118/618] feat(sdk): unify function principals --- .agents/rules/schema-types.md | 2 +- .changeset/tailor-principal-type.md | 8 + .claude/rules/schema-types.md | 2 +- example/adapters/echo.ts | 4 +- example/adapters/whoami.ts | 6 +- example/e2e/httpAdapter.test.ts | 2 +- example/e2e/resolver.test.ts | 20 +- example/migrations/0002/diff.json | 644 ++++++++++++++++++ example/resolvers/userInfo.ts | 14 +- example/tests/bundled_execution.test.ts | 6 +- .../src/resolver/getProduct.test.ts | 7 +- .../apps/admin/db/adminNote.ts | 2 +- .../create-sdk/templates/resolver/README.md | 4 +- .../resolver/src/resolver/add.test.ts | 7 +- .../src/resolver/incrementUserAge.test.ts | 7 +- .../resolver/src/resolver/showEnv.test.ts | 4 +- .../src/resolver/showUserInfo.test.ts | 21 +- .../resolver/src/resolver/showUserInfo.ts | 6 +- .../resolver/src/resolver/upsertUsers.test.ts | 4 +- .../src/resolver/resolveApproval.test.ts | 7 +- .../workflow/src/workflow/approval.test.ts | 10 +- .../src/workflow/order-fulfillment.test.ts | 28 +- packages/sdk/docs/services/auth.md | 20 +- packages/sdk/docs/services/resolver.md | 8 +- packages/sdk/docs/services/tailordb.md | 6 +- packages/sdk/docs/testing.md | 78 ++- packages/sdk/e2e/function-test-run.test.ts | 10 +- .../__test_fixtures__/resolvers/showInfo.ts | 8 +- .../src/cli/commands/function/bundle.test.ts | 2 +- .../sdk/src/cli/commands/function/bundle.ts | 48 +- .../sdk/src/cli/commands/function/detect.ts | 2 +- .../sdk/src/cli/services/resolver/bundler.ts | 2 +- .../tailordb/hooks-validate-bundler.ts | 6 +- .../src/cli/services/workflow/bundler.test.ts | 1 + .../sdk/src/cli/shared/runtime-exprs.test.ts | 137 +++- packages/sdk/src/cli/shared/runtime-exprs.ts | 56 +- packages/sdk/src/configure/index.ts | 4 +- .../services/executor/executor.test.ts | 6 +- .../configure/services/executor/operation.ts | 4 +- .../services/executor/trigger/event.ts | 4 +- .../services/resolver/resolver.test.ts | 31 +- .../configure/services/resolver/resolver.ts | 10 +- .../services/tailordb/schema.test.ts | 105 +-- .../src/configure/services/tailordb/schema.ts | 24 +- .../src/configure/services/tailordb/types.ts | 4 +- .../configure/services/workflow/job.test.ts | 176 ++++- .../src/configure/services/workflow/job.ts | 11 +- .../configure/services/workflow/registry.ts | 4 +- .../services/workflow/test-env-key.ts | 58 +- .../sdk/src/configure/types/field.types.ts | 8 +- packages/sdk/src/configure/types/type.test.ts | 78 +-- packages/sdk/src/configure/types/type.ts | 26 +- packages/sdk/src/configure/user.ts | 10 - .../sdk/src/parser/service/tailordb/field.ts | 99 ++- .../sdk/src/parser/service/tailordb/index.ts | 2 +- packages/sdk/src/plugin/with-context.ts | 6 +- packages/sdk/src/runtime/context.ts | 23 +- packages/sdk/src/runtime/types.ts | 74 +- packages/sdk/src/utils/test/index.test.ts | 10 +- packages/sdk/src/utils/test/index.ts | 15 +- packages/sdk/src/utils/test/mock.ts | 6 +- 61 files changed, 1534 insertions(+), 463 deletions(-) create mode 100644 .changeset/tailor-principal-type.md create mode 100644 example/migrations/0002/diff.json delete mode 100644 packages/sdk/src/configure/user.ts diff --git a/.agents/rules/schema-types.md b/.agents/rules/schema-types.md index 7b70dba19..588d2a841 100644 --- a/.agents/rules/schema-types.md +++ b/.agents/rules/schema-types.md @@ -27,7 +27,7 @@ modules** — files named `types.ts` (or `*.types.ts` under `configure/`): | `parser/service/tailordb/types.ts` | Parsed data structures (`TailorDBType`, `ParsedField`, permissions, ...) | | `parser/service/idp/types.ts` | Normalized IdP permission types shared by parser and CLI | | `plugin/types.ts` | Plugin authoring types, generation hook contexts, generator config | -| `runtime/types.ts` | Runtime principal/env types (`TailorUser`, `TailorActor`, `TailorEnv`) | +| `runtime/types.ts` | Runtime principal/env types (`TailorPrincipal`, `TailorEnv`) | Rules for pure type modules (enforced by oxlint): diff --git a/.changeset/tailor-principal-type.md b/.changeset/tailor-principal-type.md new file mode 100644 index 000000000..e2946b79c --- /dev/null +++ b/.changeset/tailor-principal-type.md @@ -0,0 +1,8 @@ +--- +"@tailor-platform/sdk": major +"@tailor-platform/sdk-codemod": patch +--- + +Unify function principal context around `TailorPrincipal`. + +Resolver contexts now use `caller` and `invoker` as `TailorPrincipal | null`, workflow and executor invokers also use `TailorPrincipal | null`, and event executor `actor` uses `TailorPrincipal | null` with `id`/`type` fields. The legacy `TailorUser`, `TailorInvoker`, `TailorActor`, `TailorActorType`, and `unauthenticatedTailorUser` exports are removed. diff --git a/.claude/rules/schema-types.md b/.claude/rules/schema-types.md index 7e4b505d9..2ae7685de 100644 --- a/.claude/rules/schema-types.md +++ b/.claude/rules/schema-types.md @@ -39,7 +39,7 @@ modules** — files named `types.ts` (or `*.types.ts` under `configure/`): | `parser/service/tailordb/types.ts` | Parsed data structures (`TailorDBType`, `ParsedField`, permissions, ...) | | `parser/service/idp/types.ts` | Normalized IdP permission types shared by parser and CLI | | `plugin/types.ts` | Plugin authoring types, generation hook contexts, generator config | -| `runtime/types.ts` | Runtime principal/env types (`TailorUser`, `TailorActor`, `TailorEnv`) | +| `runtime/types.ts` | Runtime principal/env types (`TailorPrincipal`, `TailorEnv`) | Rules for pure type modules (enforced by oxlint): diff --git a/example/adapters/echo.ts b/example/adapters/echo.ts index c2dd2d97a..8c1548ad6 100644 --- a/example/adapters/echo.ts +++ b/example/adapters/echo.ts @@ -8,10 +8,10 @@ export default createHttpAdapter({ pathPattern: "/echo", input: { get: () => ({ - query: `query { getResult: showUserInfo { user { role } } }`, + query: `query { getResult: showUserInfo { caller { role } } }`, }), post: () => ({ - query: `query { postResult: showUserInfo { user { role } } }`, + query: `query { postResult: showUserInfo { caller { role } } }`, }), }, output: (resp) => { diff --git a/example/adapters/whoami.ts b/example/adapters/whoami.ts index 4c6d22ece..b57e794a3 100644 --- a/example/adapters/whoami.ts +++ b/example/adapters/whoami.ts @@ -35,7 +35,7 @@ export default createHttpAdapter({ get: () => ({ query: `query Whoami { showUserInfo { - user { + caller { id type role @@ -53,7 +53,7 @@ export default createHttpAdapter({ const data = resp.data as | { showUserInfo?: { - user?: Record; + caller?: Record; invoker?: Record; }; } @@ -63,7 +63,7 @@ export default createHttpAdapter({ const xml = `\n` + `` + - actorXml("user", info?.user) + + actorXml("caller", info?.caller) + actorXml("invoker", info?.invoker) + ``; return { diff --git a/example/e2e/httpAdapter.test.ts b/example/e2e/httpAdapter.test.ts index 4271ff223..05f3890b0 100644 --- a/example/e2e/httpAdapter.test.ts +++ b/example/e2e/httpAdapter.test.ts @@ -21,7 +21,7 @@ describe("HTTP adapter routing", () => { expect(res.headers.get("content-type") ?? "").toContain("application/xml"); const body = await res.text(); expect(body).toContain(""); - expect(body).toMatch(/[\s\S]*<\/user>/); + expect(body).toMatch(/[\s\S]*<\/caller>/); }); test("POST /api/whoami fails with 404 because the adapter only declares GET", async () => { diff --git a/example/e2e/resolver.test.ts b/example/e2e/resolver.test.ts index a16448dcb..1bde34561 100644 --- a/example/e2e/resolver.test.ts +++ b/example/e2e/resolver.test.ts @@ -261,7 +261,7 @@ describe("dataplane", () => { const query = gql` query { showUserInfo { - user { + caller { id type workspaceId @@ -280,7 +280,7 @@ describe("dataplane", () => { expect(result.errors).toBeUndefined(); expect(result.data).toEqual({ showUserInfo: { - user: { + caller: { id: expect.any(String), type: "machine_user", workspaceId: expect.any(String), @@ -310,13 +310,15 @@ describe("dataplane", () => { const responseFields = userInfo?.response?.type?.fields ?? []; - const userField = responseFields.find((f) => f.name === "user"); - expect(userField?.description).toBe("Authenticated user"); - const userSubFields = userField?.type?.fields ?? []; - expect(userSubFields.find((f) => f.name === "id")?.description).toBe("User ID"); - expect(userSubFields.find((f) => f.name === "type")?.description).toBe("User type"); - expect(userSubFields.find((f) => f.name === "workspaceId")?.description).toBe("Workspace ID"); - expect(userSubFields.find((f) => f.name === "role")?.description).toBe("User role"); + const callerField = responseFields.find((f) => f.name === "caller"); + expect(callerField?.description).toBe("Authenticated caller"); + const callerSubFields = callerField?.type?.fields ?? []; + expect(callerSubFields.find((f) => f.name === "id")?.description).toBe("User ID"); + expect(callerSubFields.find((f) => f.name === "type")?.description).toBe("User type"); + expect(callerSubFields.find((f) => f.name === "workspaceId")?.description).toBe( + "Workspace ID", + ); + expect(callerSubFields.find((f) => f.name === "role")?.description).toBe("User role"); const invokerField = responseFields.find((f) => f.name === "invoker"); expect(invokerField?.description).toBe("Function invoker"); diff --git a/example/migrations/0002/diff.json b/example/migrations/0002/diff.json new file mode 100644 index 000000000..e3abafca4 --- /dev/null +++ b/example/migrations/0002/diff.json @@ -0,0 +1,644 @@ +{ + "version": 1, + "namespace": "tailordb", + "createdAt": "2026-06-17T04:37:34.736Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "name", + "before": { + "type": "string", + "required": true, + "validate": [ + { + "script": { + "expr": "(({value})=>value.length>5)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "Name must be longer than 5 characters" + } + ] + }, + "after": { + "type": "string", + "required": true, + "validate": [ + { + "script": { + "expr": "(({value})=>value.length>5)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "Name must be longer than 5 characters" + } + ] + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "city", + "before": { + "type": "string", + "required": false, + "validate": [ + { + "script": { + "expr": "(({value})=>value?value.length>1:true)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "failed by `({value})=>value?value.length>1:true`" + }, + { + "script": { + "expr": "(({value})=>value?value.length<100:true)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "failed by `({value})=>value?value.length<100:true`" + } + ] + }, + "after": { + "type": "string", + "required": false, + "validate": [ + { + "script": { + "expr": "(({value})=>value?value.length>1:true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({value})=>value?value.length>1:true`" + }, + { + "script": { + "expr": "(({value})=>value?value.length<100:true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({value})=>value?value.length<100:true`" + } + ] + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "fullAddress", + "before": { + "type": "string", + "required": true, + "hooks": { + "create": { + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "update": { + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "string", + "required": true, + "hooks": { + "create": { + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "attachedFiles", + "before": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "size": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({value})=>value>0)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "failed by `({value})=>value>0`" + } + ] + }, + "type": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "text" + }, + { + "value": "image" + } + ] + } + } + }, + "after": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "size": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({value})=>value>0)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({value})=>value>0`" + } + ] + }, + "type": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "text" + }, + { + "value": "image" + } + ] + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": false +} diff --git a/example/resolvers/userInfo.ts b/example/resolvers/userInfo.ts index 4d4943dc6..0678924be 100644 --- a/example/resolvers/userInfo.ts +++ b/example/resolvers/userInfo.ts @@ -6,11 +6,11 @@ export default createResolver({ operation: "query", body: (context) => { return { - user: { - id: context.user.id, - type: context.user.type, - workspaceId: context.user.workspaceId, - role: context.user.attributes?.role ?? "MANAGER", + caller: { + id: context.caller?.id ?? "", + type: context.caller?.type ?? "", + workspaceId: context.caller?.workspaceId ?? "", + role: context.caller?.attributes.role ?? "MANAGER", }, invoker: { id: context.invoker!.id, @@ -22,14 +22,14 @@ export default createResolver({ }, output: t .object({ - user: t + caller: t .object({ id: t.string().description("User ID"), type: t.string().description("User type"), workspaceId: t.string().description("Workspace ID"), role: t.enum(["MANAGER", "STAFF"]).description("User role"), }) - .description("Authenticated user"), + .description("Authenticated caller"), invoker: t .object({ id: t.string().description("Invoker ID"), diff --git a/example/tests/bundled_execution.test.ts b/example/tests/bundled_execution.test.ts index 160e7fd0f..3d214bf25 100644 --- a/example/tests/bundled_execution.test.ts +++ b/example/tests/bundled_execution.test.ts @@ -77,7 +77,7 @@ describe("bundled execution tests", () => { expect(result).toEqual(10); }); - test("resolvers/showUserInfo.js returns user and invoker information", async () => { + test("resolvers/showUserInfo.js returns caller and invoker information", async () => { using _invokerSpy = vi.spyOn(globalThis.tailor.context, "getInvoker").mockReturnValue({ id: "f1e2d3c4-b5a6-4798-89a0-1b2c3d4e5f60", type: "machine_user", @@ -88,7 +88,7 @@ describe("bundled execution tests", () => { const main = await importActualMain("resolvers/showUserInfo.js"); const payload = { - user: { + caller: { id: "57485cfe-fc74-4d46-8660-f0e95d1fbf98", type: "user", workspaceId: "b39bdd61-d442-4a4e-8599-33a78a4e19ab", @@ -97,7 +97,7 @@ describe("bundled execution tests", () => { }; const result = await main(payload); expect(result).toEqual({ - user: { + caller: { id: "57485cfe-fc74-4d46-8660-f0e95d1fbf98", type: "user", workspaceId: "b39bdd61-d442-4a4e-8599-33a78a4e19ab", diff --git a/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts b/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts index c569ebcd0..598ee996f 100644 --- a/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts +++ b/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts @@ -1,4 +1,3 @@ -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { mockTailordb } from "@tailor-platform/sdk/vitest"; import { describe, expect, test } from "vitest"; import resolver from "./getProduct"; @@ -22,7 +21,8 @@ describe("getProduct resolver", () => { const result = await resolver.body({ input: { productId: "product-1" }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: {}, }); @@ -51,7 +51,8 @@ describe("getProduct resolver", () => { const result = await resolver.body({ input: { productId: "product-2" }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: {}, }); diff --git a/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts b/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts index b3f5997c3..0a39ee828 100644 --- a/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts +++ b/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts @@ -8,7 +8,7 @@ export const adminNote = db .type("AdminNote", { title: db.string(), content: db.string(), - authorId: db.uuid().hooks({ create: ({ user }) => user.id }), + authorId: db.uuid().hooks({ create: ({ invoker }) => invoker?.id ?? crypto.randomUUID() }), ...db.fields.timestamps(), }) // NOTE: This permits all operations for simplicity. diff --git a/packages/create-sdk/templates/resolver/README.md b/packages/create-sdk/templates/resolver/README.md index f0c531a61..f5c771dce 100644 --- a/packages/create-sdk/templates/resolver/README.md +++ b/packages/create-sdk/templates/resolver/README.md @@ -8,11 +8,11 @@ Demonstrates all resolver patterns with comprehensive testing approaches. - Database query resolver (Kysely with transactions) - Database mutation resolver (dependency injection pattern) - Environment variable access -- User context access +- Caller and invoker context access ## Testing Approaches -1. **Direct `body()` call** - Simple resolvers with `unauthenticatedTailorUser` +1. **Direct `body()` call** - Simple resolvers with explicit `caller` / `invoker` context values 2. **`tailor-runtime` environment + `mockTailordb`** - Database resolvers via `mockTailordb` from `@tailor-platform/sdk/vitest` (no `vi.stubGlobal` needed) 3. **Dependency injection** - Extract `DbOperations` interface for testability diff --git a/packages/create-sdk/templates/resolver/src/resolver/add.test.ts b/packages/create-sdk/templates/resolver/src/resolver/add.test.ts index 23ee62307..373e38462 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/add.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/add.test.ts @@ -1,4 +1,3 @@ -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { describe, expect, test } from "vitest"; import resolver from "./add"; @@ -6,7 +5,8 @@ describe("add resolver", () => { test("adds two positive numbers", async () => { const result = await resolver.body({ input: { left: 1, right: 2 }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); expect(result).toBe(3); @@ -15,7 +15,8 @@ describe("add resolver", () => { test("handles negative numbers", async () => { const result = await resolver.body({ input: { left: -5, right: 3 }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); expect(result).toBe(-2); diff --git a/packages/create-sdk/templates/resolver/src/resolver/incrementUserAge.test.ts b/packages/create-sdk/templates/resolver/src/resolver/incrementUserAge.test.ts index 3649f21fe..574aee83f 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/incrementUserAge.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/incrementUserAge.test.ts @@ -1,4 +1,3 @@ -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { mockTailordb } from "@tailor-platform/sdk/vitest"; import { describe, expect, test } from "vitest"; import resolver from "./incrementUserAge"; @@ -15,7 +14,8 @@ describe("incrementUserAge resolver", () => { const result = await resolver.body({ input: { email: "test@example.com" }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); expect(result).toEqual({ oldAge: 30, newAge: 31 }); @@ -32,7 +32,8 @@ describe("incrementUserAge resolver", () => { const result = resolver.body({ input: { email: "test@example.com" }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); await expect(result).rejects.toThrowError(/no result/i); diff --git a/packages/create-sdk/templates/resolver/src/resolver/showEnv.test.ts b/packages/create-sdk/templates/resolver/src/resolver/showEnv.test.ts index 1973897a9..19316895b 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/showEnv.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/showEnv.test.ts @@ -1,4 +1,3 @@ -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { describe, expect, test } from "vitest"; import resolver from "./showEnv"; @@ -6,7 +5,8 @@ describe("showEnv resolver", () => { test("returns environment variables", async () => { const result = await resolver.body({ input: undefined as never, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); expect(result).toEqual({ appName: "Resolver Template", version: 1 }); diff --git a/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts b/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts index 2a2f9739a..991ae9e4f 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts @@ -1,4 +1,4 @@ -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; +import type { TailorPrincipal } from "@tailor-platform/sdk"; import { describe, expect, test } from "vitest"; import resolver from "./showUserInfo"; @@ -6,26 +6,29 @@ describe("showUserInfo resolver", () => { test("returns default user info", async () => { const result = await resolver.body({ input: undefined as never, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); expect(result).toEqual({ - userId: unauthenticatedTailorUser.id, - userType: unauthenticatedTailorUser.type, - workspaceId: unauthenticatedTailorUser.workspaceId, + userId: "anonymous", + userType: "anonymous", + workspaceId: "", }); }); test("returns custom user info", async () => { - const customUser = { - ...unauthenticatedTailorUser, + const customCaller = { id: "user-123", type: "machine_user" as const, workspaceId: "ws-456", - }; + attributes: { role: "admin" }, + attributeList: [], + } satisfies TailorPrincipal; const result = await resolver.body({ input: undefined as never, - user: customUser, + caller: customCaller, + invoker: customCaller, env: { appName: "Resolver Template", version: 1 }, }); expect(result).toEqual({ diff --git a/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.ts b/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.ts index b848df640..6e33d0f8a 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.ts @@ -6,9 +6,9 @@ const resolver = createResolver({ operation: "query", body: (context) => { return { - userId: context.user.id, - userType: context.user.type, - workspaceId: context.user.workspaceId, + userId: context.caller?.id ?? "anonymous", + userType: context.caller?.type ?? "anonymous", + workspaceId: context.caller?.workspaceId ?? "", }; }, output: t.object({ diff --git a/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts b/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts index b953f1a33..a09b1fbd8 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts @@ -1,4 +1,3 @@ -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { createKyselyMock } from "@tailor-platform/sdk/vitest"; import { describe, expect, test, vi } from "vitest"; import { getDB, type Namespace } from "../generated/db"; @@ -30,7 +29,8 @@ describe("upsertUsers resolver", () => { { name: "Existing", email: "exists@example.com", age: 41 }, ], }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); diff --git a/packages/create-sdk/templates/workflow/src/resolver/resolveApproval.test.ts b/packages/create-sdk/templates/workflow/src/resolver/resolveApproval.test.ts index 39c9d8281..3b95af598 100644 --- a/packages/create-sdk/templates/workflow/src/resolver/resolveApproval.test.ts +++ b/packages/create-sdk/templates/workflow/src/resolver/resolveApproval.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from "vitest"; import { mockWorkflow } from "@tailor-platform/sdk/vitest"; -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import resolver from "./resolveApproval"; describe("resolveApproval resolver", () => { @@ -16,7 +15,8 @@ describe("resolveApproval resolver", () => { const result = await resolver.body({ input: { executionId: "exec-1", approved: true }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: {}, }); @@ -33,7 +33,8 @@ describe("resolveApproval resolver", () => { const result = await resolver.body({ input: { executionId: "exec-2", approved: false }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: {}, }); diff --git a/packages/create-sdk/templates/workflow/src/workflow/approval.test.ts b/packages/create-sdk/templates/workflow/src/workflow/approval.test.ts index a2a33c0cf..ace962bba 100644 --- a/packages/create-sdk/templates/workflow/src/workflow/approval.test.ts +++ b/packages/create-sdk/templates/workflow/src/workflow/approval.test.ts @@ -7,7 +7,10 @@ describe("approval workflow", () => { using wf = mockWorkflow(); wf.setWaitHandler((_key, _payload) => ({ approved: true })); - const result = await processWithApproval.body({ orderId: "order-1" }, { env: {} }); + const result = await processWithApproval.body( + { orderId: "order-1" }, + { env: {}, invoker: null }, + ); expect(result).toEqual({ orderId: "order-1", status: "approved" }); expect(wf.waitCalls).toEqual([ @@ -22,7 +25,10 @@ describe("approval workflow", () => { using wf = mockWorkflow(); wf.setWaitHandler({ approved: false }); - const result = await processWithApproval.body({ orderId: "order-2" }, { env: {} }); + const result = await processWithApproval.body( + { orderId: "order-2" }, + { env: {}, invoker: null }, + ); expect(result).toEqual({ orderId: "order-2", status: "rejected" }); }); diff --git a/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts b/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts index 75f2e3a6f..6389972f9 100644 --- a/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts +++ b/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts @@ -10,18 +10,24 @@ import workflow, { describe("order fulfillment workflow", () => { describe("individual job tests with .body()", () => { test("validateOrder accepts valid order", () => { - const result = validateOrder.body({ orderId: "order-1", amount: 100 }, { env: {} }); + const result = validateOrder.body( + { orderId: "order-1", amount: 100 }, + { env: {}, invoker: null }, + ); expect(result).toEqual({ valid: true, orderId: "order-1" }); }); test("validateOrder rejects zero amount", () => { - expect(() => validateOrder.body({ orderId: "order-1", amount: 0 }, { env: {} })).toThrow( - "Order amount must be positive", - ); + expect(() => + validateOrder.body({ orderId: "order-1", amount: 0 }, { env: {}, invoker: null }), + ).toThrow("Order amount must be positive"); }); test("processPayment returns transaction", () => { - const result = processPayment.body({ orderId: "order-1", amount: 100 }, { env: {} }); + const result = processPayment.body( + { orderId: "order-1", amount: 100 }, + { env: {}, invoker: null }, + ); expect(result).toEqual({ transactionId: "txn-order-1", amount: 100, @@ -32,7 +38,7 @@ describe("order fulfillment workflow", () => { test("sendConfirmation returns confirmation", () => { const result = sendConfirmation.body( { orderId: "order-1", transactionId: "txn-1" }, - { env: {} }, + { env: {}, invoker: null }, ); expect(result).toEqual({ orderId: "order-1", @@ -59,7 +65,10 @@ describe("order fulfillment workflow", () => { confirmed: true, }); - const result = await fulfillOrder.body({ orderId: "order-1", amount: 100 }, { env: {} }); + const result = await fulfillOrder.body( + { orderId: "order-1", amount: 100 }, + { env: {}, invoker: null }, + ); expect(validateOrder.trigger).toHaveBeenCalledWith({ orderId: "order-1", @@ -97,7 +106,10 @@ describe("order fulfillment workflow", () => { confirmed: true, }); - const result = await workflow.mainJob.body({ orderId: "order-2", amount: 200 }, { env: {} }); + const result = await workflow.mainJob.body( + { orderId: "order-2", amount: 200 }, + { env: {}, invoker: null }, + ); expect(result).toEqual({ orderId: "order-2", diff --git a/packages/sdk/docs/services/auth.md b/packages/sdk/docs/services/auth.md index 670192ebe..3e4fe1393 100644 --- a/packages/sdk/docs/services/auth.md +++ b/packages/sdk/docs/services/auth.md @@ -139,18 +139,18 @@ export const user = db.type("User", { }); ``` -The `attributeList` values are accessible via `user.attributeList` as a tuple: +The `attributeList` values are accessible via the runtime principal's `attributeList` as a tuple: ```typescript // In a resolver body: (context) => { - const [organizationId, teamId] = context.user.attributeList; + const [organizationId, teamId] = context.caller?.attributeList ?? []; }, // In TailorDB hooks .hooks({ field: { - create: ({ user }) => user.attributeList[0], // First UUID from list + create: ({ invoker }) => invoker?.attributeList[0] ?? null, // First UUID from list }, }) ``` @@ -160,7 +160,7 @@ body: (context) => { When you want to use machine users without defining a `userProfile`, define `machineUserAttributes` instead. These attributes are used for: - type-safe `machineUsers[*].attributes` -- `context.user.attributes` typing (via `tailor.d.ts`) +- runtime principal `attributes` typing (via `tailor.d.ts`) ```typescript import { defineAuth, t } from "@tailor-platform/sdk"; @@ -200,12 +200,12 @@ machineUsers: { }, ``` -**attributes**: Values for attributes enabled in `userProfile.attributes` (or all fields defined in `machineUserAttributes` when `userProfile` is omitted). All enabled fields must be set here. These values are accessible via `user.attributes`: +**attributes**: Values for attributes enabled in `userProfile.attributes` (or all fields defined in `machineUserAttributes` when `userProfile` is omitted). All enabled fields must be set here. These values are accessible via the runtime principal's `attributes`: ```typescript // In a resolver body: (context) => { - const role = context.user.attributes?.role; + const role = context.caller?.attributes.role; }, ``` @@ -230,25 +230,25 @@ machineUsers: { }, ``` -These values are accessible via `user.attributeList`: +These values are accessible via the runtime principal's `attributeList`: ```typescript // In a resolver body: (context) => { - const [organizationId, teamId] = context.user.attributeList; + const [organizationId, teamId] = context.caller?.attributeList ?? []; }, // In TailorDB hooks .hooks({ field: { - create: ({ user }) => user.attributes?.role === "ADMIN" ? "default" : null, + create: ({ invoker }) => invoker?.attributes.role === "ADMIN" ? "default" : null, }, }) // In TailorDB validate .validate({ field: [ - ({ user }) => user.attributes?.role === "ADMIN", + ({ invoker }) => invoker?.attributes.role === "ADMIN", "Only admins can set this field", ], }) diff --git a/packages/sdk/docs/services/resolver.md b/packages/sdk/docs/services/resolver.md index 153fae496..be7326f4b 100644 --- a/packages/sdk/docs/services/resolver.md +++ b/packages/sdk/docs/services/resolver.md @@ -208,7 +208,7 @@ Validation functions receive: - `value` - The field value being validated - `data` - The entire input object -- `user` - The user performing the operation +- `invoker` - The principal performing the operation You can specify validation as: @@ -234,8 +234,8 @@ Validation runs automatically before the `body` function executes. When validati Define actual resolver logic in the `body` function. Function arguments include: - `input` - Input data from GraphQL request -- `user` - The user who called this resolver; unaffected by `authInvoker` -- `invoker` - The principal running this function; equals `user` by default, or the machine user set by `authInvoker`. `null` for anonymous calls. +- `caller` - The user or machine user who called this resolver; unaffected by `authInvoker`. `null` for anonymous calls. +- `invoker` - The principal running this function; equals `caller` by default, or the machine user set by `authInvoker`. `null` for anonymous calls. - `env` - Environment variables declared in `tailor.config.ts` ### Using Kysely for Database Access @@ -371,4 +371,4 @@ export default createResolver({ The machine user name is looked up in the auth service configured on your app (`machineUsers` in `defineAuth`). The namespace is resolved automatically — no need to import `auth` from `tailor.config.ts` in resolver files. -**Note:** `authInvoker` controls the permissions for database operations and other platform actions. The `user` object passed to `body` still reflects the original caller, while `invoker` reflects the principal actually running the body. +**Note:** `authInvoker` controls the permissions for database operations and other platform actions. The `caller` object passed to `body` still reflects the original caller, while `invoker` reflects the principal actually running the body. diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index e9aad4ac0..39442b7bc 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -260,7 +260,7 @@ Add hooks to execute functions during data creation or update. Hooks receive thr - `value`: User input if provided, otherwise existing value on update or null on create - `data`: Entire record data (for accessing other field values) -- `user`: User performing the operation +- `invoker`: Principal performing the operation #### Field-level Hooks @@ -268,7 +268,7 @@ Set hooks directly on individual fields: ```typescript db.string().hooks({ - create: ({ user }) => user.id, + create: ({ invoker }) => invoker?.id ?? "", update: ({ value }) => value, }); ``` @@ -323,7 +323,7 @@ Add validation rules to fields. Validators receive three arguments (executed aft - `value`: Field value after hook transformation - `data`: Entire record data after hook transformations (for accessing other field values) -- `user`: User performing the operation +- `invoker`: Principal performing the operation Validators return `true` for success, `false` for failure. Use array form `[validator, errorMessage]` for custom error messages. diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index 9fa9f2e05..21fc563a3 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -11,15 +11,15 @@ Lean on unit tests for the day-to-day feedback loop — they run fast and exerci Unit-test entrypoints exposed by the SDK: -- `resolver.body({ input, user, env })` — invoke a resolver -- `workflowJob.body(input, { env })` — invoke a workflow job body directly +- `resolver.body({ input, caller, invoker, env })` — invoke a resolver +- `workflowJob.body(input, { env, invoker })` — invoke a workflow job body directly - `workflowJob.trigger(input)` — chain a workflow job through the workflow runtime - `runWorkflowLocally(workflow, args)` — run a workflow chain locally with real job bodies -- `executor.operation.body(args)` — invoke a function-kind executor +- `executor.operation.body({ ...args, invoker })` — invoke a function-kind executor -Helpers under `@tailor-platform/sdk/test`: +For anonymous direct calls: -- `unauthenticatedTailorUser` — default `user` value for resolver contexts +- Pass `null` for anonymous `caller` / `invoker` context in direct unit tests. Platform API mocks under `@tailor-platform/sdk/vitest` (for use with the [`tailor-runtime` Vitest environment](#runtime-environment-emulation-beta) below): @@ -99,7 +99,12 @@ test("resolver queries the database", async () => { [], // COMMIT ); - const result = await resolver.body({ input: { email: "test@example.com" } }); + const result = await resolver.body({ + input: { email: "test@example.com" }, + caller: null, + invoker: null, + env: {}, + }); expect(result).toEqual({ oldAge: 30, newAge: 31 }); expect(db.executedQueries).toHaveLength(3); @@ -121,7 +126,12 @@ test("content-based mock", async () => { return []; }); - const result = await resolver.body({ input: { userId: "1" } }); + const result = await resolver.body({ + input: { userId: "1" }, + caller: null, + invoker: null, + env: {}, + }); expect(db.executedQueries[0].query).toContain("SELECT"); }); @@ -345,7 +355,6 @@ Unit tests call `.body()` (or `.trigger()`) directly on a resolver, workflow job For pure logic with no external dependencies, invoke `.body()` directly: ```typescript -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { describe, expect, test } from "vitest"; import resolver from "../src/resolver/add"; @@ -353,7 +362,8 @@ describe("add resolver", () => { test("adds two numbers", async () => { const result = await resolver.body({ input: { left: 1, right: 2 }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: {}, }); expect(result).toBe(3); @@ -370,7 +380,6 @@ Stub the global `tailordb.Client` and queue raw query results in order. Best for > If you are running with the [`tailor-runtime` Vitest environment](#runtime-environment-emulation-beta), acquire `using db = mockTailordb()` to install and drive the mock `tailordb.Client` instead of `vi.stubGlobal()`. ```typescript -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from "vitest"; import resolver from "../src/resolver/incrementUserAge"; @@ -400,7 +409,8 @@ describe("incrementUserAge resolver", () => { const result = await resolver.body({ input: { email: "test@example.com" }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: {}, }); @@ -471,7 +481,6 @@ describe("decrementUserAge", () => { Pass `mock.db` to functions that take a Kysely instance. When a resolver or executor calls `getDB()` internally there is no such seam, so spy the generated `getDB` and point it at the mock: ```typescript -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { createKyselyMock } from "@tailor-platform/sdk/vitest"; import { describe, expect, test, vi } from "vitest"; import { getDB, type Namespace } from "../generated/db"; @@ -503,7 +512,8 @@ describe("upsertUsers resolver", () => { { name: "Existing", email: "exists@example.com", age: 41 }, ], }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: { appName: "Resolver Template", version: 1 }, }); @@ -522,7 +532,6 @@ Reach for [`mockTailordb`](#mocking-the-tailordb-client) instead when you want t Resolvers that call `waitPoint.resolve(...)` delegate to `tailor.workflow.resolve` at runtime. With the `tailor-runtime` environment active, use `mockWorkflow().setResolveHandler` to drive the user-supplied callback and inspect `resolveCalls`: ```typescript -import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; import { mockWorkflow } from "@tailor-platform/sdk/vitest"; import { describe, expect, test } from "vitest"; import resolver from "./resolveApproval"; @@ -537,7 +546,8 @@ describe("resolveApproval resolver", () => { const result = await resolver.body({ input: { executionId: "exec-1", approved: true }, - user: unauthenticatedTailorUser, + caller: null, + invoker: null, env: {}, }); @@ -551,7 +561,7 @@ describe("resolveApproval resolver", () => { ### Testing Executors -Function-kind executors expose their handler as `executor.operation.body(args)`. The shape of `args` is determined by the trigger — for example, `recordCreatedTrigger({ type: user })` produces `{ newRecord }` typed against the type's output. GraphQL, webhook, and workflow operation kinds are declarative and don't expose a user-authored body to test. +Function-kind executors expose their handler as `executor.operation.body(args)`. The shape of `args` is determined by the trigger — for example, `recordCreatedTrigger({ type: user })` produces `{ newRecord }` typed against the type's output, plus runtime fields such as `env`, `actor`, and `invoker`. GraphQL, webhook, and workflow operation kinds are declarative and don't expose a user-authored body to test. The `executor` template extracts shared DB access into a helper (`shared.ts`) and tests the helper directly against a mocked `tailordb.Client` (same TailorDB-mocking pattern as the resolver section). Executor handlers themselves stay thin and can be tested by spying on the helper: @@ -568,6 +578,14 @@ describe("onUserCreated executor", () => { throw new Error("expected function operation"); } await onUserCreated.operation.body({ + workspaceId: "workspace-1", + appNamespace: "app", + env: {}, + actor: null, + invoker: null, + event: "created", + rawEvent: "tailordb.type_record.created", + typeName: "User", newRecord: { id: "user-1", name: "Alice", @@ -596,7 +614,7 @@ Workflow jobs expose the same `.body()` entrypoint as resolvers, plus `.trigger( #### Simple job -Call `.body()` with the input and a stub `{ env: {} }`: +Call `.body()` with the input and a stub `{ env: {}, invoker: null }`: ```typescript import { describe, expect, test } from "vitest"; @@ -604,14 +622,17 @@ import { validateOrder } from "./order-fulfillment"; describe("validateOrder", () => { test("accepts a valid order", () => { - const result = validateOrder.body({ orderId: "order-1", amount: 100 }, { env: {} }); + const result = validateOrder.body( + { orderId: "order-1", amount: 100 }, + { env: {}, invoker: null }, + ); expect(result).toEqual({ valid: true, orderId: "order-1" }); }); test("rejects a non-positive amount", () => { - expect(() => validateOrder.body({ orderId: "order-1", amount: 0 }, { env: {} })).toThrow( - "Order amount must be positive", - ); + expect(() => + validateOrder.body({ orderId: "order-1", amount: 0 }, { env: {}, invoker: null }), + ).toThrow("Order amount must be positive"); }); }); ``` @@ -641,7 +662,10 @@ describe("fulfillOrder", () => { confirmed: true, }); - const result = await fulfillOrder.body({ orderId: "order-1", amount: 100 }, { env: {} }); + const result = await fulfillOrder.body( + { orderId: "order-1", amount: 100 }, + { env: {}, invoker: null }, + ); expect(validateOrder.trigger).toHaveBeenCalledWith({ orderId: "order-1", amount: 100 }); expect(result).toMatchObject({ confirmed: true, paymentStatus: "completed" }); @@ -665,7 +689,10 @@ describe("processWithApproval", () => { using wf = mockWorkflow(); wf.setWaitHandler({ approved: true }); - const result = await processWithApproval.body({ orderId: "order-1" }, { env: {} }); + const result = await processWithApproval.body( + { orderId: "order-1" }, + { env: {}, invoker: null }, + ); expect(result).toEqual({ orderId: "order-1", status: "approved" }); expect(wf.waitCalls[0]).toEqual({ @@ -678,7 +705,10 @@ describe("processWithApproval", () => { using wf = mockWorkflow(); wf.setWaitHandler({ approved: false }); - const result = await processWithApproval.body({ orderId: "order-2" }, { env: {} }); + const result = await processWithApproval.body( + { orderId: "order-2" }, + { env: {}, invoker: null }, + ); expect(result.status).toBe("rejected"); }); diff --git a/packages/sdk/e2e/function-test-run.test.ts b/packages/sdk/e2e/function-test-run.test.ts index 399250368..4eb499cbf 100644 --- a/packages/sdk/e2e/function-test-run.test.ts +++ b/packages/sdk/e2e/function-test-run.test.ts @@ -193,11 +193,11 @@ describe.sequential("E2E: function test-run", () => { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; const nilUuid = "00000000-0000-0000-0000-000000000000"; - expect(parsed.user.type).toBe("machine_user"); - expect(parsed.user.workspaceId).toBe(workspaceId); - expect(parsed.user.role).toBe("MANAGER"); - expect(parsed.user.id).toMatch(uuidRegex); - expect(parsed.user.id).not.toBe(nilUuid); + expect(parsed.caller.type).toBe("machine_user"); + expect(parsed.caller.workspaceId).toBe(workspaceId); + expect(parsed.caller.role).toBe("MANAGER"); + expect(parsed.caller.id).toMatch(uuidRegex); + expect(parsed.caller.id).not.toBe(nilUuid); expect(parsed.invoker.type).toBe("machine_user"); expect(parsed.invoker.workspaceId).toBe(workspaceId); diff --git a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/showInfo.ts b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/showInfo.ts index efac35219..0407f7fe1 100644 --- a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/showInfo.ts +++ b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/showInfo.ts @@ -6,12 +6,12 @@ export default createResolver({ operation: "query", body: (context) => { return { - id: context.user.id, - type: context.user.type, - workspaceId: context.user.workspaceId, + id: context.caller?.id ?? "", + type: context.caller?.type ?? "", + workspaceId: context.caller?.workspaceId ?? "", // platform response may omit the field // oxlint-disable-next-line typescript/no-unnecessary-condition - role: (context.user.attributes?.role as string) ?? "ADMIN", + role: (context.caller?.attributes.role as string | undefined) ?? "ADMIN", }; }, output: t diff --git a/packages/sdk/src/cli/commands/function/bundle.test.ts b/packages/sdk/src/cli/commands/function/bundle.test.ts index 963cf02b1..84d9bae0d 100644 --- a/packages/sdk/src/cli/commands/function/bundle.test.ts +++ b/packages/sdk/src/cli/commands/function/bundle.test.ts @@ -279,7 +279,7 @@ export default { workspaceId: defaultWorkspaceId, }); - expect(result.bundledCode).toContain("USER_TYPE_MACHINE_USER"); + expect(result.bundledCode).toContain("machine_user"); expect(result.bundledCode).toContain("ADMIN"); expect(result.bundledCode).toContain(defaultMachineUser.id); expect(result.bundledCode).toContain(defaultWorkspaceId); diff --git a/packages/sdk/src/cli/commands/function/bundle.ts b/packages/sdk/src/cli/commands/function/bundle.ts index d1d5a96df..9efe3c37e 100644 --- a/packages/sdk/src/cli/commands/function/bundle.ts +++ b/packages/sdk/src/cli/commands/function/bundle.ts @@ -24,7 +24,7 @@ import ml from "@/utils/multiline"; import type { DetectedFunction } from "./detect"; import type { LogLevelInput } from "@/configure/config/types"; -/** Machine user info resolved from config and API for bundle-time user context. */ +/** Machine user info resolved from config and API for bundle-time principal context. */ export interface ResolvedMachineUser { /** Machine user name */ name: string; @@ -47,7 +47,7 @@ interface BundleForTestRunOptions { inlineSourcemap?: boolean; /** Log level config value from defineConfig */ logLevel?: LogLevelInput; - /** Machine user info for injecting user context into the bundle */ + /** Machine user info for injecting principal context into the bundle */ machineUser: ResolvedMachineUser; /** Workspace ID for user context */ workspaceId: string; @@ -152,23 +152,23 @@ function generateEntry( case "resolver": { // Mirrors the production resolver bundler (services/resolver/bundler.ts). - // In production, the operationHook injects user/env into context. + // In production, the operationHook injects caller/env into context. // For test-run, we embed machine user info since there's no operationHook. - const userExpr = buildMachineUserExpr(machineUser, workspaceId); + const principalExpr = buildMachinePrincipalExpr(machineUser, workspaceId); return ml /* js */ ` import _internalResolver from "${absoluteSourcePath}"; import { t } from "@tailor-platform/sdk"; const _env = ${JSON.stringify(env)}; - const _user = ${userExpr}; + const _caller = ${principalExpr}; const $tailor_resolver_body = async (context) => { - const _invoker = ${INVOKER_EXPR}; + const _invoker = ${INVOKER_EXPR} ?? _caller; if (_internalResolver.input) { const result = t.object(_internalResolver.input).parse({ value: context, data: context, - user: _user, + invoker: _invoker, }); if (result.issues) { @@ -179,7 +179,7 @@ function generateEntry( } } - const enrichedContext = { input: context, env: _env, user: _user, invoker: _invoker }; + const enrichedContext = { input: context, env: _env, caller: _caller, invoker: _invoker }; return _internalResolver.body(enrichedContext); }; @@ -191,15 +191,15 @@ function generateEntry( // Mirrors the production executor bundler (services/executor/bundler.ts). // In production, buildExecutorArgsExpr injects actor/env into args. // For test-run, we embed machine user as actor. - const actorExpr = buildMachineActorExpr(machineUser, workspaceId); + const principalExpr = buildMachinePrincipalExpr(machineUser, workspaceId); return ml /* js */ ` import _internalExecutor from "${absoluteSourcePath}"; const _env = ${JSON.stringify(env)}; - const _actor = ${actorExpr}; + const _actor = ${principalExpr}; const __executor_function = async (args) => { - const _invoker = ${INVOKER_EXPR}; + const _invoker = ${INVOKER_EXPR} ?? _actor; return _internalExecutor.operation.body({ ...args, env: _env, actor: _actor, invoker: _invoker }); }; @@ -212,13 +212,15 @@ function generateEntry( // Note: user context is not available in TestExecScript for workflow jobs. // The production workflow bundler's user mapping is being fixed in fix/workflow-user. const exportName = assertDefined(detected.exportName, "workflow job export name missing"); + const principalExpr = buildMachinePrincipalExpr(machineUser, workspaceId); return ml /* js */ ` import { ${exportName} } from "${absoluteSourcePath}"; const env = ${JSON.stringify(env)}; + const fallbackInvoker = ${principalExpr}; export async function main(input) { - const invoker = ${INVOKER_EXPR}; + const invoker = ${INVOKER_EXPR} ?? fallbackInvoker; return await ${exportName}.body(input, { env, invoker }); } `; @@ -227,33 +229,17 @@ function generateEntry( } /** - * Build a JSON expression for a machine user TailorUser object. + * Build a JSON expression for a machine user TailorPrincipal object. * @param machineUser - Resolved machine user info * @param workspaceId - Workspace ID * @returns JSON string for the user expression */ -function buildMachineUserExpr(machineUser: ResolvedMachineUser, workspaceId: string): string { +function buildMachinePrincipalExpr(machineUser: ResolvedMachineUser, workspaceId: string): string { return JSON.stringify({ id: machineUser.id, type: "machine_user", workspaceId, - attributes: machineUser.attributes, + attributes: machineUser.attributes ?? {}, attributeList: machineUser.attributeList, }); } - -/** - * Build a JSON expression for a machine user TailorActor object. - * @param machineUser - Resolved machine user info - * @param workspaceId - Workspace ID - * @returns JSON string for the actor expression - */ -function buildMachineActorExpr(machineUser: ResolvedMachineUser, workspaceId: string): string { - return JSON.stringify({ - workspaceId, - userId: machineUser.id, - attributes: machineUser.attributes, - attributeList: machineUser.attributeList, - userType: "USER_TYPE_MACHINE_USER", - }); -} diff --git a/packages/sdk/src/cli/commands/function/detect.ts b/packages/sdk/src/cli/commands/function/detect.ts index 020c5bfdb..45fd9343e 100644 --- a/packages/sdk/src/cli/commands/function/detect.ts +++ b/packages/sdk/src/cli/commands/function/detect.ts @@ -16,7 +16,7 @@ export type FunctionType = "resolver" | "executor" | "workflow-job" | "plain"; /** Minimal schema interface for local format detection (subset of TailorField) */ interface InputSchema { - parse(args: { value: unknown; data: unknown; user: Record }): { + parse(args: { value: unknown; data: unknown; invoker: Record | null }): { issues?: readonly { message: string; path?: readonly (string | number | symbol)[] }[]; }; } diff --git a/packages/sdk/src/cli/services/resolver/bundler.ts b/packages/sdk/src/cli/services/resolver/bundler.ts index a6ee462c6..bbc3bd397 100644 --- a/packages/sdk/src/cli/services/resolver/bundler.ts +++ b/packages/sdk/src/cli/services/resolver/bundler.ts @@ -156,7 +156,7 @@ async function bundleSingleResolver( const result = t.object(_internalResolver.input).parse({ value: context.input, data: context.input, - user: context.user, + invoker, }); if (result.issues) { diff --git a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts index 61271d2dc..e21ba1928 100644 --- a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts +++ b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts @@ -4,7 +4,7 @@ import { join, resolve } from "pathe"; import * as rolldown from "rolldown"; import { getDistDir } from "@/cli/shared/dist-dir"; import { platformBundleDefinePlugin } from "@/cli/shared/platform-bundle-plugin"; -import { stringifyFunction, tailorUserMap } from "@/parser/service/tailordb/field"; +import { stringifyFunction, tailorPrincipalMap } from "@/parser/service/tailordb/field"; import { setPrecompiledScriptExpr } from "@/parser/service/tailordb/hooks-validate-precompiled-expr"; import { assertDefined } from "@/utils/assert"; import { ES_BUILTINS } from "./es-builtins"; @@ -391,7 +391,7 @@ function buildPrecompiledExpr(bundleCode: string): string { " const module = { exports: {} };\n" + " const exports = module.exports;\n" + `${bundleCode}\n` + - ` return module.exports.main({ value: _value, data: _data, user: ${tailorUserMap} });\n` + + ` return module.exports.main({ value: _value, data: _data, invoker: ${tailorPrincipalMap} });\n` + "})()" ); } @@ -439,7 +439,7 @@ async function bundleScriptTarget(args: { }): Promise { const { fn, kind, sourceFilePath, sourceBindings, tempDir, targetIndex, tsconfig } = args; const fnSource = stringifyFunction(fn); - const inlineExpr = `(${fnSource})({ value: _value, data: _data, user: ${tailorUserMap} })`; + const inlineExpr = `(${fnSource})({ value: _value, data: _data, invoker: ${tailorPrincipalMap} })`; // Check if the function has free variables that need bundling const freeVars = findUndefinedReferences(`const __fn = ${fnSource};`); diff --git a/packages/sdk/src/cli/services/workflow/bundler.test.ts b/packages/sdk/src/cli/services/workflow/bundler.test.ts index d7e08f785..4fafbf10c 100644 --- a/packages/sdk/src/cli/services/workflow/bundler.test.ts +++ b/packages/sdk/src/cli/services/workflow/bundler.test.ts @@ -110,6 +110,7 @@ export default createWorkflow({ expect(code).not.toContain("job-registry"); expect(code).not.toContain("registerJob"); expect(code).not.toContain("platformSerialize"); + expect(code).not.toContain("async_hooks"); } }); diff --git a/packages/sdk/src/cli/shared/runtime-exprs.test.ts b/packages/sdk/src/cli/shared/runtime-exprs.test.ts index ee2e7abf4..c127955d7 100644 --- a/packages/sdk/src/cli/shared/runtime-exprs.test.ts +++ b/packages/sdk/src/cli/shared/runtime-exprs.test.ts @@ -1,5 +1,33 @@ import { describe, test, expect } from "vitest"; -import { buildExecutorArgsExpr, buildResolverOperationHookExpr } from "./runtime-exprs"; +import { + INVOKER_EXPR, + buildExecutorArgsExpr, + buildResolverOperationHookExpr, +} from "./runtime-exprs"; + +const runInvokerExpr = (invoker: unknown): unknown => + Function( + "tailor", + `return ${INVOKER_EXPR};`, + )({ + context: { getInvoker: () => invoker }, + }); + +const runExecutorArgsExpr = (args: Record): Record => + Function("args", `return ${buildExecutorArgsExpr("schedule", {})};`)(args) as Record< + string, + unknown + >; + +const runResolverOperationHookExpr = ( + user: unknown, + context = { pipeline: {}, args: {} }, +): Record => + Function( + "context", + "user", + `return ${buildResolverOperationHookExpr({})}`, + )(context, user) as Record; describe("buildExecutorArgsExpr", () => { const env = { API_URL: "https://example.com", DEBUG: true }; @@ -12,7 +40,9 @@ describe("buildExecutorArgsExpr", () => { const expr = buildExecutorArgsExpr(kind, env); expect(expr).toContain("...args"); expect(expr).toContain("appNamespace: args.namespaceName"); - expect(expr).toContain("actor: args.actor"); + expect(expr).toContain("actor: (($raw)"); + expect(expr).toContain("})(args.actor)"); + expect(expr).toContain("userType"); expect(expr).toContain("attributeMap"); expect(expr).toContain("attributeList"); expect(expr).toContain(`env: ${JSON.stringify(env)}`); @@ -32,6 +62,41 @@ describe("buildExecutorArgsExpr", () => { const expr = buildExecutorArgsExpr("schedule", env); expect(expr).not.toContain("event:"); }); + + test("maps actor payloads to TailorPrincipal shape", () => { + expect( + runExecutorArgsExpr({ + namespaceName: "app", + actor: { + userType: "USER_TYPE_USER", + userId: "user-1", + workspaceId: "workspace-1", + attributeMap: { role: "admin" }, + attributes: ["role"], + }, + }).actor, + ).toEqual({ + id: "user-1", + type: "user", + workspaceId: "workspace-1", + attributes: { role: "admin" }, + attributeList: ["role"], + }); + }); + + test("maps absent actor payloads to null", () => { + const nilUuid = "00000000-0000-0000-0000-000000000000"; + const actors = [ + null, + undefined, + { userType: "USER_TYPE_UNSPECIFIED", userId: "user-1" }, + { userType: "USER_TYPE_USER", userId: nilUuid }, + { userType: "USER_TYPE_USER" }, + ]; + for (const actor of actors) { + expect(runExecutorArgsExpr({ namespaceName: "app", actor }).actor).toBeNull(); + } + }); }); describe("resolverExecuted trigger", () => { @@ -44,7 +109,8 @@ describe("buildExecutorArgsExpr", () => { test("includes actor transform and appNamespace", () => { const expr = buildExecutorArgsExpr("resolverExecuted", env); - expect(expr).toContain("actor: args.actor"); + expect(expr).toContain("actor: (($raw)"); + expect(expr).toContain("})(args.actor)"); expect(expr).toContain("appNamespace: args.namespaceName"); }); @@ -98,12 +164,12 @@ describe("buildResolverOperationHookExpr", () => { expect(expr).toContain("input: context.args"); }); - test("includes user transformation via tailorUserMap", () => { + test("includes caller transformation via tailorPrincipalMap", () => { const expr = buildResolverOperationHookExpr(env); - expect(expr).toContain("user:"); - expect(expr).toContain("user.workspace_id"); - expect(expr).toContain("user.attribute_map"); - expect(expr).toContain("user.attributes"); + expect(expr).toContain("caller:"); + expect(expr).toContain("workspace_id"); + expect(expr).toContain("attribute_map"); + expect(expr).toContain("attributes"); }); test("includes env injection", () => { @@ -120,4 +186,59 @@ describe("buildResolverOperationHookExpr", () => { const expr = buildResolverOperationHookExpr({}); expect(expr).toContain("env: {}"); }); + + test("maps caller payloads to TailorPrincipal shape", () => { + expect( + runResolverOperationHookExpr({ + type: "USER_TYPE_MACHINE_USER", + id: "machine-1", + workspace_id: "workspace-1", + attribute_map: { team: "ops" }, + attributes: ["team"], + }).caller, + ).toEqual({ + id: "machine-1", + type: "machine_user", + workspaceId: "workspace-1", + attributes: { team: "ops" }, + attributeList: ["team"], + }); + }); + + test("maps absent caller payloads to null", () => { + const nilUuid = "00000000-0000-0000-0000-000000000000"; + const users = [ + null, + undefined, + { type: "USER_TYPE_UNSPECIFIED", id: "user-1" }, + { type: "USER_TYPE_USER", id: nilUuid }, + ]; + for (const user of users) { + expect(runResolverOperationHookExpr(user).caller).toBeNull(); + } + }); +}); + +describe("INVOKER_EXPR", () => { + test("maps invoker payloads to TailorPrincipal shape", () => { + expect( + runInvokerExpr({ + id: "user-1", + type: "user", + workspaceId: "workspace-1", + attributeMap: { role: "member" }, + attributes: ["role"], + }), + ).toEqual({ + id: "user-1", + type: "user", + workspaceId: "workspace-1", + attributes: { role: "member" }, + attributeList: ["role"], + }); + }); + + test("maps anonymous invokers to null", () => { + expect(runInvokerExpr(null)).toBeNull(); + }); }); diff --git a/packages/sdk/src/cli/shared/runtime-exprs.ts b/packages/sdk/src/cli/shared/runtime-exprs.ts index 14365dde8..69221c0ff 100644 --- a/packages/sdk/src/cli/shared/runtime-exprs.ts +++ b/packages/sdk/src/cli/shared/runtime-exprs.ts @@ -7,10 +7,13 @@ * - Bundle inline: interpolated into the generated `.entry.js` wrapper and * evaluated inside the bundled script at function entry. * - * The user field mapping (server → SDK) shared across services is defined in - * `@/parser/service/tailordb` as `tailorUserMap`. + * The principal field mapping (server → SDK) shared across services is built by + * `makePrincipalExpr` from `@/parser/service/tailordb`; `tailorPrincipalMap` + * (the `caller` mapping) is one of its outputs. `INVOKER_EXPR` and + * `ACTOR_TRANSFORM_EXPR` below come from the same factory so the three stay in + * sync. */ -import { tailorUserMap } from "@/parser/service/tailordb"; +import { makePrincipalExpr, tailorPrincipalMap } from "@/parser/service/tailordb"; import type { Trigger } from "@/types/executor.generated"; // --------------------------------------------------------------------------- @@ -21,15 +24,21 @@ import type { Trigger } from "@/types/executor.generated"; * `invoker` value expression, inlined into bundler entry wrappers. * * Calls `tailor.context.getInvoker()` at function entry and maps the server - * shape to TailorInvoker. Anonymous callers (`null`) pass through as `null`. + * shape to `TailorPrincipal | null`. The payload is already in SDK type shape, + * so no `USER_TYPE_*` normalization is needed; anonymous callers (`null`) pass + * through as `null`. */ -export const INVOKER_EXPR = `(($raw) => $raw ? ({ - id: $raw.id, - type: $raw.type, - workspaceId: $raw.workspaceId, - attributes: $raw.attributeMap, - attributeList: $raw.attributes, -}) : null)(tailor.context.getInvoker())`; +export const INVOKER_EXPR = makePrincipalExpr({ + source: "tailor.context.getInvoker()", + normalize: false, + fields: { + type: { raw: "$raw.type" }, + id: "$raw.id", + workspaceId: "$raw.workspaceId", + attributes: "$raw.attributeMap", + attributeList: "$raw.attributes", + }, +}); // --------------------------------------------------------------------------- // Executor @@ -38,15 +47,20 @@ export const INVOKER_EXPR = `(($raw) => $raw ? ({ /** * Actor field transformation expression. * - * Transforms the server's actor object to match the SDK's TailorActor type: - * server `attributeMap` → SDK `attributes` - * server `attributes` → SDK `attributeList` - * other fields → passed through - * null/undefined actor → null + * Transforms the server's actor object to match `TailorPrincipal | null`. */ -const ACTOR_TRANSFORM_EXPR = - `actor: args.actor ? (({ attributeMap, attributes: attrList, ...rest }) => ` + - `({ ...rest, attributes: attributeMap, attributeList: attrList }))(args.actor) : null`; +const ACTOR_TRANSFORM_EXPR = `actor: ${makePrincipalExpr({ + source: "args.actor", + normalize: true, + requireId: true, + fields: { + type: { raw: "$raw?.userType", fallback: "$raw?.type" }, + id: "$raw?.userId ?? $raw?.id", + workspaceId: "$raw.workspaceId", + attributes: "$raw.attributeMap ?? {}", + attributeList: "$raw.attributes ?? []", + }, +})}`; /** * Build the JavaScript expression that transforms server-format executor event @@ -92,7 +106,7 @@ export function buildExecutorArgsExpr( * Transforms server context to SDK resolver context: * context.args → input * context.pipeline → spread into result - * user (global var) → TailorUser (via tailorUserMap: workspace_id→workspaceId, attribute_map→attributes, attributes→attributeList) + * user (global var) → caller (`TailorPrincipal | null`) * env → injected as JSON * @param env - Application env record to embed in the expression * @returns A JavaScript expression string for the operationHook @@ -100,5 +114,5 @@ export function buildExecutorArgsExpr( export function buildResolverOperationHookExpr( env: Record, ): string { - return `({ ...context.pipeline, input: context.args, user: ${tailorUserMap}, env: ${JSON.stringify(env)} });`; + return `({ ...context.pipeline, input: context.args, caller: ${tailorPrincipalMap}, env: ${JSON.stringify(env)} });`; } diff --git a/packages/sdk/src/configure/index.ts b/packages/sdk/src/configure/index.ts index 5d7c14857..759a9a9f8 100644 --- a/packages/sdk/src/configure/index.ts +++ b/packages/sdk/src/configure/index.ts @@ -17,13 +17,11 @@ export namespace t { export { type TailorField } from "@/configure/types/type"; export { - type TailorUser, - type TailorInvoker, + type TailorPrincipal, type AttributeMap, type AttributeList, type Env, } from "@/runtime/types"; -export { unauthenticatedTailorUser } from "@/configure/user"; export { type MachineUserNameRegistry, type MachineUserName } from "@/configure/types/machine-user"; export { type IdpNameRegistry, type IdpName } from "@/configure/types/idp-name"; diff --git a/packages/sdk/src/configure/services/executor/executor.test.ts b/packages/sdk/src/configure/services/executor/executor.test.ts index a0389346c..c2160bc9b 100644 --- a/packages/sdk/src/configure/services/executor/executor.test.ts +++ b/packages/sdk/src/configure/services/executor/executor.test.ts @@ -17,7 +17,7 @@ import { import { scheduleTrigger } from "./trigger/schedule"; import { incomingWebhookTrigger } from "./trigger/webhook"; import type { Operation } from "./operation"; -import type { TailorInvoker } from "@/runtime/types"; +import type { TailorPrincipal } from "@/runtime/types"; describe("createExecutor", () => { test("can disable executor", () => { @@ -87,7 +87,7 @@ describe("createExecutor", () => { operation: { kind: "function", body: (args) => { - expectTypeOf(args).toEqualTypeOf(); + expectTypeOf(args).toEqualTypeOf(); }, }, }); @@ -1073,7 +1073,7 @@ describe("functionTarget", () => { operation: { kind: "function", body: (args) => { - expectTypeOf(args.invoker).toEqualTypeOf(); + expectTypeOf(args.invoker).toEqualTypeOf(); }, }, }); diff --git a/packages/sdk/src/configure/services/executor/operation.ts b/packages/sdk/src/configure/services/executor/operation.ts index 02b1af93e..0c5c49c87 100644 --- a/packages/sdk/src/configure/services/executor/operation.ts +++ b/packages/sdk/src/configure/services/executor/operation.ts @@ -1,6 +1,6 @@ import type { Workflow } from "@/configure/services/workflow/workflow"; import type { MachineUserName } from "@/configure/types/machine-user"; -import type { TailorInvoker } from "@/runtime/types"; +import type { TailorPrincipal } from "@/runtime/types"; import type { FunctionOperation as ParserFunctionOperation, GqlOperation as ParserGqlOperation, @@ -11,7 +11,7 @@ import type { Client } from "@urql/core"; /** Function-based executor operation. The body receives the trigger args and the `invoker`. */ export type FunctionOperation = Omit & { - body: (args: Args & { invoker?: TailorInvoker }) => void | Promise; + body: (args: Args & { invoker: TailorPrincipal | null }) => void | Promise; authInvoker?: MachineUserName; }; diff --git a/packages/sdk/src/configure/services/executor/trigger/event.ts b/packages/sdk/src/configure/services/executor/trigger/event.ts index 42600d76d..fb21ab68e 100644 --- a/packages/sdk/src/configure/services/executor/trigger/event.ts +++ b/packages/sdk/src/configure/services/executor/trigger/event.ts @@ -1,7 +1,7 @@ import type { ResolverConfig } from "@/configure/services/resolver/resolver"; import type { TailorDBType } from "@/configure/services/tailordb/schema"; import type { IdpName } from "@/configure/types/idp-name"; -import type { TailorActor, TailorEnv } from "@/runtime/types"; +import type { TailorEnv, TailorPrincipal } from "@/runtime/types"; import type { TailorDBTrigger as ParserTailorDBTrigger, ResolverExecutedTrigger as ParserResolverExecutedTrigger, @@ -14,7 +14,7 @@ interface EventArgs { workspaceId: string; appNamespace: string; env: TailorEnv; - actor: TailorActor | null; + actor: TailorPrincipal | null; } interface RecordArgs extends EventArgs { diff --git a/packages/sdk/src/configure/services/resolver/resolver.test.ts b/packages/sdk/src/configure/services/resolver/resolver.test.ts index 7bfec2b91..f2902ea44 100644 --- a/packages/sdk/src/configure/services/resolver/resolver.test.ts +++ b/packages/sdk/src/configure/services/resolver/resolver.test.ts @@ -2,7 +2,7 @@ import { describe, expectTypeOf, test, expect } from "vitest"; import { db } from "@/configure/services/tailordb"; import { t } from "@/configure/types"; import { createResolver } from "./resolver"; -import type { TailorInvoker, TailorUser } from "@/runtime/types"; +import type { TailorPrincipal } from "@/runtime/types"; import type { output } from "@/types/helpers"; import type { ResolverInput } from "@/types/resolver.generated"; @@ -16,11 +16,11 @@ describe("createResolver", () => { result: t.string(), }), body: (context) => { - expectTypeOf(context).toHaveProperty("user"); + expectTypeOf(context).toHaveProperty("caller"); expectTypeOf(context).toHaveProperty("input"); expectTypeOf(context).toHaveProperty("invoker"); - expectTypeOf(context.user).toEqualTypeOf(); - expectTypeOf(context.invoker).toEqualTypeOf(); + expectTypeOf(context.caller).toEqualTypeOf(); + expectTypeOf(context.invoker).toEqualTypeOf(); expectTypeOf(context.input).toBeNever(); return { result: "hello" }; }, @@ -35,9 +35,9 @@ describe("createResolver", () => { success: t.bool(), }), body: (context) => { - expectTypeOf(context).toHaveProperty("user"); + expectTypeOf(context).toHaveProperty("caller"); expectTypeOf(context).toHaveProperty("input"); - expectTypeOf(context.user).toEqualTypeOf(); + expectTypeOf(context.caller).toEqualTypeOf(); expectTypeOf(context.input).toBeNever(); return { success: true }; }, @@ -59,7 +59,7 @@ describe("createResolver", () => { }), body: (context) => { expectTypeOf(context).toHaveProperty("input"); - expectTypeOf(context).toHaveProperty("user"); + expectTypeOf(context).toHaveProperty("caller"); expectTypeOf(context.input).toEqualTypeOf<{ name: string; age: number; @@ -241,7 +241,7 @@ describe("createResolver", () => { }), body: async (context) => { expectTypeOf(context).toHaveProperty("input"); - expectTypeOf(context).toHaveProperty("user"); + expectTypeOf(context).toHaveProperty("caller"); await new Promise((resolve) => setTimeout(resolve, 0)); return { data: context.input.id }; }, @@ -260,13 +260,13 @@ describe("createResolver", () => { }), body: async (context) => { expectTypeOf(context).toHaveProperty("input"); - expectTypeOf(context).toHaveProperty("user"); + expectTypeOf(context).toHaveProperty("caller"); return { success: true }; }, }); }); - test("user context always available", () => { + test("caller context is nullable", () => { createResolver({ name: "withUser", operation: "query", @@ -274,11 +274,12 @@ describe("createResolver", () => { userId: t.string(), }), body: (context) => { - expectTypeOf(context.user).toEqualTypeOf(); - expectTypeOf(context.user.id).toBeString(); - expectTypeOf(context.user.type).toBeString(); - expectTypeOf(context.user.workspaceId).toBeString(); - return { userId: context.user.id }; + expectTypeOf(context.caller).toEqualTypeOf(); + if (!context.caller) return { userId: "anonymous" }; + expectTypeOf(context.caller.id).toBeString(); + expectTypeOf(context.caller.type).toEqualTypeOf<"user" | "machine_user">(); + expectTypeOf(context.caller.workspaceId).toBeString(); + return { userId: context.caller.id }; }, }); }); diff --git a/packages/sdk/src/configure/services/resolver/resolver.ts b/packages/sdk/src/configure/services/resolver/resolver.ts index 3ec85754a..3a08c6ad4 100644 --- a/packages/sdk/src/configure/services/resolver/resolver.ts +++ b/packages/sdk/src/configure/services/resolver/resolver.ts @@ -1,14 +1,14 @@ import { t, type TailorAnyField, type TailorField } from "@/configure/types/type"; import { brandValue } from "@/utils/brand"; import type { MachineUserName } from "@/configure/types/machine-user"; -import type { TailorEnv, TailorInvoker, TailorUser } from "@/runtime/types"; +import type { TailorEnv, TailorPrincipal } from "@/runtime/types"; import type { InferFieldsOutput, output } from "@/types/helpers"; import type { ResolverInput } from "@/types/resolver.generated"; type Context | undefined> = { input: Input extends Record ? InferFieldsOutput : never; - user: TailorUser; - invoker?: TailorInvoker; + caller: TailorPrincipal | null; + invoker: TailorPrincipal | null; env: TailorEnv; }; @@ -46,7 +46,7 @@ type ResolverReturn< * Create a resolver definition for the Tailor SDK. * * The `body` function receives a context with `input` (typed from `config.input`), - * `user`, `invoker` (reflects `authInvoker` delegation), and `env`. + * `caller`, `invoker` (reflects `authInvoker` delegation), and `env`. * The return value of `body` must match the `output` type. * * `output` accepts either a single TailorField (e.g. `t.string()`) or a @@ -69,7 +69,7 @@ type ResolverReturn< * input: { * id: t.string(), * }, - * body: async ({ input, user }) => { + * body: async ({ input, caller }) => { * const db = getDB("tailordb"); * const result = await db.selectFrom("User").selectAll().where("id", "=", input.id).executeTakeFirst(); * return { name: result?.name ?? "", email: result?.email ?? "" }; diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 12ca089fe..34ca42478 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -1,11 +1,10 @@ // oxlint-disable vitest/expect-expect -- Type-only assertions are checked by TypeScript. import { describe, expectTypeOf, expect, test } from "vitest"; import { t } from "@/configure/types"; -import { unauthenticatedTailorUser } from "@/configure/user"; import { db } from "./schema"; import type { Hook } from "./types"; import type { FieldValidateInput, ValidateConfig } from "@/configure/types/field.types"; -import type { TailorUser } from "@/runtime/types"; +import type { TailorPrincipal } from "@/runtime/types"; import type { output } from "@/types/helpers"; describe("TailorDBField basic field type tests", () => { @@ -608,7 +607,7 @@ describe("TailorDBType withTimestamps option tests", () => { }>(); }); - const timestampHookUser = unauthenticatedTailorUser; + const timestampHookInvoker = null; test("createdAt create hook respects a user-specified value", () => { const { createdAt } = db.fields.timestamps(); @@ -616,7 +615,7 @@ describe("TailorDBType withTimestamps option tests", () => { expect(createHook).toBeDefined(); const specified = new Date("2025-02-10T09:00:00Z"); - const result = createHook!({ value: specified, data: {}, user: timestampHookUser }); + const result = createHook!({ value: specified, data: {}, invoker: timestampHookInvoker }); expect(result).toBe(specified); }); @@ -626,7 +625,7 @@ describe("TailorDBType withTimestamps option tests", () => { expect(createHook).toBeDefined(); const before = Date.now(); - const result = createHook!({ value: null, data: {}, user: timestampHookUser }); + const result = createHook!({ value: null, data: {}, invoker: timestampHookInvoker }); const after = Date.now(); expect(result).toBeInstanceOf(Date); expect((result as Date).getTime()).toBeGreaterThanOrEqual(before); @@ -1375,7 +1374,7 @@ describe("TailorDBType files method tests", () => { }); describe("TailorDBField runtime validation tests", () => { - const user: TailorUser = { + const invoker: TailorPrincipal = { id: "test", type: "user", workspaceId: "workspace-test", @@ -1386,66 +1385,66 @@ describe("TailorDBField runtime validation tests", () => { test("validates string field values", () => { const field = db.string(); - const result = field.parse({ value: "hello", data, user }); + const result = field.parse({ value: "hello", data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); } expect(result.value).toBe("hello"); - const bad = field.parse({ value: 123, data, user }); + const bad = field.parse({ value: 123, data, invoker }); expect(bad.issues?.[0]?.message).toBe("Expected a string: received 123"); }); test("validates enum values", () => { const field = db.enum(["active", "inactive"]); - const result = field.parse({ value: "active", data, user }); + const result = field.parse({ value: "active", data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); } expect(result.value).toBe("active"); - const bad = field.parse({ value: "unknown", data, user }); + const bad = field.parse({ value: "unknown", data, invoker }); expect(bad.issues?.[0]?.message).toBe("Must be one of [active, inactive]: received unknown"); }); test("validates integer values", () => { const field = db.int(); - const ok = field.parse({ value: 42, data, user }); + const ok = field.parse({ value: 42, data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); } expect(ok.value).toBe(42); - const bad = field.parse({ value: "not-a-number", data, user }); + const bad = field.parse({ value: "not-a-number", data, invoker }); expect(bad.issues?.[0]?.message).toBe("Expected an integer: received not-a-number"); }); test("validates float values", () => { const field = db.float(); - const ok = field.parse({ value: 3.14, data, user }); + const ok = field.parse({ value: 3.14, data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); } expect(ok.value).toBe(3.14); - const bad = field.parse({ value: "not-a-number", data, user }); + const bad = field.parse({ value: "not-a-number", data, invoker }); expect(bad.issues?.[0]?.message).toBe("Expected a number: received not-a-number"); }); test("validates boolean values", () => { const field = db.bool(); - const ok = field.parse({ value: true, data, user }); + const ok = field.parse({ value: true, data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); } expect(ok.value).toBe(true); - const bad = field.parse({ value: "true", data, user }); + const bad = field.parse({ value: "true", data, invoker }); expect(bad.issues?.[0]?.message).toBe("Expected a boolean: received true"); }); @@ -1454,21 +1453,21 @@ describe("TailorDBField runtime validation tests", () => { name: db.string(), age: db.int({ optional: true }), }); - const ok = field.parse({ value: { name: "test", age: 30 }, data, user }); + const ok = field.parse({ value: { name: "test", age: 30 }, data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); } expect(ok.value).toEqual({ name: "test", age: 30 }); - const bad = field.parse({ value: { name: 123 }, data, user }); + const bad = field.parse({ value: { name: 123 }, data, invoker }); expect(bad.issues?.[0]?.path).toEqual(["name"]); expect(bad.issues?.[0]?.message).toBe("Expected a string: received 123"); }); test("validates array values", () => { const field = db.int({ array: true }); - const ok = field.parse({ value: [1, 2, 3], data, user }); + const ok = field.parse({ value: [1, 2, 3], data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); @@ -1478,27 +1477,27 @@ describe("TailorDBField runtime validation tests", () => { test("validates UUID format", () => { const field = db.uuid(); - const ok = field.parse({ value: "123e4567-e89b-12d3-a456-426614174000", data, user }); + const ok = field.parse({ value: "123e4567-e89b-12d3-a456-426614174000", data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); } expect(ok.value).toBe("123e4567-e89b-12d3-a456-426614174000"); - const bad = field.parse({ value: "not-a-uuid", data, user }); + const bad = field.parse({ value: "not-a-uuid", data, invoker }); expect(bad.issues?.[0]?.message).toBe("Expected a valid UUID: received not-a-uuid"); }); test("validates date format", () => { const field = db.date(); - const ok = field.parse({ value: "2025-01-01", data, user }); + const ok = field.parse({ value: "2025-01-01", data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); } expect(ok.value).toBe("2025-01-01"); - const bad = field.parse({ value: "2025/01/01", data, user }); + const bad = field.parse({ value: "2025/01/01", data, invoker }); expect(bad.issues?.[0]?.message).toBe( 'Expected to match "yyyy-MM-dd" format: received 2025/01/01', ); @@ -1506,24 +1505,24 @@ describe("TailorDBField runtime validation tests", () => { test("validates time format", () => { const field = db.time(); - const ok = field.parse({ value: "10:11", data, user }); + const ok = field.parse({ value: "10:11", data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); } expect(ok.value).toBe("10:11"); - const bad = field.parse({ value: "10:11:12", data, user }); + const bad = field.parse({ value: "10:11:12", data, invoker }); expect(bad.issues?.[0]?.message).toBe('Expected to match "HH:mm" format: received 10:11:12'); }); test("validates required and optional handling", () => { const requiredField = db.string(); - const requiredMissing = requiredField.parse({ value: undefined, data, user }); + const requiredMissing = requiredField.parse({ value: undefined, data, invoker }); expect(requiredMissing.issues?.[0]?.message).toBe("Required field is missing"); const optionalField = db.string({ optional: true }); - const optionalNull = optionalField.parse({ value: undefined, data, user }); + const optionalNull = optionalField.parse({ value: undefined, data, invoker }); expect(optionalNull.issues).toBeUndefined(); if (optionalNull.issues) { throw new Error("Unexpected issues"); @@ -1972,44 +1971,56 @@ describe("TailorDBField decimal type tests", () => { test("decimal parse validates valid decimal strings", () => { const field = db.decimal(); - const user = { id: "test", _loggedIn: true } as unknown as TailorUser; - expect(field.parse({ value: "123.45", data: {}, user })).toEqual({ value: "123.45" }); - expect(field.parse({ value: "0", data: {}, user })).toEqual({ value: "0" }); - expect(field.parse({ value: "-99.99", data: {}, user })).toEqual({ value: "-99.99" }); - expect(field.parse({ value: "1000", data: {}, user })).toEqual({ value: "1000" }); - expect(field.parse({ value: ".5", data: {}, user })).toEqual({ value: ".5" }); - expect(field.parse({ value: "5.", data: {}, user })).toEqual({ value: "5." }); - expect(field.parse({ value: "4.321e+4", data: {}, user })).toEqual({ value: "4.321e+4" }); - expect(field.parse({ value: "1E-5", data: {}, user })).toEqual({ value: "1E-5" }); - expect(field.parse({ value: "2.41E-3", data: {}, user })).toEqual({ value: "2.41E-3" }); - expect(field.parse({ value: "-1.5e10", data: {}, user })).toEqual({ value: "-1.5e10" }); + const invoker: TailorPrincipal = { + id: "test", + type: "user", + workspaceId: "workspace-test", + attributes: {}, + attributeList: [], + }; + expect(field.parse({ value: "123.45", data: {}, invoker })).toEqual({ value: "123.45" }); + expect(field.parse({ value: "0", data: {}, invoker })).toEqual({ value: "0" }); + expect(field.parse({ value: "-99.99", data: {}, invoker })).toEqual({ value: "-99.99" }); + expect(field.parse({ value: "1000", data: {}, invoker })).toEqual({ value: "1000" }); + expect(field.parse({ value: ".5", data: {}, invoker })).toEqual({ value: ".5" }); + expect(field.parse({ value: "5.", data: {}, invoker })).toEqual({ value: "5." }); + expect(field.parse({ value: "4.321e+4", data: {}, invoker })).toEqual({ value: "4.321e+4" }); + expect(field.parse({ value: "1E-5", data: {}, invoker })).toEqual({ value: "1E-5" }); + expect(field.parse({ value: "2.41E-3", data: {}, invoker })).toEqual({ value: "2.41E-3" }); + expect(field.parse({ value: "-1.5e10", data: {}, invoker })).toEqual({ value: "-1.5e10" }); }); test("decimal parse rejects invalid decimal strings", () => { const field = db.decimal(); - const user = { id: "test", _loggedIn: true } as unknown as TailorUser; - const result1 = field.parse({ value: "abc", data: {}, user }); + const invoker: TailorPrincipal = { + id: "test", + type: "user", + workspaceId: "workspace-test", + attributes: {}, + attributeList: [], + }; + const result1 = field.parse({ value: "abc", data: {}, invoker }); expect(result1).toHaveProperty("issues"); - const result2 = field.parse({ value: 123, data: {}, user }); + const result2 = field.parse({ value: 123, data: {}, invoker }); expect(result2).toHaveProperty("issues"); - const result3 = field.parse({ value: "", data: {}, user }); + const result3 = field.parse({ value: "", data: {}, invoker }); expect(result3).toHaveProperty("issues"); - const result4 = field.parse({ value: "1_000_000", data: {}, user }); + const result4 = field.parse({ value: "1_000_000", data: {}, invoker }); expect(result4).toHaveProperty("issues"); - const result5 = field.parse({ value: "0b1.1p-5", data: {}, user }); + const result5 = field.parse({ value: "0b1.1p-5", data: {}, invoker }); expect(result5).toHaveProperty("issues"); - const result6 = field.parse({ value: "1e", data: {}, user }); + const result6 = field.parse({ value: "1e", data: {}, invoker }); expect(result6).toHaveProperty("issues"); - const result7 = field.parse({ value: "e5", data: {}, user }); + const result7 = field.parse({ value: "e5", data: {}, invoker }); expect(result7).toHaveProperty("issues"); - const result8 = field.parse({ value: ".", data: {}, user }); + const result8 = field.parse({ value: ".", data: {}, invoker }); expect(result8).toHaveProperty("issues"); }); }); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 556aed73f..320e530c6 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -29,7 +29,7 @@ import type { Validators, } from "@/configure/types/field.types"; import type { PluginAttachment, PluginConfigs } from "@/plugin/types"; -import type { InferredAttributeMap, TailorUser } from "@/runtime/types"; +import type { InferredAttributeMap, TailorPrincipal } from "@/runtime/types"; import type { output, InferFieldsOutput, Prettify } from "@/types/helpers"; import type { RawPermissions } from "@/types/tailordb.generated"; import type { StandardSchemaV1 } from "@standard-schema/spec"; @@ -313,13 +313,13 @@ const regex = { type FieldParseArgs = { value: unknown; data: unknown; - user: TailorUser; + invoker: TailorPrincipal | null; }; type FieldValidateValueArgs = { value: TailorToTs[T]; data: unknown; - user: TailorUser; + invoker: TailorPrincipal | null; pathArray: string[]; }; @@ -328,7 +328,7 @@ type FieldParseInternalArgs = { // oxlint-disable-next-line no-explicit-any value: any; data: unknown; - user: TailorUser; + invoker: TailorPrincipal | null; pathArray: string[]; }; @@ -376,11 +376,11 @@ function createTailorDBField< /** * Validate a single value (not an array element) * Used internally for array element validation - * @param args - Value, context data, and user + * @param args - Value, context data, and invoker * @returns Array of validation issues */ function validateValue(args: FieldValidateValueArgs): StandardSchemaV1.Issue[] { - const { value, data, user, pathArray } = args; + const { value, data, invoker, pathArray } = args; const issues: StandardSchemaV1.Issue[] = []; // Type-specific validation @@ -495,7 +495,7 @@ function createTailorDBField< const result = nestedField._parseInternal({ value: fieldValue, data, - user, + invoker, pathArray: pathArray.concat(fieldName), }); if (result.issues) { @@ -515,7 +515,7 @@ function createTailorDBField< ? { fn: validateInput, message: "Validation failed" } : { fn: validateInput[0], message: validateInput[1] }; - if (!fn({ value, data, user })) { + if (!fn({ value, data, invoker })) { issues.push({ message, path: pathArray.length > 0 ? pathArray : undefined, @@ -535,7 +535,7 @@ function createTailorDBField< function parseInternal( args: FieldParseInternalArgs, ): StandardSchemaV1.Result> { - const { value, data, user, pathArray } = args; + const { value, data, invoker, pathArray } = args; const issues: StandardSchemaV1.Issue[] = []; // 1. Check required/optional @@ -572,7 +572,7 @@ function createTailorDBField< const elementIssues = validateValue({ value: elementValue, data, - user, + invoker, pathArray: elementPath, }); if (elementIssues.length > 0) { @@ -587,7 +587,7 @@ function createTailorDBField< } // 3. Type-specific validation and custom validation - const valueIssues = validateValue({ value, data, user, pathArray }); + const valueIssues = validateValue({ value, data, invoker, pathArray }); issues.push(...valueIssues); if (issues.length > 0) { @@ -638,7 +638,7 @@ function createTailorDBField< return parseInternal({ value: args.value, data: args.data, - user: args.user, + invoker: args.invoker, pathArray: [], }); }, diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index 9fa0cbdae..71baf94ba 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -7,7 +7,7 @@ import type { FieldMetadata, TailorField, } from "@/configure/types/field.types"; -import type { InferredAttributeMap, TailorUser } from "@/runtime/types"; +import type { InferredAttributeMap, TailorPrincipal } from "@/runtime/types"; import type { InferFieldsOutput, output, Prettify } from "@/types/helpers"; import type { DBFieldMetadata as DBFieldMetadataGenerated, @@ -148,7 +148,7 @@ type HookFn = (args: { data: TData extends Record ? { readonly [K in keyof TData]?: TData[K] | null | undefined } : unknown; - user: TailorUser; + invoker: TailorPrincipal | null; }) => TReturn; export type Hook = { diff --git a/packages/sdk/src/configure/services/workflow/job.test.ts b/packages/sdk/src/configure/services/workflow/job.test.ts index 989a10936..dd1ca6f55 100644 --- a/packages/sdk/src/configure/services/workflow/job.test.ts +++ b/packages/sdk/src/configure/services/workflow/job.test.ts @@ -1,8 +1,46 @@ // oxlint-disable vitest/expect-expect -- Type-only assertions are checked by TypeScript. import { describe, expect, test, expectTypeOf } from "vitest"; +import { platformSerialize } from "@/utils/test/platform-serialize"; import { createWorkflowJob, type WorkflowJob } from "./job"; +import { getRegisteredJob } from "./registry"; +import { buildJobContext } from "./test-env-key"; import { createWorkflow } from "./workflow"; -import type { TailorInvoker } from "@/runtime/types"; +import type { TailorPrincipal } from "@/runtime/types"; + +async function withRegisteredJobRuntime(run: () => Promise): Promise { + const root = globalThis as { + tailor?: { + workflow?: { + triggerJobFunction: (name: string, args?: unknown) => unknown; + }; + }; + }; + const previousTailor = root.tailor; + + root.tailor = { + ...previousTailor, + workflow: { + triggerJobFunction: (name, args) => { + const body = getRegisteredJob(name); + if (!body) return null; + const out = body(platformSerialize(args), buildJobContext()); + return out instanceof Promise + ? out.then((value) => platformSerialize(value)) + : platformSerialize(out); + }, + }, + }; + + try { + return await run(); + } finally { + if (previousTailor) { + root.tailor = previousTailor; + } else { + delete root.tailor; + } + } +} describe("WorkflowJob type inference", () => { test("preserves literal types in output when using as const", () => { @@ -55,9 +93,143 @@ describe("WorkflowJob type inference", () => { body: (_input: undefined, context) => { expectTypeOf(context).toHaveProperty("env"); expectTypeOf(context).toHaveProperty("invoker"); - expectTypeOf(context.invoker).toEqualTypeOf(); + expectTypeOf(context.invoker).toEqualTypeOf(); + }, + }); + }); + + test("direct body calls work when process.getBuiltinModule is unavailable", async () => { + const descriptor = Object.getOwnPropertyDescriptor(process, "getBuiltinModule"); + Object.defineProperty(process, "getBuiltinModule", { + configurable: true, + value: undefined, + }); + try { + const invoker: TailorPrincipal = { + id: "principal-1", + type: "user", + workspaceId: "workspace-1", + attributes: {}, + attributeList: [], + }; + const child = createWorkflowJob({ + name: "capture-child-invoker-without-get-builtin-module", + body: (_input: undefined, context) => context.invoker?.id ?? "anonymous", + }); + const parent = createWorkflowJob({ + name: "propagate-parent-invoker-without-get-builtin-module", + body: async () => await child.trigger(), + }); + + await withRegisteredJobRuntime(async () => { + await expect(parent.body(undefined, { env: {}, invoker })).resolves.toBe("principal-1"); + }); + } finally { + if (descriptor) { + Object.defineProperty(process, "getBuiltinModule", descriptor); + } else { + delete (process as { getBuiltinModule?: unknown }).getBuiltinModule; + } + } + }); + + test("direct body calls propagate invoker to triggered child jobs", async () => { + const invoker: TailorPrincipal = { + id: "principal-1", + type: "user", + workspaceId: "workspace-1", + attributes: { role: "ADMIN" }, + attributeList: [], + }; + const child = createWorkflowJob({ + name: "capture-child-invoker", + body: (_input: undefined, context) => context.invoker?.id ?? "anonymous", + }); + const parent = createWorkflowJob({ + name: "propagate-parent-invoker", + body: async () => await child.trigger(), + }); + + await withRegisteredJobRuntime(async () => { + await expect(parent.body(undefined, { env: {}, invoker })).resolves.toBe("principal-1"); + }); + }); + + test("concurrent direct body calls isolate invokers for child triggers", async () => { + const firstInvoker: TailorPrincipal = { + id: "principal-1", + type: "user", + workspaceId: "workspace-1", + attributes: {}, + attributeList: [], + }; + const secondInvoker: TailorPrincipal = { + id: "principal-2", + type: "machine_user", + workspaceId: "workspace-1", + attributes: {}, + attributeList: [], + }; + let releaseFirst: () => void = () => {}; + let releaseSecond: () => void = () => {}; + const firstGate = new Promise((resolve) => { + releaseFirst = resolve; + }); + const secondGate = new Promise((resolve) => { + releaseSecond = resolve; + }); + const gates = { + first: firstGate, + second: secondGate, + }; + const child = createWorkflowJob({ + name: "capture-concurrent-child-invoker", + body: (_input: undefined, context) => context.invoker?.id ?? "anonymous", + }); + const parent = createWorkflowJob({ + name: "capture-concurrent-parent-invoker", + body: async (input: { gate: "first" | "second" }) => { + await gates[input.gate]; + return await child.trigger(); }, }); + + await withRegisteredJobRuntime(async () => { + const first = parent.body({ gate: "first" }, { env: {}, invoker: firstInvoker }); + const second = parent.body({ gate: "second" }, { env: {}, invoker: secondInvoker }); + + releaseFirst(); + await expect(first).resolves.toBe("principal-1"); + releaseSecond(); + await expect(second).resolves.toBe("principal-2"); + }); + }); + + test("trigger reads the runtime invoker when no body context is active", async () => { + const previousTailor = (globalThis as { tailor?: unknown }).tailor; + (globalThis as { tailor?: unknown }).tailor = { + context: { + getInvoker: () => ({ + id: "runtime-principal", + type: "machine_user", + workspaceId: "workspace-1", + attributes: ["role"], + attributeMap: { role: "SYSTEM" }, + }), + }, + }; + try { + const job = createWorkflowJob({ + name: "capture-runtime-invoker", + body: (_input: undefined, context) => context.invoker?.id ?? "anonymous", + }); + + await withRegisteredJobRuntime(async () => { + expect(job.trigger()).toBe("runtime-principal"); + }); + } finally { + (globalThis as { tailor?: unknown }).tailor = previousTailor; + } }); }); diff --git a/packages/sdk/src/configure/services/workflow/job.ts b/packages/sdk/src/configure/services/workflow/job.ts index 8f78fe857..23f8eeb39 100644 --- a/packages/sdk/src/configure/services/workflow/job.ts +++ b/packages/sdk/src/configure/services/workflow/job.ts @@ -1,6 +1,7 @@ import { brandValue } from "@/utils/brand"; import { dispatchTriggerJob, registerJob, type RegisteredJobBody } from "./registry"; -import type { TailorEnv, TailorInvoker } from "@/runtime/types"; +import { withWorkflowTestInvoker } from "./test-env-key"; +import type { TailorEnv, TailorPrincipal } from "@/runtime/types"; import type { JsonCompatible } from "@/types/helpers"; /** @@ -8,7 +9,7 @@ import type { JsonCompatible } from "@/types/helpers"; */ export type WorkflowJobContext = { env: TailorEnv; - invoker?: TailorInvoker; + invoker: TailorPrincipal | null; }; /** @@ -96,7 +97,11 @@ interface CreateWorkflowJobConfig { export function createWorkflowJob( config: CreateWorkflowJobConfig, ): WorkflowJob> { - const body = config.body as (input: I, context: WorkflowJobContext) => O | Promise; + const userBody = config.body as (input: I, context: WorkflowJobContext) => O | Promise; + const body = process.env.TAILOR_PLATFORM_BUNDLE + ? userBody + : (input: I, context: WorkflowJobContext): O | Promise => + withWorkflowTestInvoker(context.invoker, () => userBody(input, context)); // Test-only local runner registry; the platform bundle sets the flag so it is DCE'd. if (!process.env.TAILOR_PLATFORM_BUNDLE) { diff --git a/packages/sdk/src/configure/services/workflow/registry.ts b/packages/sdk/src/configure/services/workflow/registry.ts index 8f51add99..ffb8ab0e2 100644 --- a/packages/sdk/src/configure/services/workflow/registry.ts +++ b/packages/sdk/src/configure/services/workflow/registry.ts @@ -1,4 +1,4 @@ -import type { TailorEnv, TailorInvoker } from "@/runtime/types"; +import type { TailorEnv, TailorPrincipal } from "@/runtime/types"; /** * Body signature shared by workflow jobs at registry-write time. @@ -7,7 +7,7 @@ import type { TailorEnv, TailorInvoker } from "@/runtime/types"; */ export type RegisteredJobBody = ( args: unknown, - context: { env: TailorEnv; invoker?: TailorInvoker }, + context: { env: TailorEnv; invoker: TailorPrincipal | null }, ) => unknown | Promise; const JOB_REGISTRY_KEY: unique symbol = Symbol.for("tailor-platform/sdk:job-registry"); diff --git a/packages/sdk/src/configure/services/workflow/test-env-key.ts b/packages/sdk/src/configure/services/workflow/test-env-key.ts index 0e5049094..a9caa4086 100644 --- a/packages/sdk/src/configure/services/workflow/test-env-key.ts +++ b/packages/sdk/src/configure/services/workflow/test-env-key.ts @@ -9,10 +9,25 @@ * it from nested Vitest configs that do not resolve `@/` aliases. * @internal */ -import type { TailorEnv, TailorInvoker } from "../../../runtime/types"; +import { AsyncLocalStorage } from "node:async_hooks"; +import type { TailorEnv, TailorPrincipal } from "../../../runtime/types"; const SLOT_KEY = "__tailorWorkflowTestEnv"; +type AsyncLocalStorageLike = { + getStore(): T | undefined; + run(store: T, callback: () => R): R; +}; + +let invokerStorage: AsyncLocalStorageLike | undefined; + +function workflowInvokerStorage(): AsyncLocalStorageLike { + if (!invokerStorage) { + invokerStorage = new AsyncLocalStorage(); + } + return invokerStorage; +} + /** * Read the test-time env slot. * @returns Current env, or `undefined` when unset. @@ -39,6 +54,10 @@ export function clearWorkflowTestEnv(): void { delete (globalThis as unknown as Record)[SLOT_KEY]; } +export function withWorkflowTestInvoker(invoker: TailorPrincipal | null, run: () => T): T { + return workflowInvokerStorage().run(invoker, run); +} + /** * Env-var fallback read by `runWorkflowLocally()` when `mockWorkflow().setEnv()` is unset. * @deprecated Use `mockWorkflow().setEnv()` from `@tailor-platform/sdk/vitest`. @@ -46,13 +65,42 @@ export function clearWorkflowTestEnv(): void { */ export const WORKFLOW_TEST_ENV_KEY = "TAILOR_TEST_WORKFLOW_ENV"; +type RuntimeInvoker = { + id: string; + type: "user" | "machine_user"; + workspaceId: string; + attributes?: string[] | TailorPrincipal["attributes"]; + attributeMap?: TailorPrincipal["attributes"]; + attributeList?: TailorPrincipal["attributeList"]; +}; + +function readRuntimeInvoker(): TailorPrincipal | null { + const runtime = ( + globalThis as unknown as { + tailor?: { context?: { getInvoker?: () => RuntimeInvoker | null } }; + } + ).tailor?.context?.getInvoker; + const raw = runtime?.(); + if (!raw) return null; + return { + id: raw.id, + type: raw.type, + workspaceId: raw.workspaceId, + attributes: raw.attributeMap ?? (Array.isArray(raw.attributes) ? {} : (raw.attributes ?? {})), + attributeList: (raw.attributeList ?? + (Array.isArray(raw.attributes) ? raw.attributes : [])) as TailorPrincipal["attributeList"], + }; +} + // env from `mockWorkflow().setEnv()`, else the deprecated env-var. Shallow-copied // to isolate against cross-trigger mutation. -export function buildJobContext(): { env: TailorEnv; invoker?: TailorInvoker } { +export function buildJobContext(): { env: TailorEnv; invoker: TailorPrincipal | null } { + const storedInvoker = invokerStorage?.getStore(); + const invoker = storedInvoker === undefined ? readRuntimeInvoker() : storedInvoker; const fromGlobal = readWorkflowTestEnv(); - if (fromGlobal !== undefined) return { env: { ...fromGlobal } }; + if (fromGlobal !== undefined) return { env: { ...fromGlobal }, invoker }; const raw = process.env[WORKFLOW_TEST_ENV_KEY]; - if (!raw) return { env: {} as TailorEnv }; + if (!raw) return { env: {} as TailorEnv, invoker }; let parsed: unknown; try { parsed = JSON.parse(raw); @@ -67,5 +115,5 @@ export function buildJobContext(): { env: TailorEnv; invoker?: TailorInvoker } { `${WORKFLOW_TEST_ENV_KEY} must be a JSON object; provide a record or use mockWorkflow().setEnv().`, ); } - return { env: { ...(parsed as TailorEnv) } }; + return { env: { ...(parsed as TailorEnv) }, invoker }; } diff --git a/packages/sdk/src/configure/types/field.types.ts b/packages/sdk/src/configure/types/field.types.ts index cd35ee931..9a6b89021 100644 --- a/packages/sdk/src/configure/types/field.types.ts +++ b/packages/sdk/src/configure/types/field.types.ts @@ -74,14 +74,18 @@ export type ArrayFieldOutput = [O] extends [ ? T[] : T; -import type { TailorUser } from "@/runtime/types"; +import type { TailorPrincipal } from "@/runtime/types"; import type { output, InferFieldsOutput } from "@/types/helpers"; import type { NonEmptyObject } from "type-fest"; /** * Validation function type */ -export type ValidateFn = (args: { value: O; data: D; user: TailorUser }) => boolean; +export type ValidateFn = (args: { + value: O; + data: D; + invoker: TailorPrincipal | null; +}) => boolean; /** * Validation configuration with custom error message diff --git a/packages/sdk/src/configure/types/type.test.ts b/packages/sdk/src/configure/types/type.test.ts index 4783b5e6d..b20df8c78 100644 --- a/packages/sdk/src/configure/types/type.test.ts +++ b/packages/sdk/src/configure/types/type.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test, expectTypeOf } from "vitest"; import { t } from "./type"; import type { AllowedValues } from "./field"; -import type { TailorUser } from "@/runtime/types"; +import type { TailorPrincipal } from "@/runtime/types"; import type { output } from "@/types/helpers"; describe("TailorType basic field type tests", () => { @@ -471,7 +471,7 @@ describe("t.object tests", () => { }); describe("TailorField runtime validation tests", () => { - const user: TailorUser = { + const invoker: TailorPrincipal = { id: "test", type: "user", workspaceId: "workspace-test", @@ -483,7 +483,7 @@ describe("TailorField runtime validation tests", () => { describe("validates primitive types", () => { test("validates string type", () => { { - const result = t.string().parse({ value: "valid string", data, user }); + const result = t.string().parse({ value: "valid string", data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -492,7 +492,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.string().parse({ value: 123, data, user }); + const result = t.string().parse({ value: 123, data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected a string: received 123"); expect(result.issues?.[0]?.path).toBeUndefined(); @@ -501,7 +501,7 @@ describe("TailorField runtime validation tests", () => { test("validates integer type", () => { { - const result = t.int().parse({ value: 123, data, user }); + const result = t.int().parse({ value: 123, data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -510,13 +510,13 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.int().parse({ value: "invalid string", data, user }); + const result = t.int().parse({ value: "invalid string", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected an integer: received invalid string"); } { - const result = t.int().parse({ value: 1.5, data, user }); + const result = t.int().parse({ value: 1.5, data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected an integer: received 1.5"); } @@ -524,7 +524,7 @@ describe("TailorField runtime validation tests", () => { test("validates float type", () => { { - const result = t.float().parse({ value: 1.5, data, user }); + const result = t.float().parse({ value: 1.5, data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -533,13 +533,13 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.float().parse({ value: Number.NaN, data, user }); + const result = t.float().parse({ value: Number.NaN, data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected a number: received NaN"); } { - const result = t.float().parse({ value: "invalid string", data, user }); + const result = t.float().parse({ value: "invalid string", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected a number: received invalid string"); } @@ -547,7 +547,7 @@ describe("TailorField runtime validation tests", () => { test("validates boolean type", () => { { - const result = t.bool().parse({ value: true, data, user }); + const result = t.bool().parse({ value: true, data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -556,7 +556,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.bool().parse({ value: "true", data, user }); + const result = t.bool().parse({ value: "true", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected a boolean: received true"); } @@ -569,7 +569,7 @@ describe("TailorField runtime validation tests", () => { const result = t.uuid().parse({ value: "550e8400-e29b-41d4-a716-446655440000", data, - user, + invoker, }); expect(result.issues).toBeUndefined(); if (result.issues) { @@ -579,7 +579,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.uuid().parse({ value: "not-a-uuid", data, user }); + const result = t.uuid().parse({ value: "not-a-uuid", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected a valid UUID: received not-a-uuid"); } @@ -587,7 +587,7 @@ describe("TailorField runtime validation tests", () => { test("validates date format", () => { { - const result = t.date().parse({ value: "2025-12-21", data, user }); + const result = t.date().parse({ value: "2025-12-21", data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -596,7 +596,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.date().parse({ value: "2025/12/21", data, user }); + const result = t.date().parse({ value: "2025/12/21", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual( 'Expected to match "yyyy-MM-dd" format: received 2025/12/21', @@ -609,7 +609,7 @@ describe("TailorField runtime validation tests", () => { const result = t.datetime().parse({ value: "2025-12-21T10:11:12.123Z", data, - user, + invoker, }); expect(result.issues).toBeUndefined(); if (result.issues) { @@ -619,7 +619,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.datetime().parse({ value: "2025-12-21 10:11:12", data, user }); + const result = t.datetime().parse({ value: "2025-12-21 10:11:12", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual( "Expected to match ISO format: received 2025-12-21 10:11:12", @@ -629,7 +629,7 @@ describe("TailorField runtime validation tests", () => { test("vlidates time format", () => { { - const result = t.time().parse({ value: "10:11", data, user }); + const result = t.time().parse({ value: "10:11", data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -638,7 +638,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.time().parse({ value: "10:11:12", data, user }); + const result = t.time().parse({ value: "10:11:12", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual( 'Expected to match "HH:mm" format: received 10:11:12', @@ -651,7 +651,7 @@ describe("TailorField runtime validation tests", () => { test("validates enum values", () => { const status = t.enum(["active", "inactive"]); { - const result = status.parse({ value: "active", data, user }); + const result = status.parse({ value: "active", data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -660,7 +660,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = status.parse({ value: "pending", data, user }); + const result = status.parse({ value: "pending", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual( "Must be one of [active, inactive]: received pending", @@ -682,7 +682,7 @@ describe("TailorField runtime validation tests", () => { gender: "male", }, data, - user, + invoker, }); expect(result.issues).toBeUndefined(); if (result.issues) { @@ -699,7 +699,7 @@ describe("TailorField runtime validation tests", () => { const result = schema.parse({ value: { age: 1, gender: "invalid" }, data, - user, + invoker, }); expect(result.issues).toBeDefined(); expect(result.issues).toEqual([ @@ -719,7 +719,7 @@ describe("TailorField runtime validation tests", () => { value: t.string({ optional: true }), }); const now = new Date(); - const result = schema.parse({ value: now, data, user }); + const result = schema.parse({ value: now, data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual(`Expected an object: received ${String(now)}`); } @@ -728,7 +728,7 @@ describe("TailorField runtime validation tests", () => { test("validates array fields and element paths", () => { const schema = t.int({ array: true }); { - const result = schema.parse({ value: [1, 2, 3], data, user }); + const result = schema.parse({ value: [1, 2, 3], data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -737,13 +737,13 @@ describe("TailorField runtime validation tests", () => { } { - const result = schema.parse({ value: "invalid", data, user }); + const result = schema.parse({ value: "invalid", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Expected an array"); } { - const result = schema.parse({ value: [1, "x"], data, user }); + const result = schema.parse({ value: [1, "x"], data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]).toEqual({ path: ["[1]"], @@ -755,14 +755,14 @@ describe("TailorField runtime validation tests", () => { test("treats null/undefined as missing when required, and allowed when optional", () => { { const schema = t.string(); - const result = schema.parse({ value: null, data, user }); + const result = schema.parse({ value: null, data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual("Required field is missing"); } { const schema = t.string({ optional: true }); - const result = schema.parse({ value: null, data, user }); + const result = schema.parse({ value: null, data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error("Unexpected issues"); @@ -775,7 +775,7 @@ describe("TailorField runtime validation tests", () => { const result = schema.parse({ value: null, data, - user, + invoker, }); expect(result.issues).toBeUndefined(); if (result.issues) { @@ -800,7 +800,7 @@ describe("TailorField runtime validation tests", () => { "2.41E-3", "-1.5e10", ]) { - const result = t.decimal().parse({ value, data, user }); + const result = t.decimal().parse({ value, data, invoker }); expect(result.issues).toBeUndefined(); if (result.issues) { throw new Error(`Unexpected issues for "${value}"`); @@ -811,11 +811,11 @@ describe("TailorField runtime validation tests", () => { test("rejects invalid decimal values", () => { for (const value of ["abc", "", "1_000_000", "0b1.1p-5", "1e", "e5", "."]) { - const result = t.decimal().parse({ value, data, user }); + const result = t.decimal().parse({ value, data, invoker }); expect(result.issues).toBeDefined(); } { - const result = t.decimal().parse({ value: 123, data, user }); + const result = t.decimal().parse({ value: 123, data, invoker }); expect(result.issues).toBeDefined(); } }); @@ -823,7 +823,7 @@ describe("TailorField runtime validation tests", () => { }); describe("TailorField clone-on-write / no aliasing", () => { - const user: TailorUser = { + const invoker: TailorPrincipal = { id: "test", type: "user", workspaceId: "workspace-test", @@ -923,10 +923,10 @@ describe("TailorField clone-on-write / no aliasing", () => { const status = t.enum(["active", "inactive"]); const cloned = status.description("status field"); - const ok = cloned.parse({ value: "active", data, user }); + const ok = cloned.parse({ value: "active", data, invoker }); expect(ok.issues).toBeUndefined(); - const ng = cloned.parse({ value: "pending", data, user }); + const ng = cloned.parse({ value: "pending", data, invoker }); expect(ng.issues).toBeDefined(); expect(ng.issues?.[0]?.message).toEqual("Must be one of [active, inactive]: received pending"); }); @@ -938,7 +938,7 @@ describe("TailorField clone-on-write / no aliasing", () => { return args.value.length > 0; }); - const result = field.parse({ value: "x", data, user }); + const result = field.parse({ value: "x", data, invoker }); expect(result.issues).toBeUndefined(); expect(calls).toEqual(["x"]); }); @@ -953,7 +953,7 @@ describe("TailorField clone-on-write / no aliasing", () => { expect(described.metadata.description).toBe("name"); expect(validated.metadata.description).toBeUndefined(); - const failed = described.parse({ value: "", data, user }); + const failed = described.parse({ value: "", data, invoker }); expect(failed.issues).toBeDefined(); }); }); diff --git a/packages/sdk/src/configure/types/type.ts b/packages/sdk/src/configure/types/type.ts index 7d1d44a00..84a62bbb2 100644 --- a/packages/sdk/src/configure/types/type.ts +++ b/packages/sdk/src/configure/types/type.ts @@ -9,7 +9,7 @@ import type { TailorField as TailorFieldBase, FieldValidateInput, } from "@/configure/types/field.types"; -import type { TailorUser } from "@/runtime/types"; +import type { TailorPrincipal } from "@/runtime/types"; import type { InferFieldsOutput, Prettify } from "@/types/helpers"; import type { StandardSchemaV1 } from "@standard-schema/spec"; @@ -73,7 +73,7 @@ export interface TailorField< /** * Parse and validate a value against this field's validation rules * Returns StandardSchema Result type with success or failure - * @param args - Value, context data, and user + * @param args - Value, context data, and invoker * @returns Validation result */ parse(args: FieldParseArgs): StandardSchemaV1.Result; @@ -110,13 +110,13 @@ const regex = { type FieldParseArgs = { value: unknown; data: unknown; - user: TailorUser; + invoker: TailorPrincipal | null; }; type FieldValidateValueArgs = { value: TailorToTs[T]; data: unknown; - user: TailorUser; + invoker: TailorPrincipal | null; pathArray: string[]; }; @@ -125,7 +125,7 @@ type FieldParseInternalArgs = { // oxlint-disable-next-line no-explicit-any value: any; data: unknown; - user: TailorUser; + invoker: TailorPrincipal | null; pathArray: string[]; }; @@ -186,11 +186,11 @@ function createTailorField< /** * Validate a single value (not an array element) * Used internally for array element validation - * @param args - Value, context data, and user + * @param args - Value, context data, and invoker * @returns Array of validation issues */ function validateValue(args: FieldValidateValueArgs): StandardSchemaV1.Issue[] { - const { value, data, user, pathArray } = args; + const { value, data, invoker, pathArray } = args; const issues: StandardSchemaV1.Issue[] = []; const path = pathArray.length > 0 ? pathArray : undefined; @@ -306,7 +306,7 @@ function createTailorField< const result = nestedField._parseInternal({ value: fieldValue, data, - user, + invoker, pathArray: pathArray.concat(fieldName), }); if (result.issues) { @@ -326,7 +326,7 @@ function createTailorField< ? { fn: validateInput, message: "Validation failed" } : { fn: validateInput[0], message: validateInput[1] }; - if (!fn({ value, data, user })) { + if (!fn({ value, data, invoker })) { issues.push({ message, path, @@ -346,7 +346,7 @@ function createTailorField< function parseInternal( args: FieldParseInternalArgs, ): StandardSchemaV1.Result> { - const { value, data, user, pathArray } = args; + const { value, data, invoker, pathArray } = args; const issues: StandardSchemaV1.Issue[] = []; const path = pathArray.length > 0 ? pathArray : undefined; @@ -384,7 +384,7 @@ function createTailorField< const elementIssues = validateValue({ value: elementValue, data, - user, + invoker, pathArray: elementPath, }); if (elementIssues.length > 0) { @@ -399,7 +399,7 @@ function createTailorField< } // 3. Type-specific validation and custom validation - const valueIssues = validateValue({ value, data, user, pathArray }); + const valueIssues = validateValue({ value, data, invoker, pathArray }); issues.push(...valueIssues); if (issues.length > 0) { @@ -462,7 +462,7 @@ function createTailorField< return parseInternal({ value: args.value, data: args.data, - user: args.user, + invoker: args.invoker, pathArray: [], }); }, diff --git a/packages/sdk/src/configure/user.ts b/packages/sdk/src/configure/user.ts deleted file mode 100644 index 35191d8ea..000000000 --- a/packages/sdk/src/configure/user.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { TailorUser } from "@/runtime/types"; - -/** Represents an unauthenticated user in the Tailor platform. */ -export const unauthenticatedTailorUser: TailorUser = { - id: "00000000-0000-0000-0000-000000000000", - type: "", - workspaceId: "00000000-0000-0000-0000-000000000000", - attributes: null, - attributeList: [], -}; diff --git a/packages/sdk/src/parser/service/tailordb/field.ts b/packages/sdk/src/parser/service/tailordb/field.ts index 1f9731bc2..1048b1ce4 100644 --- a/packages/sdk/src/parser/service/tailordb/field.ts +++ b/packages/sdk/src/parser/service/tailordb/field.ts @@ -7,9 +7,98 @@ import type { import type { OperatorFieldConfig } from "@/parser/service/tailordb/types"; import type { TailorDBTypeRaw as TailorDBTypeSchemaOutput } from "@/types/tailordb.generated"; -// Since there's naming difference between platform and sdk, -// use this mapping in all scripts to provide variables that match sdk types. -export const tailorUserMap = /* js */ `{ id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes }`; +const NIL_UUID = "00000000-0000-0000-0000-000000000000"; + +/** + * Per-source field expressions for {@link makePrincipalExpr}. Each value is a JS + * snippet evaluated against the raw server payload bound to `$raw`. + */ +interface PrincipalFieldExprs { + /** + * Raw type accessor. `raw` is matched against `USER_TYPE_*` when normalizing; + * `fallback` (defaulting to `raw`) supplies the type for non-matching values, + * which lets sources whose primary field is empty fall back to a secondary. + */ + type: { raw: string; fallback?: string }; + /** Raw id accessor. */ + id: string; + workspaceId: string; + attributes: string; + attributeList: string; +} + +interface MakePrincipalExprOptions { + source: string; + fields: PrincipalFieldExprs; + normalize: boolean; + requireId?: boolean; +} + +/** + * Build the server→SDK principal mapping expression shared across services. + * + * All principal-bearing call sites (`caller`, `actor`, `invoker`) must agree on + * the `TailorPrincipal | null` shape, so they are generated here rather than + * hand-written per service. + * @param options - Mapping source and field expressions + * @param options.source - Expression yielding the raw server payload (e.g. `user`) + * @param options.fields - Per-source accessors for each principal field + * @param options.normalize - When true, map `USER_TYPE_*` to SDK type literals and + * return `null` for unspecified types or the nil-UUID id; when false, the + * payload is already in SDK shape and only `null` pass-through is applied + * @param options.requireId - When true, missing ids are also mapped to `null` + * @returns A JS expression string evaluating to `TailorPrincipal | null` + */ +export function makePrincipalExpr(options: MakePrincipalExprOptions): string { + const { source, fields, normalize, requireId = false } = options; + const body = /* js */ `{ + id, + type, + workspaceId: ${fields.workspaceId}, + attributes: ${fields.attributes}, + attributeList: ${fields.attributeList}, + }`; + if (!normalize) { + return /* js */ `(($raw) => { + if (!$raw) { + return null; + } + const id = ${fields.id}; + const type = ${fields.type.raw}; + return ${body}; +})(${source})`; + } + const missingIdGuard = requireId ? " || !id" : ""; + return /* js */ `(($raw) => { + if (!$raw) { + return null; + } + const type = ${fields.type.raw} === "USER_TYPE_USER" + ? "user" + : ${fields.type.raw} === "USER_TYPE_MACHINE_USER" + ? "machine_user" + : ${fields.type.fallback ?? fields.type.raw}; + const id = ${fields.id}; + if (!type || type === "USER_TYPE_UNSPECIFIED" || id === "${NIL_UUID}"${missingIdGuard}) { + return null; + } + return ${body}; +})(${source})`; +} + +// Since there's naming difference between platform and SDK, use this mapping in +// all scripts to provide variables that match `TailorPrincipal | null`. +export const tailorPrincipalMap = makePrincipalExpr({ + source: "user", + normalize: true, + fields: { + type: { raw: "$raw?.type" }, + id: "$raw.id", + workspaceId: "$raw.workspace_id ?? $raw.workspaceId", + attributes: "$raw.attribute_map ?? $raw.attributeMap ?? {}", + attributeList: "$raw.attributes ?? []", + }, +}); /** * Convert a function to a string representation. @@ -46,7 +135,7 @@ const convertHookToExpr = (fn: Function): string => { return precompiledExpr; } const normalized = stringifyFunction(fn); - return `(${normalized})({ value: _value, data: _data, user: ${tailorUserMap} })`; + return `(${normalized})({ value: _value, data: _data, invoker: ${tailorPrincipalMap} })`; }; /** @@ -89,7 +178,7 @@ export function parseFieldConfig( script: { expr: getPrecompiledScriptExpr(fn) ?? - `(${fn.toString().trim()})({ value: _value, data: _data, user: ${tailorUserMap} })`, + `(${fn.toString().trim()})({ value: _value, data: _data, invoker: ${tailorPrincipalMap} })`, }, errorMessage: message, }; diff --git a/packages/sdk/src/parser/service/tailordb/index.ts b/packages/sdk/src/parser/service/tailordb/index.ts index 5cb0f781b..2a5692b98 100644 --- a/packages/sdk/src/parser/service/tailordb/index.ts +++ b/packages/sdk/src/parser/service/tailordb/index.ts @@ -1,3 +1,3 @@ -export { stringifyFunction, tailorUserMap } from "./field"; +export { makePrincipalExpr, stringifyFunction, tailorPrincipalMap } from "./field"; export { parseTypes } from "./type-parser"; export { TailorDBServiceConfigSchema, TailorDBTypeSchema } from "./schema"; diff --git a/packages/sdk/src/plugin/with-context.ts b/packages/sdk/src/plugin/with-context.ts index 8e61f4786..5ff0e1dcd 100644 --- a/packages/sdk/src/plugin/with-context.ts +++ b/packages/sdk/src/plugin/with-context.ts @@ -4,7 +4,7 @@ * context (like type references and namespace) at runtime. */ -import type { TailorActor, TailorEnv } from "@/runtime/types"; +import type { TailorEnv, TailorPrincipal } from "@/runtime/types"; /** * Plugin executor factory function type. @@ -28,8 +28,8 @@ export interface PluginFunctionArgs { appNamespace: string; /** Environment variables */ env: TailorEnv; - /** Actor (user) who triggered the event, null for system events */ - actor: TailorActor | null; + /** Principal that triggered the event, null for system events */ + actor: TailorPrincipal | null; /** Name of the TailorDB type */ typeName: string; /** TailorDB connections by namespace */ diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index 5fac684a5..87b5aa616 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -12,24 +12,15 @@ * } */ +import type { TailorPrincipal } from "@/runtime/types"; + /** * Information about the invoker of the current function execution. * - * Matches the shape of `TailorUser` and `TailorActor` — `attributes` is the - * attribute map and `attributeList` is the array of attribute IDs. + * Matches the public `TailorPrincipal` shape — `attributes` is the attribute + * map and `attributeList` is the array of attribute IDs. */ -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[]; -} +export type Invoker = TailorPrincipal; /** * Raw platform-side invoker payload returned by `tailor.context.getInvoker()`. @@ -70,7 +61,7 @@ export function getInvoker(): Invoker | null { id: raw.id, type: raw.type, workspaceId: raw.workspaceId, - attributes: raw.attributeMap, - attributeList: raw.attributes, + attributes: raw.attributeMap as Invoker["attributes"], + attributeList: raw.attributes as Invoker["attributeList"], }; } diff --git a/packages/sdk/src/runtime/types.ts b/packages/sdk/src/runtime/types.ts index 99b928e00..921869e9f 100644 --- a/packages/sdk/src/runtime/types.ts +++ b/packages/sdk/src/runtime/types.ts @@ -20,78 +20,18 @@ export type InferredAttributeList = AttributeList["__tuple"] extends [] ? string[] : AttributeList["__tuple"]; -/** Represents a user in the Tailor platform. */ -export type TailorUser = { - /** - * The ID of the user. - * For unauthenticated users, this will be a nil UUID. - */ +/** Represents a user or machine user principal in the Tailor Platform. */ +export type TailorPrincipal = { + /** The ID of the principal. */ id: string; - /** - * The type of the user. - * For unauthenticated users, this will be an empty string. - */ - type: "user" | "machine_user" | ""; - /** The ID of the workspace the user belongs to. */ - workspaceId: string; - /** - * A map of the user's attributes. - * For unauthenticated users, this will be null. - */ - attributes: InferredAttributeMap | null; - /** - * A list of the user's attributes. - * For unauthenticated users, this will be an empty array. - */ - attributeList: InferredAttributeList; -}; - -/** - * The invoker of the current function execution. - * - * Reflects `authInvoker` delegation: when `authInvoker` is specified, this is - * the machine user; otherwise it is the calling user. - * Distinct from resolver's `user` (the authenticated caller) and executor's - * `actor` (the subject of the event). - * - * `null` for anonymous requests. - * - * TODO(v2): unify with `TailorUser` — same underlying principal shape. - */ -export type TailorInvoker = { - /** The ID of the invoker (user ID or machine user ID). */ - id: string; - /** The type of the invoker. */ + /** The type of the principal. */ type: "user" | "machine_user"; - /** The ID of the workspace the invoker belongs to. */ + /** The ID of the workspace the principal belongs to. */ workspaceId: string; - /** A map of the invoker's attributes. */ + /** A map of the principal's attributes. */ attributes: InferredAttributeMap; - /** A list of the invoker's attribute IDs. */ - attributeList: InferredAttributeList; -} | null; - -/** User type enum values from the Tailor Platform server. */ -export type TailorActorType = "USER_TYPE_USER" | "USER_TYPE_MACHINE_USER" | "USER_TYPE_UNSPECIFIED"; - -/** Represents an actor in event triggers. */ -export type TailorActor = { - /** The ID of the workspace the user belongs to. */ - workspaceId: string; - /** The ID of the user. */ - userId: string; - /** - * A map of the user's attributes. - * Maps from server's `attributeMap` field. - */ - attributes: InferredAttributeMap | null; - /** - * A list of the user's attributes. - * Maps from server's `attributes` field. - */ + /** A list of the principal's attribute IDs. */ attributeList: InferredAttributeList; - /** The type of the user. */ - userType: TailorActorType; }; // Interface for module augmentation diff --git a/packages/sdk/src/utils/test/index.test.ts b/packages/sdk/src/utils/test/index.test.ts index a9a0ee38b..c9e777636 100644 --- a/packages/sdk/src/utils/test/index.test.ts +++ b/packages/sdk/src/utils/test/index.test.ts @@ -184,12 +184,12 @@ describe("createTailorDBHook", () => { }); describe("create hook on a top-level field", () => { - test("invokes the create hook with value, full data, and the unauthenticated user", () => { - const seen: { value: unknown; data: unknown; userId: string }[] = []; + test("invokes the create hook with value, full data, and a null invoker", () => { + const seen: { value: unknown; data: unknown; invoker: unknown }[] = []; const type = db.type("Order", { total: db.float(), tax: db.float() }).hooks({ tax: { - create: ({ value, data, user }) => { - seen.push({ value, data, userId: user.id }); + create: ({ value, data, invoker }) => { + seen.push({ value, data, invoker }); return (data as { total: number }).total * 0.1; }, }, @@ -200,7 +200,7 @@ describe("createTailorDBHook", () => { { value: undefined, data: { total: 100, tax: undefined }, - userId: "00000000-0000-0000-0000-000000000000", + invoker: null, }, ]); }); diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index 23667ca3a..cbcb239d5 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -1,4 +1,4 @@ -import type { output, TailorUser } from "@/configure"; +import type { output } from "@/configure"; import type { TailorDBType } from "@/configure/services/tailordb/schema"; import type { TailorField } from "@/configure/types/type"; import type { StandardSchemaV1 } from "@standard-schema/spec"; @@ -13,15 +13,6 @@ export { createImportMain, } from "./mock"; -/** Represents an unauthenticated user in the Tailor platform. */ -export const unauthenticatedTailorUser = { - id: "00000000-0000-0000-0000-000000000000", - type: "", - workspaceId: "00000000-0000-0000-0000-000000000000", - attributes: null, - attributeList: [], -} as const satisfies TailorUser; - /** * Creates a hook function that processes TailorDB type fields * - Uses existing id from data if provided, otherwise generates UUID for id fields @@ -61,7 +52,7 @@ export function createTailorDBHook>(type: T) { hooked[key] = field.metadata.hooks.create({ value: (data as Record)[key], data: data, - user: unauthenticatedTailorUser, + invoker: null, }); if (hooked[key] instanceof Date) { hooked[key] = hooked[key].toISOString(); @@ -98,7 +89,7 @@ export function createStandardSchema>( const result = schemaType.parse({ value: hooked, data: hooked, - user: unauthenticatedTailorUser, + invoker: null, }); if (result.issues) { return result; diff --git a/packages/sdk/src/utils/test/mock.ts b/packages/sdk/src/utils/test/mock.ts index 5a134c7bc..0abe98595 100644 --- a/packages/sdk/src/utils/test/mock.ts +++ b/packages/sdk/src/utils/test/mock.ts @@ -1,7 +1,7 @@ import * as path from "node:path"; import { pathToFileURL } from "node:url"; import type { ContextInvoker } from "@/runtime/context"; -import type { TailorInvoker } from "@/runtime/types"; +import type { TailorPrincipal } from "@/runtime/types"; type MainFunction = (args: Record) => unknown | Promise; type QueryResolver = (query: string, params: unknown[]) => unknown[]; @@ -134,9 +134,9 @@ export function setupWorkflowMock(handler: JobHandler): { * Sets up a mock for `globalThis.tailor.context.getInvoker` used in bundled * resolver/executor/workflow tests. * @deprecated With the `tailor-runtime` environment from `@tailor-platform/sdk/vitest`, drive the invoker via `vi.spyOn(globalThis.tailor.context, "getInvoker").mockReturnValue(...)` for bundled tests, or pass `invoker` directly to `.body()` when unit-testing resolvers/executors/workflow jobs against the TypeScript source. - * @param invoker - The `TailorInvoker` value to return, or `null` for anonymous. + * @param invoker - The `TailorPrincipal` value to return, or `null` for anonymous. */ -export function setupInvokerMock(invoker: TailorInvoker): void { +export function setupInvokerMock(invoker: TailorPrincipal | null): void { const raw: ContextInvoker | null = invoker ? { id: invoker.id, From 95ab11ad33f6476ebb0131e80302dd64ddff08b9 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 21:01:05 +0900 Subject: [PATCH 119/618] fix(sdk-codemod): skip nested principal destructures --- .../v2/principal-unify/scripts/transform.ts | 41 +++++++++++-------- .../input.ts | 8 ++++ .../input.ts | 5 +++ 3 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-nested-principal-pattern/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-nested-principal-pattern/input.ts diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 1aeee16ae..6a6a5df6b 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -678,6 +678,17 @@ function patternBindsName(pat: SgNode, name: string): boolean { return false; } +function hasNestedPropertyPattern(pat: SgNode, name: string): boolean { + if (pat.kind() !== "object_pattern") return false; + for (const child of pat.children()) { + if (child.kind() !== "pair_pattern") continue; + const key = child.field("key"); + if (key?.text() !== name) continue; + return child.field("value")?.kind() !== "identifier"; + } + return false; +} + function functionRebindsName(fn: SgNode, name: string): boolean { const single = fn.field("parameter"); if (single && patternBindsName(single, name)) return true; @@ -980,6 +991,7 @@ function transformResolverBody( if (!pattern) return; if (pattern.kind() === "object_pattern") { + if (hasNestedPropertyPattern(pattern, "user")) return; if (hasPrincipalAssignmentTarget(body, "user")) return; if (hasCallerBindingConflict(pattern, body)) return; @@ -1124,15 +1136,13 @@ function transformResolverBody( }); } else if (k === "pair_pattern") { const key = child.field("key"); - if (key && key.text() === "user") { + const value = child.field("value"); + if (key && key.text() === "user" && value?.kind() === "identifier") { edits.push(key.replace("caller")); - const value = child.field("value"); - if (value?.kind() === "identifier") { - principalAliasBindings.push({ - name: value.text(), - bindingStart: value.range().start.index, - }); - } + principalAliasBindings.push({ + name: value.text(), + bindingStart: value.range().start.index, + }); } } } @@ -1482,15 +1492,13 @@ function transformPrincipalCallbackParam( }); } else if (kind === "pair_pattern") { const key = child.field("key"); - if (key?.text() === "user") { + const value = child.field("value"); + if (key?.text() === "user" && value?.kind() === "identifier") { edits.push(key.replace("invoker")); - const value = child.field("value"); - if (value?.kind() === "identifier") { - principalAliasBindings.push({ - name: value.text(), - bindingStart: value.range().start.index, - }); - } + principalAliasBindings.push({ + name: value.text(), + bindingStart: value.range().start.index, + }); } } } @@ -1503,6 +1511,7 @@ function transformPrincipalCallbackParam( } if (pattern.kind() !== "object_pattern") return; + if (hasNestedPropertyPattern(pattern, "user")) return; let hasUserParamProperty = false; let renamesBinding = false; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-nested-principal-pattern/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-nested-principal-pattern/input.ts new file mode 100644 index 000000000..2128998b2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/resolver-nested-principal-pattern/input.ts @@ -0,0 +1,8 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ user: { id } }) => id, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-nested-principal-pattern/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-nested-principal-pattern/input.ts new file mode 100644 index 000000000..00363a5ac --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-nested-principal-pattern/input.ts @@ -0,0 +1,5 @@ +import { db } from "@tailor-platform/sdk"; + +export const role = db.string().hooks({ + create: ({ user: { id } }) => id, +}); From b1938838f3fd7108f6303fdb7756ec6309224adf Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 21:02:40 +0900 Subject: [PATCH 120/618] fix(sdk-codemod): harden nested principal pattern detection --- .../codemods/v2/principal-unify/scripts/transform.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 6a6a5df6b..873e34c89 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -684,7 +684,7 @@ function hasNestedPropertyPattern(pat: SgNode, name: string): boolean { if (child.kind() !== "pair_pattern") continue; const key = child.field("key"); if (key?.text() !== name) continue; - return child.field("value")?.kind() !== "identifier"; + if (child.field("value")?.kind() !== "identifier") return true; } return false; } From 41774d175d6e42ecce9fd0be458e1fea199fe78a Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 21:13:43 +0900 Subject: [PATCH 121/618] feat(cli)!: store login tokens in keyring by default --- .changeset/keyring-default-storage.md | 5 + packages/sdk/docs/cli-reference.md | 6 ++ packages/sdk/src/cli/shared/context.test.ts | 104 +++++++++++++++++--- packages/sdk/src/cli/shared/context.ts | 32 ++++-- 4 files changed, 130 insertions(+), 17 deletions(-) create mode 100644 .changeset/keyring-default-storage.md diff --git a/.changeset/keyring-default-storage.md b/.changeset/keyring-default-storage.md new file mode 100644 index 000000000..c225600c5 --- /dev/null +++ b/.changeset/keyring-default-storage.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Store CLI login tokens in the OS keyring by default when available. Set `TAILOR_USE_KEYRING=0`, `false`, or `off` to keep file-based token storage. diff --git a/packages/sdk/docs/cli-reference.md b/packages/sdk/docs/cli-reference.md index c720f2918..2c96a61d1 100644 --- a/packages/sdk/docs/cli-reference.md +++ b/packages/sdk/docs/cli-reference.md @@ -72,6 +72,7 @@ You can use environment variables to configure workspace and authentication: | `TAILOR_PLATFORM_FOLDER_ID` | Folder ID for folder commands | | `TAILOR_PLATFORM_TOKEN` | Authentication token (alternative to `login`) | | `TAILOR_TOKEN` | **Deprecated.** Use `TAILOR_PLATFORM_TOKEN` instead | +| `TAILOR_USE_KEYRING` | Set to `0`, `false`, or `off` to disable OS keyring token storage | | `TAILOR_PLATFORM_PROFILE` | Workspace profile name | | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | Path to SDK config file | | `TAILOR_PLATFORM_SDK_DTS_PATH` | Output path for generated `tailor.d.ts` type definition file | @@ -92,6 +93,11 @@ Token resolution follows this priority order: 3. Profile specified via `--profile` option or `TAILOR_PLATFORM_PROFILE` 4. Current user from platform config (`~/.config/tailor-platform/config.yaml`) +`tailor-sdk login` stores local CLI login tokens in the OS keyring by default +when available. If the keyring is unavailable, tokens are stored in the platform +config file. Set `TAILOR_USE_KEYRING=0`, `false`, or `off` to force file-based +token storage. + ### Workspace ID Priority Workspace ID resolution follows this priority order: diff --git a/packages/sdk/src/cli/shared/context.test.ts b/packages/sdk/src/cli/shared/context.test.ts index 15f4ffa2d..2d4d19413 100644 --- a/packages/sdk/src/cli/shared/context.test.ts +++ b/packages/sdk/src/cli/shared/context.test.ts @@ -10,6 +10,7 @@ import { loadMachineUserName, loadWorkspaceId, readPlatformConfig, + saveUserTokens, writePlatformConfig, } from "./context"; import { isCLIError } from "./errors"; @@ -18,6 +19,7 @@ import { resetKeyringState } from "./token-store"; import type * as ClientModule from "./client"; const xdgTempDir = vi.hoisted(() => `/tmp/tailor-xdg-${Date.now()}-${Math.random()}`); +const keyringPasswords = vi.hoisted(() => new Map()); vi.mock("xdg-basedir", () => ({ xdgConfig: xdgTempDir, @@ -25,11 +27,19 @@ vi.mock("xdg-basedir", () => ({ vi.mock("@napi-rs/keyring", () => ({ Entry: class { - setPassword() {} + private key: string; + constructor(service: string, account: string) { + this.key = `${service}:${account}`; + } + setPassword(password: string) { + keyringPasswords.set(this.key, password); + } getPassword(): string | null { - return null; + return keyringPasswords.get(this.key) ?? null; + } + deletePassword() { + keyringPasswords.delete(this.key); } - deletePassword() {} }, })); @@ -52,6 +62,7 @@ vi.mock("./client", async (importOriginal) => { beforeEach(() => { clientMocks.fetchUserInfo.mockReset(); clientMocks.refreshToken.mockReset(); + keyringPasswords.clear(); }); describe("loadConfigPath", () => { @@ -753,11 +764,12 @@ describe("loadAccessToken", () => { expect(config.version).toBe(3); expect(config.users["legacy@example.com"]).toBeUndefined(); expect(config.users["platform-user-sub"]).toMatchObject({ - storage: "file", - access_token: "new-access-token", - refresh_token: "new-refresh-token", + storage: "keyring", email: "legacy@example.com", }); + expect(keyringPasswords.get("tailor-platform-cli:platform-user-sub")).toBe( + JSON.stringify({ accessToken: "new-access-token", refreshToken: "new-refresh-token" }), + ); expect(config.current_user).toBe("platform-user-sub"); expect(config.profiles.default?.user).toBe("platform-user-sub"); }); @@ -833,10 +845,11 @@ describe("loadAccessToken", () => { expect(clientMocks.fetchUserInfo).toHaveBeenCalledWith("new-access-token"); expect(config.users["platform-user-sub"]).toBeUndefined(); expect(config.users["legacy@example.com"]).toMatchObject({ - storage: "file", - access_token: "new-access-token", - refresh_token: "new-refresh-token", + storage: "keyring", }); + expect(keyringPasswords.get("tailor-platform-cli:legacy@example.com")).toBe( + JSON.stringify({ accessToken: "new-access-token", refreshToken: "new-refresh-token" }), + ); expect(config.current_user).toBe("legacy@example.com"); expect(config.profiles.default?.user).toBe("legacy@example.com"); }); @@ -876,6 +889,77 @@ describe("profile readonly field", () => { }); }); +describe("saveUserTokens", () => { + const futureDate = new Date(Date.now() + 3600 * 1000).toISOString(); + const originalEnv = process.env; + type PlatformConfig = Parameters[0]; + + function createEmptyConfig(): PlatformConfig { + return { + version: 3, + min_sdk_version: "2.0.0", + users: {}, + profiles: {}, + current_user: null, + }; + } + + beforeEach(() => { + vi.resetModules(); + resetKeyringState(); + process.env = { ...originalEnv }; + delete process.env.TAILOR_USE_KEYRING; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + test("stores tokens in the OS keyring by default when available", async () => { + const config = createEmptyConfig(); + + await saveUserTokens( + config, + "platform-user-sub", + { accessToken: "access-token", refreshToken: "refresh-token" }, + futureDate, + { email: "user@example.com" }, + ); + + expect(config.users["platform-user-sub"]).toEqual({ + storage: "keyring", + token_expires_at: futureDate, + email: "user@example.com", + }); + expect(keyringPasswords.get("tailor-platform-cli:platform-user-sub")).toBe( + JSON.stringify({ accessToken: "access-token", refreshToken: "refresh-token" }), + ); + }); + + test.each(["0", "false", "off"])( + "stores tokens in the config file when TAILOR_USE_KEYRING=%s", + async (value) => { + process.env.TAILOR_USE_KEYRING = value; + const config = createEmptyConfig(); + + await saveUserTokens( + config, + "platform-user-sub", + { accessToken: "access-token", refreshToken: "refresh-token" }, + futureDate, + ); + + expect(config.users["platform-user-sub"]).toEqual({ + storage: "file", + access_token: "access-token", + refresh_token: "refresh-token", + token_expires_at: futureDate, + }); + expect(keyringPasswords.has("tailor-platform-cli:platform-user-sub")).toBe(false); + }, + ); +}); + describe("V1 to V3 migration", () => { const futureDate = new Date(Date.now() + 3600 * 1000).toISOString(); @@ -932,8 +1016,6 @@ describe("keyring user persistence on V2 -> V1 downgrade", () => { vi.resetModules(); resetKeyringState(); process.env = { ...originalEnv }; - // Downgrade only happens when TAILOR_USE_KEYRING is unset, which is the - // default for every command that is not opting into keyring storage. delete process.env.TAILOR_USE_KEYRING; }); diff --git a/packages/sdk/src/cli/shared/context.ts b/packages/sdk/src/cli/shared/context.ts index 7fc49c19e..bed968d3d 100644 --- a/packages/sdk/src/cli/shared/context.ts +++ b/packages/sdk/src/cli/shared/context.ts @@ -185,6 +185,27 @@ function migrateV1ToV3(v1Config: PfConfigV1): PfConfig { return migrateV2ToV3(migrateV1ToV2(v1Config)); } +function parseKeyringPreference(value: string | undefined): boolean | undefined { + if (!value) return undefined; + switch (value.trim().toLowerCase()) { + case "0": + case "false": + case "off": + return false; + case "1": + case "true": + case "on": + return true; + default: + return true; + } +} + +async function shouldStoreTokensInKeyring(): Promise { + if (parseKeyringPreference(process.env.TAILOR_USE_KEYRING) === false) return false; + return await isKeyringAvailable(); +} + async function warnIfNewerConfigAvailable(config: { latest_version?: number; latest_min_sdk_version?: string; @@ -301,7 +322,7 @@ function toV1ForDisk(config: PfConfigV2): PfConfigV1 { * V1 and downgrading it would silently drop the user's login. V3 configs are * kept as V3 because user keys are canonical subject IDs and may include email * metadata that is not representable in older versions. - * Set TAILOR_USE_KEYRING to write V2 format unconditionally. + * Set TAILOR_USE_KEYRING=1 to write V2 format unconditionally. * * The config file may contain access/refresh tokens when the OS keyring is * unavailable, so it is written via {@link writeSecretFile} so other users @@ -312,10 +333,9 @@ export function writePlatformConfig(config: PfConfig | PfConfigV2 | PfConfigV1) const configPath = platformConfigPath(); const hasKeyringUser = config.version === 2 && Object.values(config.users).some((u) => u?.storage === "keyring"); + const forceV2DiskFormat = parseKeyringPreference(process.env.TAILOR_USE_KEYRING) === true; const diskConfig = - config.version === 2 && !process.env.TAILOR_USE_KEYRING && !hasKeyringUser - ? toV1ForDisk(config) - : config; + config.version === 2 && !forceV2DiskFormat && !hasKeyringUser ? toV1ForDisk(config) : config; writeSecretFile(configPath, stringifyYAML(diskConfig)); } @@ -550,7 +570,7 @@ export async function resolveTokens( } /** - * Save tokens for a user, writing to keyring or config as appropriate. + * Save tokens for a user, writing to keyring by default when available. * @param config - Platform config * @param user - User identifier * @param tokens - Token data to save @@ -567,7 +587,7 @@ export async function saveUserTokens( metadata?: { email?: string }, ): Promise { const email = metadata?.email ?? config.users[user]?.email; - if (process.env.TAILOR_USE_KEYRING && (await isKeyringAvailable())) { + if (await shouldStoreTokensInKeyring()) { await saveKeyringTokens(user, tokens); config.users[user] = { token_expires_at: expiresAt, From 0effd13b45c8082952a2fd92adf2cc81fe89e5f2 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 21:17:46 +0900 Subject: [PATCH 122/618] fix(cli): fall back when keyring storage fails --- packages/sdk/src/cli/shared/context.test.ts | 28 ++++++++++++++++++++- packages/sdk/src/cli/shared/context.ts | 23 +++++++++++++---- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/sdk/src/cli/shared/context.test.ts b/packages/sdk/src/cli/shared/context.test.ts index 2d4d19413..7c1ac6d74 100644 --- a/packages/sdk/src/cli/shared/context.test.ts +++ b/packages/sdk/src/cli/shared/context.test.ts @@ -15,11 +15,12 @@ import { } from "./context"; import { isCLIError } from "./errors"; import { logger } from "./logger"; -import { resetKeyringState } from "./token-store"; +import { isKeyringAvailable, resetKeyringState } from "./token-store"; import type * as ClientModule from "./client"; const xdgTempDir = vi.hoisted(() => `/tmp/tailor-xdg-${Date.now()}-${Math.random()}`); const keyringPasswords = vi.hoisted(() => new Map()); +const keyringSetPasswordFailure = vi.hoisted(() => ({ error: undefined as Error | undefined })); vi.mock("xdg-basedir", () => ({ xdgConfig: xdgTempDir, @@ -32,6 +33,7 @@ vi.mock("@napi-rs/keyring", () => ({ this.key = `${service}:${account}`; } setPassword(password: string) { + if (keyringSetPasswordFailure.error) throw keyringSetPasswordFailure.error; keyringPasswords.set(this.key, password); } getPassword(): string | null { @@ -63,6 +65,7 @@ beforeEach(() => { clientMocks.fetchUserInfo.mockReset(); clientMocks.refreshToken.mockReset(); keyringPasswords.clear(); + keyringSetPasswordFailure.error = undefined; }); describe("loadConfigPath", () => { @@ -958,6 +961,29 @@ describe("saveUserTokens", () => { expect(keyringPasswords.has("tailor-platform-cli:platform-user-sub")).toBe(false); }, ); + + test("falls back to the config file when keyring storage fails", async () => { + const config = createEmptyConfig(); + + expect(await isKeyringAvailable()).toBe(true); + keyringSetPasswordFailure.error = new Error("keyring denied"); + using warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); + + await saveUserTokens( + config, + "platform-user-sub", + { accessToken: "access-token", refreshToken: "refresh-token" }, + futureDate, + ); + + expect(config.users["platform-user-sub"]).toEqual({ + storage: "file", + access_token: "access-token", + refresh_token: "refresh-token", + token_expires_at: futureDate, + }); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("keyring denied")); + }); }); describe("V1 to V3 migration", () => { diff --git a/packages/sdk/src/cli/shared/context.ts b/packages/sdk/src/cli/shared/context.ts index bed968d3d..3f275dc15 100644 --- a/packages/sdk/src/cli/shared/context.ts +++ b/packages/sdk/src/cli/shared/context.ts @@ -102,6 +102,7 @@ type PfConfigV1 = z.output; type PfConfigV2 = z.output; type PfConfig = z.output; type PfUser = z.output; +type UserTokens = { accessToken: string; refreshToken?: string }; type LoadWorkspaceIdOptions = { workspaceId?: string; profile?: string; @@ -201,9 +202,22 @@ function parseKeyringPreference(value: string | undefined): boolean | undefined } } -async function shouldStoreTokensInKeyring(): Promise { +function formatUnknownError(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +async function trySaveTokensInKeyring(user: string, tokens: UserTokens): Promise { if (parseKeyringPreference(process.env.TAILOR_USE_KEYRING) === false) return false; - return await isKeyringAvailable(); + if (!(await isKeyringAvailable())) return false; + try { + await saveKeyringTokens(user, tokens); + return true; + } catch (error) { + logger.warn( + `System keyring failed to store credentials. Tokens will be stored in the config file. ${formatUnknownError(error)}`, + ); + return false; + } } async function warnIfNewerConfigAvailable(config: { @@ -582,13 +596,12 @@ export async function resolveTokens( export async function saveUserTokens( config: PfConfig, user: string, - tokens: { accessToken: string; refreshToken?: string }, + tokens: UserTokens, expiresAt: string, metadata?: { email?: string }, ): Promise { const email = metadata?.email ?? config.users[user]?.email; - if (await shouldStoreTokensInKeyring()) { - await saveKeyringTokens(user, tokens); + if (await trySaveTokensInKeyring(user, tokens)) { config.users[user] = { token_expires_at: expiresAt, storage: "keyring", From fbf6e591c621c3a27ba535ea8161cfb530025362 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 21:20:51 +0900 Subject: [PATCH 123/618] fix(sdk-codemod): handle namespace principal types --- packages/sdk-codemod/CHANGELOG.md | 2 +- .../v2/principal-unify/scripts/transform.ts | 112 ++++++++++++++---- .../tests/namespace-import/expected.ts | 19 +++ .../tests/namespace-import/input.ts | 19 +++ packages/sdk-codemod/package.json | 2 +- 5 files changed, 129 insertions(+), 25 deletions(-) diff --git a/packages/sdk-codemod/CHANGELOG.md b/packages/sdk-codemod/CHANGELOG.md index 04c95dd5f..955385eb7 100644 --- a/packages/sdk-codemod/CHANGELOG.md +++ b/packages/sdk-codemod/CHANGELOG.md @@ -1,6 +1,6 @@ # @tailor-platform/sdk-codemod -## 0.3.0-next.0 +## 0.4.0-next.0 ### Minor Changes diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 873e34c89..83464d911 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -304,13 +304,18 @@ function transformActorTypeComparisonLiterals( function transformTailorActorTypeInitializerLiterals( root: SgNode, actorTypeLocalNames: Set, + sdkNamespaceNames: Set, edits: Edit[], ): void { - if (actorTypeLocalNames.size === 0) return; + if (actorTypeLocalNames.size === 0 && sdkNamespaceNames.size === 0) return; const transformedLiteralStarts = new Set(); const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); for (const decl of declarators) { - if (!isTailorActorTypeReference(decl, actorTypeLocalNames)) continue; + if ( + !isSdkTypeReference(decl, actorTypeLocalNames, "TailorActorType", sdkNamespaceNames, root) + ) { + continue; + } const value = decl.field("value"); if (!value) continue; transformActorTypeLiteralsInNode(value, edits, transformedLiteralStarts); @@ -370,15 +375,21 @@ function transformActorTypeBindingComparisons( function transformTailorActorTypeBindingComparisons( root: SgNode, actorTypeLocalNames: Set, + sdkNamespaceNames: Set, edits: Edit[], ): void { - if (actorTypeLocalNames.size === 0) return; + if (actorTypeLocalNames.size === 0 && sdkNamespaceNames.size === 0) return; for (const kind of NESTED_FN_KINDS) { const fns = root.findAll({ rule: { kind } }); for (const fn of fns) { const param = getFirstFunctionParam(fn); - if (!param || !isTailorActorTypeReference(param, actorTypeLocalNames)) continue; + if ( + !param || + !isSdkTypeReference(param, actorTypeLocalNames, "TailorActorType", sdkNamespaceNames, root) + ) { + continue; + } const pattern = getFunctionParamPattern(param); const body = fn.field("body"); if (!pattern || pattern.kind() !== "identifier" || !body) continue; @@ -392,7 +403,11 @@ function transformTailorActorTypeBindingComparisons( const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); for (const decl of declarators) { - if (!isTailorActorTypeReference(decl, actorTypeLocalNames)) continue; + if ( + !isSdkTypeReference(decl, actorTypeLocalNames, "TailorActorType", sdkNamespaceNames, root) + ) { + continue; + } const name = decl.field("name"); if (!name || name.kind() !== "identifier") continue; transformActorTypeBindingComparisons( @@ -426,26 +441,43 @@ function transformActorBindingMemberAccesses( } } -function isTailorActorTypeReference(node: SgNode, actorTypeLocalNames: Set): boolean { +function isSdkTypeReference( + node: SgNode, + localNames: Set, + sdkTypeName: string, + sdkNamespaceNames: Set, + root: SgNode, +): boolean { const typeAnnotation = node.field("type"); if (!typeAnnotation) return false; const typeIds = typeAnnotation.findAll({ rule: { kind: "type_identifier" } }); - return typeIds.some((id) => actorTypeLocalNames.has(id.text())); + return typeIds.some( + (id) => + localNames.has(id.text()) || + (id.text() === sdkTypeName && + isSdkNamespaceQualifiedTypeIdentifier(id, sdkNamespaceNames, root)), + ); } function transformTailorActorTypedMemberAccesses( root: SgNode, actorTypeLocalNames: Set, + sdkNamespaceNames: Set, edits: Edit[], transformedActorPropertyStarts: Set, ): void { - if (actorTypeLocalNames.size === 0) return; + if (actorTypeLocalNames.size === 0 && sdkNamespaceNames.size === 0) return; for (const kind of NESTED_FN_KINDS) { const fns = root.findAll({ rule: { kind } }); for (const fn of fns) { const param = getFirstFunctionParam(fn); - if (!param || !isTailorActorTypeReference(param, actorTypeLocalNames)) continue; + if ( + !param || + !isSdkTypeReference(param, actorTypeLocalNames, "TailorActor", sdkNamespaceNames, root) + ) { + continue; + } const pattern = getFunctionParamPattern(param); const body = fn.field("body"); if (!pattern || pattern.kind() !== "identifier" || !body) continue; @@ -460,7 +492,9 @@ function transformTailorActorTypedMemberAccesses( const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); for (const decl of declarators) { - if (!isTailorActorTypeReference(decl, actorTypeLocalNames)) continue; + if (!isSdkTypeReference(decl, actorTypeLocalNames, "TailorActor", sdkNamespaceNames, root)) { + continue; + } const name = decl.field("name"); if (!name || name.kind() !== "identifier") continue; transformActorBindingMemberAccesses( @@ -909,10 +943,28 @@ function isSdkNamespaceQualifiedTypeIdentifier( return !!namespaceObject && isUnshadowedNamespaceObject(namespaceObject, namespaceNames, root); } -function renamedQualifiedTypeIdentifierText(name: string): string | null { - if (name === "TailorInvoker") return "TailorPrincipal | null"; - if (name === "TailorActorType") return 'TailorPrincipal["type"] | undefined'; - return TYPE_RENAME_MAP[name] ?? null; +function namespaceQualifiedTypeReplacement( + typeId: SgNode, + namespaceNames: Set, + root: SgNode, +): { target: SgNode; text: string } | null { + if (typeId.kind() !== "type_identifier") return null; + const parent = typeId.parent(); + if (!parent || parent.kind() !== "nested_type_identifier") return null; + const namespaceObject = parent.children().find((c: SgNode) => c.kind() === "identifier"); + if (!namespaceObject || !isUnshadowedNamespaceObject(namespaceObject, namespaceNames, root)) { + return null; + } + + const namespace = namespaceObject.text(); + if (typeId.text() === "TailorInvoker") { + return { target: parent, text: `(${namespace}.TailorPrincipal | null)` }; + } + if (typeId.text() === "TailorActorType") { + return { target: parent, text: `(${namespace}.TailorPrincipal["type"] | undefined)` }; + } + const renamed = TYPE_RENAME_MAP[typeId.text()]; + return renamed ? { target: typeId, text: renamed } : null; } /** @@ -2182,11 +2234,22 @@ export default function transform(source: string): string | null { transformTailorActorTypedMemberAccesses( tree, actorTypeLocalNames, + sdkNamespaceNames, edits, transformedActorPropertyStarts, ); - transformTailorActorTypeInitializerLiterals(tree, actorTypeValueLocalNames, edits); - transformTailorActorTypeBindingComparisons(tree, actorTypeValueLocalNames, edits); + transformTailorActorTypeInitializerLiterals( + tree, + actorTypeValueLocalNames, + sdkNamespaceNames, + edits, + ); + transformTailorActorTypeBindingComparisons( + tree, + actorTypeValueLocalNames, + sdkNamespaceNames, + edits, + ); let importRemoved = false; const globalEmittedRenamed = new Set(); @@ -2390,15 +2453,18 @@ export default function transform(source: string): string | null { }); for (const id of typeIdents) { if (typeContext.transformedPrincipalTypeStarts.has(id.range().start.index)) continue; + const qualifiedReplacement = namespaceQualifiedTypeReplacement(id, sdkNamespaceNames, tree); + if (qualifiedReplacement) { + edits.push(qualifiedReplacement.target.replace(qualifiedReplacement.text)); + continue; + } const newName = sdkRenameSourceNames.has(id.text()) ? renamedTypeIdentifierText(id.text()) - : isSdkNamespaceQualifiedTypeIdentifier(id, sdkNamespaceNames, tree) - ? renamedQualifiedTypeIdentifierText(id.text()) - : nullableInvokerAliasLocalNames.has(id.text()) - ? renamedTypeIdentifierText("TailorInvoker") - : actorTypeAliasLocalNames.has(id.text()) - ? renamedTypeIdentifierText("TailorActorType") - : null; + : nullableInvokerAliasLocalNames.has(id.text()) + ? renamedTypeIdentifierText("TailorInvoker") + : actorTypeAliasLocalNames.has(id.text()) + ? renamedTypeIdentifierText("TailorActorType") + : null; if (!newName) continue; edits.push(id.replace(newName)); } diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/expected.ts index 68ad8eb27..e49dc3660 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/expected.ts @@ -1,11 +1,29 @@ import * as sdk from "@tailor-platform/sdk"; type User = sdk.TailorPrincipal; +type Invokers = (sdk.TailorPrincipal | null)[]; const role = sdk.db.string().hooks({ create: ({ invoker }: { invoker: sdk.TailorPrincipal | null }) => invoker?.id ?? "anonymous", }); +export const actorTypeValue: (sdk.TailorPrincipal["type"] | undefined) = "user"; +export const allowedActorTypes: (sdk.TailorPrincipal["type"] | undefined)[] = [ + "user", + "machine_user", +]; + +export function isMachineUser(type: (sdk.TailorPrincipal["type"] | undefined)) { + return type === "machine_user"; +} + +export function actorFields(actor: sdk.TailorPrincipal | null) { + return { + id: actor?.id, + type: actor?.type, + }; +} + export default sdk.createResolver({ name: "n", operation: "query", @@ -17,3 +35,4 @@ export default sdk.createResolver({ }); export const helper = (u: User) => role.parse({ value: u.id, data: {}, invoker: null }); +export const invokers: Invokers = []; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/input.ts index 2511b6a68..884d29e5b 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/namespace-import/input.ts @@ -1,11 +1,29 @@ import * as sdk from "@tailor-platform/sdk"; type User = sdk.TailorUser; +type Invokers = sdk.TailorInvoker[]; const role = sdk.db.string().hooks({ create: ({ user }: { user: sdk.TailorUser | null }) => user?.id ?? "anonymous", }); +export const actorTypeValue: sdk.TailorActorType = "USER_TYPE_USER"; +export const allowedActorTypes: sdk.TailorActorType[] = [ + "USER_TYPE_USER", + "USER_TYPE_MACHINE_USER", +]; + +export function isMachineUser(type: sdk.TailorActorType) { + return type === "USER_TYPE_MACHINE_USER"; +} + +export function actorFields(actor: sdk.TailorActor | null) { + return { + id: actor?.userId, + type: actor?.userType, + }; +} + export default sdk.createResolver({ name: "n", operation: "query", @@ -17,3 +35,4 @@ export default sdk.createResolver({ }); export const helper = (u: User) => role.parse({ value: u.id, data: {}, user: null }); +export const invokers: Invokers = []; diff --git a/packages/sdk-codemod/package.json b/packages/sdk-codemod/package.json index 74e823117..83ccbb6d6 100644 --- a/packages/sdk-codemod/package.json +++ b/packages/sdk-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk-codemod", - "version": "0.3.0-next.0", + "version": "0.4.0-next.0", "description": "Codemod runner for Tailor Platform SDK upgrades", "license": "MIT", "repository": { From 3afb481a17a648176bf94ace56314187dc982ef8 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 21:25:13 +0900 Subject: [PATCH 124/618] test(cli): update profile creation keyring expectation --- .../src/cli/commands/profile/create.test.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/sdk/src/cli/commands/profile/create.test.ts b/packages/sdk/src/cli/commands/profile/create.test.ts index a132006ab..df94a9cb7 100644 --- a/packages/sdk/src/cli/commands/profile/create.test.ts +++ b/packages/sdk/src/cli/commands/profile/create.test.ts @@ -9,17 +9,26 @@ import { createCommand } from "./create"; import type * as ClientModule from "@/cli/shared/client"; const xdgTempDir = vi.hoisted(() => `/tmp/tailor-profile-create-${Date.now()}-${Math.random()}`); +const keyringPasswords = vi.hoisted(() => new Map()); const validUUID = "12345678-1234-4abc-8def-123456789012"; vi.mock("xdg-basedir", () => ({ xdgConfig: xdgTempDir })); vi.mock("@napi-rs/keyring", () => ({ Entry: class { - setPassword() {} + private key: string; + constructor(service: string, account: string) { + this.key = `${service}:${account}`; + } + setPassword(password: string) { + keyringPasswords.set(this.key, password); + } getPassword(): string | null { - return null; + return keyringPasswords.get(this.key) ?? null; + } + deletePassword() { + keyringPasswords.delete(this.key); } - deletePassword() {} }, })); @@ -53,6 +62,7 @@ describe("profile create with a migrating legacy email user", () => { beforeEach(() => { vi.clearAllMocks(); resetKeyringState(); + keyringPasswords.clear(); vi.stubEnv("TAILOR_PLATFORM_PROFILE", undefined); }); @@ -105,7 +115,10 @@ describe("profile create with a migrating legacy email user", () => { const config = await readPlatformConfig(); expect(config.profiles.myprofile?.user).toBe("platform-user-sub"); - expect(config.users["platform-user-sub"]).toMatchObject({ storage: "file" }); + expect(config.users["platform-user-sub"]).toMatchObject({ storage: "keyring" }); + expect(keyringPasswords.get("tailor-platform-cli:platform-user-sub")).toBe( + JSON.stringify({ accessToken: "new-access-token", refreshToken: "new-refresh-token" }), + ); expect(config.users["legacy@example.com"]).toBeUndefined(); }); }); From e246983b14cb245f9882301df8026af3d70539f1 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 21:37:02 +0900 Subject: [PATCH 125/618] fix(cli): clear stale keyring tokens on fallback --- .../src/cli/commands/profile/create.test.ts | 1 + packages/sdk/src/cli/shared/context.test.ts | 33 +++++++++++++++++++ packages/sdk/src/cli/shared/context.ts | 3 ++ 3 files changed, 37 insertions(+) diff --git a/packages/sdk/src/cli/commands/profile/create.test.ts b/packages/sdk/src/cli/commands/profile/create.test.ts index df94a9cb7..5038e6ac2 100644 --- a/packages/sdk/src/cli/commands/profile/create.test.ts +++ b/packages/sdk/src/cli/commands/profile/create.test.ts @@ -64,6 +64,7 @@ describe("profile create with a migrating legacy email user", () => { resetKeyringState(); keyringPasswords.clear(); vi.stubEnv("TAILOR_PLATFORM_PROFILE", undefined); + vi.stubEnv("TAILOR_USE_KEYRING", undefined); }); afterEach(() => { diff --git a/packages/sdk/src/cli/shared/context.test.ts b/packages/sdk/src/cli/shared/context.test.ts index 7c1ac6d74..4160cecd6 100644 --- a/packages/sdk/src/cli/shared/context.test.ts +++ b/packages/sdk/src/cli/shared/context.test.ts @@ -511,6 +511,7 @@ describe("loadAccessToken", () => { vi.stubEnv("TAILOR_PLATFORM_TOKEN", undefined); vi.stubEnv("TAILOR_TOKEN", undefined); vi.stubEnv("TAILOR_PLATFORM_PROFILE", undefined); + vi.stubEnv("TAILOR_USE_KEYRING", undefined); writePlatformConfig({ version: 2, min_sdk_version: "1.29.0", @@ -984,6 +985,38 @@ describe("saveUserTokens", () => { }); expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("keyring denied")); }); + + test("deletes stale keyring tokens when falling back to the config file", async () => { + const config = createEmptyConfig(); + config.users["platform-user-sub"] = { + storage: "keyring", + token_expires_at: futureDate, + }; + keyringPasswords.set( + "tailor-platform-cli:platform-user-sub", + JSON.stringify({ accessToken: "stale-access-token", refreshToken: "stale-refresh-token" }), + ); + + expect(await isKeyringAvailable()).toBe(true); + keyringSetPasswordFailure.error = new Error("keyring denied"); + using warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); + + await saveUserTokens( + config, + "platform-user-sub", + { accessToken: "access-token", refreshToken: "refresh-token" }, + futureDate, + ); + + expect(config.users["platform-user-sub"]).toEqual({ + storage: "file", + access_token: "access-token", + refresh_token: "refresh-token", + token_expires_at: futureDate, + }); + expect(keyringPasswords.has("tailor-platform-cli:platform-user-sub")).toBe(false); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("keyring denied")); + }); }); describe("V1 to V3 migration", () => { diff --git a/packages/sdk/src/cli/shared/context.ts b/packages/sdk/src/cli/shared/context.ts index 3f275dc15..cfd8cbae8 100644 --- a/packages/sdk/src/cli/shared/context.ts +++ b/packages/sdk/src/cli/shared/context.ts @@ -608,6 +608,9 @@ export async function saveUserTokens( ...(email ? { email } : {}), }; } else { + if (config.users[user]?.storage === "keyring") { + await deleteKeyringTokens(user); + } config.users[user] = { access_token: tokens.accessToken, refresh_token: tokens.refreshToken, From da08e1301254693ad2d6f8d7f982f9d2f94fa8f7 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 21:38:08 +0900 Subject: [PATCH 126/618] fix(cli): resolve workspace profile users by email --- .../src/cli/commands/workspace/create.test.ts | 36 +++++++++++++++++++ .../sdk/src/cli/commands/workspace/create.ts | 16 ++++++--- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/cli/commands/workspace/create.test.ts b/packages/sdk/src/cli/commands/workspace/create.test.ts index ee9388af8..eeef55f48 100644 --- a/packages/sdk/src/cli/commands/workspace/create.test.ts +++ b/packages/sdk/src/cli/commands/workspace/create.test.ts @@ -48,6 +48,24 @@ function seedConfig() { }); } +function seedV3Config() { + writePlatformConfig({ + version: 3, + min_sdk_version: "2.0.0", + users: { + "user-subject-1": { + storage: "file", + token_expires_at: "2099-12-31T00:00:00Z", + access_token: "mock-token", + refresh_token: undefined, + email: "u@example.com", + }, + }, + profiles: {}, + current_user: "user-subject-1", + }); +} + function stubClient() { vi.mocked(initOperatorClient).mockResolvedValue({ listAvailableWorkspaceRegions: vi.fn().mockResolvedValue({ regions: ["us-west"] }), @@ -129,6 +147,24 @@ describe("workspace create --permission", () => { expect(config.profiles.bootstrap?.readonly).toBeUndefined(); }); + test("stores the resolved user key when --profile-user is an email in v3 config", async () => { + seedV3Config(); + using _logger = silenceLogger("out", "success", "warn"); + await runCommand(createCommand, [ + "--name", + "test-ws", + "--region", + "us-west", + "--profile-name", + "bootstrap", + "--profile-user", + "u@example.com", + ]); + + const config = await readPlatformConfig(); + expect(config.profiles.bootstrap?.user).toBe("user-subject-1"); + }); + test("creates no profile when --permission read is passed without --profile-name", async () => { using _logger = silenceLogger("out", "success", "warn"); // Matches the existing --profile-user behavior: profile-only flags are diff --git a/packages/sdk/src/cli/commands/workspace/create.ts b/packages/sdk/src/cli/commands/workspace/create.ts index 130d45176..ba42338bd 100644 --- a/packages/sdk/src/cli/commands/workspace/create.ts +++ b/packages/sdk/src/cli/commands/workspace/create.ts @@ -2,7 +2,12 @@ import { arg } from "politty"; import { z } from "zod"; import { initOperatorClient, type OperatorClient } from "@/cli/shared/client"; import { defineAppCommand } from "@/cli/shared/command"; -import { loadAccessToken, readPlatformConfig, writePlatformConfig } from "@/cli/shared/context"; +import { + findConfigUserKey, + loadAccessToken, + readPlatformConfig, + writePlatformConfig, +} from "@/cli/shared/context"; import { logger } from "@/cli/shared/logger"; import { assertWritable } from "@/cli/shared/readonly-guard"; import { assertDefined } from "@/utils/assert"; @@ -139,16 +144,17 @@ export const createCommand = defineAppCommand({ throw new Error(`Profile "${profileName}" already exists.`); } - const profileUser = args["profile-user"] || config.current_user; - if (!profileUser) { + const requestedProfileUser = args["profile-user"] || config.current_user; + if (!requestedProfileUser) { throw new Error( "Current user not found. Please login or specify --profile-user to create a profile.", ); } - if (!config.users[profileUser]) { + const profileUser = findConfigUserKey(config, requestedProfileUser); + if (!profileUser || !config.users[profileUser]) { throw new Error( - `User "${profileUser}" not found.\nPlease verify your user name and login using 'tailor-sdk login' command.`, + `User "${requestedProfileUser}" not found.\nPlease verify your user name and login using 'tailor-sdk login' command.`, ); } config.profiles[profileName] = { From 21e474964fe9ae376cd9fbe208ffdc3d6c128d6e Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 22:03:53 +0900 Subject: [PATCH 127/618] ci: sync docs consistency workflow --- .github/workflows/docs-consistency-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs-consistency-check.yml b/.github/workflows/docs-consistency-check.yml index 116b3d55e..bef2748f5 100644 --- a/.github/workflows/docs-consistency-check.yml +++ b/.github/workflows/docs-consistency-check.yml @@ -37,7 +37,7 @@ jobs: persist-credentials: false - name: Check docs-implementation consistency - uses: anthropics/claude-code-action@4d7e1f0cd85743fdc93b1c8040ab54395da024e2 # v1.0.149 + uses: anthropics/claude-code-action@9dd8b95a392eb34b6f5fb56cf5a64cb735912d4b # v1.0.150 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} prompt: | From 32d4d72dd464b94e7c0a3d83b24230a170bd84c0 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 22:23:54 +0900 Subject: [PATCH 128/618] fix(cli): remove keyring env override --- .changeset/keyring-default-storage.md | 2 +- packages/sdk/docs/cli-reference.md | 4 +--- .../src/cli/commands/profile/create.test.ts | 1 - packages/sdk/src/cli/shared/context.test.ts | 19 ++++++++-------- packages/sdk/src/cli/shared/context.ts | 22 +------------------ 5 files changed, 12 insertions(+), 36 deletions(-) diff --git a/.changeset/keyring-default-storage.md b/.changeset/keyring-default-storage.md index c225600c5..b3beffd46 100644 --- a/.changeset/keyring-default-storage.md +++ b/.changeset/keyring-default-storage.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk": major --- -Store CLI login tokens in the OS keyring by default when available. Set `TAILOR_USE_KEYRING=0`, `false`, or `off` to keep file-based token storage. +Store CLI login tokens in the OS keyring by default when available. If the keyring is unavailable, tokens are stored in the platform config file. diff --git a/packages/sdk/docs/cli-reference.md b/packages/sdk/docs/cli-reference.md index 2c96a61d1..819541ed5 100644 --- a/packages/sdk/docs/cli-reference.md +++ b/packages/sdk/docs/cli-reference.md @@ -72,7 +72,6 @@ You can use environment variables to configure workspace and authentication: | `TAILOR_PLATFORM_FOLDER_ID` | Folder ID for folder commands | | `TAILOR_PLATFORM_TOKEN` | Authentication token (alternative to `login`) | | `TAILOR_TOKEN` | **Deprecated.** Use `TAILOR_PLATFORM_TOKEN` instead | -| `TAILOR_USE_KEYRING` | Set to `0`, `false`, or `off` to disable OS keyring token storage | | `TAILOR_PLATFORM_PROFILE` | Workspace profile name | | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | Path to SDK config file | | `TAILOR_PLATFORM_SDK_DTS_PATH` | Output path for generated `tailor.d.ts` type definition file | @@ -95,8 +94,7 @@ Token resolution follows this priority order: `tailor-sdk login` stores local CLI login tokens in the OS keyring by default when available. If the keyring is unavailable, tokens are stored in the platform -config file. Set `TAILOR_USE_KEYRING=0`, `false`, or `off` to force file-based -token storage. +config file. ### Workspace ID Priority diff --git a/packages/sdk/src/cli/commands/profile/create.test.ts b/packages/sdk/src/cli/commands/profile/create.test.ts index 5038e6ac2..df94a9cb7 100644 --- a/packages/sdk/src/cli/commands/profile/create.test.ts +++ b/packages/sdk/src/cli/commands/profile/create.test.ts @@ -64,7 +64,6 @@ describe("profile create with a migrating legacy email user", () => { resetKeyringState(); keyringPasswords.clear(); vi.stubEnv("TAILOR_PLATFORM_PROFILE", undefined); - vi.stubEnv("TAILOR_USE_KEYRING", undefined); }); afterEach(() => { diff --git a/packages/sdk/src/cli/shared/context.test.ts b/packages/sdk/src/cli/shared/context.test.ts index 4160cecd6..1df3cda1c 100644 --- a/packages/sdk/src/cli/shared/context.test.ts +++ b/packages/sdk/src/cli/shared/context.test.ts @@ -511,7 +511,6 @@ describe("loadAccessToken", () => { vi.stubEnv("TAILOR_PLATFORM_TOKEN", undefined); vi.stubEnv("TAILOR_TOKEN", undefined); vi.stubEnv("TAILOR_PLATFORM_PROFILE", undefined); - vi.stubEnv("TAILOR_USE_KEYRING", undefined); writePlatformConfig({ version: 2, min_sdk_version: "1.29.0", @@ -912,7 +911,6 @@ describe("saveUserTokens", () => { vi.resetModules(); resetKeyringState(); process.env = { ...originalEnv }; - delete process.env.TAILOR_USE_KEYRING; }); afterEach(() => { @@ -941,7 +939,7 @@ describe("saveUserTokens", () => { }); test.each(["0", "false", "off"])( - "stores tokens in the config file when TAILOR_USE_KEYRING=%s", + "ignores TAILOR_USE_KEYRING=%s and stores tokens in the OS keyring", async (value) => { process.env.TAILOR_USE_KEYRING = value; const config = createEmptyConfig(); @@ -954,12 +952,12 @@ describe("saveUserTokens", () => { ); expect(config.users["platform-user-sub"]).toEqual({ - storage: "file", - access_token: "access-token", - refresh_token: "refresh-token", + storage: "keyring", token_expires_at: futureDate, }); - expect(keyringPasswords.has("tailor-platform-cli:platform-user-sub")).toBe(false); + expect(keyringPasswords.get("tailor-platform-cli:platform-user-sub")).toBe( + JSON.stringify({ accessToken: "access-token", refreshToken: "refresh-token" }), + ); }, ); @@ -1075,14 +1073,13 @@ describe("keyring user persistence on V2 -> V1 downgrade", () => { vi.resetModules(); resetKeyringState(); process.env = { ...originalEnv }; - delete process.env.TAILOR_USE_KEYRING; }); afterEach(() => { process.env = originalEnv; }); - test("keeps the config V2 and preserves the keyring user when written without TAILOR_USE_KEYRING", async () => { + test("keeps the config V2 and preserves the keyring user", async () => { writePlatformConfig({ version: 2, min_sdk_version: "1.29.0", @@ -1154,7 +1151,9 @@ describe("keyring user persistence on V2 -> V1 downgrade", () => { expect(diskConfig.current_user).toBe("platform-user-sub"); }); - test("still downgrades a file-only config to V1 for backward compatibility", () => { + test("ignores TAILOR_USE_KEYRING and still downgrades a file-only config to V1", () => { + process.env.TAILOR_USE_KEYRING = "1"; + writePlatformConfig({ version: 2, min_sdk_version: "1.29.0", diff --git a/packages/sdk/src/cli/shared/context.ts b/packages/sdk/src/cli/shared/context.ts index cfd8cbae8..b2ce5d17f 100644 --- a/packages/sdk/src/cli/shared/context.ts +++ b/packages/sdk/src/cli/shared/context.ts @@ -186,28 +186,11 @@ function migrateV1ToV3(v1Config: PfConfigV1): PfConfig { return migrateV2ToV3(migrateV1ToV2(v1Config)); } -function parseKeyringPreference(value: string | undefined): boolean | undefined { - if (!value) return undefined; - switch (value.trim().toLowerCase()) { - case "0": - case "false": - case "off": - return false; - case "1": - case "true": - case "on": - return true; - default: - return true; - } -} - function formatUnknownError(error: unknown): string { return error instanceof Error ? error.message : String(error); } async function trySaveTokensInKeyring(user: string, tokens: UserTokens): Promise { - if (parseKeyringPreference(process.env.TAILOR_USE_KEYRING) === false) return false; if (!(await isKeyringAvailable())) return false; try { await saveKeyringTokens(user, tokens); @@ -336,7 +319,6 @@ function toV1ForDisk(config: PfConfigV2): PfConfigV1 { * V1 and downgrading it would silently drop the user's login. V3 configs are * kept as V3 because user keys are canonical subject IDs and may include email * metadata that is not representable in older versions. - * Set TAILOR_USE_KEYRING=1 to write V2 format unconditionally. * * The config file may contain access/refresh tokens when the OS keyring is * unavailable, so it is written via {@link writeSecretFile} so other users @@ -347,9 +329,7 @@ export function writePlatformConfig(config: PfConfig | PfConfigV2 | PfConfigV1) const configPath = platformConfigPath(); const hasKeyringUser = config.version === 2 && Object.values(config.users).some((u) => u?.storage === "keyring"); - const forceV2DiskFormat = parseKeyringPreference(process.env.TAILOR_USE_KEYRING) === true; - const diskConfig = - config.version === 2 && !forceV2DiskFormat && !hasKeyringUser ? toV1ForDisk(config) : config; + const diskConfig = config.version === 2 && !hasKeyringUser ? toV1ForDisk(config) : config; writeSecretFile(configPath, stringifyYAML(diskConfig)); } From 6234022d7dc03813b8dade831b86f63a5f7a20e6 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 22:36:09 +0900 Subject: [PATCH 129/618] feat(codemod): generate v2 migration guide from the codemod registry Add a generator that renders packages/sdk/docs/migration/v2.md from the codemod registry (the single source of truth). Each entry renders its name, automation level (Automatic / Partially automatic / Manual, derived from whether it has a transform and residual signals), description, optional before/after examples, and the LLM/manual prompt for cases the codemods cannot fully migrate on their own. - Add an optional `examples` field (before/after pairs) to CodemodPackage. - Add `pnpm codemod:docs:update` / `codemod:docs:check` (the latter wired into `pnpm check` via `check:codemod-docs`), mirroring `agent:rules`. - Prompts render as fenced text blocks so the doc stays formatter-stable and prompts are not constrained to Markdown. --- .changeset/codemod-migration-docs.md | 6 + package.json | 3 + .../sdk-codemod/scripts/sync-codemod-docs.ts | 35 ++++++ .../sdk-codemod/src/migration-doc.test.ts | 61 ++++++++++ packages/sdk-codemod/src/migration-doc.ts | 74 ++++++++++++ packages/sdk-codemod/src/registry.ts | 9 +- packages/sdk-codemod/src/types.ts | 12 ++ packages/sdk/docs/migration/v2.md | 114 ++++++++++++++++++ 8 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 .changeset/codemod-migration-docs.md create mode 100644 packages/sdk-codemod/scripts/sync-codemod-docs.ts create mode 100644 packages/sdk-codemod/src/migration-doc.test.ts create mode 100644 packages/sdk-codemod/src/migration-doc.ts create mode 100644 packages/sdk/docs/migration/v2.md diff --git a/.changeset/codemod-migration-docs.md b/.changeset/codemod-migration-docs.md new file mode 100644 index 000000000..1e54130ce --- /dev/null +++ b/.changeset/codemod-migration-docs.md @@ -0,0 +1,6 @@ +--- +"@tailor-platform/sdk-codemod": patch +"@tailor-platform/sdk": patch +--- + +Generate the v2 migration guide (`packages/sdk/docs/migration/v2.md`) from the codemod registry, which is the single source of truth. Each entry renders its name, automation level (Automatic / Partially automatic / Manual), description, optional before/after `examples`, and — for changes the codemods cannot fully migrate on their own — the LLM/manual migration prompt. Run `pnpm codemod:docs:update` to regenerate and `pnpm codemod:docs:check` (wired into `pnpm check`) to verify it is in sync. diff --git a/package.json b/package.json index 79376b5d5..7526241eb 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,11 @@ "deploy": "pnpm run build && pnpm -r run deploy", "agent:rules:update": "node scripts/sync-agent-rules.mjs --write", "agent:rules:check": "node scripts/sync-agent-rules.mjs --check", + "codemod:docs:update": "tsx packages/sdk-codemod/scripts/sync-codemod-docs.ts --write", + "codemod:docs:check": "tsx packages/sdk-codemod/scripts/sync-codemod-docs.ts --check", "check": "pnpm run build && pnpm --filter @tailor-platform/sdk --filter example run generate && pnpm run format && pnpm run \"/^check:/\"", "check:agent-rules": "pnpm run agent:rules:check", + "check:codemod-docs": "pnpm run codemod:docs:check", "check:lint": "pnpm -r run lint:fix && pnpm run lint:vitest", "check:typecheck": "pnpm -r run typecheck:go", "check:knip": "pnpm -r run knip", diff --git a/packages/sdk-codemod/scripts/sync-codemod-docs.ts b/packages/sdk-codemod/scripts/sync-codemod-docs.ts new file mode 100644 index 000000000..b6df51933 --- /dev/null +++ b/packages/sdk-codemod/scripts/sync-codemod-docs.ts @@ -0,0 +1,35 @@ +#!/usr/bin/env tsx +import { readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { renderMigrationDoc } from "../src/migration-doc"; +import { allCodemods } from "../src/registry"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +// packages/sdk-codemod/scripts -> packages/sdk/docs/migration/v2.md +const docPath = resolve(scriptDir, "../../sdk/docs/migration/v2.md"); + +function parseMode(args: string[]): "write" | "check" { + const modes = args.filter((arg) => arg === "--check" || arg === "--write"); + if (modes.length !== 1 || modes.length !== args.length) { + process.stderr.write("Usage: tsx scripts/sync-codemod-docs.ts --check|--write\n"); + process.exit(2); + } + return modes[0] === "--write" ? "write" : "check"; +} + +const mode = parseMode(process.argv.slice(2)); +const expected = renderMigrationDoc(allCodemods); + +if (mode === "write") { + await writeFile(docPath, expected, "utf-8"); + process.stderr.write(`Wrote ${docPath}\n`); +} else { + const actual = await readFile(docPath, "utf-8").catch(() => null); + if (actual !== expected) { + process.stderr.write( + "Migration doc is out of date. Run `pnpm codemod:docs:update` to regenerate it.\n", + ); + process.exit(1); + } +} diff --git a/packages/sdk-codemod/src/migration-doc.test.ts b/packages/sdk-codemod/src/migration-doc.test.ts new file mode 100644 index 000000000..c5e9c0dc9 --- /dev/null +++ b/packages/sdk-codemod/src/migration-doc.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, test } from "vitest"; +import { automationLevel, renderMigrationDoc } from "./migration-doc"; +import type { CodemodPackage } from "./types"; + +function makeCodemod(overrides: Partial): CodemodPackage { + return { + id: "v2/example", + name: "Example", + description: "Example change.", + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/example/scripts/transform.js", + ...overrides, + }; +} + +describe("automationLevel", () => { + test("transform with no residual signals is Automatic", () => { + expect(automationLevel(makeCodemod({}))).toBe("Automatic"); + }); + + test("transform with legacyPatterns/suspiciousPatterns/prompt is Partially automatic", () => { + expect(automationLevel(makeCodemod({ legacyPatterns: ["x"] }))).toBe("Partially automatic"); + expect(automationLevel(makeCodemod({ suspiciousPatterns: ["x"], prompt: "do it" }))).toBe( + "Partially automatic", + ); + }); + + test("no scriptPath is Manual", () => { + expect(automationLevel(makeCodemod({ scriptPath: undefined as unknown as string }))).toBe( + "Manual", + ); + }); +}); + +describe("renderMigrationDoc", () => { + test("renders heading, automation level, examples, and prompt", () => { + const doc = renderMigrationDoc([ + makeCodemod({ + name: "executeScript arg", + description: "Pass a value, not a string.", + suspiciousPatterns: ["executeScript"], + prompt: "Unwrap JSON.stringify in the arg.", + examples: [{ before: "arg: JSON.stringify(x)", after: "arg: x" }], + }), + ]); + + expect(doc).toContain("# Migrating to v2"); + expect(doc).toContain("## executeScript arg"); + expect(doc).toContain("**Migration:** Partially automatic"); + expect(doc).toContain("// Before\narg: JSON.stringify(x)"); + expect(doc).toContain("// After\narg: x"); + expect(doc).toContain("```text\nUnwrap JSON.stringify in the arg.\n```"); + }); + + test("omits the prompt block for Automatic entries", () => { + const doc = renderMigrationDoc([makeCodemod({ name: "Auto", description: "Auto change." })]); + expect(doc).toContain("**Migration:** Automatic"); + expect(doc).not.toContain("How to finish"); + }); +}); diff --git a/packages/sdk-codemod/src/migration-doc.ts b/packages/sdk-codemod/src/migration-doc.ts new file mode 100644 index 000000000..a6cffe4f7 --- /dev/null +++ b/packages/sdk-codemod/src/migration-doc.ts @@ -0,0 +1,74 @@ +import type { CodemodPackage } from "./types"; + +export type AutomationLevel = "Automatic" | "Partially automatic" | "Manual"; + +/** + * Classify how much of a migration the codemod automates. + * - `Automatic`: a transform fully covers it, with no residual to flag. + * - `Partially automatic`: a transform covers the common cases but flags + * residuals (via `legacyPatterns`/`suspiciousPatterns`/`prompt`) to finish. + * - `Manual`: no transform; the change is migrated by hand (optionally guided + * by a `prompt`). Whether a person or an LLM does it does not matter here. + * @param codemod - The codemod registry entry + * @returns The automation level + */ +export function automationLevel(codemod: CodemodPackage): AutomationLevel { + if (!codemod.scriptPath) return "Manual"; + const flagsResidual = + (codemod.legacyPatterns?.length ?? 0) > 0 || + (codemod.suspiciousPatterns?.length ?? 0) > 0 || + codemod.prompt != null; + return flagsResidual ? "Partially automatic" : "Automatic"; +} + +function renderEntry(codemod: CodemodPackage): string { + const lines: string[] = [ + `## ${codemod.name}`, + "", + `**Migration:** ${automationLevel(codemod)}`, + "", + ]; + lines.push(codemod.description, ""); + + for (const example of codemod.examples ?? []) { + if (example.caption) lines.push(example.caption, ""); + lines.push("```ts", "// Before", example.before, "", "// After", example.after, "```", ""); + } + + if (automationLevel(codemod) !== "Automatic" && codemod.prompt != null) { + lines.push( + "How to finish the cases the codemod cannot migrate on its own:", + "", + "```text", + codemod.prompt.trim(), + "```", + "", + ); + } + + return lines.join("\n"); +} + +/** + * Render the v2 migration guide from the codemod registry. The registry is the + * single source of truth; missing detail is added to the codemod definitions. + * @param codemods - All registered codemods, in registration order + * @returns The migration guide as Markdown + */ +export function renderMigrationDoc(codemods: CodemodPackage[]): string { + const header = [ + "# Migrating to v2", + "", + "", + "", + "Run the codemods, then finish anything reported as not migrated automatically:", + "", + "```sh", + "npx @tailor-platform/sdk-codemod --from --to ", + "```", + "", + ].join("\n"); + + const body = codemods.map(renderEntry).join("\n"); + return `${header}\n${body}`.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n"; +} diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 0cf6728a7..76727b3c6 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -5,7 +5,8 @@ import type { CodemodPackage } from "./types"; const CODEMODS_ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "codemods"); -const allCodemods: CodemodPackage[] = [ +/** All registered codemods, in registration order. */ +export const allCodemods: CodemodPackage[] = [ { id: "v2/define-generators-to-plugins", name: "defineGenerators → definePlugins", @@ -138,6 +139,12 @@ const allCodemods: CodemodPackage[] = [ "JSON.stringify wrapper) so executeScript serializes it once. Leave calls that", "already pass a plain value unchanged.", ].join("\n"), + examples: [ + { + before: "await executeScript({ ...opts, arg: JSON.stringify({ a: 1 }) });", + after: "await executeScript({ ...opts, arg: { a: 1 } });", + }, + ], }, ]; diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index e2f61dffb..563d10bab 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -1,3 +1,13 @@ +/** A before/after code pair shown in the generated migration doc. */ +export interface CodemodExample { + /** Code as written before the migration. */ + before: string; + /** Code after the migration. */ + after: string; + /** Optional one-line caption explaining the example. */ + caption?: string; +} + /** * Metadata for a codemod package. */ @@ -41,6 +51,8 @@ export interface CodemodPackage { * by `suspiciousPatterns`. */ prompt?: string; + /** Before/after examples shown in the generated migration doc. */ + examples?: CodemodExample[]; } /** A batch of files an LLM should review for one codemod, with its prompt. */ diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md new file mode 100644 index 000000000..0af1f666e --- /dev/null +++ b/packages/sdk/docs/migration/v2.md @@ -0,0 +1,114 @@ +# Migrating to v2 + + + +Run the codemods, then finish anything reported as not migrated automatically: + +```sh +npx @tailor-platform/sdk-codemod --from --to +``` + +## defineGenerators → definePlugins + +**Migration:** Partially automatic + +Migrate defineGenerators() tuple syntax to definePlugins() with explicit plugin imports + +## @tailor-platform/sdk/cli plugin imports → dedicated subpaths + +**Migration:** Automatic + +Rewrite deprecated plugin re-export imports (kyselyTypePlugin, enumConstantsPlugin, fileUtilsPlugin, seedPlugin) from `@tailor-platform/sdk/cli` to their dedicated plugin subpaths + +## function test-run --arg input unwrap + +**Migration:** Automatic + +Strip the deprecated {input: ...} wrapper from `tailor-sdk function test-run --arg` JSON in scripts and docs + +## tailor-sdk-skills → tailor-sdk skills install + +**Migration:** Partially automatic + +Replace deprecated `tailor-sdk-skills` invocations with `tailor-sdk skills install` + +## Unify TailorUser/TailorActor/TailorInvoker → TailorPrincipal + +**Migration:** Partially automatic + +Rename TailorUser/TailorActor/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, and rename resolver body `user` to `caller` + +## tailor-sdk apply → tailor-sdk deploy + +**Migration:** Automatic + +Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor-sdk deploy` command + +## v2 CLI rename + +**Migration:** Partially automatic + +Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs + +## auth.invoker("name") → "name" + +**Migration:** Partially automatic + +Replace `auth.invoker("name")` calls with the bare `"name"` string and drop the `auth` import when no other reference remains. The `auth.invoker()` helper is deprecated in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle. + +How to finish the cases the codemod cannot migrate on its own: + +```text +In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the +machine user name passed directly as a string. The codemod already rewrote the +string-literal form auth.invoker("name") to "name". These files still contain +auth.invoker(...) because the argument is not a plain string literal (a variable, +template literal, function call, or property access). + +For each remaining auth.invoker() call: +1. Replace the whole call with as-is (e.g. auth.invoker(name) becomes name). +2. Make sure evaluates to the machine user name (a string); adjust it if it + resolves to an object or an auth config value instead. +3. After removing every auth.invoker usage in a file, delete the now-unused auth + import (keeping it pulls Node-only config modules into runtime bundles); leave + the import if auth is still referenced elsewhere. + +Do not change behavior beyond removing the auth.invoker() indirection. +``` + +## Tailordb → tailordb (lowercase ambient namespace) + +**Migration:** Partially automatic + +Rewrite references to the removed capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the lowercase `tailordb.*` namespace exposed by `@tailor-platform/sdk/runtime/globals`. + +## executeScript arg JSON.stringify → value + +**Migration:** Partially automatic + +Unwrap `JSON.stringify(...)` passed as the `executeScript` `arg` option. In v2 `arg` takes a JSON-serializable value and is serialized internally, so a pre-stringified argument double-encodes. + +```ts +// Before +await executeScript({ ...opts, arg: JSON.stringify({ a: 1 }) }); + +// After +await executeScript({ ...opts, arg: { a: 1 } }); +``` + +How to finish the cases the codemod cannot migrate on its own: + +```text +In Tailor SDK v2 the executeScript() arg option takes a JSON-serializable value +and is serialized internally, so a pre-stringified argument double-encodes. The +codemod already rewrote the direct form arg: JSON.stringify(X) to arg: X. Review +the executeScript calls in these files for cases it could not rewrite — where the +arg value is reached indirectly, for example: +- a variable holding a JSON.stringify(...) result (const s = JSON.stringify(x); ... arg: s) +- JSON.stringify(x, null, 2) or another multi-argument form +- an options object built or spread dynamically + +For each such call, pass the underlying value directly as arg (drop the +JSON.stringify wrapper) so executeScript serializes it once. Leave calls that +already pass a plain value unchanged. +``` From bb5b05b1dab20c207b8eea9ec363c8b384fa7df9 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 22:39:10 +0900 Subject: [PATCH 130/618] docs(sdk): update v2 deploy references --- packages/sdk/docs/services/auth.md | 4 ++-- packages/sdk/docs/services/tailordb-migration.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/sdk/docs/services/auth.md b/packages/sdk/docs/services/auth.md index 3e4fe1393..3d6092ed4 100644 --- a/packages/sdk/docs/services/auth.md +++ b/packages/sdk/docs/services/auth.md @@ -308,7 +308,7 @@ export default createResolver({ }); ``` -Type narrowing is provided by the generated `tailor.d.ts` (the `MachineUserNameRegistry` interface). Run `tailor-sdk generate` (or `apply`) after defining new machine users to refresh it. +Type narrowing is provided by the generated `tailor.d.ts` (the `MachineUserNameRegistry` interface). Run `tailor-sdk generate` (or `deploy`) after defining new machine users to refresh it. ## OAuth 2.0 Clients @@ -432,7 +432,7 @@ The authorize command opens a browser for the OAuth2 flow. The authorization cod ### Change Detection -The SDK uses hash-based change detection for connection configs. Only connections whose configuration has changed since the last `apply` are updated (revoked and recreated). Deleting the `.tailor-sdk/` directory forces all connections to be re-sent. +The SDK uses hash-based change detection for connection configs. Only connections whose configuration has changed since the last `deploy` are updated (revoked and recreated). Deleting the `.tailor-sdk/` directory forces all connections to be re-sent. > [!WARNING] > The secret hash lives in `.tailor-sdk/secrets-state.json`, which is gitignored and not shared across machines or CI. When that state is missing — a clean checkout, CI, another machine, or after deleting `.tailor-sdk/` — a deploy cannot confirm the secret is unchanged, so it revokes and recreates the connection and discards the token stored by `authconnection authorize`. For shared and CI workflows, manage the connection and create its token from the Console (`tailor-sdk authconnection open`) instead. diff --git a/packages/sdk/docs/services/tailordb-migration.md b/packages/sdk/docs/services/tailordb-migration.md index 276dba4a5..f3e1129c3 100644 --- a/packages/sdk/docs/services/tailordb-migration.md +++ b/packages/sdk/docs/services/tailordb-migration.md @@ -339,7 +339,7 @@ Migration numbers are assigned sequentially, so two developers branching off the ### CI / CD -- For non-interactive environments, pass `--yes` to `migration generate` and `--yes` to `apply`. `apply` runs migrations automatically when the `migrations/` directory is configured. +- For non-interactive environments, pass `--yes` to `migration generate` and `--yes` to `deploy`. `deploy` runs migrations automatically when the `migrations/` directory is configured. - Run `tailor-sdk tailordb migration status` in CI to detect "developer forgot to commit a migration" situations early. The exit code is non-zero only on errors, so check the output. - Avoid running migrations in parallel against the same workspace — there is no locking. Serialize deploys per environment. @@ -379,7 +379,7 @@ There is no automatic down-migration. To roll back a schema/data change in produ 2. `migration generate --name "rollback 0005 email"` produces `0006` with a removal diff. 3. Apply. -In **development workspaces**, a quicker option is to fix `0005/migrate.ts` in place, run `migration set ` to re-mark it pending, and apply. Do not do this on production — it confuses migration history across environments. +In **development workspaces**, a quicker option is to fix `0005/migrate.ts` in place, run `migration set ` to re-mark it pending, and deploy. Do not do this on production — it confuses migration history across environments. ## Machine User and Permissions From 46028433255198c6d8c19710774fc5838ddffcfb Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 22:56:37 +0900 Subject: [PATCH 131/618] docs(codemod): add before/after examples and fold AI prompt in migration guide - Add before/after `examples` (with an optional per-example `lang`) to every codemod so the generated migration guide is understandable on its own, without running the codemod. - Render each example as labelled Before/After fenced blocks (language-aware, so shell/json examples are not mis-tagged as TS). - Fold the migration `prompt` into a collapsed
block labelled as a prompt for an AI agent, keeping the human-facing guidance (description + examples) prominent. - Add a `prompt` to the partially-automatic codemods that lacked one (defineGenerators, sdk-skills, principal-unify, cli-rename, Tailordb namespace), since "partially automatic" implies residual cases to finish. - Ignore the generated `packages/sdk/docs/migration/v2.md` in oxfmt (it owns its own formatting and is verified by `codemod:docs:check`), matching how other generated files are excluded. --- .oxfmtrc.json | 1 + .../sdk-codemod/src/migration-doc.test.ts | 5 +- packages/sdk-codemod/src/migration-doc.ts | 21 +- packages/sdk-codemod/src/registry.ts | 110 +++++++++ packages/sdk-codemod/src/types.ts | 2 + packages/sdk/docs/migration/v2.md | 216 +++++++++++++++++- 6 files changed, 347 insertions(+), 8 deletions(-) diff --git a/.oxfmtrc.json b/.oxfmtrc.json index d9b88623c..ad8e59aac 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -27,6 +27,7 @@ "example/seed/", "packages/tailor-proto/", "packages/sdk-codemod/codemods/**/tests/", + "packages/sdk/docs/migration/v2.md", "generated/" ] } diff --git a/packages/sdk-codemod/src/migration-doc.test.ts b/packages/sdk-codemod/src/migration-doc.test.ts index c5e9c0dc9..c648f894c 100644 --- a/packages/sdk-codemod/src/migration-doc.test.ts +++ b/packages/sdk-codemod/src/migration-doc.test.ts @@ -48,8 +48,9 @@ describe("renderMigrationDoc", () => { expect(doc).toContain("# Migrating to v2"); expect(doc).toContain("## executeScript arg"); expect(doc).toContain("**Migration:** Partially automatic"); - expect(doc).toContain("// Before\narg: JSON.stringify(x)"); - expect(doc).toContain("// After\narg: x"); + expect(doc).toContain("Before:\n\n```ts\narg: JSON.stringify(x)\n```"); + expect(doc).toContain("After:\n\n```ts\narg: x\n```"); + expect(doc).toContain("Prompt for an AI agent"); expect(doc).toContain("```text\nUnwrap JSON.stringify in the arg.\n```"); }); diff --git a/packages/sdk-codemod/src/migration-doc.ts b/packages/sdk-codemod/src/migration-doc.ts index a6cffe4f7..b33cd7b6c 100644 --- a/packages/sdk-codemod/src/migration-doc.ts +++ b/packages/sdk-codemod/src/migration-doc.ts @@ -31,18 +31,35 @@ function renderEntry(codemod: CodemodPackage): string { lines.push(codemod.description, ""); for (const example of codemod.examples ?? []) { + const fence = "```" + (example.lang ?? "ts"); if (example.caption) lines.push(example.caption, ""); - lines.push("```ts", "// Before", example.before, "", "// After", example.after, "```", ""); + lines.push( + "Before:", + "", + fence, + example.before, + "```", + "", + "After:", + "", + fence, + example.after, + "```", + "", + ); } if (automationLevel(codemod) !== "Automatic" && codemod.prompt != null) { lines.push( - "How to finish the cases the codemod cannot migrate on its own:", + "
", + "Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own)", "", "```text", codemod.prompt.trim(), "```", "", + "
", + "", ); } diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 76727b3c6..b40b8f9b0 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -16,6 +16,30 @@ export const allCodemods: CodemodPackage[] = [ until: "2.0.0", scriptPath: "v2/define-generators-to-plugins/scripts/transform.js", legacyPatterns: ["defineGenerators"], + examples: [ + { + before: [ + 'import { defineGenerators } from "@tailor-platform/sdk";', + "", + "export const generators = defineGenerators(", + ' ["@tailor-platform/kysely-type", { distPath: "db.ts" }],', + ");", + ].join("\n"), + after: [ + 'import { definePlugins } from "@tailor-platform/sdk";', + 'import { kyselyTypePlugin } from "@tailor-platform/sdk/plugin/kysely-type";', + "", + 'export const generators = definePlugins(kyselyTypePlugin({ distPath: "db.ts" }));', + ].join("\n"), + }, + ], + prompt: [ + "defineGenerators() is replaced by definePlugins() in v2. The codemod rewrites the", + "known plugin tuples (kysely-type, enum-constants, file-utils, seed). For any", + "remaining defineGenerators([...]) the codemod left in place — a plugin it does not", + "know, or a non-tuple/spread form — convert it to definePlugins(pluginFn(config)),", + "importing the matching plugin from its @tailor-platform/sdk/plugin/ subpath.", + ].join("\n"), }, { id: "v2/plugin-cli-import", @@ -25,6 +49,12 @@ export const allCodemods: CodemodPackage[] = [ since: "1.0.0", until: "2.0.0", scriptPath: "v2/plugin-cli-import/scripts/transform.js", + examples: [ + { + before: 'import { kyselyTypePlugin } from "@tailor-platform/sdk/cli";', + after: 'import { kyselyTypePlugin } from "@tailor-platform/sdk/plugin/kysely-type";', + }, + ], }, { id: "v2/test-run-arg-input", @@ -35,6 +65,13 @@ export const allCodemods: CodemodPackage[] = [ until: "2.0.0", scriptPath: "v2/test-run-arg-input/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh}", "**/*.md"], + examples: [ + { + lang: "sh", + before: 'tailor-sdk function test-run resolvers/add.ts --arg \'{"input":{"a":1}}\'', + after: "tailor-sdk function test-run resolvers/add.ts --arg '{\"a\":1}'", + }, + ], }, { id: "v2/sdk-skills-shim", @@ -46,6 +83,19 @@ export const allCodemods: CodemodPackage[] = [ scriptPath: "v2/sdk-skills-shim/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], legacyPatterns: ["tailor-sdk-skills"], + examples: [ + { + lang: "sh", + before: "npx tailor-sdk-skills", + after: "tailor-sdk skills install", + }, + ], + prompt: [ + "The standalone tailor-sdk-skills binary is removed in v2; call the skills install", + "subcommand on the main tailor-sdk CLI instead. Replace any remaining", + "tailor-sdk-skills invocations the codemod did not rewrite with", + "`tailor-sdk skills install`.", + ].join("\n"), }, { id: "v2/principal-unify", @@ -56,6 +106,27 @@ export const allCodemods: CodemodPackage[] = [ until: "2.0.0", scriptPath: "v2/principal-unify/scripts/transform.js", legacyPatterns: ["TailorUser", "TailorActor", "TailorInvoker", "unauthenticatedTailorUser"], + examples: [ + { + caption: "Type references unify under `TailorPrincipal`:", + before: 'import type { TailorUser } from "@tailor-platform/sdk";', + after: 'import type { TailorPrincipal } from "@tailor-platform/sdk";', + }, + { + caption: "The resolver body `user` becomes `caller`:", + before: "body: ({ input, user }) => user.id,", + after: "body: ({ input, caller }) => caller.id,", + }, + ], + prompt: [ + "Finish the cases the codemod left for manual migration:", + "- Rename user -> caller in resolver bodies the codemod skipped because a `caller`", + " binding already exists or renaming would shadow/collide with another value.", + "- Replace member-access on the removed unauthenticatedTailorUser (e.g.", + " unauthenticatedTailorUser.id); the codemod only replaced standalone references", + " with null and left member access to surface a type error.", + "Use TailorPrincipal for the unified user/actor/invoker type.", + ].join("\n"), }, { id: "v2/apply-to-deploy", @@ -66,6 +137,13 @@ export const allCodemods: CodemodPackage[] = [ until: "2.0.0", scriptPath: "v2/apply-to-deploy/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], + examples: [ + { + lang: "sh", + before: "tailor-sdk apply --profile prod", + after: "tailor-sdk deploy --profile prod", + }, + ], }, { id: "v2/cli-rename", @@ -77,6 +155,19 @@ export const allCodemods: CodemodPackage[] = [ scriptPath: "v2/cli-rename/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], legacyPatterns: ["tailor-sdk crash-report", "--machineuser"], + examples: [ + { + lang: "sh", + before: "tailor-sdk crash-report list\ntailor-sdk login --machineuser", + after: "tailor-sdk crashreport list\ntailor-sdk login --machine-user", + }, + ], + prompt: [ + "Apply the v2 CLI renames the codemod did not reach (only `tailor-sdk`-prefixed", + "invocations are rewritten): `tailor-sdk crash-report` -> `tailor-sdk crashreport`", + "and the `--machineuser` option -> `--machine-user`. Leave unrelated commands that", + "happen to use `--machineuser` alone.", + ].join("\n"), }, { id: "v2/auth-invoker-unwrap", @@ -104,6 +195,12 @@ export const allCodemods: CodemodPackage[] = [ "", "Do not change behavior beyond removing the auth.invoker() indirection.", ].join("\n"), + examples: [ + { + before: 'createResolver({ invoker: auth.invoker("manager") });', + after: 'createResolver({ invoker: "manager" });', + }, + ], }, { id: "v2/tailordb-namespace", @@ -114,6 +211,19 @@ export const allCodemods: CodemodPackage[] = [ until: "2.0.0", scriptPath: "v2/tailordb-namespace/scripts/transform.js", legacyPatterns: ["Tailordb."], + examples: [ + { + before: 'const command: Tailordb.CommandType = "SELECT";', + after: 'const command: tailordb.CommandType = "SELECT";', + }, + ], + prompt: [ + "The capital-cased Tailordb ambient namespace is removed in v2; use the lowercase", + "tailordb.* namespace from @tailor-platform/sdk/runtime/globals. The codemod rewrites", + "the known members (QueryResult, CommandType, Client). Rewrite any other remaining", + "Tailordb.* reference to its tailordb.* equivalent (and confirm the member still", + "exists on the lowercase namespace).", + ].join("\n"), }, { id: "v2/execute-script-arg", diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index 563d10bab..b18665082 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -6,6 +6,8 @@ export interface CodemodExample { after: string; /** Optional one-line caption explaining the example. */ caption?: string; + /** Fenced-code-block language for the example (default: "ts"). */ + lang?: string; } /** diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 0af1f666e..84e5f0068 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -14,49 +14,223 @@ npx @tailor-platform/sdk-codemod --from --to Migrate defineGenerators() tuple syntax to definePlugins() with explicit plugin imports +Before: + +```ts +import { defineGenerators } from "@tailor-platform/sdk"; + +export const generators = defineGenerators( + ["@tailor-platform/kysely-type", { distPath: "db.ts" }], +); +``` + +After: + +```ts +import { definePlugins } from "@tailor-platform/sdk"; +import { kyselyTypePlugin } from "@tailor-platform/sdk/plugin/kysely-type"; + +export const generators = definePlugins(kyselyTypePlugin({ distPath: "db.ts" })); +``` + +
+Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own) + +```text +defineGenerators() is replaced by definePlugins() in v2. The codemod rewrites the +known plugin tuples (kysely-type, enum-constants, file-utils, seed). For any +remaining defineGenerators([...]) the codemod left in place — a plugin it does not +know, or a non-tuple/spread form — convert it to definePlugins(pluginFn(config)), +importing the matching plugin from its @tailor-platform/sdk/plugin/ subpath. +``` + +
+ ## @tailor-platform/sdk/cli plugin imports → dedicated subpaths **Migration:** Automatic Rewrite deprecated plugin re-export imports (kyselyTypePlugin, enumConstantsPlugin, fileUtilsPlugin, seedPlugin) from `@tailor-platform/sdk/cli` to their dedicated plugin subpaths +Before: + +```ts +import { kyselyTypePlugin } from "@tailor-platform/sdk/cli"; +``` + +After: + +```ts +import { kyselyTypePlugin } from "@tailor-platform/sdk/plugin/kysely-type"; +``` + ## function test-run --arg input unwrap **Migration:** Automatic Strip the deprecated {input: ...} wrapper from `tailor-sdk function test-run --arg` JSON in scripts and docs +Before: + +```sh +tailor-sdk function test-run resolvers/add.ts --arg '{"input":{"a":1}}' +``` + +After: + +```sh +tailor-sdk function test-run resolvers/add.ts --arg '{"a":1}' +``` + ## tailor-sdk-skills → tailor-sdk skills install **Migration:** Partially automatic Replace deprecated `tailor-sdk-skills` invocations with `tailor-sdk skills install` +Before: + +```sh +npx tailor-sdk-skills +``` + +After: + +```sh +tailor-sdk skills install +``` + +
+Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own) + +```text +The standalone tailor-sdk-skills binary is removed in v2; call the skills install +subcommand on the main tailor-sdk CLI instead. Replace any remaining +tailor-sdk-skills invocations the codemod did not rewrite with +`tailor-sdk skills install`. +``` + +
+ ## Unify TailorUser/TailorActor/TailorInvoker → TailorPrincipal **Migration:** Partially automatic Rename TailorUser/TailorActor/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, and rename resolver body `user` to `caller` +Type references unify under `TailorPrincipal`: + +Before: + +```ts +import type { TailorUser } from "@tailor-platform/sdk"; +``` + +After: + +```ts +import type { TailorPrincipal } from "@tailor-platform/sdk"; +``` + +The resolver body `user` becomes `caller`: + +Before: + +```ts +body: ({ input, user }) => user.id, +``` + +After: + +```ts +body: ({ input, caller }) => caller.id, +``` + +
+Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own) + +```text +Finish the cases the codemod left for manual migration: +- Rename user -> caller in resolver bodies the codemod skipped because a `caller` + binding already exists or renaming would shadow/collide with another value. +- Replace member-access on the removed unauthenticatedTailorUser (e.g. + unauthenticatedTailorUser.id); the codemod only replaced standalone references + with null and left member access to surface a type error. +Use TailorPrincipal for the unified user/actor/invoker type. +``` + +
+ ## tailor-sdk apply → tailor-sdk deploy **Migration:** Automatic Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor-sdk deploy` command +Before: + +```sh +tailor-sdk apply --profile prod +``` + +After: + +```sh +tailor-sdk deploy --profile prod +``` + ## v2 CLI rename **Migration:** Partially automatic Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs +Before: + +```sh +tailor-sdk crash-report list +tailor-sdk login --machineuser +``` + +After: + +```sh +tailor-sdk crashreport list +tailor-sdk login --machine-user +``` + +
+Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own) + +```text +Apply the v2 CLI renames the codemod did not reach (only `tailor-sdk`-prefixed +invocations are rewritten): `tailor-sdk crash-report` -> `tailor-sdk crashreport` +and the `--machineuser` option -> `--machine-user`. Leave unrelated commands that +happen to use `--machineuser` alone. +``` + +
+ ## auth.invoker("name") → "name" **Migration:** Partially automatic Replace `auth.invoker("name")` calls with the bare `"name"` string and drop the `auth` import when no other reference remains. The `auth.invoker()` helper is deprecated in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle. -How to finish the cases the codemod cannot migrate on its own: +Before: + +```ts +createResolver({ invoker: auth.invoker("manager") }); +``` + +After: + +```ts +createResolver({ invoker: "manager" }); +``` + +
+Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own) ```text In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the @@ -76,27 +250,59 @@ For each remaining auth.invoker() call: Do not change behavior beyond removing the auth.invoker() indirection. ``` +
+ ## Tailordb → tailordb (lowercase ambient namespace) **Migration:** Partially automatic Rewrite references to the removed capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the lowercase `tailordb.*` namespace exposed by `@tailor-platform/sdk/runtime/globals`. +Before: + +```ts +const command: Tailordb.CommandType = "SELECT"; +``` + +After: + +```ts +const command: tailordb.CommandType = "SELECT"; +``` + +
+Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own) + +```text +The capital-cased Tailordb ambient namespace is removed in v2; use the lowercase +tailordb.* namespace from @tailor-platform/sdk/runtime/globals. The codemod rewrites +the known members (QueryResult, CommandType, Client). Rewrite any other remaining +Tailordb.* reference to its tailordb.* equivalent (and confirm the member still +exists on the lowercase namespace). +``` + +
+ ## executeScript arg JSON.stringify → value **Migration:** Partially automatic Unwrap `JSON.stringify(...)` passed as the `executeScript` `arg` option. In v2 `arg` takes a JSON-serializable value and is serialized internally, so a pre-stringified argument double-encodes. +Before: + ```ts -// Before await executeScript({ ...opts, arg: JSON.stringify({ a: 1 }) }); +``` -// After +After: + +```ts await executeScript({ ...opts, arg: { a: 1 } }); ``` -How to finish the cases the codemod cannot migrate on its own: +
+Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own) ```text In Tailor SDK v2 the executeScript() arg option takes a JSON-serializable value @@ -112,3 +318,5 @@ For each such call, pass the underlying value directly as arg (drop the JSON.stringify wrapper) so executeScript serializes it once. Leave calls that already pass a plain value unchanged. ``` + +
From c870ddc87109c03ba8e74dc97becff142f7f49f2 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 23:14:39 +0900 Subject: [PATCH 132/618] chore: remove changelog whitespace drift --- packages/sdk-codemod/CHANGELOG.md | 4 ++-- packages/sdk/CHANGELOG.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/sdk-codemod/CHANGELOG.md b/packages/sdk-codemod/CHANGELOG.md index f89ec73b6..37127d7b8 100644 --- a/packages/sdk-codemod/CHANGELOG.md +++ b/packages/sdk-codemod/CHANGELOG.md @@ -6,12 +6,12 @@ - [#1460](https://github.com/tailor-platform/sdk/pull/1460) [`f49c6d1`](https://github.com/tailor-platform/sdk/commit/f49c6d1b5a856969cb4e04ae7d3a87ed34aa020f) Thanks [@dqn](https://github.com/dqn)! - Remove the v1 runtime globals compatibility layer. Importing from `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` declarations; opt into globals with `@tailor-platform/sdk/runtime/globals` or use the typed wrappers from `@tailor-platform/sdk/runtime`. - + The capital-cased `Tailordb.*` namespace is removed. If your project still references `Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, or `typeof Tailordb.Client`, migrate before upgrading: run `pnpm dlx @tailor-platform/sdk-codemod v2/tailordb-namespace` to rewrite them to lowercase `tailordb.*`, then add `import "@tailor-platform/sdk/runtime/globals"` so the rewritten references resolve. - [#1457](https://github.com/tailor-platform/sdk/pull/1457) [`84325f8`](https://github.com/tailor-platform/sdk/commit/84325f8602a5631b7c323c997b1425235509920e) Thanks [@dqn](https://github.com/dqn)! - Remove deprecated CLI aliases for the v2 command surface. Use `tailor-sdk deploy` instead of `tailor-sdk apply`, `tailor-sdk crashreport` instead of `tailor-sdk crash-report`, and the hyphenated `--machine-user` option instead of the hidden `--machineuser` alias. - + Fix the v2 CLI rename codemod to migrate the hidden `--machineuser` option to `--machine-user`. ## 0.3.0-next.0 diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 683ea5b4f..a2a0a7e03 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -18,7 +18,7 @@ - [#1460](https://github.com/tailor-platform/sdk/pull/1460) [`f49c6d1`](https://github.com/tailor-platform/sdk/commit/f49c6d1b5a856969cb4e04ae7d3a87ed34aa020f) Thanks [@dqn](https://github.com/dqn)! - Remove the v1 runtime globals compatibility layer. Importing from `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` declarations; opt into globals with `@tailor-platform/sdk/runtime/globals` or use the typed wrappers from `@tailor-platform/sdk/runtime`. - + The capital-cased `Tailordb.*` namespace is removed. If your project still references `Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, or `typeof Tailordb.Client`, migrate before upgrading: run `pnpm dlx @tailor-platform/sdk-codemod v2/tailordb-namespace` to rewrite them to lowercase `tailordb.*`, then add `import "@tailor-platform/sdk/runtime/globals"` so the rewritten references resolve. @@ -27,7 +27,7 @@ - [#1457](https://github.com/tailor-platform/sdk/pull/1457) [`84325f8`](https://github.com/tailor-platform/sdk/commit/84325f8602a5631b7c323c997b1425235509920e) Thanks [@dqn](https://github.com/dqn)! - Remove deprecated CLI aliases for the v2 command surface. Use `tailor-sdk deploy` instead of `tailor-sdk apply`, `tailor-sdk crashreport` instead of `tailor-sdk crash-report`, and the hyphenated `--machine-user` option instead of the hidden `--machineuser` alias. - + Fix the v2 CLI rename codemod to migrate the hidden `--machineuser` option to `--machine-user`. From 097047d36c2333c20efa0a65d6db7d52d3ca28ea Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 23:14:48 +0900 Subject: [PATCH 133/618] chore: retrigger CI after syncing with v2 From 5973c4845646480bc48e731b8d5a649208f0f0ba Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 23:15:24 +0900 Subject: [PATCH 134/618] chore: remove unrelated PR drift --- packages/sdk/docs/services/auth.md | 4 +-- .../sdk/docs/services/tailordb-migration.md | 4 +-- .../src/cli/commands/workspace/create.test.ts | 36 ------------------- .../sdk/src/cli/commands/workspace/create.ts | 16 +++------ 4 files changed, 9 insertions(+), 51 deletions(-) diff --git a/packages/sdk/docs/services/auth.md b/packages/sdk/docs/services/auth.md index 3d6092ed4..3e4fe1393 100644 --- a/packages/sdk/docs/services/auth.md +++ b/packages/sdk/docs/services/auth.md @@ -308,7 +308,7 @@ export default createResolver({ }); ``` -Type narrowing is provided by the generated `tailor.d.ts` (the `MachineUserNameRegistry` interface). Run `tailor-sdk generate` (or `deploy`) after defining new machine users to refresh it. +Type narrowing is provided by the generated `tailor.d.ts` (the `MachineUserNameRegistry` interface). Run `tailor-sdk generate` (or `apply`) after defining new machine users to refresh it. ## OAuth 2.0 Clients @@ -432,7 +432,7 @@ The authorize command opens a browser for the OAuth2 flow. The authorization cod ### Change Detection -The SDK uses hash-based change detection for connection configs. Only connections whose configuration has changed since the last `deploy` are updated (revoked and recreated). Deleting the `.tailor-sdk/` directory forces all connections to be re-sent. +The SDK uses hash-based change detection for connection configs. Only connections whose configuration has changed since the last `apply` are updated (revoked and recreated). Deleting the `.tailor-sdk/` directory forces all connections to be re-sent. > [!WARNING] > The secret hash lives in `.tailor-sdk/secrets-state.json`, which is gitignored and not shared across machines or CI. When that state is missing — a clean checkout, CI, another machine, or after deleting `.tailor-sdk/` — a deploy cannot confirm the secret is unchanged, so it revokes and recreates the connection and discards the token stored by `authconnection authorize`. For shared and CI workflows, manage the connection and create its token from the Console (`tailor-sdk authconnection open`) instead. diff --git a/packages/sdk/docs/services/tailordb-migration.md b/packages/sdk/docs/services/tailordb-migration.md index f3e1129c3..276dba4a5 100644 --- a/packages/sdk/docs/services/tailordb-migration.md +++ b/packages/sdk/docs/services/tailordb-migration.md @@ -339,7 +339,7 @@ Migration numbers are assigned sequentially, so two developers branching off the ### CI / CD -- For non-interactive environments, pass `--yes` to `migration generate` and `--yes` to `deploy`. `deploy` runs migrations automatically when the `migrations/` directory is configured. +- For non-interactive environments, pass `--yes` to `migration generate` and `--yes` to `apply`. `apply` runs migrations automatically when the `migrations/` directory is configured. - Run `tailor-sdk tailordb migration status` in CI to detect "developer forgot to commit a migration" situations early. The exit code is non-zero only on errors, so check the output. - Avoid running migrations in parallel against the same workspace — there is no locking. Serialize deploys per environment. @@ -379,7 +379,7 @@ There is no automatic down-migration. To roll back a schema/data change in produ 2. `migration generate --name "rollback 0005 email"` produces `0006` with a removal diff. 3. Apply. -In **development workspaces**, a quicker option is to fix `0005/migrate.ts` in place, run `migration set ` to re-mark it pending, and deploy. Do not do this on production — it confuses migration history across environments. +In **development workspaces**, a quicker option is to fix `0005/migrate.ts` in place, run `migration set ` to re-mark it pending, and apply. Do not do this on production — it confuses migration history across environments. ## Machine User and Permissions diff --git a/packages/sdk/src/cli/commands/workspace/create.test.ts b/packages/sdk/src/cli/commands/workspace/create.test.ts index eeef55f48..ee9388af8 100644 --- a/packages/sdk/src/cli/commands/workspace/create.test.ts +++ b/packages/sdk/src/cli/commands/workspace/create.test.ts @@ -48,24 +48,6 @@ function seedConfig() { }); } -function seedV3Config() { - writePlatformConfig({ - version: 3, - min_sdk_version: "2.0.0", - users: { - "user-subject-1": { - storage: "file", - token_expires_at: "2099-12-31T00:00:00Z", - access_token: "mock-token", - refresh_token: undefined, - email: "u@example.com", - }, - }, - profiles: {}, - current_user: "user-subject-1", - }); -} - function stubClient() { vi.mocked(initOperatorClient).mockResolvedValue({ listAvailableWorkspaceRegions: vi.fn().mockResolvedValue({ regions: ["us-west"] }), @@ -147,24 +129,6 @@ describe("workspace create --permission", () => { expect(config.profiles.bootstrap?.readonly).toBeUndefined(); }); - test("stores the resolved user key when --profile-user is an email in v3 config", async () => { - seedV3Config(); - using _logger = silenceLogger("out", "success", "warn"); - await runCommand(createCommand, [ - "--name", - "test-ws", - "--region", - "us-west", - "--profile-name", - "bootstrap", - "--profile-user", - "u@example.com", - ]); - - const config = await readPlatformConfig(); - expect(config.profiles.bootstrap?.user).toBe("user-subject-1"); - }); - test("creates no profile when --permission read is passed without --profile-name", async () => { using _logger = silenceLogger("out", "success", "warn"); // Matches the existing --profile-user behavior: profile-only flags are diff --git a/packages/sdk/src/cli/commands/workspace/create.ts b/packages/sdk/src/cli/commands/workspace/create.ts index ba42338bd..130d45176 100644 --- a/packages/sdk/src/cli/commands/workspace/create.ts +++ b/packages/sdk/src/cli/commands/workspace/create.ts @@ -2,12 +2,7 @@ import { arg } from "politty"; import { z } from "zod"; import { initOperatorClient, type OperatorClient } from "@/cli/shared/client"; import { defineAppCommand } from "@/cli/shared/command"; -import { - findConfigUserKey, - loadAccessToken, - readPlatformConfig, - writePlatformConfig, -} from "@/cli/shared/context"; +import { loadAccessToken, readPlatformConfig, writePlatformConfig } from "@/cli/shared/context"; import { logger } from "@/cli/shared/logger"; import { assertWritable } from "@/cli/shared/readonly-guard"; import { assertDefined } from "@/utils/assert"; @@ -144,17 +139,16 @@ export const createCommand = defineAppCommand({ throw new Error(`Profile "${profileName}" already exists.`); } - const requestedProfileUser = args["profile-user"] || config.current_user; - if (!requestedProfileUser) { + const profileUser = args["profile-user"] || config.current_user; + if (!profileUser) { throw new Error( "Current user not found. Please login or specify --profile-user to create a profile.", ); } - const profileUser = findConfigUserKey(config, requestedProfileUser); - if (!profileUser || !config.users[profileUser]) { + if (!config.users[profileUser]) { throw new Error( - `User "${requestedProfileUser}" not found.\nPlease verify your user name and login using 'tailor-sdk login' command.`, + `User "${profileUser}" not found.\nPlease verify your user name and login using 'tailor-sdk login' command.`, ); } config.profiles[profileName] = { From 7005d7002de589fb21989f9c42f81e8c5f839ead Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 23:28:08 +0900 Subject: [PATCH 135/618] feat(codemod): support codemod-less (manual) registry entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make `scriptPath` optional so the registry can describe migrations that have no automatic transform — they ship only guidance (description, before/after `examples`, a `prompt`, and/or `suspiciousPatterns`). - runner: skip the transform when an entry has no scriptPath; a manual entry with a `prompt` but no scoping pattern (no suspiciousPatterns/legacyPatterns) is surfaced as a project-wide `llmReviews` entry (empty `files`). - The migration-doc generator classifies these as "Manual" and renders their examples + prompt like any other entry. - Add the openDownloadStream -> downloadStream change as the first manual entry to demonstrate and populate the guide. --- .changeset/codemod-migration-docs.md | 2 + packages/sdk-codemod/src/index.ts | 12 +++--- packages/sdk-codemod/src/migration-doc.ts | 9 ++++- packages/sdk-codemod/src/registry.ts | 21 ++++++++++ packages/sdk-codemod/src/runner.test.ts | 47 +++++++++++++++++++++++ packages/sdk-codemod/src/runner.ts | 23 +++++++---- packages/sdk-codemod/src/types.ts | 8 +++- packages/sdk/docs/migration/v2.md | 44 +++++++++++++++++---- 8 files changed, 143 insertions(+), 23 deletions(-) diff --git a/.changeset/codemod-migration-docs.md b/.changeset/codemod-migration-docs.md index 1e54130ce..2e02efdc8 100644 --- a/.changeset/codemod-migration-docs.md +++ b/.changeset/codemod-migration-docs.md @@ -4,3 +4,5 @@ --- Generate the v2 migration guide (`packages/sdk/docs/migration/v2.md`) from the codemod registry, which is the single source of truth. Each entry renders its name, automation level (Automatic / Partially automatic / Manual), description, optional before/after `examples`, and — for changes the codemods cannot fully migrate on their own — the LLM/manual migration prompt. Run `pnpm codemod:docs:update` to regenerate and `pnpm codemod:docs:check` (wired into `pnpm check`) to verify it is in sync. + +`scriptPath` is now optional, so the registry can also describe codemod-less ("manual") migrations that ship only guidance (`examples` / `prompt` / `suspiciousPatterns`) with no automatic transform. A manual entry with a `prompt` but no scoping pattern is surfaced as a project-wide `llmReviews` entry at runtime. diff --git a/packages/sdk-codemod/src/index.ts b/packages/sdk-codemod/src/index.ts index c2b7f452e..fb3ef44d6 100644 --- a/packages/sdk-codemod/src/index.ts +++ b/packages/sdk-codemod/src/index.ts @@ -18,9 +18,11 @@ const packageJson = await readPackageJSON(path.dirname(fileURLToPath(import.meta * @param review - The review task (codemod id, prompt, files) */ function printLlmReview(review: LlmReview): void { - process.stderr.write( - `\n🤖 LLM-assisted review suggested (${review.codemodId}) — the codemod cannot safely migrate these automatically:\n`, - ); + const scope = + review.files.length > 0 + ? "the codemod cannot safely migrate these automatically" + : "review the project for this manual change"; + process.stderr.write(`\n🤖 LLM-assisted review suggested (${review.codemodId}) — ${scope}:\n`); for (const file of review.files) { process.stderr.write(` - ${file}\n`); } @@ -88,10 +90,10 @@ human-readable form, so \`stdout\` stays pure JSON for piping.`, return; } - // Resolve script paths for all applicable codemods + // Resolve script paths for all applicable codemods (manual entries have none) const codemodEntries = codemods.map((codemod) => ({ codemod, - scriptPath: resolveCodemodScript(codemod.scriptPath), + scriptPath: codemod.scriptPath ? resolveCodemodScript(codemod.scriptPath) : undefined, })); for (const { codemod } of codemodEntries) { diff --git a/packages/sdk-codemod/src/migration-doc.ts b/packages/sdk-codemod/src/migration-doc.ts index b33cd7b6c..4139fccb6 100644 --- a/packages/sdk-codemod/src/migration-doc.ts +++ b/packages/sdk-codemod/src/migration-doc.ts @@ -49,10 +49,15 @@ function renderEntry(codemod: CodemodPackage): string { ); } - if (automationLevel(codemod) !== "Automatic" && codemod.prompt != null) { + const level = automationLevel(codemod); + if (level !== "Automatic" && codemod.prompt != null) { + const summary = + level === "Manual" + ? "Prompt for an AI agent (to perform this migration)" + : "Prompt for an AI agent (to finish the cases the codemod could not migrate)"; lines.push( "
", - "Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own)", + `${summary}`, "", "```text", codemod.prompt.trim(), diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index b40b8f9b0..e9d1ce7d2 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -256,6 +256,27 @@ export const allCodemods: CodemodPackage[] = [ }, ], }, + { + id: "v2/open-download-stream", + name: "openDownloadStream → downloadStream", + description: + "The deprecated `openDownloadStream` file-streaming API is removed in v2. Use `downloadStream` for streamed file downloads. The generated file utilities now emit `downloadFileStream` (which calls `downloadStream` and returns `FileDownloadStreamResponse`) instead of the removed `openFileDownloadStream` helper.", + since: "1.0.0", + until: "2.0.0", + // No scriptPath: this is a codemod-less ("manual") migration. + examples: [ + { + before: "const res = await openDownloadStream(namespace, typeName, fieldName, recordId);", + after: "const res = await downloadStream(namespace, typeName, fieldName, recordId);", + }, + ], + prompt: [ + "The openDownloadStream file-streaming API is removed in v2. Replace every call to", + "openDownloadStream with downloadStream (same arguments). If you used the generated", + "openFileDownloadStream helper, switch to downloadFileStream, which calls", + "downloadStream and returns FileDownloadStreamResponse.", + ].join("\n"), + }, ]; /** diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index b75650743..eb3e2609e 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -413,6 +413,53 @@ describe("runCodemods", () => { expect(result.llmReviews).toEqual([]); }); + test("emits a blanket LLM review for a codemod-less manual entry", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-manual-test-")); + tmpDir = dir; + await fs.promises.writeFile(path.join(dir, "a.ts"), "const x = 1;\n"); + + const result = await runCodemods( + [ + { + // No scriptPath: a manual entry that ships only a prompt. + codemod: makeCodemod("test/manual", "unused.ts", ["**/*.ts"], undefined, { + prompt: "Do the manual change.", + }), + scriptPath: undefined, + }, + ], + dir, + true, + ); + + expect(result.changed).toBe(false); + expect(result.llmReviews).toEqual([ + { codemodId: "test/manual", prompt: "Do the manual change.", files: [] }, + ]); + }); + + test("does not emit a blanket review for a legacy-pattern entry with a prompt", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-legacy-prompt-test-")); + tmpDir = dir; + await fs.promises.writeFile(path.join(dir, "a.ts"), "const x = 1;\n"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/legacy", partialTransformPath, ["**/*.ts"], ["needle"], { + prompt: "Finish the residual.", + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + // legacy-pattern entries surface via warnings, not a blanket llmReview. + expect(result.llmReviews).toEqual([]); + }); + test("AND-group legacy pattern warns only when every substring co-occurs", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-and-group-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 93d5248aa..4cf064660 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -123,7 +123,8 @@ async function loadTransform(scriptPath: string): Promise { /** A loaded transform with its file matcher. */ interface LoadedTransform { id: string; - transform: TransformFn; + /** Undefined for codemod-less ("manual") entries that ship only guidance. */ + transform?: TransformFn; matches: (relativePath: string) => boolean; legacyPatterns: Array; suspiciousPatterns: string[]; @@ -166,7 +167,7 @@ function legacyPatternWarnings( * @returns Combined result of all codemod executions */ export async function runCodemods( - codemods: Array<{ codemod: CodemodPackage; scriptPath: string }>, + codemods: Array<{ codemod: CodemodPackage; scriptPath?: string }>, targetPath: string, dryRun: boolean, ): Promise { @@ -176,7 +177,7 @@ export async function runCodemods( const patterns = codemod.filePatterns ?? DEFAULT_FILE_PATTERNS; loaded.push({ id: codemod.id, - transform: await loadTransform(scriptPath), + transform: scriptPath ? await loadTransform(scriptPath) : undefined, matches: picomatch(patterns, { dot: true }), legacyPatterns: codemod.legacyPatterns ?? [], suspiciousPatterns: codemod.suspiciousPatterns ?? [], @@ -208,6 +209,7 @@ export async function runCodemods( let current = original; for (const lt of matchedTransforms) { + if (!lt.transform) continue; const result = await lt.transform(current, absolute); if (result != null) { current = result; @@ -238,10 +240,17 @@ export async function runCodemods( const llmReviews: LlmReview[] = []; for (const lt of loaded) { - const files = suspiciousByCodemod.get(lt.id); - if (!lt.prompt || !files) continue; - // Sort for deterministic output regardless of filesystem traversal order. - llmReviews.push({ codemodId: lt.id, prompt: lt.prompt, files: files.toSorted() }); + if (!lt.prompt) continue; + if (lt.suspiciousPatterns.length > 0) { + // File-scoped: only surface when a suspicious pattern actually matched. + const files = suspiciousByCodemod.get(lt.id); + // Sort for deterministic output regardless of filesystem traversal order. + if (files) llmReviews.push({ codemodId: lt.id, prompt: lt.prompt, files: files.toSorted() }); + } else if (lt.legacyPatterns.length === 0) { + // Codemod-less manual change with no pattern to scope by: surface as + // project-wide guidance (legacyPattern-only entries warn instead). + llmReviews.push({ codemodId: lt.id, prompt: lt.prompt, files: [] }); + } } return { diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index b18665082..9d4a57486 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -24,8 +24,12 @@ export interface CodemodPackage { since: string; /** Target version this codemod upgrades to (semver, exclusive upper bound) */ until: string; - /** Path to the jssg transform script relative to the codemods root */ - scriptPath: string; + /** + * Path to the jssg transform script relative to the codemods root. Omit for a + * codemod-less ("manual") migration that ships only guidance — `prompt`, + * `examples`, and/or `suspiciousPatterns` — with no automatic transform. + */ + scriptPath?: string; /** Target language for codemod CLI (default: "typescript") */ language?: string; /** Custom file glob patterns. Defaults to TypeScript patterns when omitted. */ diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 84e5f0068..a63c266a9 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -34,7 +34,7 @@ export const generators = definePlugins(kyselyTypePlugin({ distPath: "db.ts" })) ```
-Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own) +Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text defineGenerators() is replaced by definePlugins() in v2. The codemod rewrites the @@ -101,7 +101,7 @@ tailor-sdk skills install ```
-Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own) +Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text The standalone tailor-sdk-skills binary is removed in v2; call the skills install @@ -147,7 +147,7 @@ body: ({ input, caller }) => caller.id, ```
-Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own) +Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text Finish the cases the codemod left for manual migration: @@ -200,7 +200,7 @@ tailor-sdk login --machine-user ```
-Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own) +Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text Apply the v2 CLI renames the codemod did not reach (only `tailor-sdk`-prefixed @@ -230,7 +230,7 @@ createResolver({ invoker: "manager" }); ```
-Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own) +Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the @@ -271,7 +271,7 @@ const command: tailordb.CommandType = "SELECT"; ```
-Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own) +Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text The capital-cased Tailordb ambient namespace is removed in v2; use the lowercase @@ -302,7 +302,7 @@ await executeScript({ ...opts, arg: { a: 1 } }); ```
-Prompt for an AI agent (to finish the cases the codemod cannot migrate on its own) +Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text In Tailor SDK v2 the executeScript() arg option takes a JSON-serializable value @@ -320,3 +320,33 @@ already pass a plain value unchanged. ```
+ +## openDownloadStream → downloadStream + +**Migration:** Manual + +The deprecated `openDownloadStream` file-streaming API is removed in v2. Use `downloadStream` for streamed file downloads. The generated file utilities now emit `downloadFileStream` (which calls `downloadStream` and returns `FileDownloadStreamResponse`) instead of the removed `openFileDownloadStream` helper. + +Before: + +```ts +const res = await openDownloadStream(namespace, typeName, fieldName, recordId); +``` + +After: + +```ts +const res = await downloadStream(namespace, typeName, fieldName, recordId); +``` + +
+Prompt for an AI agent (to perform this migration) + +```text +The openDownloadStream file-streaming API is removed in v2. Replace every call to +openDownloadStream with downloadStream (same arguments). If you used the generated +openFileDownloadStream helper, switch to downloadFileStream, which calls +downloadStream and returns FileDownloadStreamResponse. +``` + +
From c4982d40ed7197bd90bd100b67499dfea861cbc7 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 18 Jun 2026 00:12:48 +0900 Subject: [PATCH 136/618] docs(codemod): add the remaining codemod-less v2 migrations and notices Populate the migration guide with the v2 changes that have no codemod: - Manual migrations (user edits code): runtime globals are opt-in (import "@tailor-platform/sdk/runtime/globals"), and workflow `.trigger()` / trigger tests (mockWorkflow / runWorkflowLocally). Both ship examples + an AI prompt; the workflow entry flags `.trigger(` usages. - Behavioral notices (no source change): CLI tokens in the OS keyring, CLI users keyed by subject ID, and function logs requiring a content hash. A Manual entry with no examples and no prompt now renders as a "Behavioral change (no code change required)" notice instead of a "Manual" migration, so the no-action notices are not mislabelled. --- packages/sdk-codemod/src/migration-doc.ts | 17 +++-- packages/sdk-codemod/src/registry.ts | 76 +++++++++++++++++++ packages/sdk/docs/migration/v2.md | 91 +++++++++++++++++++++++ 3 files changed, 177 insertions(+), 7 deletions(-) diff --git a/packages/sdk-codemod/src/migration-doc.ts b/packages/sdk-codemod/src/migration-doc.ts index 4139fccb6..5cbdb317e 100644 --- a/packages/sdk-codemod/src/migration-doc.ts +++ b/packages/sdk-codemod/src/migration-doc.ts @@ -22,12 +22,16 @@ export function automationLevel(codemod: CodemodPackage): AutomationLevel { } function renderEntry(codemod: CodemodPackage): string { - const lines: string[] = [ - `## ${codemod.name}`, - "", - `**Migration:** ${automationLevel(codemod)}`, - "", - ]; + const level = automationLevel(codemod); + // A Manual entry that ships no examples and no prompt is an informational + // notice (a runtime/behavioral change with nothing in user source to edit), + // not a hand-migration. + const isNotice = + level === "Manual" && (codemod.examples?.length ?? 0) === 0 && codemod.prompt == null; + const header = isNotice + ? "**Type:** Behavioral change (no code change required)" + : `**Migration:** ${level}`; + const lines: string[] = [`## ${codemod.name}`, "", header, ""]; lines.push(codemod.description, ""); for (const example of codemod.examples ?? []) { @@ -49,7 +53,6 @@ function renderEntry(codemod: CodemodPackage): string { ); } - const level = automationLevel(codemod); if (level !== "Automatic" && codemod.prompt != null) { const summary = level === "Manual" diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index e9d1ce7d2..e7187dbe8 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -277,6 +277,82 @@ export const allCodemods: CodemodPackage[] = [ "downloadStream and returns FileDownloadStreamResponse.", ].join("\n"), }, + { + id: "v2/runtime-globals-opt-in", + name: "Runtime globals are opt-in", + description: + 'Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Opt in explicitly with `import "@tailor-platform/sdk/runtime/globals"`, or use the typed wrappers from `@tailor-platform/sdk/runtime`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.)', + since: "1.0.0", + until: "2.0.0", + // No scriptPath: which files rely on the ambient globals cannot be + // determined reliably enough to rewrite automatically. + examples: [ + { + caption: "Add the opt-in import to files that use the ambient globals:", + before: + "// relied on ambient `tailordb` / `tailor` just from importing the SDK\nconst result = await tailordb.query(/* ... */);", + after: + 'import "@tailor-platform/sdk/runtime/globals";\n\nconst result = await tailordb.query(/* ... */);', + }, + ], + prompt: [ + "In v2, importing @tailor-platform/sdk no longer activates the ambient tailor.* /", + "tailordb.* globals. For each file that uses those globals, add", + 'import "@tailor-platform/sdk/runtime/globals"; at the top, or migrate it to the', + "typed wrappers exported from @tailor-platform/sdk/runtime. Do not add the import to", + "files that do not use the globals.", + ].join("\n"), + }, + { + id: "v2/workflow-trigger-dispatch", + name: "Workflow .trigger() and trigger tests", + description: + "Workflow job `.trigger()` now aligns with the platform runtime: it returns the job result directly instead of a Promise wrapper, and tests no longer run job bodies locally. Mock trigger responses with `mockWorkflow()` (`setJobHandler` / `enqueueResult`, assert via `triggeredJobs`), or use `runWorkflowLocally()` for a full-chain local run.", + since: "1.0.0", + until: "2.0.0", + suspiciousPatterns: [".trigger("], + examples: [ + { + caption: "Tests must mock the workflow runtime instead of running bodies locally:", + before: + 'const result = await orderJob.trigger({ id });\nexpect(result.status).toBe("done");', + after: + 'using wf = mockWorkflow();\nwf.setJobHandler((jobName) => (jobName === "order-job" ? { status: "done" } : null));\nconst result = await orderJob.trigger({ id });\nexpect(result.status).toBe("done");', + }, + ], + prompt: [ + "Workflow job .trigger() now uses the platform workflow runtime instead of running", + "the job body locally. In tests, acquire `using wf = mockWorkflow()` and provide", + "trigger responses (setJobHandler / enqueueResult), or use runWorkflowLocally() for a", + "full-chain local run; an unmocked trigger now throws. Outside tests, treat the", + "trigger result as the job output directly (no Promise wrapper to unwrap).", + ].join("\n"), + }, + { + id: "v2/cli-token-keyring-storage", + name: "CLI tokens stored in the OS keyring", + description: + "CLI login tokens are stored in the OS keyring by default when available, falling back to the platform config file when it is not. No source change is required; re-login if you need tokens moved into the keyring.", + since: "1.0.0", + until: "2.0.0", + // Runtime/CLI behavior change — no user source to migrate. + }, + { + id: "v2/cli-users-by-subject", + name: "CLI users keyed by subject ID", + description: + "The CLI stores human users by their stable subject ID instead of email (email is kept for display). Legacy email-keyed entries are migrated automatically on the next login or token refresh. No source change is required.", + since: "1.0.0", + until: "2.0.0", + }, + { + id: "v2/function-logs-content-hash", + name: "function logs require a content hash for source mapping", + description: + "`tailor-sdk function logs` maps stack traces against the function bundle only when the execution recorded a `contentHash`. Executions without one now show raw stack traces instead of mapped frames. No source change is required.", + since: "1.0.0", + until: "2.0.0", + }, ]; /** diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index a63c266a9..0fa8f4968 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -350,3 +350,94 @@ downloadStream and returns FileDownloadStreamResponse. ```
+ +## Runtime globals are opt-in + +**Migration:** Manual + +Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Opt in explicitly with `import "@tailor-platform/sdk/runtime/globals"`, or use the typed wrappers from `@tailor-platform/sdk/runtime`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.) + +Add the opt-in import to files that use the ambient globals: + +Before: + +```ts +// relied on ambient `tailordb` / `tailor` just from importing the SDK +const result = await tailordb.query(/* ... */); +``` + +After: + +```ts +import "@tailor-platform/sdk/runtime/globals"; + +const result = await tailordb.query(/* ... */); +``` + +
+Prompt for an AI agent (to perform this migration) + +```text +In v2, importing @tailor-platform/sdk no longer activates the ambient tailor.* / +tailordb.* globals. For each file that uses those globals, add +import "@tailor-platform/sdk/runtime/globals"; at the top, or migrate it to the +typed wrappers exported from @tailor-platform/sdk/runtime. Do not add the import to +files that do not use the globals. +``` + +
+ +## Workflow .trigger() and trigger tests + +**Migration:** Manual + +Workflow job `.trigger()` now aligns with the platform runtime: it returns the job result directly instead of a Promise wrapper, and tests no longer run job bodies locally. Mock trigger responses with `mockWorkflow()` (`setJobHandler` / `enqueueResult`, assert via `triggeredJobs`), or use `runWorkflowLocally()` for a full-chain local run. + +Tests must mock the workflow runtime instead of running bodies locally: + +Before: + +```ts +const result = await orderJob.trigger({ id }); +expect(result.status).toBe("done"); +``` + +After: + +```ts +using wf = mockWorkflow(); +wf.setJobHandler((jobName) => (jobName === "order-job" ? { status: "done" } : null)); +const result = await orderJob.trigger({ id }); +expect(result.status).toBe("done"); +``` + +
+Prompt for an AI agent (to perform this migration) + +```text +Workflow job .trigger() now uses the platform workflow runtime instead of running +the job body locally. In tests, acquire `using wf = mockWorkflow()` and provide +trigger responses (setJobHandler / enqueueResult), or use runWorkflowLocally() for a +full-chain local run; an unmocked trigger now throws. Outside tests, treat the +trigger result as the job output directly (no Promise wrapper to unwrap). +``` + +
+ +## CLI tokens stored in the OS keyring + +**Type:** Behavioral change (no code change required) + +CLI login tokens are stored in the OS keyring by default when available, falling back to the platform config file when it is not. No source change is required; re-login if you need tokens moved into the keyring. + +## CLI users keyed by subject ID + +**Type:** Behavioral change (no code change required) + +The CLI stores human users by their stable subject ID instead of email (email is kept for display). Legacy email-keyed entries are migrated automatically on the next login or token refresh. No source change is required. + +## function logs require a content hash for source mapping + +**Type:** Behavioral change (no code change required) + +`tailor-sdk function logs` maps stack traces against the function bundle only when the execution recorded a `contentHash`. Executions without one now show raw stack traces instead of mapped frames. No source change is required. From 83145db9a0d243aa68c1b641c2b6026771a62188 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 22:00:40 +0900 Subject: [PATCH 137/618] fix(tailordb): set updatedAt on create --- .changeset/timestamps-updated-at-create.md | 7 ++++ example/e2e/tailordb.test.ts | 8 ++--- example/generated/tailordb.ts | 20 +++++------ example/seed/data/Customer.schema.ts | 4 +-- example/seed/data/Event.schema.ts | 4 +-- example/seed/data/Invoice.schema.ts | 4 +-- example/seed/data/NestedProfile.schema.ts | 4 +-- example/seed/data/PurchaseOrder.schema.ts | 4 +-- example/seed/data/SalesOrder.schema.ts | 4 +-- example/seed/data/Supplier.schema.ts | 4 +-- example/seed/data/User.schema.ts | 4 +-- example/seed/data/UserLog.schema.ts | 4 +-- example/seed/data/UserSetting.schema.ts | 4 +-- example/tests/fixtures/expected/db.ts | 20 +++++------ .../expected/seed/data/Customer.schema.ts | 4 +-- .../expected/seed/data/Event.schema.ts | 4 +-- .../expected/seed/data/Invoice.schema.ts | 4 +-- .../seed/data/NestedProfile.schema.ts | 4 +-- .../seed/data/PurchaseOrder.schema.ts | 4 +-- .../expected/seed/data/SalesOrder.schema.ts | 4 +-- .../expected/seed/data/Supplier.schema.ts | 4 +-- .../expected/seed/data/User.schema.ts | 4 +-- .../expected/seed/data/UserLog.schema.ts | 4 +-- .../expected/seed/data/UserSetting.schema.ts | 4 +-- example/workflows/jobs/fetch-customer.ts | 2 +- packages/sdk/docs/services/tailordb.md | 2 ++ .../services/tailordb/schema.test.ts | 33 +++++++++++++++++-- .../src/configure/services/tailordb/schema.ts | 12 +++---- .../plugin/builtin/kysely-type/index.test.ts | 2 +- .../kysely-type/type-processor.test.ts | 2 +- 30 files changed, 113 insertions(+), 75 deletions(-) create mode 100644 .changeset/timestamps-updated-at-create.md diff --git a/.changeset/timestamps-updated-at-create.md b/.changeset/timestamps-updated-at-create.md new file mode 100644 index 000000000..422bb9de7 --- /dev/null +++ b/.changeset/timestamps-updated-at-create.md @@ -0,0 +1,7 @@ +--- +"@tailor-platform/sdk": major +--- + +Set `db.fields.timestamps()` `updatedAt` when records are created and make the generated field non-null. The helper now creates non-null `createdAt` and `updatedAt` fields with create hooks that preserve provided values and fall back to the current time. + +`updatedAt` no longer gets an update hook from the helper; define a custom `updatedAt` field if your schema should refresh it automatically on record updates. diff --git a/example/e2e/tailordb.test.ts b/example/e2e/tailordb.test.ts index 91499ca17..0b2ba1d35 100644 --- a/example/e2e/tailordb.test.ts +++ b/example/e2e/tailordb.test.ts @@ -51,7 +51,7 @@ describe("controlplane", () => { }, updatedAt: { type: "datetime", - required: false, + required: true, array: false, hooks: expect.any(Object), }, @@ -497,8 +497,8 @@ describe("dataplane", () => { createUser: { id: string; name: string; - createdAt?: string; - updatedAt?: string; + createdAt: string; + updatedAt: string; }; } const createResult = await graphQLClient.rawRequest(create); @@ -508,7 +508,7 @@ describe("dataplane", () => { id: expect.any(String), name: "alice", createdAt: expect.any(String), - updatedAt: null, + updatedAt: expect.any(String), }, }); const userId = createResult.data.createUser.id; diff --git a/example/generated/tailordb.ts b/example/generated/tailordb.ts index db9ea0e47..e769dbf3a 100644 --- a/example/generated/tailordb.ts +++ b/example/generated/tailordb.ts @@ -27,7 +27,7 @@ export interface Namespace { fullAddress: Generated; state: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Invoice: { @@ -38,7 +38,7 @@ export interface Namespace { sequentialId: Serial; status: "draft" | "sent" | "paid" | "cancelled" | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } NestedProfile: { @@ -57,7 +57,7 @@ export interface Namespace { }>; archived: boolean | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } PurchaseOrder: { @@ -73,7 +73,7 @@ export interface Namespace { type: "text" | "image"; }[]; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } SalesOrder: { @@ -86,7 +86,7 @@ export interface Namespace { cancelReason: string | null; canceledAt: Timestamp | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } SalesOrderCreated: { @@ -115,7 +115,7 @@ export interface Namespace { state: "Alabama" | "Alaska"; city: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } User: { @@ -126,7 +126,7 @@ export interface Namespace { department: string | null; role: "MANAGER" | "STAFF"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } UserLog: { @@ -134,7 +134,7 @@ export interface Namespace { userID: string; message: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } UserSetting: { @@ -142,7 +142,7 @@ export interface Namespace { language: "jp" | "en"; userID: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } }, "analyticsdb": { @@ -150,7 +150,7 @@ export interface Namespace { id: Generated; name: "CLICK" | "VIEW" | "PURCHASE"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/example/seed/data/Customer.schema.ts b/example/seed/data/Customer.schema.ts index 756c2f29e..3d82e446d 100644 --- a/example/seed/data/Customer.schema.ts +++ b/example/seed/data/Customer.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { customer } from "../../tailordb/customer"; const schemaType = t.object({ - ...customer.pickFields(["id","fullAddress","createdAt"], { optional: true }), - ...customer.omitFields(["id","fullAddress","createdAt"]), + ...customer.pickFields(["id","fullAddress","createdAt","updatedAt"], { optional: true }), + ...customer.omitFields(["id","fullAddress","createdAt","updatedAt"]), }); const hook = createTailorDBHook(customer); diff --git a/example/seed/data/Event.schema.ts b/example/seed/data/Event.schema.ts index 0bc3d8691..94943c552 100644 --- a/example/seed/data/Event.schema.ts +++ b/example/seed/data/Event.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { event } from "../../analyticsdb/event"; const schemaType = t.object({ - ...event.pickFields(["id","createdAt"], { optional: true }), - ...event.omitFields(["id","createdAt"]), + ...event.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...event.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(event); diff --git a/example/seed/data/Invoice.schema.ts b/example/seed/data/Invoice.schema.ts index b25da906f..614863d24 100644 --- a/example/seed/data/Invoice.schema.ts +++ b/example/seed/data/Invoice.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { invoice } from "../../tailordb/invoice"; const schemaType = t.object({ - ...invoice.pickFields(["id","createdAt"], { optional: true }), - ...invoice.omitFields(["id","createdAt","invoiceNumber","sequentialId"]), + ...invoice.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...invoice.omitFields(["id","createdAt","updatedAt","invoiceNumber","sequentialId"]), }); const hook = createTailorDBHook(invoice); diff --git a/example/seed/data/NestedProfile.schema.ts b/example/seed/data/NestedProfile.schema.ts index 2c52ea377..18efb54dc 100644 --- a/example/seed/data/NestedProfile.schema.ts +++ b/example/seed/data/NestedProfile.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { nestedProfile } from "../../tailordb/nested"; const schemaType = t.object({ - ...nestedProfile.pickFields(["id","createdAt"], { optional: true }), - ...nestedProfile.omitFields(["id","createdAt"]), + ...nestedProfile.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...nestedProfile.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(nestedProfile); diff --git a/example/seed/data/PurchaseOrder.schema.ts b/example/seed/data/PurchaseOrder.schema.ts index 3a26ef3a3..7b4aef58a 100644 --- a/example/seed/data/PurchaseOrder.schema.ts +++ b/example/seed/data/PurchaseOrder.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { purchaseOrder } from "../../tailordb/purchaseOrder"; const schemaType = t.object({ - ...purchaseOrder.pickFields(["id","createdAt"], { optional: true }), - ...purchaseOrder.omitFields(["id","createdAt"]), + ...purchaseOrder.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...purchaseOrder.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(purchaseOrder); diff --git a/example/seed/data/SalesOrder.schema.ts b/example/seed/data/SalesOrder.schema.ts index 3f2533204..132da34f6 100644 --- a/example/seed/data/SalesOrder.schema.ts +++ b/example/seed/data/SalesOrder.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { salesOrder } from "../../tailordb/salesOrder"; const schemaType = t.object({ - ...salesOrder.pickFields(["id","createdAt"], { optional: true }), - ...salesOrder.omitFields(["id","createdAt"]), + ...salesOrder.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...salesOrder.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(salesOrder); diff --git a/example/seed/data/Supplier.schema.ts b/example/seed/data/Supplier.schema.ts index bac16337c..722f453e7 100644 --- a/example/seed/data/Supplier.schema.ts +++ b/example/seed/data/Supplier.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { supplier } from "../../tailordb/supplier"; const schemaType = t.object({ - ...supplier.pickFields(["id","createdAt"], { optional: true }), - ...supplier.omitFields(["id","createdAt"]), + ...supplier.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...supplier.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(supplier); diff --git a/example/seed/data/User.schema.ts b/example/seed/data/User.schema.ts index 6c5a84d86..16ee30b3f 100644 --- a/example/seed/data/User.schema.ts +++ b/example/seed/data/User.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { user } from "../../tailordb/user"; const schemaType = t.object({ - ...user.pickFields(["id","createdAt"], { optional: true }), - ...user.omitFields(["id","createdAt"]), + ...user.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...user.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(user); diff --git a/example/seed/data/UserLog.schema.ts b/example/seed/data/UserLog.schema.ts index 32dfc98fa..1efa27fdd 100644 --- a/example/seed/data/UserLog.schema.ts +++ b/example/seed/data/UserLog.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { userLog } from "../../tailordb/userLog"; const schemaType = t.object({ - ...userLog.pickFields(["id","createdAt"], { optional: true }), - ...userLog.omitFields(["id","createdAt"]), + ...userLog.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...userLog.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(userLog); diff --git a/example/seed/data/UserSetting.schema.ts b/example/seed/data/UserSetting.schema.ts index 553d42c9e..fecf7e43e 100644 --- a/example/seed/data/UserSetting.schema.ts +++ b/example/seed/data/UserSetting.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { userSetting } from "../../tailordb/userSetting"; const schemaType = t.object({ - ...userSetting.pickFields(["id","createdAt"], { optional: true }), - ...userSetting.omitFields(["id","createdAt"]), + ...userSetting.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...userSetting.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(userSetting); diff --git a/example/tests/fixtures/expected/db.ts b/example/tests/fixtures/expected/db.ts index db9ea0e47..e769dbf3a 100644 --- a/example/tests/fixtures/expected/db.ts +++ b/example/tests/fixtures/expected/db.ts @@ -27,7 +27,7 @@ export interface Namespace { fullAddress: Generated; state: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Invoice: { @@ -38,7 +38,7 @@ export interface Namespace { sequentialId: Serial; status: "draft" | "sent" | "paid" | "cancelled" | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } NestedProfile: { @@ -57,7 +57,7 @@ export interface Namespace { }>; archived: boolean | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } PurchaseOrder: { @@ -73,7 +73,7 @@ export interface Namespace { type: "text" | "image"; }[]; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } SalesOrder: { @@ -86,7 +86,7 @@ export interface Namespace { cancelReason: string | null; canceledAt: Timestamp | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } SalesOrderCreated: { @@ -115,7 +115,7 @@ export interface Namespace { state: "Alabama" | "Alaska"; city: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } User: { @@ -126,7 +126,7 @@ export interface Namespace { department: string | null; role: "MANAGER" | "STAFF"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } UserLog: { @@ -134,7 +134,7 @@ export interface Namespace { userID: string; message: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } UserSetting: { @@ -142,7 +142,7 @@ export interface Namespace { language: "jp" | "en"; userID: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } }, "analyticsdb": { @@ -150,7 +150,7 @@ export interface Namespace { id: Generated; name: "CLICK" | "VIEW" | "PURCHASE"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/example/tests/fixtures/expected/seed/data/Customer.schema.ts b/example/tests/fixtures/expected/seed/data/Customer.schema.ts index 145333a65..11ecb751c 100644 --- a/example/tests/fixtures/expected/seed/data/Customer.schema.ts +++ b/example/tests/fixtures/expected/seed/data/Customer.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { customer } from "../../../../../tailordb/customer"; const schemaType = t.object({ - ...customer.pickFields(["id","fullAddress","createdAt"], { optional: true }), - ...customer.omitFields(["id","fullAddress","createdAt"]), + ...customer.pickFields(["id","fullAddress","createdAt","updatedAt"], { optional: true }), + ...customer.omitFields(["id","fullAddress","createdAt","updatedAt"]), }); const hook = createTailorDBHook(customer); diff --git a/example/tests/fixtures/expected/seed/data/Event.schema.ts b/example/tests/fixtures/expected/seed/data/Event.schema.ts index 2c9f19fc4..50a3f4d02 100644 --- a/example/tests/fixtures/expected/seed/data/Event.schema.ts +++ b/example/tests/fixtures/expected/seed/data/Event.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { event } from "../../../../../analyticsdb/event"; const schemaType = t.object({ - ...event.pickFields(["id","createdAt"], { optional: true }), - ...event.omitFields(["id","createdAt"]), + ...event.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...event.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(event); diff --git a/example/tests/fixtures/expected/seed/data/Invoice.schema.ts b/example/tests/fixtures/expected/seed/data/Invoice.schema.ts index 020f3b52e..a2a7cfc49 100644 --- a/example/tests/fixtures/expected/seed/data/Invoice.schema.ts +++ b/example/tests/fixtures/expected/seed/data/Invoice.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { invoice } from "../../../../../tailordb/invoice"; const schemaType = t.object({ - ...invoice.pickFields(["id","createdAt"], { optional: true }), - ...invoice.omitFields(["id","createdAt","invoiceNumber","sequentialId"]), + ...invoice.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...invoice.omitFields(["id","createdAt","updatedAt","invoiceNumber","sequentialId"]), }); const hook = createTailorDBHook(invoice); diff --git a/example/tests/fixtures/expected/seed/data/NestedProfile.schema.ts b/example/tests/fixtures/expected/seed/data/NestedProfile.schema.ts index 38dc3b4e1..2424ec58c 100644 --- a/example/tests/fixtures/expected/seed/data/NestedProfile.schema.ts +++ b/example/tests/fixtures/expected/seed/data/NestedProfile.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { nestedProfile } from "../../../../../tailordb/nested"; const schemaType = t.object({ - ...nestedProfile.pickFields(["id","createdAt"], { optional: true }), - ...nestedProfile.omitFields(["id","createdAt"]), + ...nestedProfile.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...nestedProfile.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(nestedProfile); diff --git a/example/tests/fixtures/expected/seed/data/PurchaseOrder.schema.ts b/example/tests/fixtures/expected/seed/data/PurchaseOrder.schema.ts index 4e4f269a4..c73327132 100644 --- a/example/tests/fixtures/expected/seed/data/PurchaseOrder.schema.ts +++ b/example/tests/fixtures/expected/seed/data/PurchaseOrder.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { purchaseOrder } from "../../../../../tailordb/purchaseOrder"; const schemaType = t.object({ - ...purchaseOrder.pickFields(["id","createdAt"], { optional: true }), - ...purchaseOrder.omitFields(["id","createdAt"]), + ...purchaseOrder.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...purchaseOrder.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(purchaseOrder); diff --git a/example/tests/fixtures/expected/seed/data/SalesOrder.schema.ts b/example/tests/fixtures/expected/seed/data/SalesOrder.schema.ts index db0543332..37a210130 100644 --- a/example/tests/fixtures/expected/seed/data/SalesOrder.schema.ts +++ b/example/tests/fixtures/expected/seed/data/SalesOrder.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { salesOrder } from "../../../../../tailordb/salesOrder"; const schemaType = t.object({ - ...salesOrder.pickFields(["id","createdAt"], { optional: true }), - ...salesOrder.omitFields(["id","createdAt"]), + ...salesOrder.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...salesOrder.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(salesOrder); diff --git a/example/tests/fixtures/expected/seed/data/Supplier.schema.ts b/example/tests/fixtures/expected/seed/data/Supplier.schema.ts index 3fb6af855..db4627803 100644 --- a/example/tests/fixtures/expected/seed/data/Supplier.schema.ts +++ b/example/tests/fixtures/expected/seed/data/Supplier.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { supplier } from "../../../../../tailordb/supplier"; const schemaType = t.object({ - ...supplier.pickFields(["id","createdAt"], { optional: true }), - ...supplier.omitFields(["id","createdAt"]), + ...supplier.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...supplier.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(supplier); diff --git a/example/tests/fixtures/expected/seed/data/User.schema.ts b/example/tests/fixtures/expected/seed/data/User.schema.ts index 4c9d24707..21342b67d 100644 --- a/example/tests/fixtures/expected/seed/data/User.schema.ts +++ b/example/tests/fixtures/expected/seed/data/User.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { user } from "../../../../../tailordb/user"; const schemaType = t.object({ - ...user.pickFields(["id","createdAt"], { optional: true }), - ...user.omitFields(["id","createdAt"]), + ...user.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...user.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(user); diff --git a/example/tests/fixtures/expected/seed/data/UserLog.schema.ts b/example/tests/fixtures/expected/seed/data/UserLog.schema.ts index fd3b3c52b..556c81e96 100644 --- a/example/tests/fixtures/expected/seed/data/UserLog.schema.ts +++ b/example/tests/fixtures/expected/seed/data/UserLog.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { userLog } from "../../../../../tailordb/userLog"; const schemaType = t.object({ - ...userLog.pickFields(["id","createdAt"], { optional: true }), - ...userLog.omitFields(["id","createdAt"]), + ...userLog.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...userLog.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(userLog); diff --git a/example/tests/fixtures/expected/seed/data/UserSetting.schema.ts b/example/tests/fixtures/expected/seed/data/UserSetting.schema.ts index d5f2fb052..87fec2df1 100644 --- a/example/tests/fixtures/expected/seed/data/UserSetting.schema.ts +++ b/example/tests/fixtures/expected/seed/data/UserSetting.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { userSetting } from "../../../../../tailordb/userSetting"; const schemaType = t.object({ - ...userSetting.pickFields(["id","createdAt"], { optional: true }), - ...userSetting.omitFields(["id","createdAt"]), + ...userSetting.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...userSetting.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(userSetting); diff --git a/example/workflows/jobs/fetch-customer.ts b/example/workflows/jobs/fetch-customer.ts index 98bd60bf0..d7891370e 100644 --- a/example/workflows/jobs/fetch-customer.ts +++ b/example/workflows/jobs/fetch-customer.ts @@ -14,7 +14,7 @@ export const fetchCustomer = createWorkflowJob({ return { ...customer, createdAt: customer.createdAt.toISOString(), - updatedAt: customer.updatedAt?.toISOString() ?? null, + updatedAt: customer.updatedAt.toISOString(), }; }, }); diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index 39442b7bc..780e04450 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -409,6 +409,8 @@ export const user = db.type("User", { }); ``` +`db.fields.timestamps()` adds non-null `createdAt` and `updatedAt` datetime fields. Both fields are populated when a record is created; provided values are preserved so seed data can use historical timestamps. Define a custom `updatedAt` field if it should refresh automatically on record updates. + ## Type Modifiers ### Composite Indexes diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 34ca42478..69a04827e 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -603,7 +603,7 @@ describe("TailorDBType withTimestamps option tests", () => { id: string; name: string; createdAt: string | Date; - updatedAt?: string | Date | null; + updatedAt: string | Date; }>(); }); @@ -631,6 +631,35 @@ describe("TailorDBType withTimestamps option tests", () => { expect((result as Date).getTime()).toBeGreaterThanOrEqual(before); expect((result as Date).getTime()).toBeLessThanOrEqual(after); }); + + test("updatedAt create hook respects a user-specified value", () => { + const { updatedAt } = db.fields.timestamps(); + const createHook = updatedAt.metadata.hooks?.create; + expect(createHook).toBeDefined(); + + const specified = new Date("2025-02-10T09:00:00Z"); + const result = createHook!({ value: specified, data: {}, user: timestampHookUser }); + expect(result).toBe(specified); + }); + + test("updatedAt create hook falls back to now when no value is given", () => { + const { updatedAt } = db.fields.timestamps(); + const createHook = updatedAt.metadata.hooks?.create; + expect(createHook).toBeDefined(); + + const before = Date.now(); + const result = createHook!({ value: null, data: {}, user: timestampHookUser }); + const after = Date.now(); + expect(result).toBeInstanceOf(Date); + expect((result as Date).getTime()).toBeGreaterThanOrEqual(before); + expect((result as Date).getTime()).toBeLessThanOrEqual(after); + }); + + test("updatedAt does not define an update hook", () => { + const { updatedAt } = db.fields.timestamps(); + + expect(updatedAt.metadata.hooks?.update).toBeUndefined(); + }); }); describe("TailorDBType composite type tests", () => { @@ -832,7 +861,7 @@ describe("TailorDBType plural form tests", () => { title: string; content?: string | null; createdAt: string | Date; - updatedAt?: string | Date | null; + updatedAt: string | Date; }>(); expect(_postType.name).toBe("Post"); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 320e530c6..07052b613 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -1162,9 +1162,9 @@ export const db = { fields: { /** * Creates standard timestamp fields (createdAt, updatedAt) with auto-hooks. - * createdAt is set on create, updatedAt is set on update. - * A user-specified createdAt is respected when provided (e.g. seeding historical - * records); the current time is used only when the value is omitted. + * createdAt and updatedAt are set on create. + * User-specified timestamp values are respected when provided (e.g. seeding + * historical records); the current time is used only when the value is omitted. * @returns An object with createdAt and updatedAt fields * @example * const model = db.type("Model", { @@ -1176,9 +1176,9 @@ export const db = { createdAt: datetime() .hooks({ create: ({ value }) => value ?? new Date() }) .description("Record creation timestamp"), - updatedAt: datetime({ optional: true }) - .hooks({ update: () => new Date() }) - .description("Record last update timestamp"), + updatedAt: datetime() + .hooks({ create: ({ value }) => value ?? new Date() }) + .description("Record update timestamp"), }), }, }; diff --git a/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts index 30299a11b..40700616e 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts @@ -81,7 +81,7 @@ describe("KyselyTypePlugin integration tests", () => { expect(result.typeDef).toContain("lastLogin: Timestamp | null;"); expect(result.typeDef).toContain("tags: string[];"); expect(result.typeDef).toContain("createdAt: Generated;"); - expect(result.typeDef).toContain("updatedAt: Timestamp | null;"); + expect(result.typeDef).toContain("updatedAt: Generated;"); }); test("should have correct id and description", () => { diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts index 968d2c2f2..f38b612eb 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts @@ -284,7 +284,7 @@ describe("Kysely TypeProcessor", () => { expect(result.typeDef).toContain("UserWithTimestamp: {"); expect(result.typeDef).toContain("name: string"); expect(result.typeDef).toContain("createdAt: Generated;"); - expect(result.typeDef).toContain("updatedAt: Timestamp | null;"); + expect(result.typeDef).toContain("updatedAt: Generated;"); }); test("should always include Generated for id field", async () => { From b45422ae06a311c84cd823325237be940493c820 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 22:02:52 +0900 Subject: [PATCH 138/618] docs(changeset): note updatedAt migration impact --- .changeset/timestamps-updated-at-create.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/timestamps-updated-at-create.md b/.changeset/timestamps-updated-at-create.md index 422bb9de7..951504ae9 100644 --- a/.changeset/timestamps-updated-at-create.md +++ b/.changeset/timestamps-updated-at-create.md @@ -5,3 +5,5 @@ Set `db.fields.timestamps()` `updatedAt` when records are created and make the generated field non-null. The helper now creates non-null `createdAt` and `updatedAt` fields with create hooks that preserve provided values and fall back to the current time. `updatedAt` no longer gets an update hook from the helper; define a custom `updatedAt` field if your schema should refresh it automatically on record updates. + +Existing TailorDB schemas that already use this helper will change `updatedAt` from optional to required. Backfill existing records that have `updatedAt: null` before applying the schema change. From 64737790c32ce25ab0b3226f7067a8ecaff9b712 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 22:21:25 +0900 Subject: [PATCH 139/618] fix(tailordb): align updatedAt generated artifacts --- .changeset/timestamps-updated-at-create.md | 3 + example/migrations/0003/db.ts | 132 ++++++++ example/migrations/0003/diff.json | 283 ++++++++++++++++++ example/migrations/0003/migrate.ts | 70 +++++ .../templates/executor/src/generated/db.ts | 6 +- .../templates/generators/src/generated/db.ts | 6 +- .../generators/src/seed/data/Order.schema.ts | 4 +- .../src/seed/data/Product.schema.ts | 4 +- .../generators/src/seed/data/User.schema.ts | 4 +- .../src/generated/kysely-tailordb.ts | 2 +- .../src/generated/kysely-tailordb.ts | 16 +- .../templates/resolver/src/generated/db.ts | 2 +- .../templates/tailordb/src/generated/db.ts | 6 +- .../templates/workflow/src/generated/db.ts | 4 +- .../migrate/db-types-generator.test.ts | 31 ++ .../tailordb/migrate/db-types-generator.ts | 19 ++ 16 files changed, 565 insertions(+), 27 deletions(-) create mode 100644 example/migrations/0003/db.ts create mode 100644 example/migrations/0003/diff.json create mode 100644 example/migrations/0003/migrate.ts diff --git a/.changeset/timestamps-updated-at-create.md b/.changeset/timestamps-updated-at-create.md index 951504ae9..033e9a2f1 100644 --- a/.changeset/timestamps-updated-at-create.md +++ b/.changeset/timestamps-updated-at-create.md @@ -1,9 +1,12 @@ --- "@tailor-platform/sdk": major +"@tailor-platform/create-sdk": major --- Set `db.fields.timestamps()` `updatedAt` when records are created and make the generated field non-null. The helper now creates non-null `createdAt` and `updatedAt` fields with create hooks that preserve provided values and fall back to the current time. +Update create-sdk templates so scaffolded projects use the new non-null `updatedAt` Kysely types and seed schemas. + `updatedAt` no longer gets an update hook from the helper; define a custom `updatedAt` field if your schema should refresh it automatically on record updates. Existing TailorDB schemas that already use this helper will change `updatedAt` from optional to required. Backfill existing records that have `updatedAt: null` before applying the schema change. diff --git a/example/migrations/0003/db.ts b/example/migrations/0003/db.ts new file mode 100644 index 000000000..3dc32b41c --- /dev/null +++ b/example/migrations/0003/db.ts @@ -0,0 +1,132 @@ +/** + * Auto-generated Kysely types for migration script. + * These types reflect the database schema state at this migration point. + * + * DO NOT EDIT - This file is auto-generated by the migration system. + */ + +import { + type ColumnType, + type Transaction as KyselyTransaction, +} from "@tailor-platform/sdk/kysely"; +import type { Env } from "@tailor-platform/sdk"; + +type Timestamp = ColumnType; +type Generated = + T extends ColumnType + ? ColumnType + : ColumnType; + +interface Database { + Customer: { + id: Generated; + name: string; + email: string; + phone: string | null; + country: string; + postalCode: string; + address: string | null; + city: string | null; + fullAddress: string; + state: string; + createdAt: Timestamp; + updatedAt: ColumnType; + }; + Invoice: { + id: Generated; + invoiceNumber: string; + salesOrderID: string; + amount: number | null; + sequentialId: number; + status: "draft" | "sent" | "paid" | "cancelled" | null; + createdAt: Timestamp; + updatedAt: ColumnType; + }; + NestedProfile: { + id: Generated; + userInfo: string; + metadata: string; + archived: boolean | null; + createdAt: Timestamp; + updatedAt: ColumnType; + }; + PurchaseOrder: { + id: Generated; + supplierID: string; + totalPrice: number; + discount: number | null; + status: string; + attachedFiles: string[]; + createdAt: Timestamp; + updatedAt: ColumnType; + }; + SalesOrder: { + id: Generated; + customerID: string; + approvedByUserIDs: string[] | null; + totalPrice: number | null; + discount: number | null; + status: string | null; + cancelReason: string | null; + canceledAt: Timestamp | null; + createdAt: Timestamp; + updatedAt: ColumnType; + }; + SalesOrderCreated: { + id: Generated; + salesOrderID: string; + customerID: string; + totalPrice: number | null; + status: string | null; + }; + Selfie: { + id: Generated; + name: string; + parentID: string | null; + dependId: string | null; + }; + Supplier: { + id: Generated; + name: string; + phone: string; + fax: string | null; + email: string | null; + postalCode: string; + country: string; + state: "Alabama" | "Alaska"; + city: string; + createdAt: Timestamp; + updatedAt: ColumnType; + }; + User: { + id: Generated; + name: string; + email: string; + status: string | null; + department: string | null; + role: "MANAGER" | "STAFF"; + createdAt: Timestamp; + updatedAt: ColumnType; + }; + UserLog: { + id: Generated; + userID: string; + message: string; + createdAt: Timestamp; + updatedAt: ColumnType; + }; + UserSetting: { + id: Generated; + language: "jp" | "en"; + userID: string; + createdAt: Timestamp; + updatedAt: ColumnType; + }; +} + +export type Transaction = KyselyTransaction; + +/** Context passed as the second argument to the migration's `main` function. */ +export type MigrationContext = { + env: keyof Env extends never ? Record : Env; +}; diff --git a/example/migrations/0003/diff.json b/example/migrations/0003/diff.json new file mode 100644 index 000000000..e3052d201 --- /dev/null +++ b/example/migrations/0003/diff.json @@ -0,0 +1,283 @@ +{ + "version": 1, + "namespace": "tailordb", + "createdAt": "2026-06-17T13:17:08.592Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + } + } + ], + "hasBreakingChanges": true, + "breakingChanges": [ + { + "typeName": "Customer", + "fieldName": "updatedAt", + "reason": "Field changed from optional to required" + }, + { + "typeName": "Invoice", + "fieldName": "updatedAt", + "reason": "Field changed from optional to required" + }, + { + "typeName": "NestedProfile", + "fieldName": "updatedAt", + "reason": "Field changed from optional to required" + }, + { + "typeName": "PurchaseOrder", + "fieldName": "updatedAt", + "reason": "Field changed from optional to required" + }, + { + "typeName": "SalesOrder", + "fieldName": "updatedAt", + "reason": "Field changed from optional to required" + }, + { + "typeName": "Supplier", + "fieldName": "updatedAt", + "reason": "Field changed from optional to required" + }, + { + "typeName": "User", + "fieldName": "updatedAt", + "reason": "Field changed from optional to required" + }, + { + "typeName": "UserLog", + "fieldName": "updatedAt", + "reason": "Field changed from optional to required" + }, + { + "typeName": "UserSetting", + "fieldName": "updatedAt", + "reason": "Field changed from optional to required" + } + ], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": true +} diff --git a/example/migrations/0003/migrate.ts b/example/migrations/0003/migrate.ts new file mode 100644 index 000000000..ba45b71dd --- /dev/null +++ b/example/migrations/0003/migrate.ts @@ -0,0 +1,70 @@ +/** + * Migration script for tailordb + * + * This script runs between the Pre-migration and Post-migration phases of + * 'tailor-sdk deploy'. Use it to transform existing data so that the schema + * change can complete safely (for breaking changes, this is hard-required; + * for warning-tier changes it is optional). Edit this file to implement + * your data migration logic. + * + * The transaction is managed by the deploy command. + * If any operation fails, all changes will be rolled back. + */ + +import type { Transaction } from "./db"; + +export async function main(trx: Transaction): Promise { + await trx + .updateTable("Customer") + .set((eb) => ({ updatedAt: eb.ref("createdAt") })) + .where("updatedAt", "is", null) + .execute(); + + await trx + .updateTable("Invoice") + .set((eb) => ({ updatedAt: eb.ref("createdAt") })) + .where("updatedAt", "is", null) + .execute(); + + await trx + .updateTable("NestedProfile") + .set((eb) => ({ updatedAt: eb.ref("createdAt") })) + .where("updatedAt", "is", null) + .execute(); + + await trx + .updateTable("PurchaseOrder") + .set((eb) => ({ updatedAt: eb.ref("createdAt") })) + .where("updatedAt", "is", null) + .execute(); + + await trx + .updateTable("SalesOrder") + .set((eb) => ({ updatedAt: eb.ref("createdAt") })) + .where("updatedAt", "is", null) + .execute(); + + await trx + .updateTable("Supplier") + .set((eb) => ({ updatedAt: eb.ref("createdAt") })) + .where("updatedAt", "is", null) + .execute(); + + await trx + .updateTable("User") + .set((eb) => ({ updatedAt: eb.ref("createdAt") })) + .where("updatedAt", "is", null) + .execute(); + + await trx + .updateTable("UserLog") + .set((eb) => ({ updatedAt: eb.ref("createdAt") })) + .where("updatedAt", "is", null) + .execute(); + + await trx + .updateTable("UserSetting") + .set((eb) => ({ updatedAt: eb.ref("createdAt") })) + .where("updatedAt", "is", null) + .execute(); +} diff --git a/packages/create-sdk/templates/executor/src/generated/db.ts b/packages/create-sdk/templates/executor/src/generated/db.ts index 1bdcc0d1f..c50b7fcbe 100644 --- a/packages/create-sdk/templates/executor/src/generated/db.ts +++ b/packages/create-sdk/templates/executor/src/generated/db.ts @@ -20,7 +20,7 @@ export interface Namespace { entityId: string; message: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Notification: { @@ -30,7 +30,7 @@ export interface Namespace { body: string; isRead: boolean; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } User: { @@ -39,7 +39,7 @@ export interface Namespace { email: string; role: "ADMIN" | "MEMBER"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/packages/create-sdk/templates/generators/src/generated/db.ts b/packages/create-sdk/templates/generators/src/generated/db.ts index 0da558bfe..a9b7441d5 100644 --- a/packages/create-sdk/templates/generators/src/generated/db.ts +++ b/packages/create-sdk/templates/generators/src/generated/db.ts @@ -28,7 +28,7 @@ export interface Namespace { totalPrice: number; status: "PENDING" | "CONFIRMED" | "SHIPPED" | "DELIVERED" | "CANCELLED"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Product: { @@ -39,7 +39,7 @@ export interface Namespace { status: "DRAFT" | "ACTIVE" | "DISCONTINUED"; categoryId: string | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } User: { @@ -48,7 +48,7 @@ export interface Namespace { email: string; role: "ADMIN" | "MEMBER" | "VIEWER"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/packages/create-sdk/templates/generators/src/seed/data/Order.schema.ts b/packages/create-sdk/templates/generators/src/seed/data/Order.schema.ts index dffeb95f3..569a38ee1 100644 --- a/packages/create-sdk/templates/generators/src/seed/data/Order.schema.ts +++ b/packages/create-sdk/templates/generators/src/seed/data/Order.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { order } from "../../db/order"; const schemaType = t.object({ - ...order.pickFields(["id","createdAt"], { optional: true }), - ...order.omitFields(["id","createdAt"]), + ...order.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...order.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(order); diff --git a/packages/create-sdk/templates/generators/src/seed/data/Product.schema.ts b/packages/create-sdk/templates/generators/src/seed/data/Product.schema.ts index 2bf00829c..9a7a449bd 100644 --- a/packages/create-sdk/templates/generators/src/seed/data/Product.schema.ts +++ b/packages/create-sdk/templates/generators/src/seed/data/Product.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { product } from "../../db/product"; const schemaType = t.object({ - ...product.pickFields(["id","createdAt"], { optional: true }), - ...product.omitFields(["id","createdAt"]), + ...product.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...product.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(product); diff --git a/packages/create-sdk/templates/generators/src/seed/data/User.schema.ts b/packages/create-sdk/templates/generators/src/seed/data/User.schema.ts index 2cbbdf2c5..5dbc6522e 100644 --- a/packages/create-sdk/templates/generators/src/seed/data/User.schema.ts +++ b/packages/create-sdk/templates/generators/src/seed/data/User.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { user } from "../../db/user"; const schemaType = t.object({ - ...user.pickFields(["id","createdAt"], { optional: true }), - ...user.omitFields(["id","createdAt"]), + ...user.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...user.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(user); diff --git a/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts b/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts index c42a19651..67fefdd51 100644 --- a/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts +++ b/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts @@ -19,7 +19,7 @@ export interface Namespace { email: string; role: "MANAGER" | "STAFF"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts b/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts index 001ea023e..6ab154858 100644 --- a/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts +++ b/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts @@ -18,7 +18,7 @@ export interface Namespace { name: string; description: string | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Contact: { @@ -28,7 +28,7 @@ export interface Namespace { phone: string | null; address: string | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Inventory: { @@ -36,14 +36,14 @@ export interface Namespace { productId: string; quantity: number; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Notification: { id: Generated; message: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Order: { @@ -54,7 +54,7 @@ export interface Namespace { orderType: "PURCHASE" | "SALES"; contactId: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } OrderItem: { @@ -65,7 +65,7 @@ export interface Namespace { unitPrice: number; totalPrice: Generated; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Product: { @@ -74,7 +74,7 @@ export interface Namespace { description: string | null; categoryId: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } User: { @@ -83,7 +83,7 @@ export interface Namespace { email: string; role: "MANAGER" | "STAFF"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/packages/create-sdk/templates/resolver/src/generated/db.ts b/packages/create-sdk/templates/resolver/src/generated/db.ts index e36ba8aa9..b892f0e12 100644 --- a/packages/create-sdk/templates/resolver/src/generated/db.ts +++ b/packages/create-sdk/templates/resolver/src/generated/db.ts @@ -19,7 +19,7 @@ export interface Namespace { email: string; age: number; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/packages/create-sdk/templates/tailordb/src/generated/db.ts b/packages/create-sdk/templates/tailordb/src/generated/db.ts index f68627d27..46840cc83 100644 --- a/packages/create-sdk/templates/tailordb/src/generated/db.ts +++ b/packages/create-sdk/templates/tailordb/src/generated/db.ts @@ -32,7 +32,7 @@ export interface Namespace { isInternal: boolean; }>; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Task: { @@ -46,7 +46,7 @@ export interface Namespace { categoryId: string | null; isArchived: Generated; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } User: { @@ -56,7 +56,7 @@ export interface Namespace { role: "ADMIN" | "MEMBER" | "VIEWER"; bio: string | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/packages/create-sdk/templates/workflow/src/generated/db.ts b/packages/create-sdk/templates/workflow/src/generated/db.ts index 51e0e14cb..bf14fa403 100644 --- a/packages/create-sdk/templates/workflow/src/generated/db.ts +++ b/packages/create-sdk/templates/workflow/src/generated/db.ts @@ -19,7 +19,7 @@ export interface Namespace { amount: number; status: "PENDING" | "PROCESSING" | "COMPLETED" | "FAILED"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } User: { @@ -28,7 +28,7 @@ export interface Namespace { email: string; age: number; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.test.ts index 03b105324..70a915aa0 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.test.ts @@ -386,6 +386,37 @@ describe("db-types-generator", () => { expect(content).toContain("email: ColumnType;"); }); + test("generates ColumnType for optional to required datetime change", async () => { + const snapshot = createMockSnapshot({ + User: { + fields: { + updatedAt: { type: "datetime", required: true }, + }, + }, + }); + createMigrationDir(testDir, 1); + + const diff = createMockDiff( + [ + { + kind: "field_modified", + typeName: "User", + fieldName: "updatedAt", + before: { type: "datetime", required: false }, + after: { type: "datetime", required: true }, + }, + ], + { hasBreakingChanges: true, requiresMigrationScript: true }, + ); + + const filePath = await writeDbTypesFile(snapshot, testDir, 1, diff); + const content = fs.readFileSync(filePath, "utf-8"); + + expect(content).toContain( + "updatedAt: ColumnType;", + ); + }); + test("generates ColumnType for added required fields", async () => { const snapshot = createMockSnapshot({ User: { diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.ts b/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.ts index fa266d76d..2b0ed9a2d 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.ts @@ -325,6 +325,16 @@ function generateEnumChangeColumnType( return `ColumnType<${selectType}, ${afterType}, ${afterType}>`; } +function generateOptionalToRequiredDateColumnType(config: SnapshotFieldConfig): string | null { + if (config.type !== "date" && config.type !== "datetime") return null; + + if (config.array) { + return "ColumnType"; + } + + return "ColumnType"; +} + /** * Generate field type from snapshot field config * @param {SnapshotFieldConfig} config - Field configuration @@ -373,6 +383,15 @@ function generateFieldType( // Handle nullable/required modifiers if (isOptionalToRequired) { + const dateColumnType = generateOptionalToRequiredDateColumnType(config); + if (dateColumnType) { + return { + type: dateColumnType, + usedTimestamp: false, + usedColumnType: true, + }; + } + // For fields changing from optional to required: // SELECT returns T | null (existing data might be null) // INSERT/UPDATE requires T (must provide a value) From 1b2303c75fe75f987efeafe6a7879f4d38d71ff8 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 22:36:10 +0900 Subject: [PATCH 140/618] fix(tailordb): omit updatedAt from generated inputs --- example/resolvers/passThrough.ts | 18 +++++++++++------- .../src/resolver/registerOrder.ts | 4 ++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/example/resolvers/passThrough.ts b/example/resolvers/passThrough.ts index adeaa78e7..6fa561461 100644 --- a/example/resolvers/passThrough.ts +++ b/example/resolvers/passThrough.ts @@ -2,8 +2,8 @@ import { createResolver, t } from "@tailor-platform/sdk"; import { nestedProfile } from "../tailordb/nested"; const inputFields = { - ...nestedProfile.pickFields(["id", "createdAt"], { optional: true }), - ...nestedProfile.omitFields(["id", "createdAt"]), + ...nestedProfile.pickFields(["id", "createdAt", "updatedAt"], { optional: true }), + ...nestedProfile.omitFields(["id", "createdAt", "updatedAt"]), }; export default createResolver({ operation: "query", @@ -13,10 +13,14 @@ export default createResolver({ id: t.uuid({ optional: true }), input: t.object(inputFields), }, - body: ({ input }) => ({ - ...input.input, - id: input.id ?? crypto.randomUUID(), - createdAt: new Date(), - }), + body: ({ input }) => { + const now = new Date(); + return { + ...input.input, + id: input.id ?? crypto.randomUUID(), + createdAt: input.input.createdAt ?? now, + updatedAt: input.input.updatedAt ?? now, + }; + }, output: nestedProfile.fields, }); diff --git a/packages/create-sdk/templates/inventory-management/src/resolver/registerOrder.ts b/packages/create-sdk/templates/inventory-management/src/resolver/registerOrder.ts index ee1f332a7..e3d9c5333 100644 --- a/packages/create-sdk/templates/inventory-management/src/resolver/registerOrder.ts +++ b/packages/create-sdk/templates/inventory-management/src/resolver/registerOrder.ts @@ -4,8 +4,8 @@ import { orderItem } from "../db/orderItem"; import { type DB, getDB } from "../generated/kysely-tailordb"; const input = { - order: t.object(order.omitFields(["id", "createdAt"])), - items: t.object(orderItem.omitFields(["id", "createdAt"]), { array: true }), + order: t.object(order.omitFields(["id", "createdAt", "updatedAt"])), + items: t.object(orderItem.omitFields(["id", "createdAt", "updatedAt"]), { array: true }), }; interface Input { order: t.infer; From 3f99e683aed6e53d21e499c32499f8d288bcf839 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 22:49:21 +0900 Subject: [PATCH 141/618] docs(tailordb): update timestamp extraction examples --- packages/sdk/docs/services/tailordb.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index 780e04450..6561d48a0 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -518,8 +518,8 @@ const user = db.type("User", { ...db.fields.timestamps(), }); -// Pick id and createdAt, making them optional -user.pickFields(["id", "createdAt"], { optional: true }); +// Pick id, createdAt, and updatedAt, making them optional +user.pickFields(["id", "createdAt", "updatedAt"], { optional: true }); ``` Available options: @@ -534,8 +534,8 @@ Available options: Return all fields except the specified ones: ```typescript -// All fields except id and createdAt -user.omitFields(["id", "createdAt"]); +// All fields except id, createdAt, and updatedAt +user.omitFields(["id", "createdAt", "updatedAt"]); ``` #### Common Pattern: Input Schema Composition @@ -550,9 +550,9 @@ export default createResolver({ name: "createUser", operation: "mutation", input: { - // id/createdAt are optional (auto-generated), other fields are required - ...user.pickFields(["id", "createdAt"], { optional: true }), - ...user.omitFields(["id", "createdAt"]), + // id/createdAt/updatedAt are optional (auto-generated), other fields are required + ...user.pickFields(["id", "createdAt", "updatedAt"], { optional: true }), + ...user.omitFields(["id", "createdAt", "updatedAt"]), }, output: t.object({ id: t.uuid() }), body: async (context) => { @@ -569,8 +569,8 @@ import { t } from "@tailor-platform/sdk"; import { invoice } from "../../tailordb/invoice"; const schemaType = t.object({ - ...invoice.pickFields(["id", "createdAt"], { optional: true }), - ...invoice.omitFields(["id", "createdAt", "invoiceNumber", "sequentialId"]), + ...invoice.pickFields(["id", "createdAt", "updatedAt"], { optional: true }), + ...invoice.omitFields(["id", "createdAt", "updatedAt", "invoiceNumber", "sequentialId"]), }); ``` From f43c1a199fba4bd23411d903ff829a90f60dc380 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 23:05:09 +0900 Subject: [PATCH 142/618] fix(tailordb): add analyticsdb timestamp migration --- .../migrations/analyticsdb/0000/schema.json | 56 +++++++++++++++ example/migrations/analyticsdb/0001/db.ts | 34 ++++++++++ example/migrations/analyticsdb/0001/diff.json | 68 +++++++++++++++++++ .../migrations/analyticsdb/0001/migrate.ts | 22 ++++++ example/tailor.config.ts | 7 +- 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 example/migrations/analyticsdb/0000/schema.json create mode 100644 example/migrations/analyticsdb/0001/db.ts create mode 100644 example/migrations/analyticsdb/0001/diff.json create mode 100644 example/migrations/analyticsdb/0001/migrate.ts diff --git a/example/migrations/analyticsdb/0000/schema.json b/example/migrations/analyticsdb/0000/schema.json new file mode 100644 index 000000000..056ce8173 --- /dev/null +++ b/example/migrations/analyticsdb/0000/schema.json @@ -0,0 +1,56 @@ +{ + "version": 1, + "namespace": "analyticsdb", + "createdAt": "2026-06-17T14:01:24.799Z", + "types": { + "Event": { + "name": "Event", + "pluralForm": "Events", + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "CLICK" + }, + { + "value": "VIEW" + }, + { + "value": "PURCHASE" + } + ] + }, + "createdAt": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "updatedAt": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + } + }, + "settings": {}, + "files": { + "screenshot": "screenshot image" + } + } + } +} diff --git a/example/migrations/analyticsdb/0001/db.ts b/example/migrations/analyticsdb/0001/db.ts new file mode 100644 index 000000000..e4f20afd9 --- /dev/null +++ b/example/migrations/analyticsdb/0001/db.ts @@ -0,0 +1,34 @@ +/** + * Auto-generated Kysely types for migration script. + * These types reflect the database schema state at this migration point. + * + * DO NOT EDIT - This file is auto-generated by the migration system. + */ + +import { + type ColumnType, + type Transaction as KyselyTransaction, +} from "@tailor-platform/sdk/kysely"; +import type { Env } from "@tailor-platform/sdk"; + +type Timestamp = ColumnType; +type Generated = + T extends ColumnType + ? ColumnType + : ColumnType; + +interface Database { + Event: { + id: Generated; + name: "CLICK" | "VIEW" | "PURCHASE"; + createdAt: Timestamp; + updatedAt: ColumnType; + }; +} + +export type Transaction = KyselyTransaction; + +/** Context passed as the second argument to the migration's `main` function. */ +export type MigrationContext = { + env: keyof Env extends never ? Record : Env; +}; diff --git a/example/migrations/analyticsdb/0001/diff.json b/example/migrations/analyticsdb/0001/diff.json new file mode 100644 index 000000000..e1b6102b0 --- /dev/null +++ b/example/migrations/analyticsdb/0001/diff.json @@ -0,0 +1,68 @@ +{ + "version": 1, + "namespace": "analyticsdb", + "createdAt": "2026-06-17T14:01:59.003Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Event", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Event", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + } + } + ], + "hasBreakingChanges": true, + "breakingChanges": [ + { + "typeName": "Event", + "fieldName": "updatedAt", + "reason": "Field changed from optional to required" + } + ], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": true +} diff --git a/example/migrations/analyticsdb/0001/migrate.ts b/example/migrations/analyticsdb/0001/migrate.ts new file mode 100644 index 000000000..072eda32b --- /dev/null +++ b/example/migrations/analyticsdb/0001/migrate.ts @@ -0,0 +1,22 @@ +/** + * Migration script for analyticsdb + * + * This script runs between the Pre-migration and Post-migration phases of + * 'tailor-sdk deploy'. Use it to transform existing data so that the schema + * change can complete safely (for breaking changes, this is hard-required; + * for warning-tier changes it is optional). Edit this file to implement + * your data migration logic. + * + * The transaction is managed by the deploy command. + * If any operation fails, all changes will be rolled back. + */ + +import type { Transaction } from "./db"; + +export async function main(trx: Transaction): Promise { + await trx + .updateTable("Event") + .set((eb) => ({ updatedAt: eb.ref("createdAt") })) + .where("updatedAt", "is", null) + .execute(); +} diff --git a/example/tailor.config.ts b/example/tailor.config.ts index a24dd5a6a..08c4e1052 100644 --- a/example/tailor.config.ts +++ b/example/tailor.config.ts @@ -124,7 +124,12 @@ export default defineConfig({ directory: "./migrations", }, }, - analyticsdb: { files: ["./analyticsdb/*.ts"] }, + analyticsdb: { + files: ["./analyticsdb/*.ts"], + migration: { + directory: "./migrations/analyticsdb", + }, + }, }, resolver: { "my-resolver": { files: ["./resolvers/*.ts"] }, From d595585d4204115540db31444a8a12c3532338f5 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 23:26:10 +0900 Subject: [PATCH 143/618] fix(tailordb): keep createdAt behavior unchanged --- .changeset/timestamps-updated-at-create.md | 2 +- .../migrations/analyticsdb/0000/schema.json | 2 +- example/migrations/analyticsdb/0001/diff.json | 25 ------------------- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/.changeset/timestamps-updated-at-create.md b/.changeset/timestamps-updated-at-create.md index 033e9a2f1..c5672090b 100644 --- a/.changeset/timestamps-updated-at-create.md +++ b/.changeset/timestamps-updated-at-create.md @@ -3,7 +3,7 @@ "@tailor-platform/create-sdk": major --- -Set `db.fields.timestamps()` `updatedAt` when records are created and make the generated field non-null. The helper now creates non-null `createdAt` and `updatedAt` fields with create hooks that preserve provided values and fall back to the current time. +Set `db.fields.timestamps()` `updatedAt` when records are created and make the generated field non-null. `createdAt` keeps its existing create-time behavior, while `updatedAt` now gets a create hook that preserves provided values and falls back to the current time. Update create-sdk templates so scaffolded projects use the new non-null `updatedAt` Kysely types and seed schemas. diff --git a/example/migrations/analyticsdb/0000/schema.json b/example/migrations/analyticsdb/0000/schema.json index 056ce8173..b84f03d30 100644 --- a/example/migrations/analyticsdb/0000/schema.json +++ b/example/migrations/analyticsdb/0000/schema.json @@ -32,7 +32,7 @@ "description": "Record creation timestamp", "hooks": { "create": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" } } }, diff --git a/example/migrations/analyticsdb/0001/diff.json b/example/migrations/analyticsdb/0001/diff.json index e1b6102b0..88deee302 100644 --- a/example/migrations/analyticsdb/0001/diff.json +++ b/example/migrations/analyticsdb/0001/diff.json @@ -3,31 +3,6 @@ "namespace": "analyticsdb", "createdAt": "2026-06-17T14:01:59.003Z", "changes": [ - { - "kind": "field_modified", - "typeName": "Event", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" - } - } - } - }, { "kind": "field_modified", "typeName": "Event", From 9d25ab41b18afec5e71cfb8d81341f142c961aea Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 17 Jun 2026 23:38:35 +0900 Subject: [PATCH 144/618] fix(tailordb): preserve updatedAt update hook --- .changeset/timestamps-updated-at-create.md | 4 +-- example/migrations/0003/diff.json | 27 +++++++++++++++++++ example/migrations/analyticsdb/0001/diff.json | 3 +++ packages/sdk/docs/services/tailordb.md | 2 +- .../services/tailordb/schema.test.ts | 11 ++++++-- .../src/configure/services/tailordb/schema.ts | 5 +++- 6 files changed, 45 insertions(+), 7 deletions(-) diff --git a/.changeset/timestamps-updated-at-create.md b/.changeset/timestamps-updated-at-create.md index c5672090b..c88c6344b 100644 --- a/.changeset/timestamps-updated-at-create.md +++ b/.changeset/timestamps-updated-at-create.md @@ -3,10 +3,8 @@ "@tailor-platform/create-sdk": major --- -Set `db.fields.timestamps()` `updatedAt` when records are created and make the generated field non-null. `createdAt` keeps its existing create-time behavior, while `updatedAt` now gets a create hook that preserves provided values and falls back to the current time. +Set `db.fields.timestamps()` `updatedAt` when records are created and make the generated field non-null. `createdAt` keeps its existing create-time behavior, while `updatedAt` keeps its update-time behavior and now also gets a create hook that preserves provided values and falls back to the current time. Update create-sdk templates so scaffolded projects use the new non-null `updatedAt` Kysely types and seed schemas. -`updatedAt` no longer gets an update hook from the helper; define a custom `updatedAt` field if your schema should refresh it automatically on record updates. - Existing TailorDB schemas that already use this helper will change `updatedAt` from optional to required. Backfill existing records that have `updatedAt: null` before applying the schema change. diff --git a/example/migrations/0003/diff.json b/example/migrations/0003/diff.json index e3052d201..40cbdc159 100644 --- a/example/migrations/0003/diff.json +++ b/example/migrations/0003/diff.json @@ -24,6 +24,9 @@ "hooks": { "create": { "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" } } } @@ -49,6 +52,9 @@ "hooks": { "create": { "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" } } } @@ -74,6 +80,9 @@ "hooks": { "create": { "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" } } } @@ -99,6 +108,9 @@ "hooks": { "create": { "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" } } } @@ -124,6 +136,9 @@ "hooks": { "create": { "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" } } } @@ -149,6 +164,9 @@ "hooks": { "create": { "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" } } } @@ -174,6 +192,9 @@ "hooks": { "create": { "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" } } } @@ -199,6 +220,9 @@ "hooks": { "create": { "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" } } } @@ -224,6 +248,9 @@ "hooks": { "create": { "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" } } } diff --git a/example/migrations/analyticsdb/0001/diff.json b/example/migrations/analyticsdb/0001/diff.json index 88deee302..51ed4c8d1 100644 --- a/example/migrations/analyticsdb/0001/diff.json +++ b/example/migrations/analyticsdb/0001/diff.json @@ -24,6 +24,9 @@ "hooks": { "create": { "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" } } } diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index 6561d48a0..50588631a 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -409,7 +409,7 @@ export const user = db.type("User", { }); ``` -`db.fields.timestamps()` adds non-null `createdAt` and `updatedAt` datetime fields. Both fields are populated when a record is created; provided values are preserved so seed data can use historical timestamps. Define a custom `updatedAt` field if it should refresh automatically on record updates. +`db.fields.timestamps()` adds non-null `createdAt` and `updatedAt` datetime fields. Both fields are populated when a record is created; provided values are preserved so seed data can use historical timestamps. `updatedAt` is also refreshed automatically when a record is updated. ## Type Modifiers diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 69a04827e..ba629fcf2 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -655,10 +655,17 @@ describe("TailorDBType withTimestamps option tests", () => { expect((result as Date).getTime()).toBeLessThanOrEqual(after); }); - test("updatedAt does not define an update hook", () => { + test("updatedAt update hook uses current time", () => { const { updatedAt } = db.fields.timestamps(); + const updateHook = updatedAt.metadata.hooks?.update; + expect(updateHook).toBeDefined(); - expect(updatedAt.metadata.hooks?.update).toBeUndefined(); + const before = Date.now(); + const result = updateHook!({ value: null, data: {}, user: timestampHookUser }); + const after = Date.now(); + expect(result).toBeInstanceOf(Date); + expect((result as Date).getTime()).toBeGreaterThanOrEqual(before); + expect((result as Date).getTime()).toBeLessThanOrEqual(after); }); }); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 07052b613..2d664c73c 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -1177,7 +1177,10 @@ export const db = { .hooks({ create: ({ value }) => value ?? new Date() }) .description("Record creation timestamp"), updatedAt: datetime() - .hooks({ create: ({ value }) => value ?? new Date() }) + .hooks({ + create: ({ value }) => value ?? new Date(), + update: () => new Date(), + }) .description("Record update timestamp"), }), }, From 55125fc4ce0e1c81b45054992a160f7a22f7423e Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 18 Jun 2026 00:44:42 +0900 Subject: [PATCH 145/618] fix(tailordb): align timestamp migrations with v2 --- example/migrations/0003/diff.json | 59 ++++++++++--------- .../migrations/analyticsdb/0000/schema.json | 4 +- example/migrations/analyticsdb/0001/diff.json | 6 +- .../services/tailordb/schema.test.ts | 6 +- 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/example/migrations/0003/diff.json b/example/migrations/0003/diff.json index 40cbdc159..b97ecf496 100644 --- a/example/migrations/0003/diff.json +++ b/example/migrations/0003/diff.json @@ -1,7 +1,7 @@ { "version": 1, "namespace": "tailordb", - "createdAt": "2026-06-17T13:17:08.592Z", + "createdAt": "2026-06-17T15:41:21.506Z", "changes": [ { "kind": "field_modified", @@ -13,7 +13,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } }, @@ -23,10 +23,10 @@ "description": "Record update timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -41,7 +41,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } }, @@ -51,10 +51,10 @@ "description": "Record update timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -69,7 +69,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } }, @@ -79,10 +79,10 @@ "description": "Record update timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -97,7 +97,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } }, @@ -107,10 +107,10 @@ "description": "Record update timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -125,7 +125,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } }, @@ -135,10 +135,10 @@ "description": "Record update timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -153,7 +153,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } }, @@ -163,10 +163,10 @@ "description": "Record update timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -181,7 +181,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } }, @@ -191,10 +191,10 @@ "description": "Record update timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -209,7 +209,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } }, @@ -219,10 +219,10 @@ "description": "Record update timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -237,7 +237,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } }, @@ -247,10 +247,10 @@ "description": "Record update timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } @@ -306,5 +306,6 @@ ], "hasWarnings": false, "warnings": [], - "requiresMigrationScript": true + "requiresMigrationScript": true, + "description": "set updatedAt on create" } diff --git a/example/migrations/analyticsdb/0000/schema.json b/example/migrations/analyticsdb/0000/schema.json index b84f03d30..4b5cfd05f 100644 --- a/example/migrations/analyticsdb/0000/schema.json +++ b/example/migrations/analyticsdb/0000/schema.json @@ -32,7 +32,7 @@ "description": "Record creation timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } }, @@ -42,7 +42,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } diff --git a/example/migrations/analyticsdb/0001/diff.json b/example/migrations/analyticsdb/0001/diff.json index 51ed4c8d1..dfd283801 100644 --- a/example/migrations/analyticsdb/0001/diff.json +++ b/example/migrations/analyticsdb/0001/diff.json @@ -13,7 +13,7 @@ "description": "Record last update timestamp", "hooks": { "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } }, @@ -23,10 +23,10 @@ "description": "Record update timestamp", "hooks": { "create": { - "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" }, "update": { - "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" } } } diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index ba629fcf2..df148032b 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -638,7 +638,7 @@ describe("TailorDBType withTimestamps option tests", () => { expect(createHook).toBeDefined(); const specified = new Date("2025-02-10T09:00:00Z"); - const result = createHook!({ value: specified, data: {}, user: timestampHookUser }); + const result = createHook!({ value: specified, data: {}, invoker: timestampHookInvoker }); expect(result).toBe(specified); }); @@ -648,7 +648,7 @@ describe("TailorDBType withTimestamps option tests", () => { expect(createHook).toBeDefined(); const before = Date.now(); - const result = createHook!({ value: null, data: {}, user: timestampHookUser }); + const result = createHook!({ value: null, data: {}, invoker: timestampHookInvoker }); const after = Date.now(); expect(result).toBeInstanceOf(Date); expect((result as Date).getTime()).toBeGreaterThanOrEqual(before); @@ -661,7 +661,7 @@ describe("TailorDBType withTimestamps option tests", () => { expect(updateHook).toBeDefined(); const before = Date.now(); - const result = updateHook!({ value: null, data: {}, user: timestampHookUser }); + const result = updateHook!({ value: null, data: {}, invoker: timestampHookInvoker }); const after = Date.now(); expect(result).toBeInstanceOf(Date); expect((result as Date).getTime()).toBeGreaterThanOrEqual(before); From 11df2682eec9b298806e394c4239f5881a3d7af1 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 18 Jun 2026 09:02:05 +0900 Subject: [PATCH 146/618] refactor(codemod): separate behavioral-change notices from migrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an explicit `notice` flag instead of inferring "notice" from the absence of examples/prompt. Notices are rendered in their own "Behavioral changes (no migration required)" section (as sub-sections, with no automation/migration label), keeping them off the Automatic/Partially automatic/Manual axis. Reclassify the ambient runtime globals change as a notice: normal SDK development does not use the ambient `tailor.*` / `tailordb.*` globals, so the opt-in import is only relevant in the rare case that code relied on them — not a migration step. The keyring storage, users-by-subject, and function-logs content-hash changes are marked as notices too. --- .../sdk-codemod/src/migration-doc.test.ts | 20 +++++++ packages/sdk-codemod/src/migration-doc.ts | 34 ++++++++---- packages/sdk-codemod/src/registry.ts | 27 +++------- packages/sdk-codemod/src/types.ts | 6 +++ packages/sdk/docs/migration/v2.md | 54 ++++--------------- 5 files changed, 65 insertions(+), 76 deletions(-) diff --git a/packages/sdk-codemod/src/migration-doc.test.ts b/packages/sdk-codemod/src/migration-doc.test.ts index c648f894c..893ee123b 100644 --- a/packages/sdk-codemod/src/migration-doc.test.ts +++ b/packages/sdk-codemod/src/migration-doc.test.ts @@ -59,4 +59,24 @@ describe("renderMigrationDoc", () => { expect(doc).toContain("**Migration:** Automatic"); expect(doc).not.toContain("How to finish"); }); + + test("renders notices in a separate section without a migration label", () => { + const doc = renderMigrationDoc([ + makeCodemod({ name: "Real migration", description: "Edit your code." }), + makeCodemod({ + name: "Keyring storage", + description: "Tokens move to the OS keyring.", + scriptPath: undefined as unknown as string, + notice: true, + }), + ]); + + expect(doc).toContain("## Behavioral changes (no migration required)"); + // The notice is a sub-section, not labelled as a migration. + expect(doc).toContain("### Keyring storage"); + expect(doc.split("### Keyring storage")[1]).not.toContain("**Migration:**"); + // A real migration keeps its migration label and stays out of the notices section. + expect(doc).toContain("## Real migration"); + expect(doc.indexOf("## Real migration")).toBeLessThan(doc.indexOf("## Behavioral changes")); + }); }); diff --git a/packages/sdk-codemod/src/migration-doc.ts b/packages/sdk-codemod/src/migration-doc.ts index 5cbdb317e..e84b5572a 100644 --- a/packages/sdk-codemod/src/migration-doc.ts +++ b/packages/sdk-codemod/src/migration-doc.ts @@ -23,15 +23,7 @@ export function automationLevel(codemod: CodemodPackage): AutomationLevel { function renderEntry(codemod: CodemodPackage): string { const level = automationLevel(codemod); - // A Manual entry that ships no examples and no prompt is an informational - // notice (a runtime/behavioral change with nothing in user source to edit), - // not a hand-migration. - const isNotice = - level === "Manual" && (codemod.examples?.length ?? 0) === 0 && codemod.prompt == null; - const header = isNotice - ? "**Type:** Behavioral change (no code change required)" - : `**Migration:** ${level}`; - const lines: string[] = [`## ${codemod.name}`, "", header, ""]; + const lines: string[] = [`## ${codemod.name}`, "", `**Migration:** ${level}`, ""]; lines.push(codemod.description, ""); for (const example of codemod.examples ?? []) { @@ -74,6 +66,11 @@ function renderEntry(codemod: CodemodPackage): string { return lines.join("\n"); } +/** Render an informational behavioral-change notice (no migration). */ +function renderNotice(codemod: CodemodPackage): string { + return [`### ${codemod.name}`, "", codemod.description, ""].join("\n"); +} + /** * Render the v2 migration guide from the codemod registry. The registry is the * single source of truth; missing detail is added to the codemod definitions. @@ -94,6 +91,21 @@ export function renderMigrationDoc(codemods: CodemodPackage[]): string { "", ].join("\n"); - const body = codemods.map(renderEntry).join("\n"); - return `${header}\n${body}`.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n"; + const migrations = codemods.filter((c) => !c.notice); + const notices = codemods.filter((c) => c.notice); + + const sections = [header, migrations.map(renderEntry).join("\n")]; + if (notices.length > 0) { + sections.push( + [ + "## Behavioral changes (no migration required)", + "", + "These v2 changes alter runtime or CLI behavior; no source change is needed.", + "", + notices.map(renderNotice).join("\n"), + ].join("\n"), + ); + } + + return `${sections.join("\n")}`.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n"; } diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index e7187dbe8..1335364c0 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -279,29 +279,12 @@ export const allCodemods: CodemodPackage[] = [ }, { id: "v2/runtime-globals-opt-in", - name: "Runtime globals are opt-in", + name: "Ambient runtime globals are opt-in", description: - 'Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Opt in explicitly with `import "@tailor-platform/sdk/runtime/globals"`, or use the typed wrappers from `@tailor-platform/sdk/runtime`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.)', + 'Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Normal SDK development does not need them — use the SDK APIs and the typed wrappers from `@tailor-platform/sdk/runtime`. Only if you relied on the ambient globals directly, add `import "@tailor-platform/sdk/runtime/globals"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.)', since: "1.0.0", until: "2.0.0", - // No scriptPath: which files rely on the ambient globals cannot be - // determined reliably enough to rewrite automatically. - examples: [ - { - caption: "Add the opt-in import to files that use the ambient globals:", - before: - "// relied on ambient `tailordb` / `tailor` just from importing the SDK\nconst result = await tailordb.query(/* ... */);", - after: - 'import "@tailor-platform/sdk/runtime/globals";\n\nconst result = await tailordb.query(/* ... */);', - }, - ], - prompt: [ - "In v2, importing @tailor-platform/sdk no longer activates the ambient tailor.* /", - "tailordb.* globals. For each file that uses those globals, add", - 'import "@tailor-platform/sdk/runtime/globals"; at the top, or migrate it to the', - "typed wrappers exported from @tailor-platform/sdk/runtime. Do not add the import to", - "files that do not use the globals.", - ].join("\n"), + notice: true, }, { id: "v2/workflow-trigger-dispatch", @@ -335,7 +318,7 @@ export const allCodemods: CodemodPackage[] = [ "CLI login tokens are stored in the OS keyring by default when available, falling back to the platform config file when it is not. No source change is required; re-login if you need tokens moved into the keyring.", since: "1.0.0", until: "2.0.0", - // Runtime/CLI behavior change — no user source to migrate. + notice: true, }, { id: "v2/cli-users-by-subject", @@ -344,6 +327,7 @@ export const allCodemods: CodemodPackage[] = [ "The CLI stores human users by their stable subject ID instead of email (email is kept for display). Legacy email-keyed entries are migrated automatically on the next login or token refresh. No source change is required.", since: "1.0.0", until: "2.0.0", + notice: true, }, { id: "v2/function-logs-content-hash", @@ -352,6 +336,7 @@ export const allCodemods: CodemodPackage[] = [ "`tailor-sdk function logs` maps stack traces against the function bundle only when the execution recorded a `contentHash`. Executions without one now show raw stack traces instead of mapped frames. No source change is required.", since: "1.0.0", until: "2.0.0", + notice: true, }, ]; diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index 9d4a57486..f96e437e9 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -59,6 +59,12 @@ export interface CodemodPackage { prompt?: string; /** Before/after examples shown in the generated migration doc. */ examples?: CodemodExample[]; + /** + * Marks an informational behavioral change (a runtime/CLI change with no + * source to migrate), not a migration. Rendered in a separate "Behavioral + * changes" section, never on the automation-level axis. + */ + notice?: boolean; } /** A batch of files an LLM should review for one codemod, with its prompt. */ diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 0fa8f4968..0a776ad66 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -351,42 +351,6 @@ downloadStream and returns FileDownloadStreamResponse.
-## Runtime globals are opt-in - -**Migration:** Manual - -Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Opt in explicitly with `import "@tailor-platform/sdk/runtime/globals"`, or use the typed wrappers from `@tailor-platform/sdk/runtime`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.) - -Add the opt-in import to files that use the ambient globals: - -Before: - -```ts -// relied on ambient `tailordb` / `tailor` just from importing the SDK -const result = await tailordb.query(/* ... */); -``` - -After: - -```ts -import "@tailor-platform/sdk/runtime/globals"; - -const result = await tailordb.query(/* ... */); -``` - -
-Prompt for an AI agent (to perform this migration) - -```text -In v2, importing @tailor-platform/sdk no longer activates the ambient tailor.* / -tailordb.* globals. For each file that uses those globals, add -import "@tailor-platform/sdk/runtime/globals"; at the top, or migrate it to the -typed wrappers exported from @tailor-platform/sdk/runtime. Do not add the import to -files that do not use the globals. -``` - -
- ## Workflow .trigger() and trigger tests **Migration:** Manual @@ -424,20 +388,22 @@ trigger result as the job output directly (no Promise wrapper to unwrap).
-## CLI tokens stored in the OS keyring +## Behavioral changes (no migration required) -**Type:** Behavioral change (no code change required) +These v2 changes alter runtime or CLI behavior; no source change is needed. -CLI login tokens are stored in the OS keyring by default when available, falling back to the platform config file when it is not. No source change is required; re-login if you need tokens moved into the keyring. +### Ambient runtime globals are opt-in -## CLI users keyed by subject ID +Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Normal SDK development does not need them — use the SDK APIs and the typed wrappers from `@tailor-platform/sdk/runtime`. Only if you relied on the ambient globals directly, add `import "@tailor-platform/sdk/runtime/globals"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.) -**Type:** Behavioral change (no code change required) +### CLI tokens stored in the OS keyring -The CLI stores human users by their stable subject ID instead of email (email is kept for display). Legacy email-keyed entries are migrated automatically on the next login or token refresh. No source change is required. +CLI login tokens are stored in the OS keyring by default when available, falling back to the platform config file when it is not. No source change is required; re-login if you need tokens moved into the keyring. + +### CLI users keyed by subject ID -## function logs require a content hash for source mapping +The CLI stores human users by their stable subject ID instead of email (email is kept for display). Legacy email-keyed entries are migrated automatically on the next login or token refresh. No source change is required. -**Type:** Behavioral change (no code change required) +### function logs require a content hash for source mapping `tailor-sdk function logs` maps stack traces against the function bundle only when the execution recorded a `contentHash`. Executions without one now show raw stack traces instead of mapped frames. No source change is required. From 843dc3c2680149dc9124a75e2ab420898006e57b Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 18 Jun 2026 16:19:26 +0900 Subject: [PATCH 147/618] feat(codemod): add `list` command and note the v2-specific shape - Add a `sdk-codemod list` subcommand that prints every registered rule (id, name, kind = Automatic / Partially automatic / Manual / Notice, and version range) as a human-readable table on stderr and JSON on stdout. The existing `--from`/`--to` migration run is unchanged. - Document, in a code comment on the migration-doc generator, that the doc generation is currently shaped around v1->v2 (hardcoded title, `v2.md` path, `v2/*` rule ids) and should be cleaned up or generalized to be version-agnostic before this lands on `main`. --- packages/sdk-codemod/src/index.ts | 34 ++++++++++++++++++++++- packages/sdk-codemod/src/migration-doc.ts | 6 ++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/src/index.ts b/packages/sdk-codemod/src/index.ts index fb3ef44d6..183a06c5a 100644 --- a/packages/sdk-codemod/src/index.ts +++ b/packages/sdk-codemod/src/index.ts @@ -5,12 +5,43 @@ import * as path from "pathe"; import { readPackageJSON } from "pkg-types"; import { arg, defineCommand, runMain } from "politty"; import { z } from "zod"; -import { getApplicableCodemods, resolveCodemodScript } from "./registry"; +import { automationLevel } from "./migration-doc"; +import { allCodemods, getApplicableCodemods, resolveCodemodScript } from "./registry"; import { runCodemods } from "./runner"; import type { LlmReview, RunOutput } from "./types"; const packageJson = await readPackageJSON(path.dirname(fileURLToPath(import.meta.url)) + "/.."); +/** One rule in the `list` command output. */ +interface RuleSummary { + id: string; + name: string; + /** Automatic / Partially automatic / Manual, or "Notice" for behavioral changes. */ + kind: string; + since: string; + until: string; +} + +const listCommand = defineCommand({ + name: "list", + description: "List the available codemod rules (id, name, kind, version range).", + args: z.object({}).strict(), + run: () => { + const rules: RuleSummary[] = allCodemods.map((codemod) => ({ + id: codemod.id, + name: codemod.name, + kind: codemod.notice ? "Notice" : automationLevel(codemod), + since: codemod.since, + until: codemod.until, + })); + // Human-readable table to stderr; machine-readable JSON to stdout. + for (const rule of rules) { + process.stderr.write(` ${rule.id} [${rule.kind}] ${rule.name}\n`); + } + process.stdout.write(JSON.stringify(rules) + "\n"); + }, +}); + /** * Print an LLM-assisted review task to stderr: the flagged files plus the * codemod's migration prompt, ready to hand to an LLM for the cases the @@ -32,6 +63,7 @@ function printLlmReview(review: LlmReview): void { const main = defineCommand({ name: packageJson.name ?? "sdk-codemod", description: packageJson.description ?? "Codemod runner for Tailor Platform SDK upgrades", + subCommands: { list: listCommand }, notes: `Applies the codemods matching the \`--from\`/\`--to\` version range to the \`--target\` directory, then writes a JSON summary to \`stdout\`: diff --git a/packages/sdk-codemod/src/migration-doc.ts b/packages/sdk-codemod/src/migration-doc.ts index e84b5572a..0ddaf7d9e 100644 --- a/packages/sdk-codemod/src/migration-doc.ts +++ b/packages/sdk-codemod/src/migration-doc.ts @@ -78,6 +78,12 @@ function renderNotice(codemod: CodemodPackage): string { * @returns The migration guide as Markdown */ export function renderMigrationDoc(codemods: CodemodPackage[]): string { + // NOTE: This generator (and the registry it reads) is shaped around the v1->v2 + // migration: the title, the `v2.md` output path, and the `v2/*` rule ids / + // `until: "2.0.0"` ranges are all hardcoded. Before this lands on `main`, + // either clean up the v2-specific scaffolding or generalize it to be + // version-agnostic (parameterize the target version, output path, and rule + // namespace) so it can serve future major migrations too. const header = [ "# Migrating to v2", "", From 815ff004c9c33f0ae33db6542cc9ecccac48adbd Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 18 Jun 2026 16:20:23 +0900 Subject: [PATCH 148/618] docs(codemod): mention the list command in the changeset --- .changeset/codemod-migration-docs.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/codemod-migration-docs.md b/.changeset/codemod-migration-docs.md index 2e02efdc8..6efdcd67d 100644 --- a/.changeset/codemod-migration-docs.md +++ b/.changeset/codemod-migration-docs.md @@ -6,3 +6,5 @@ Generate the v2 migration guide (`packages/sdk/docs/migration/v2.md`) from the codemod registry, which is the single source of truth. Each entry renders its name, automation level (Automatic / Partially automatic / Manual), description, optional before/after `examples`, and — for changes the codemods cannot fully migrate on their own — the LLM/manual migration prompt. Run `pnpm codemod:docs:update` to regenerate and `pnpm codemod:docs:check` (wired into `pnpm check`) to verify it is in sync. `scriptPath` is now optional, so the registry can also describe codemod-less ("manual") migrations that ship only guidance (`examples` / `prompt` / `suspiciousPatterns`) with no automatic transform. A manual entry with a `prompt` but no scoping pattern is surfaced as a project-wide `llmReviews` entry at runtime. + +Add a `sdk-codemod list` command that prints every registered rule (id, name, kind, version range). From 7cadaa7c4987b81130ca80ba80bc5d5b26276394 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 19 Jun 2026 02:07:21 +0900 Subject: [PATCH 149/618] refactor: rename auth invoker option --- .changeset/invoker-option-rename.md | 9 +++ example/resolvers/triggerWorkflow.ts | 4 +- example/resolvers/userInfo.ts | 2 +- ...auth-invoker.types.ts => invoker.types.ts} | 6 +- .../src/executor/checkInventory.ts | 2 +- .../templates/workflow/e2e/workflow.test.ts | 2 +- .../v2/auth-invoker-unwrap/codemod.yaml | 2 +- .../auth-invoker-unwrap/scripts/transform.ts | 11 +-- .../tests/basic-resolver/expected.ts | 2 +- .../multi-import-drops-only-auth/expected.ts | 2 +- .../tests/multi-import-keeps-auth/expected.ts | 2 +- packages/sdk-codemod/src/registry.ts | 9 +-- packages/sdk/docs/services/auth.md | 4 +- packages/sdk/docs/services/executor.md | 6 +- packages/sdk/docs/services/resolver.md | 10 +-- packages/sdk/docs/services/workflow.md | 8 +-- packages/sdk/docs/testing.md | 2 +- packages/sdk/e2e/function-test-run.test.ts | 12 ++-- .../cli/commands/deploy/auth-invoker.test.ts | 30 -------- .../src/cli/commands/deploy/auth-invoker.ts | 31 -------- .../src/cli/commands/deploy/executor.test.ts | 4 +- .../sdk/src/cli/commands/deploy/executor.ts | 8 +-- .../src/cli/commands/deploy/invoker.test.ts | 30 ++++++++ .../sdk/src/cli/commands/deploy/invoker.ts | 31 ++++++++ .../src/cli/commands/deploy/resolver.test.ts | 24 +++---- .../sdk/src/cli/commands/deploy/resolver.ts | 8 +-- .../cli/commands/deploy/tailordb/migration.ts | 11 ++- .../sdk/src/cli/commands/function/test-run.ts | 4 +- .../commands/workflow/start.api-types.test.ts | 32 ++++----- .../src/cli/commands/workflow/start.test.ts | 10 +-- .../sdk/src/cli/commands/workflow/start.ts | 16 ++--- .../services/workflow/ast-transformer.test.ts | 70 +++++++++---------- .../src/cli/services/workflow/bundler.test.ts | 4 +- .../services/workflow/trigger-transformer.ts | 46 ++++++------ .../sdk/src/cli/shared/trigger-context.ts | 4 +- packages/sdk/src/cli/shared/type-generator.ts | 2 +- .../services/executor/executor.test.ts | 4 +- .../configure/services/executor/executor.ts | 2 +- .../configure/services/executor/operation.ts | 12 ++-- .../services/resolver/resolver.test.ts | 10 +-- .../configure/services/resolver/resolver.ts | 10 +-- .../configure/services/workflow/registry.ts | 8 ++- .../configure/services/workflow/workflow.ts | 2 +- .../sdk/src/parser/service/executor/schema.ts | 6 +- .../sdk/src/parser/service/resolver/schema.ts | 2 +- packages/sdk/src/runtime/globals.ts | 6 +- packages/sdk/src/runtime/workflow.test.ts | 2 +- packages/sdk/src/runtime/workflow.ts | 30 +++++--- packages/sdk/src/types/executor.generated.ts | 20 +++--- packages/sdk/src/types/resolver.generated.ts | 2 +- 50 files changed, 298 insertions(+), 278 deletions(-) create mode 100644 .changeset/invoker-option-rename.md rename example/tests/{auth-invoker.types.ts => invoker.types.ts} (77%) delete mode 100644 packages/sdk/src/cli/commands/deploy/auth-invoker.test.ts delete mode 100644 packages/sdk/src/cli/commands/deploy/auth-invoker.ts create mode 100644 packages/sdk/src/cli/commands/deploy/invoker.test.ts create mode 100644 packages/sdk/src/cli/commands/deploy/invoker.ts diff --git a/.changeset/invoker-option-rename.md b/.changeset/invoker-option-rename.md new file mode 100644 index 000000000..a6ddc8fee --- /dev/null +++ b/.changeset/invoker-option-rename.md @@ -0,0 +1,9 @@ +--- +"@tailor-platform/sdk": major +"@tailor-platform/sdk-codemod": patch +"@tailor-platform/create-sdk": patch +--- + +Rename resolver, executor, workflow trigger, and typed workflow start machine-user options from `authInvoker` to `invoker`. + +Update create-sdk templates and the v2 auth invoker codemod to generate the new `invoker` option. diff --git a/example/resolvers/triggerWorkflow.ts b/example/resolvers/triggerWorkflow.ts index 5c6c3297b..4fa4447c5 100644 --- a/example/resolvers/triggerWorkflow.ts +++ b/example/resolvers/triggerWorkflow.ts @@ -10,13 +10,13 @@ export default createResolver({ customerId: t.string().description("Customer ID for the order"), }, body: async ({ input }) => { - // Trigger the workflow with authInvoker (machine user name is type-narrowed via tailor.d.ts) + // Trigger the workflow with invoker (machine user name is type-narrowed via tailor.d.ts) const workflowRunId = await orderProcessingWorkflow.trigger( { orderId: input.orderId, customerId: input.customerId, }, - { authInvoker: "manager-machine-user" }, + { invoker: "manager-machine-user" }, ); return { diff --git a/example/resolvers/userInfo.ts b/example/resolvers/userInfo.ts index 0678924be..abcccba0b 100644 --- a/example/resolvers/userInfo.ts +++ b/example/resolvers/userInfo.ts @@ -40,5 +40,5 @@ export default createResolver({ .description("Function invoker"), }) .description("User and invoker information"), - authInvoker: "manager-machine-user", + invoker: "manager-machine-user", }); diff --git a/example/tests/auth-invoker.types.ts b/example/tests/invoker.types.ts similarity index 77% rename from example/tests/auth-invoker.types.ts rename to example/tests/invoker.types.ts index b8f2f088d..6869f92c5 100644 --- a/example/tests/auth-invoker.types.ts +++ b/example/tests/invoker.types.ts @@ -1,8 +1,8 @@ // Type-level checks against the generated `tailor.d.ts`. Once `tailor-sdk // generate` augments `MachineUserNameRegistry`, `MachineUserName` narrows to the // registered machine user union for both SDK entries — `@tailor-platform/sdk` -// (resolver `authInvoker`) and `@tailor-platform/sdk/cli` (workflow-start -// `authInvoker`), which share the single `@tailor-platform/sdk` augmentation. +// (resolver `invoker`) and `@tailor-platform/sdk/cli` (workflow-start +// `invoker`), which share the single `@tailor-platform/sdk` augmentation. import type { MachineUserName as SdkMachineUserName } from "@tailor-platform/sdk"; import type { MachineUserName as CliMachineUserName } from "@tailor-platform/sdk/cli"; @@ -14,4 +14,4 @@ const unknownSdkInvoker: SdkMachineUserName = "unknown-machine-user"; // @ts-expect-error - unknown machine user names are rejected once tailor.d.ts is generated const unknownCliInvoker: CliMachineUserName = "unknown-machine-user"; -export const authInvokerTypeChecks = [sdkInvoker, cliInvoker, unknownSdkInvoker, unknownCliInvoker]; +export const invokerTypeChecks = [sdkInvoker, cliInvoker, unknownSdkInvoker, unknownCliInvoker]; diff --git a/packages/create-sdk/templates/inventory-management/src/executor/checkInventory.ts b/packages/create-sdk/templates/inventory-management/src/executor/checkInventory.ts index 8106daa3c..091b671d8 100644 --- a/packages/create-sdk/templates/inventory-management/src/executor/checkInventory.ts +++ b/packages/create-sdk/templates/inventory-management/src/executor/checkInventory.ts @@ -21,6 +21,6 @@ export default createExecutor({ }) .execute(); }, - authInvoker: "manager", + invoker: "manager", }, }); diff --git a/packages/create-sdk/templates/workflow/e2e/workflow.test.ts b/packages/create-sdk/templates/workflow/e2e/workflow.test.ts index e60009b17..67f3b7e15 100644 --- a/packages/create-sdk/templates/workflow/e2e/workflow.test.ts +++ b/packages/create-sdk/templates/workflow/e2e/workflow.test.ts @@ -10,7 +10,7 @@ describe("workflow", () => { const { executionId, wait } = await startWorkflow({ workflow: userProfileSyncWorkflow, - authInvoker: "admin", + invoker: "admin", arg: { name: "workflow-test-user", email: testEmail, diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/codemod.yaml b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/codemod.yaml index fae6d759f..42ed8e210 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/codemod.yaml +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/codemod.yaml @@ -1,6 +1,6 @@ name: "@tailor-platform/auth-invoker-unwrap" version: "1.0.0" -description: 'Replace `auth.invoker("name")` with the bare string `"name"` and drop the no-longer-needed `auth` import' +description: 'Replace `authInvoker: auth.invoker("name")` with `invoker: "name"` and drop the no-longer-needed `auth` import' engine: jssg language: typescript since: "1.0.0" diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts index 63bbd413b..a2ef61ccd 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts @@ -140,13 +140,14 @@ function findAuthImports(root: SgNode): SgNode[] { } /** - * Replace `auth.invoker("name")` calls with the bare `"name"` string literal. + * Replace `auth.invoker("name")` calls with the bare `"name"` string literal + * and rename matching `authInvoker:` option keys to `invoker:`. * If no other `auth` references remain after the rewrite, drop the `auth` * specifier (or the entire import line when `auth` was its sole specifier). * - * `auth.invoker()` was deprecated in favor of passing the machine user name - * directly; carrying the `auth` import only for `.invoker()` would otherwise - * pull config-layer (Node-only) modules into runtime bundles. + * `auth.invoker()` was removed in favor of passing the machine user name + * directly to `invoker`; carrying the `auth` import only for `.invoker()` + * would otherwise pull config-layer modules into runtime bundles. * @param source - File contents * @param filePath - Absolute path to the file (kept for the runner signature) * @returns Transformed source or null when nothing matched. @@ -177,6 +178,8 @@ export default function transform(source: string, _filePath: string): string | n let result = root.commitEdits(edits); + result = result.replace(/\bauthInvoker(\s*):/g, "invoker$1:"); + // Normalize: drop the leading blank line that an import removal at the top // of the file leaves behind, and collapse runs of 3+ newlines. result = result.replace(/^[\t ]*\n+/, "").replace(/\n{3,}/g, "\n\n"); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/expected.ts index 70ea71c4b..76eb3e77a 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/expected.ts @@ -14,7 +14,7 @@ export default createResolver({ orderId: input.orderId, customerId: input.customerId, }, - { authInvoker: "manager-machine-user" }, + { invoker: "manager-machine-user" }, ); return workflowRunId; }, diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts index be4be98b8..5b3794146 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts @@ -1,6 +1,6 @@ import { db } from "../tailor.config"; export const cfg = { - authInvoker: "kiosk", + invoker: "kiosk", table: db.type("Order"), }; diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts index 9db289de7..e227da334 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts @@ -1,7 +1,7 @@ import { auth, db } from "../tailor.config"; export const cfg = { - authInvoker: "kiosk", + invoker: "kiosk", // `auth` is still referenced below, so the import must be preserved. ownerType: auth.machineUser, table: db.type("Order"), diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index c34dc74c7..f5ac23441 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -85,9 +85,9 @@ const allCodemods: CodemodPackage[] = [ }, { id: "v2/auth-invoker-unwrap", - name: 'auth.invoker("name") → "name"', + name: 'auth.invoker("name") → invoker: "name"', description: - 'Replace `auth.invoker("name")` calls with the bare `"name"` string and drop the `auth` import when no other reference remains. The `auth.invoker()` helper is deprecated in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle.', + 'Replace `authInvoker: auth.invoker("name")` with `invoker: "name"` and drop the `auth` import when no other reference remains. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle.', since: "1.0.0", until: "2.0.0", scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js", @@ -95,7 +95,7 @@ const allCodemods: CodemodPackage[] = [ prompt: [ "In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the", "machine user name passed directly as a string. The codemod already rewrote the", - 'string-literal form auth.invoker("name") to "name". These files still contain', + 'string-literal form authInvoker: auth.invoker("name") to invoker: "name". These files still contain', "auth.invoker(...) because the argument is not a plain string literal (a variable,", "template literal, function call, or property access).", "", @@ -103,7 +103,8 @@ const allCodemods: CodemodPackage[] = [ "1. Replace the whole call with as-is (e.g. auth.invoker(name) becomes name).", "2. Make sure evaluates to the machine user name (a string); adjust it if it", " resolves to an object or an auth config value instead.", - "3. After removing every auth.invoker usage in a file, delete the now-unused auth", + "3. Rename any remaining authInvoker option key to invoker.", + "4. After removing every auth.invoker usage in a file, delete the now-unused auth", " import (keeping it pulls Node-only config modules into runtime bundles); leave", " the import if auth is still referenced elsewhere.", "", diff --git a/packages/sdk/docs/services/auth.md b/packages/sdk/docs/services/auth.md index 3e4fe1393..07a5d0418 100644 --- a/packages/sdk/docs/services/auth.md +++ b/packages/sdk/docs/services/auth.md @@ -269,7 +269,7 @@ tailor-sdk machineuser token ### Specifying a machine user invoker -Resolvers, executors, and `workflow.trigger()` accept an `authInvoker` option that chooses which machine user runs the operation. Pass the machine user name as a plain string — it is type-narrowed to the names you registered in `machineUsers`. +Resolvers, executors, and `workflow.trigger()` accept an `invoker` option that chooses which machine user runs the operation. Pass the machine user name as a plain string — it is type-narrowed to the names you registered in `machineUsers`. ```typescript // tailor.config.ts @@ -298,7 +298,7 @@ export default createResolver({ // Trigger workflow with machine user permissions const workflowRunId = await myWorkflow.trigger( { id: input.id }, - { authInvoker: "admin-machine-user" }, + { invoker: "admin-machine-user" }, ); return { workflowRunId }; }, diff --git a/packages/sdk/docs/services/executor.md b/packages/sdk/docs/services/executor.md index 4e784761e..954b588c9 100644 --- a/packages/sdk/docs/services/executor.md +++ b/packages/sdk/docs/services/executor.md @@ -219,7 +219,7 @@ createExecutor({ }); ``` -Executor callbacks receive the trigger args, including `env` from `defineConfig({ env })`. `function` and `jobFunction` `body` args also include an `invoker` field: the principal running this function, overridden by `authInvoker` when set; `null` for anonymous calls. Other operation kinds (`graphql`, `webhook`, `workflow`) receive `env` through their callback args but do not pass `invoker` into those callbacks. +Executor callbacks receive the trigger args, including `env` from `defineConfig({ env })`. `function` and `jobFunction` `body` args also include an `invoker` field: the principal running this function, or the machine user configured through the operation `invoker` option; `null` for anonymous calls. Other operation kinds (`graphql`, `webhook`, `workflow`) receive `env` through their callback args but do not pass `invoker` into those callbacks. ### Job Function Operation @@ -331,7 +331,7 @@ createExecutor({ ### Authentication for Operations -GraphQL and Workflow operations can specify an `authInvoker` to execute with machine user credentials. Pass the machine user name as a plain string — it is type-narrowed to the names defined in your auth config: +GraphQL and Workflow operations can specify an `invoker` to execute with machine user credentials. Pass the machine user name as a plain string — it is type-narrowed to the names defined in your auth config: ```typescript import { createExecutor, scheduleTrigger } from "@tailor-platform/sdk"; @@ -342,7 +342,7 @@ export default createExecutor({ operation: { kind: "graphql", query: `mutation { cleanupOldRecords { count } }`, - authInvoker: "batch-processor", + invoker: "batch-processor", }, }); ``` diff --git a/packages/sdk/docs/services/resolver.md b/packages/sdk/docs/services/resolver.md index be7326f4b..6c44f617d 100644 --- a/packages/sdk/docs/services/resolver.md +++ b/packages/sdk/docs/services/resolver.md @@ -234,8 +234,8 @@ Validation runs automatically before the `body` function executes. When validati Define actual resolver logic in the `body` function. Function arguments include: - `input` - Input data from GraphQL request -- `caller` - The user or machine user who called this resolver; unaffected by `authInvoker`. `null` for anonymous calls. -- `invoker` - The principal running this function; equals `caller` by default, or the machine user set by `authInvoker`. `null` for anonymous calls. +- `caller` - The user or machine user who called this resolver; unaffected by `invoker`. `null` for anonymous calls. +- `invoker` - The principal running this function; equals `caller` by default, or the machine user configured through the resolver `invoker` option. `null` for anonymous calls. - `env` - Environment variables declared in `tailor.config.ts` ### Using Kysely for Database Access @@ -352,7 +352,7 @@ createResolver({ ## Authentication -Specify an `authInvoker` to execute the resolver with machine user credentials. Pass the machine user name as a plain string — it is type-narrowed to the names you defined in your auth config: +Specify an `invoker` to execute the resolver with machine user credentials. Pass the machine user name as a plain string — it is type-narrowed to the names you defined in your auth config: ```typescript import { createResolver, t } from "@tailor-platform/sdk"; @@ -365,10 +365,10 @@ export default createResolver({ // Executes as "batch-processor" machine user return { result: "ok" }; }, - authInvoker: "batch-processor", + invoker: "batch-processor", }); ``` The machine user name is looked up in the auth service configured on your app (`machineUsers` in `defineAuth`). The namespace is resolved automatically — no need to import `auth` from `tailor.config.ts` in resolver files. -**Note:** `authInvoker` controls the permissions for database operations and other platform actions. The `caller` object passed to `body` still reflects the original caller, while `invoker` reflects the principal actually running the body. +**Note:** The `invoker` option controls the permissions for database operations and other platform actions. The `caller` object passed to `body` still reflects the original caller, while the `invoker` body field reflects the principal actually running the body. diff --git a/packages/sdk/docs/services/workflow.md b/packages/sdk/docs/services/workflow.md index d07ed5c02..fc1797eaf 100644 --- a/packages/sdk/docs/services/workflow.md +++ b/packages/sdk/docs/services/workflow.md @@ -180,8 +180,8 @@ export const processOrder = createWorkflowJob({ name: "process-order", body: (input: { customerId: string }, { env, invoker }) => { // `env` contains values from `tailor.config.ts` -> `env`. - // `invoker` is the principal running this job, overridden by `authInvoker` - // when set; `null` for anonymous calls. + // `invoker` is the principal running this job, or the machine user + // configured through the trigger `invoker` option; `null` for anonymous calls. // Trigger other jobs by calling .trigger() on the job object. const customer = fetchCustomer.trigger({ customerId: input.customerId, @@ -356,7 +356,7 @@ export default createWorkflow({ You can start a workflow execution from a resolver using `workflow.trigger()`. - `workflow.trigger(args, options?)` returns a workflow run ID (`Promise`). -- To run with machine-user permissions, pass `{ authInvoker: "" }`. The name is type-narrowed to the machine users defined in your auth config. +- To run with machine-user permissions, pass `{ invoker: "" }`. The name is type-narrowed to the machine users defined in your auth config. ```typescript import { createResolver, t } from "@tailor-platform/sdk"; @@ -372,7 +372,7 @@ export default createResolver({ body: async ({ input }) => { const workflowRunId = await orderProcessingWorkflow.trigger( { orderId: input.orderId, customerId: input.customerId }, - { authInvoker: "manager-machine-user" }, + { invoker: "manager-machine-user" }, ); return { workflowRunId }; diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index 21fc563a3..7e7ab0dde 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -845,7 +845,7 @@ describe("user-profile-sync workflow", () => { test("executes end to end", { timeout: 180_000 }, async () => { const { executionId, wait } = await startWorkflow({ workflow: userProfileSync, - authInvoker: "admin", + invoker: "admin", arg: { name: "workflow-test", email: `wf-${randomUUID()}@example.com`, diff --git a/packages/sdk/e2e/function-test-run.test.ts b/packages/sdk/e2e/function-test-run.test.ts index 4eb499cbf..29b87c725 100644 --- a/packages/sdk/e2e/function-test-run.test.ts +++ b/packages/sdk/e2e/function-test-run.test.ts @@ -45,7 +45,7 @@ const exampleDir = path.resolve(sdkRoot, "..", "..", "example"); let workspaceId: string; let client: OperatorClient; -let authInvoker: AuthInvoker; +let invoker: AuthInvoker; let machineUser: ResolvedMachineUser; const env = { foo: 1, bar: "hello", baz: true }; const AUTH_NAMESPACE = "my-auth"; @@ -76,7 +76,7 @@ async function runTestRun( name: scriptName, code, arg: options?.arg === undefined ? undefined : JSON.parse(options.arg), - invoker: authInvoker, + invoker, }); return { ...result, scriptName }; } @@ -105,7 +105,7 @@ async function runTestRun( name: scriptName, code: bundledCode, arg: resolvedArg === undefined ? undefined : JSON.parse(resolvedArg), - invoker: authInvoker, + invoker, }); return { ...result, scriptName, functionType: detected.type, functionName: detected.name }; @@ -166,7 +166,7 @@ describe.sequential("E2E: function test-run", () => { attributeList: [], }; - authInvoker = create(AuthInvokerSchema, { + invoker = create(AuthInvokerSchema, { namespace: AUTH_NAMESPACE, machineUserName: MACHINE_USER_NAME, }); @@ -309,7 +309,7 @@ describe.sequential("E2E: function test-run", () => { name: "add.js", code, arg: { a: 5, b: 7 }, - invoker: authInvoker, + invoker, }); expect(result.success).toBe(true); @@ -338,7 +338,7 @@ describe.sequential("E2E: function test-run", () => { workspaceId, name: scriptName, code: bundledCode, - invoker: authInvoker, + invoker, }); expect(result.success).toBe(false); diff --git a/packages/sdk/src/cli/commands/deploy/auth-invoker.test.ts b/packages/sdk/src/cli/commands/deploy/auth-invoker.test.ts deleted file mode 100644 index 65ec1116e..000000000 --- a/packages/sdk/src/cli/commands/deploy/auth-invoker.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { normalizeAuthInvoker } from "./auth-invoker"; - -describe("normalizeAuthInvoker", () => { - test("returns undefined when authInvoker is undefined", () => { - expect(normalizeAuthInvoker(undefined, "my-auth", "ctx")).toBeUndefined(); - }); - - test("expands a string to the object form using authNamespace", () => { - expect(normalizeAuthInvoker("kiosk", "my-auth", "ctx")).toEqual({ - namespace: "my-auth", - machineUserName: "kiosk", - }); - }); - - test("passes through object form unchanged", () => { - expect( - normalizeAuthInvoker({ namespace: "other-auth", machineUserName: "kiosk" }, "my-auth", "ctx"), - ).toEqual({ - namespace: "other-auth", - machineUserName: "kiosk", - }); - }); - - test("throws when string authInvoker is given without an authNamespace", () => { - expect(() => normalizeAuthInvoker("kiosk", undefined, 'Resolver "foo"')).toThrow( - /Resolver "foo".*Configure an Auth service before using authInvoker/, - ); - }); -}); diff --git a/packages/sdk/src/cli/commands/deploy/auth-invoker.ts b/packages/sdk/src/cli/commands/deploy/auth-invoker.ts deleted file mode 100644 index 16c980caf..000000000 --- a/packages/sdk/src/cli/commands/deploy/auth-invoker.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { AuthInvoker } from "@/types/auth.generated"; - -/** - * Normalize an authInvoker value to the object form required by the proto payload. - * - * Accepts either: - * - `undefined` — returns undefined - * - a plain string (machine user name) — expands to `{ namespace, machineUserName }` using `authNamespace` - * - an internal object `{ namespace, machineUserName }` — returned as-is - * @param authInvoker - String machine user name or internal object form - * @param authNamespace - Auth service namespace (required when authInvoker is a string) - * @param context - Contextual label used in error messages (e.g. `resolver "foo"`) - * @returns Object form of auth invoker, or undefined - */ -export function normalizeAuthInvoker( - authInvoker: string | AuthInvoker | undefined, - authNamespace: string | undefined, - context: string, -): { namespace: string; machineUserName: string } | undefined { - if (authInvoker === undefined) return undefined; - if (typeof authInvoker === "string") { - if (!authNamespace) { - throw new Error( - `${context} uses a string authInvoker ("${authInvoker}"), but no Auth service is configured. ` + - `Configure an Auth service before using authInvoker.`, - ); - } - return { namespace: authNamespace, machineUserName: authInvoker }; - } - return authInvoker; -} diff --git a/packages/sdk/src/cli/commands/deploy/executor.test.ts b/packages/sdk/src/cli/commands/deploy/executor.test.ts index f81e1aaa5..6824c44de 100644 --- a/packages/sdk/src/cli/commands/deploy/executor.test.ts +++ b/packages/sdk/src/cli/commands/deploy/executor.test.ts @@ -677,7 +677,7 @@ describe("planExecutor", () => { expect(variablesExpr).toContain("error: args.failed?.error"); }); - test("string authInvoker uses external auth config name", async () => { + test("string invoker uses external auth config name", async () => { const client = createMockClient([]); const executor: Executor = { name: "test-executor", @@ -691,7 +691,7 @@ describe("planExecutor", () => { operation: { kind: "function", body: () => {}, - authInvoker: "batch-user", + invoker: "batch-user", }, }; const application = createMockApplication([executor], { diff --git a/packages/sdk/src/cli/commands/deploy/executor.ts b/packages/sdk/src/cli/commands/deploy/executor.ts index 9300dcf5a..81096ed82 100644 --- a/packages/sdk/src/cli/commands/deploy/executor.ts +++ b/packages/sdk/src/cli/commands/deploy/executor.ts @@ -18,7 +18,6 @@ import { type OperatorClient } from "@/cli/shared/client"; import { buildExecutorArgsExpr } from "@/cli/shared/runtime-exprs"; import { stringifyFunction } from "@/parser/service/tailordb"; import { assertDefined } from "@/utils/assert"; -import { normalizeAuthInvoker } from "./auth-invoker"; import { createChangeSet, type ChangeSet } from "./change-set"; import { areNormalizedEqual, normalizeProtoConfig } from "./compare"; import { executorFunctionName } from "./function-registry"; @@ -27,6 +26,7 @@ import { type GroupedDisplayEntry, type RelatedFunctionRegistryChanges, } from "./grouped-display"; +import { normalizeInvoker } from "./invoker"; import { buildMetaRequest, hasMatchingSdkVersion, resourceTrn } from "./label"; import { fetchExistingResourcesWithLabels, @@ -550,7 +550,7 @@ function protoExecutor( expr: `(${stringifyFunction(target.variables)})(${argsExpr})`, } : undefined, - invoker: normalizeAuthInvoker(target.authInvoker, authNamespace, invokerContext), + invoker: normalizeInvoker(target.invoker, authNamespace, invokerContext), }, }, }; @@ -573,7 +573,7 @@ function protoExecutor( variables: { expr: argsExpr, }, - invoker: normalizeAuthInvoker(target.authInvoker, authNamespace, invokerContext), + invoker: normalizeInvoker(target.invoker, authNamespace, invokerContext), }, }, }; @@ -591,7 +591,7 @@ function protoExecutor( ? { expr: `(${stringifyFunction(target.args)})(${argsExpr})` } : { expr: JSON.stringify(target.args) } : undefined, - invoker: normalizeAuthInvoker(target.authInvoker, authNamespace, invokerContext), + invoker: normalizeInvoker(target.invoker, authNamespace, invokerContext), }, }, }; diff --git a/packages/sdk/src/cli/commands/deploy/invoker.test.ts b/packages/sdk/src/cli/commands/deploy/invoker.test.ts new file mode 100644 index 000000000..0726d3921 --- /dev/null +++ b/packages/sdk/src/cli/commands/deploy/invoker.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from "vitest"; +import { normalizeInvoker } from "./invoker"; + +describe("normalizeInvoker", () => { + test("returns undefined when invoker is undefined", () => { + expect(normalizeInvoker(undefined, "my-auth", "ctx")).toBeUndefined(); + }); + + test("expands a string to the object form using authNamespace", () => { + expect(normalizeInvoker("kiosk", "my-auth", "ctx")).toEqual({ + namespace: "my-auth", + machineUserName: "kiosk", + }); + }); + + test("passes through object form unchanged", () => { + expect( + normalizeInvoker({ namespace: "other-auth", machineUserName: "kiosk" }, "my-auth", "ctx"), + ).toEqual({ + namespace: "other-auth", + machineUserName: "kiosk", + }); + }); + + test("throws when string invoker is given without an authNamespace", () => { + expect(() => normalizeInvoker("kiosk", undefined, 'Resolver "foo"')).toThrow( + /Resolver "foo".*Configure an Auth service before using invoker/, + ); + }); +}); diff --git a/packages/sdk/src/cli/commands/deploy/invoker.ts b/packages/sdk/src/cli/commands/deploy/invoker.ts new file mode 100644 index 000000000..b83a2d01c --- /dev/null +++ b/packages/sdk/src/cli/commands/deploy/invoker.ts @@ -0,0 +1,31 @@ +import type { AuthInvoker } from "@/types/auth.generated"; + +/** + * Normalize an invoker value to the object form required by the proto payload. + * + * Accepts either: + * - `undefined` — returns undefined + * - a plain string (machine user name) — expands to `{ namespace, machineUserName }` using `authNamespace` + * - an internal object `{ namespace, machineUserName }` — returned as-is + * @param invoker - String machine user name or internal object form + * @param authNamespace - Auth service namespace (required when invoker is a string) + * @param context - Contextual label used in error messages (e.g. `resolver "foo"`) + * @returns Object form of the invoker, or undefined + */ +export function normalizeInvoker( + invoker: string | AuthInvoker | undefined, + authNamespace: string | undefined, + context: string, +): { namespace: string; machineUserName: string } | undefined { + if (invoker === undefined) return undefined; + if (typeof invoker === "string") { + if (!authNamespace) { + throw new Error( + `${context} uses a string invoker ("${invoker}"), but no Auth service is configured. ` + + `Configure an Auth service before using invoker.`, + ); + } + return { namespace: authNamespace, machineUserName: invoker }; + } + return invoker; +} diff --git a/packages/sdk/src/cli/commands/deploy/resolver.test.ts b/packages/sdk/src/cli/commands/deploy/resolver.test.ts index 2099774ba..abfe5e1e7 100644 --- a/packages/sdk/src/cli/commands/deploy/resolver.test.ts +++ b/packages/sdk/src/cli/commands/deploy/resolver.test.ts @@ -432,7 +432,7 @@ describe("planPipeline (resolver service level)", () => { expect(result.changeSet.resolver.unchanged).toHaveLength(0); }); - test("resolver is updated when authInvoker differs", async () => { + test("resolver is updated when invoker differs", async () => { const resolver = { name: "test-resolver", operation: 0, @@ -441,7 +441,7 @@ describe("planPipeline (resolver service level)", () => { type: "string", metadata: {}, }, - authInvoker: { namespace: "my-auth", machineUserName: "batch-user" }, + invoker: { namespace: "my-auth", machineUserName: "batch-user" }, }; const pipeline = { namespace: "my-resolver", @@ -482,7 +482,7 @@ describe("planPipeline (resolver service level)", () => { }); }); -describe("processResolver authInvoker mapping", () => { +describe("processResolver invoker mapping", () => { const workspaceId = "test-workspace"; const appName = "test-app"; @@ -528,7 +528,7 @@ describe("processResolver authInvoker mapping", () => { vi.clearAllMocks(); }); - test("authInvoker is mapped to proto invoker field", async () => { + test("invoker is mapped to proto invoker field", async () => { const client = createMockClient([{ name: "test-ns", label: appName }]); const resolverService = { @@ -540,7 +540,7 @@ describe("processResolver authInvoker mapping", () => { operation: "query", body: () => "hello", output: { type: "string", metadata: {}, fields: {} }, - authInvoker: { namespace: "my-auth", machineUserName: "batch-user" }, + invoker: { namespace: "my-auth", machineUserName: "batch-user" }, }, }, loadResolvers: vi.fn().mockResolvedValue(undefined), @@ -577,7 +577,7 @@ describe("processResolver authInvoker mapping", () => { }); }); - test("string authInvoker is normalized using the configured auth service name", async () => { + test("string invoker is normalized using the configured auth service name", async () => { const client = createMockClient([{ name: "test-ns", label: appName }]); const resolverService = { @@ -589,7 +589,7 @@ describe("processResolver authInvoker mapping", () => { operation: "query", body: () => "hello", output: { type: "string", metadata: {}, fields: {} }, - authInvoker: "batch-user", + invoker: "batch-user", }, }, loadResolvers: vi.fn().mockResolvedValue(undefined), @@ -627,7 +627,7 @@ describe("processResolver authInvoker mapping", () => { }); }); - test("string authInvoker without auth service configured throws", async () => { + test("string invoker without auth service configured throws", async () => { const client = createMockClient([{ name: "test-ns", label: appName }]); const resolverService = { @@ -639,7 +639,7 @@ describe("processResolver authInvoker mapping", () => { operation: "query", body: () => "hello", output: { type: "string", metadata: {}, fields: {} }, - authInvoker: "batch-user", + invoker: "batch-user", }, }, loadResolvers: vi.fn().mockResolvedValue(undefined), @@ -667,7 +667,7 @@ describe("processResolver authInvoker mapping", () => { await expect(planPipeline(ctx)).rejects.toThrow(/no Auth service is configured/); }); - test("string authInvoker uses external auth config name", async () => { + test("string invoker uses external auth config name", async () => { const client = createMockClient([{ name: "test-ns", label: appName }]); const resolverService = { @@ -679,7 +679,7 @@ describe("processResolver authInvoker mapping", () => { operation: "query", body: () => "hello", output: { type: "string", metadata: {}, fields: {} }, - authInvoker: "batch-user", + invoker: "batch-user", }, }, loadResolvers: vi.fn().mockResolvedValue(undefined), @@ -719,7 +719,7 @@ describe("processResolver authInvoker mapping", () => { }); }); - test("invoker is undefined when authInvoker is not set", async () => { + test("invoker is undefined when invoker is not set", async () => { const client = createMockClient([{ name: "test-ns", label: appName }]); const resolverService = { diff --git a/packages/sdk/src/cli/commands/deploy/resolver.ts b/packages/sdk/src/cli/commands/deploy/resolver.ts index fb9eb3e66..42a85e090 100644 --- a/packages/sdk/src/cli/commands/deploy/resolver.ts +++ b/packages/sdk/src/cli/commands/deploy/resolver.ts @@ -21,7 +21,6 @@ import { getApplicationAuthNamespace } from "@/cli/shared/auth-namespace"; import { fetchAll, type OperatorClient } from "@/cli/shared/client"; import { buildResolverOperationHookExpr } from "@/cli/shared/runtime-exprs"; import { assertDefined } from "@/utils/assert"; -import { normalizeAuthInvoker } from "./auth-invoker"; import { createChangeSet, type ChangeSet } from "./change-set"; import { areNormalizedEqual, normalizeProtoConfig } from "./compare"; import { resolverFunctionName } from "./function-registry"; @@ -30,6 +29,7 @@ import { type GroupedDisplayEntry, type RelatedFunctionRegistryChanges, } from "./grouped-display"; +import { normalizeInvoker } from "./invoker"; import { buildMetaRequest, hasMatchingSdkVersion, @@ -582,11 +582,7 @@ function processResolver( expr: buildResolverOperationHookExpr(env), }, postScript: `args.body`, - invoker: normalizeAuthInvoker( - resolver.authInvoker, - authNamespace, - `Resolver "${resolver.name}"`, - ), + invoker: normalizeInvoker(resolver.invoker, authNamespace, `Resolver "${resolver.name}"`), }, ]; diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/migration.ts b/packages/sdk/src/cli/commands/deploy/tailordb/migration.ts index 53cd8853a..ff21f230a 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/migration.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/migration.ts @@ -34,7 +34,7 @@ import type { TailorDBServiceConfig } from "@/types/tailordb.generated"; export interface MigrationExecutionOptions { client: OperatorClient; workspaceId: string; - authInvoker: AuthInvoker; + invoker: AuthInvoker; env: Record; } @@ -174,7 +174,7 @@ async function executeSingleMigration( options: MigrationExecutionOptions, migration: PendingMigration, ): Promise { - const { client, workspaceId, authInvoker, env } = options; + const { client, workspaceId, invoker, env } = options; const migrationName = `migration-${migration.namespace}-${formatMigrationNumber(migration.number)}.js`; @@ -192,7 +192,7 @@ async function executeSingleMigration( workspaceId, name: migrationName, code: bundleResult.bundledCode, - invoker: authInvoker, + invoker, }); return { @@ -271,8 +271,7 @@ export async function executeMigrations( ); } - // Create authInvoker for this namespace - const authInvoker = create(AuthInvokerSchema, { + const invoker = create(AuthInvokerSchema, { namespace: context.authNamespace, machineUserName, }); @@ -280,7 +279,7 @@ export async function executeMigrations( const options: MigrationExecutionOptions = { client: context.client, workspaceId: context.workspaceId, - authInvoker, + invoker, env: context.env, }; diff --git a/packages/sdk/src/cli/commands/function/test-run.ts b/packages/sdk/src/cli/commands/function/test-run.ts index 2ac4293c4..be252d108 100644 --- a/packages/sdk/src/cli/commands/function/test-run.ts +++ b/packages/sdk/src/cli/commands/function/test-run.ts @@ -156,7 +156,7 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file } // 5. Execute via TestExecScript - const authInvoker = create(AuthInvokerSchema, { + const invoker = create(AuthInvokerSchema, { namespace: authNamespace, machineUserName: machineUser.name, }); @@ -180,7 +180,7 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file name: scriptName, code: bundledCode, arg: parsedArg, - invoker: authInvoker, + invoker, }); // 7. Display result diff --git a/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts b/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts index b8e87c0e8..e9b33d994 100644 --- a/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts +++ b/packages/sdk/src/cli/commands/workflow/start.api-types.test.ts @@ -3,7 +3,7 @@ import { describe, test, expectTypeOf } from "vitest"; import { createWorkflow, createWorkflowJob } from "@/configure/services/workflow"; import { type StartWorkflowOptions, type StartWorkflowTypedOptions } from "./start"; -// `authInvoker` is typed as `MachineUserName`, which falls back to `string` until +// `invoker` is typed as `MachineUserName`, which falls back to `string` until // `tailor.d.ts` augments `MachineUserNameRegistry`. Narrowing to the registered // machine user union (and rejection of unknown names) is covered against a real // generated `tailor.d.ts` in `example/`; here we only assert arg inference and @@ -90,13 +90,13 @@ describe("startWorkflow API types", () => { test("infers arg type from workflow", () => { acceptsCalculationWorkflowOptions({ workflow: calculationWorkflow, - authInvoker: "admin", + invoker: "admin", arg: { a: 1, b: 2 }, }); acceptsCalculationWorkflowOptions({ workflow: calculationWorkflow, - authInvoker: "admin", + invoker: "admin", // @ts-expect-error - arg shape must match workflow input arg: { x: 1, y: 2 }, }); @@ -109,19 +109,19 @@ describe("startWorkflow API types", () => { // @ts-expect-error - arg is required for workflows with input acceptsCalculationWorkflowOptions({ workflow: calculationWorkflow, - authInvoker: "admin", + invoker: "admin", }); }); test("does not allow arg for workflows without input", () => { acceptsNoInputWorkflowOptions({ workflow: noInputWorkflow, - authInvoker: "admin", + invoker: "admin", }); acceptsNoInputWorkflowOptions({ workflow: noInputWorkflow, - authInvoker: "admin", + invoker: "admin", // @ts-expect-error - no-input workflow must not receive arg arg: { any: "value" }, }); @@ -130,7 +130,7 @@ describe("startWorkflow API types", () => { test("accepts machine user names as strings", () => { acceptsCalculationWorkflowOptions({ workflow: calculationWorkflow, - authInvoker: "worker", + invoker: "worker", arg: { a: 1, b: 2 }, }); }); @@ -138,12 +138,12 @@ describe("startWorkflow API types", () => { test("keeps default generic usable when StartWorkflowTypedOptions generic is omitted", () => { acceptsDefaultWorkflowOptions({ workflow: noInputWorkflow, - authInvoker: "admin", + invoker: "admin", }); acceptsDefaultWorkflowOptions({ workflow: calculationWorkflow, - authInvoker: "admin", + invoker: "admin", arg: { a: 1, b: 2 }, }); }); @@ -151,26 +151,26 @@ describe("startWorkflow API types", () => { test("supports union workflow types without collapsing arg type", () => { acceptsUnionWorkflowOptions({ workflow: calculationWorkflow, - authInvoker: "admin", + invoker: "admin", arg: { a: 1, b: 2 }, }); acceptsUnionWorkflowOptions({ workflow: noInputWorkflow, - authInvoker: "admin", + invoker: "admin", }); }); test("supports union workflow input types without collapsing arg type", () => { acceptsUnionInputWorkflowOptions({ workflow: calculationWorkflow, - authInvoker: "admin", + invoker: "admin", arg: { a: 1, b: 2 }, }); acceptsUnionInputWorkflowOptions({ workflow: textWorkflow, - authInvoker: "admin", + invoker: "admin", arg: { message: "hello" }, }); @@ -183,13 +183,13 @@ describe("startWorkflow API types", () => { test("supports plain workflow unions without collapsing arg type", () => { acceptsPlainUnionWorkflowOptions({ workflow: plainWorkflowA, - authInvoker: "admin", + invoker: "admin", arg: { foo: 1 }, }); acceptsPlainUnionWorkflowOptions({ workflow: plainWorkflowB, - authInvoker: "admin", + invoker: "admin", arg: { bar: "x" }, }); @@ -213,7 +213,7 @@ describe("startWorkflow API types", () => { acceptsDeprecatedOptions({ // @ts-expect-error - deprecated options must keep legacy name/machineUser shape workflow: calculationWorkflow, - authInvoker: "admin", + invoker: "admin", arg: { a: 1, b: 2 }, }); }); diff --git a/packages/sdk/src/cli/commands/workflow/start.test.ts b/packages/sdk/src/cli/commands/workflow/start.test.ts index 92d0a1f3d..96c9ddadd 100644 --- a/packages/sdk/src/cli/commands/workflow/start.test.ts +++ b/packages/sdk/src/cli/commands/workflow/start.test.ts @@ -73,7 +73,7 @@ describe("startWorkflow runtime overload", () => { body: () => undefined, }, }, - authInvoker: "typed-user", + invoker: "typed-user", } as never); expect(loadConfig).toHaveBeenCalledTimes(1); @@ -95,7 +95,7 @@ describe("startWorkflow runtime overload", () => { ); }); - test("typed shape resolves auth namespace from config and sends proto authInvoker", async () => { + test("typed shape resolves auth namespace from config and sends proto credentials", async () => { await startWorkflow({ workflow: { name: "typed-workflow", @@ -103,7 +103,7 @@ describe("startWorkflow runtime overload", () => { body: () => undefined, }, }, - authInvoker: "typed-user", + invoker: "typed-user", }); expect(loadConfig).toHaveBeenCalledTimes(1); @@ -140,7 +140,7 @@ describe("startWorkflow runtime overload", () => { body: () => undefined, }, }, - authInvoker: "typed-user", + invoker: "typed-user", }); expect(testStartWorkflowMock).toHaveBeenCalledWith( @@ -167,7 +167,7 @@ describe("startWorkflow runtime overload", () => { body: () => undefined, }, }, - authInvoker: "typed-user", + invoker: "typed-user", }), ).rejects.toThrow("my-app does not have an auth configuration"); expect(testStartWorkflowMock).not.toHaveBeenCalled(); diff --git a/packages/sdk/src/cli/commands/workflow/start.ts b/packages/sdk/src/cli/commands/workflow/start.ts index 2f040111e..eb028f8a3 100644 --- a/packages/sdk/src/cli/commands/workflow/start.ts +++ b/packages/sdk/src/cli/commands/workflow/start.ts @@ -32,7 +32,7 @@ type WorkflowLike = { }; }; -type AuthInvoker = { +type WorkflowInvoker = { namespace: string; machineUserName: M; }; @@ -68,7 +68,7 @@ export interface StartWorkflowOptions { type StartWorkflowTypedBaseOptions = { workflow: W; - authInvoker: MachineUserName; + invoker: MachineUserName; workspaceId?: string; profile?: string; configPath?: string; @@ -227,7 +227,7 @@ interface StartWorkflowCoreOptions { client: Awaited>; workspaceId: string; workflowName: string; - authInvoker: AuthInvoker; + invoker: WorkflowInvoker; arg?: unknown; interval?: number; } @@ -239,7 +239,7 @@ async function startWorkflowCore( try { const workflow = await resolveWorkflow(client, workspaceId, workflowName); - const authInvoker = create(AuthInvokerSchema, options.authInvoker); + const invoker = create(AuthInvokerSchema, options.invoker); const arg = options.arg === undefined ? undefined @@ -250,7 +250,7 @@ async function startWorkflowCore( const { executionId } = await client.testStartWorkflow({ workspaceId, workflowId: workflow.id, - authInvoker, + authInvoker: invoker, arg, }); @@ -323,7 +323,7 @@ async function startWorkflowByName( client, workspaceId, workflowName: options.name, - authInvoker: { + invoker: { namespace: authNamespace, machineUserName: machineUser, }, @@ -369,9 +369,9 @@ export async function startWorkflow( client, workspaceId, workflowName: options.workflow.name, - authInvoker: { + invoker: { namespace: authNamespace, - machineUserName: options.authInvoker, + machineUserName: options.invoker, }, arg: options.arg, interval: options.interval, diff --git a/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts b/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts index ff4ffe879..f1e2f820b 100644 --- a/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts +++ b/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts @@ -760,7 +760,7 @@ describe("AST Transformer - transformFunctionTriggers", () => { const source = ` const workflowRunId = await orderWorkflow.trigger( { orderId: "123", customerId: "456" }, - { authInvoker: "admin" } + { invoker: "admin" } ); `; const workflowNameMap = new Map([["orderWorkflow", "order-processing"]]); @@ -773,10 +773,10 @@ const workflowRunId = await orderWorkflow.trigger( expect(result).toContain('{ authInvoker: "admin" }'); }); - test("transforms workflow.trigger() with shorthand authInvoker", () => { + test("transforms workflow.trigger() with shorthand invoker", () => { const source = ` -const authInvoker = "admin"; -const result = await myWorkflow.trigger({ id: 1 }, { authInvoker }); +const invoker = "admin"; +const result = await myWorkflow.trigger({ id: 1 }, { invoker }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -784,12 +784,12 @@ const result = await myWorkflow.trigger({ id: 1 }, { authInvoker }); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain('tailor.workflow.triggerWorkflow("my-workflow"'); - expect(result).toContain("{ authInvoker: authInvoker }"); + expect(result).toContain("{ authInvoker: invoker }"); }); - test("wraps a string-literal authInvoker with the runtime normalizer when authNamespace is provided", () => { + test("wraps a string-literal invoker with the runtime normalizer when authNamespace is provided", () => { const source = ` -const result = await myWorkflow.trigger({ id: 1 }, { authInvoker: "kiosk" }); +const result = await myWorkflow.trigger({ id: 1 }, { invoker: "kiosk" }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -804,17 +804,17 @@ const result = await myWorkflow.trigger({ id: 1 }, { authInvoker: "kiosk" }); ); expect(result).toContain('tailor.workflow.triggerWorkflow("my-workflow"'); - expect(result).toContain('{ authInvoker: __tailor_normalizeAuthInvoker("kiosk") }'); + expect(result).toContain('{ authInvoker: __tailor_normalizeInvoker("kiosk") }'); // Helper injected at the top of the file with the namespace baked in expect(result).toContain( - 'const __tailor_normalizeAuthInvoker = (machineUserName) => ({ namespace: "my-auth", machineUserName });', + 'const __tailor_normalizeInvoker = (machineUserName) => ({ namespace: "my-auth", machineUserName });', ); }); - test("wraps a variable-reference authInvoker with the runtime normalizer", () => { + test("wraps a variable-reference invoker with the runtime normalizer", () => { const source = ` const invoker = "kiosk"; -const result = await myWorkflow.trigger({ id: 1 }, { authInvoker: invoker }); +const result = await myWorkflow.trigger({ id: 1 }, { invoker }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -828,13 +828,13 @@ const result = await myWorkflow.trigger({ id: 1 }, { authInvoker: invoker }); "my-auth", ); - expect(result).toContain("{ authInvoker: __tailor_normalizeAuthInvoker(invoker) }"); + expect(result).toContain("{ authInvoker: __tailor_normalizeInvoker(invoker) }"); }); - test("wraps a shorthand authInvoker with the runtime normalizer", () => { + test("wraps a shorthand invoker with the runtime normalizer", () => { const source = ` -const authInvoker = "kiosk"; -const result = await myWorkflow.trigger({ id: 1 }, { authInvoker }); +const invoker = "kiosk"; +const result = await myWorkflow.trigger({ id: 1 }, { invoker }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -848,13 +848,13 @@ const result = await myWorkflow.trigger({ id: 1 }, { authInvoker }); "my-auth", ); - expect(result).toContain("{ authInvoker: __tailor_normalizeAuthInvoker(authInvoker) }"); + expect(result).toContain("{ authInvoker: __tailor_normalizeInvoker(invoker) }"); }); test("injects the normalizer helper only once per file even for multiple trigger calls", () => { const source = ` -await myWorkflow.trigger({ id: 1 }, { authInvoker: "kiosk" }); -await myWorkflow.trigger({ id: 2 }, { authInvoker: "batch" }); +await myWorkflow.trigger({ id: 1 }, { invoker: "kiosk" }); +await myWorkflow.trigger({ id: 2 }, { invoker: "batch" }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -868,13 +868,13 @@ await myWorkflow.trigger({ id: 2 }, { authInvoker: "batch" }); "my-auth", ); - const matches = result.match(/const __tailor_normalizeAuthInvoker =/g); + const matches = result.match(/const __tailor_normalizeInvoker =/g); expect(matches).toHaveLength(1); }); - test("keeps authInvoker unchanged and omits the helper when authNamespace is not provided", () => { + test("keeps invoker unchanged and omits the helper when authNamespace is not provided", () => { const source = ` -const result = await myWorkflow.trigger({ id: 1 }, { authInvoker: "kiosk" }); +const result = await myWorkflow.trigger({ id: 1 }, { invoker: "kiosk" }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -882,7 +882,7 @@ const result = await myWorkflow.trigger({ id: 1 }, { authInvoker: "kiosk" }); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain('{ authInvoker: "kiosk" }'); - expect(result).not.toContain("__tailor_normalizeAuthInvoker"); + expect(result).not.toContain("__tailor_normalizeInvoker"); }); }); @@ -936,7 +936,7 @@ const event = button.trigger("click"); test("only transforms trigger calls for known workflows and jobs", () => { const source = ` // Known workflow - should be transformed -const wfResult = await orderWorkflow.trigger({ id: 1 }, { authInvoker: "admin" }); +const wfResult = await orderWorkflow.trigger({ id: 1 }, { invoker: "admin" }); // Known job - should be transformed const jobResult = await fetchData.trigger({ id: 2 }); @@ -983,7 +983,7 @@ async function processOrder(orderId: string) { // Then trigger a workflow for processing const workflowRunId = await orderWorkflow.trigger( { orderId, data }, - { authInvoker: "system" } + { invoker: "system" } ); return { data, workflowRunId }; @@ -1034,7 +1034,7 @@ const notification = await sendNotification.trigger({ message: "Hello" }); test("does not wrap workflow.trigger() calls (already async)", () => { const source = ` -const executionId = await orderWorkflow.trigger({ orderId: "123" }, { authInvoker }); +const executionId = await orderWorkflow.trigger({ orderId: "123" }, { invoker }); `; const workflowNameMap = new Map([["orderWorkflow", "order-processing"]]); const jobNameMap = new Map(); @@ -1126,7 +1126,7 @@ export const job = createWorkflowJob({ body: async () => { const result = await simpleWorkflow.trigger( { input: 0 }, - { authInvoker: "admin" } + { invoker: "admin" } ); return result; }, @@ -1155,8 +1155,8 @@ import simpleWorkflow from "./simple"; export const job = createWorkflowJob({ name: "my-job", body: async () => { - await simpleWorkflow.trigger({ input: 1 }, { authInvoker: "admin" }); - await simpleWorkflow.trigger({ input: 2 }, { authInvoker: "admin" }); + await simpleWorkflow.trigger({ input: 1 }, { invoker: "admin" }); + await simpleWorkflow.trigger({ input: 2 }, { invoker: "admin" }); }, }); `; @@ -1184,7 +1184,7 @@ console.log(simpleWorkflow); export const job = createWorkflowJob({ name: "my-job", body: async () => { - await simpleWorkflow.trigger({ input: 0 }, { authInvoker: "admin" }); + await simpleWorkflow.trigger({ input: 0 }, { invoker: "admin" }); }, }); `; @@ -1237,8 +1237,8 @@ import workflowB from "./workflow-b"; export const job = createWorkflowJob({ name: "my-job", body: async () => { - await workflowA.trigger({ input: 1 }, { authInvoker: "admin" }); - await workflowB.trigger({ input: 2 }, { authInvoker: "admin" }); + await workflowA.trigger({ input: 1 }, { invoker: "admin" }); + await workflowB.trigger({ input: 2 }, { invoker: "admin" }); }, }); `; @@ -1271,7 +1271,7 @@ export const job = createWorkflowJob({ name: "my-job", body: async () => { someHelper(); - await simpleWorkflow.trigger({ input: 0 }, { authInvoker: "admin" }); + await simpleWorkflow.trigger({ input: 0 }, { invoker: "admin" }); }, }); `; @@ -1300,7 +1300,7 @@ import simpleWorkflow from "./simple"; export const job = createWorkflowJob({ name: "my-job", body: async () => { - await simpleWorkflow.trigger({ input: 0 }, { authInvoker: "admin" }); + await simpleWorkflow.trigger({ input: 0 }, { invoker: "admin" }); function helper(simpleWorkflow) { console.log(simpleWorkflow); } @@ -1330,7 +1330,7 @@ import simpleWorkflow from "./simple"; export const job = createWorkflowJob({ name: "my-job", body: async () => { - await simpleWorkflow.trigger({ input: 0 }, { authInvoker: "admin" }); + await simpleWorkflow.trigger({ input: 0 }, { invoker: "admin" }); const config = { simpleWorkflow: "some-value" }; return config; }, @@ -1398,7 +1398,7 @@ export const job = createWorkflowJob({ body: async () => { const result = await simpleWorkflow.trigger( { input: 0 }, - { authInvoker: "admin" } + { invoker: "admin" } ); return result; }, diff --git a/packages/sdk/src/cli/services/workflow/bundler.test.ts b/packages/sdk/src/cli/services/workflow/bundler.test.ts index 4fafbf10c..6b633a3ca 100644 --- a/packages/sdk/src/cli/services/workflow/bundler.test.ts +++ b/packages/sdk/src/cli/services/workflow/bundler.test.ts @@ -60,7 +60,7 @@ import simpleWorkflow from "./simple"; export const callerJob = createWorkflowJob({ name: "caller-job", body: async () => { - const executionId = await simpleWorkflow.trigger({ input: 0 }, { authInvoker: "admin" }); + const executionId = await simpleWorkflow.trigger({ input: 0 }, { invoker: "admin" }); return { executionId }; }, }); @@ -149,7 +149,7 @@ import simpleWorkflow from "./simple.mjs"; export const callerJob = createWorkflowJob({ name: "caller-job", body: async () => { - const executionId = await simpleWorkflow.trigger({ input: 0 }, { authInvoker: "admin" }); + const executionId = await simpleWorkflow.trigger({ input: 0 }, { invoker: "admin" }); return { executionId }; }, }); diff --git a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts b/packages/sdk/src/cli/services/workflow/trigger-transformer.ts index dde891e8a..a75af190e 100644 --- a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts +++ b/packages/sdk/src/cli/services/workflow/trigger-transformer.ts @@ -18,7 +18,7 @@ import type { ImportDefaultSpecifier, } from "@oxc-project/types"; -interface AuthInvokerInfo { +interface InvokerInfo { isShorthand: boolean; valueText: string; } @@ -28,15 +28,14 @@ interface ExtendedTriggerCall { identifierName: string; callRange: { start: number; end: number }; argsText: string; - // For workflow triggers, extracted authInvoker info from config - authInvoker?: AuthInvokerInfo; + invoker?: InvokerInfo; } /** * Name of the injected runtime normalizer helper. Chosen to be unique enough * to avoid collisions with user code. */ -const NORMALIZER_IDENTIFIER = "__tailor_normalizeAuthInvoker"; +const NORMALIZER_IDENTIFIER = "__tailor_normalizeInvoker"; /** * Build the source text of the injected normalizer helper. @@ -52,16 +51,13 @@ function buildNormalizerHelperSource(authNamespace: string): string { } /** - * Extract authInvoker info from a config object expression - * Returns the authInvoker value text and whether it's a shorthand property + * Extract invoker info from a config object expression. + * Returns the invoker value text and whether it's a shorthand property. * @param configArg - Config argument node * @param sourceText - Source code text - * @returns Extracted authInvoker info, if any + * @returns Extracted invoker info, if any */ -function extractAuthInvokerInfo( - configArg: unknown, - sourceText: string, -): AuthInvokerInfo | undefined { +function extractInvokerInfo(configArg: unknown, sourceText: string): InvokerInfo | undefined { if (!configArg || typeof configArg !== "object") return undefined; const arg = configArg as { type?: string }; @@ -69,7 +65,6 @@ function extractAuthInvokerInfo( const objExpr = configArg as ObjectExpression; - // Find authInvoker property for (const prop of objExpr.properties) { if (prop.type !== "Property") continue; @@ -81,9 +76,9 @@ function extractAuthInvokerInfo( ? (objProp.key as { value?: string }).value : null; - if (keyName === "authInvoker") { + if (keyName === "invoker") { if (objProp.shorthand) { - return { isShorthand: true, valueText: "authInvoker" }; + return { isShorthand: true, valueText: "invoker" }; } // Extract value text directly from source const valueText = sourceText.slice(objProp.value.start, objProp.value.end); @@ -320,14 +315,14 @@ function detectExtendedTriggerCalls( if (isWorkflow && argCount >= 2) { const secondArg = callExpr.arguments[1]; - const authInvoker = extractAuthInvokerInfo(secondArg, sourceText); - if (authInvoker) { + const invoker = extractInvokerInfo(secondArg, sourceText); + if (invoker) { calls.push({ kind: "workflow", identifierName, callRange: { start: callExpr.start, end: callExpr.end }, argsText, - authInvoker, + invoker, }); } } else if (isJob) { @@ -365,7 +360,7 @@ function detectExtendedTriggerCalls( * @param jobNameMap - Map from variable name to job name * @param workflowFileMap - Map from file path (without extension) to workflow name for default exports * @param currentFilePath - Path of the current file being transformed (for resolving relative imports) - * @param authNamespace - Auth service namespace used to expand string-literal `authInvoker` to object form + * @param authNamespace - Auth service namespace used to expand string-literal `invoker` to object form * @returns Transformed source code with trigger calls rewritten */ export function transformFunctionTriggers( @@ -417,7 +412,7 @@ export function transformFunctionTriggers( const triggerCalls = detectExtendedTriggerCalls(program, source, workflowNames, jobNames); const replacements: Replacement[] = []; - // Whether any workflow trigger authInvoker was wrapped with the runtime + // Whether any workflow trigger invoker was wrapped with the runtime // normalizer. Used to decide whether to inject the helper at the top. let needsNormalizerHelper = false; @@ -425,27 +420,26 @@ export function transformFunctionTriggers( const transformedCallsPerIdentifier = new Map(); for (const call of triggerCalls) { - if (call.kind === "workflow" && call.authInvoker) { + if (call.kind === "workflow" && call.invoker) { // Workflow trigger - get workflow name from map const workflowName = localWorkflowNameMap.get(call.identifierName); if (workflowName) { - // Resolve the source expression for authInvoker. - const rawExpr = call.authInvoker.isShorthand ? "authInvoker" : call.authInvoker.valueText; + const rawExpr = call.invoker.isShorthand ? "invoker" : call.invoker.valueText; // Wrap with the runtime normalizer so string expressions become the // object form the platform RPC expects. The normalizer is injected // once at the top of the file. // When no auth service is configured we can't expand a string, so // we pass through unchanged (platform will reject a string with a // clear error). - let authInvokerExpr: string; + let invokerExpr: string; if (authNamespace) { - authInvokerExpr = `${NORMALIZER_IDENTIFIER}(${rawExpr})`; + invokerExpr = `${NORMALIZER_IDENTIFIER}(${rawExpr})`; needsNormalizerHelper = true; } else { - authInvokerExpr = rawExpr; + invokerExpr = rawExpr; } // Transform to tailor.workflow.triggerWorkflow - const transformedCall = `tailor.workflow.triggerWorkflow("${workflowName}", ${call.argsText || "undefined"}, { authInvoker: ${authInvokerExpr} })`; + const transformedCall = `tailor.workflow.triggerWorkflow("${workflowName}", ${call.argsText || "undefined"}, { authInvoker: ${invokerExpr} })`; replacements.push({ start: call.callRange.start, end: call.callRange.end, diff --git a/packages/sdk/src/cli/shared/trigger-context.ts b/packages/sdk/src/cli/shared/trigger-context.ts index 983889f93..3a8239818 100644 --- a/packages/sdk/src/cli/shared/trigger-context.ts +++ b/packages/sdk/src/cli/shared/trigger-context.ts @@ -18,7 +18,7 @@ export interface TriggerContext { /** Maps file path (without extension) to workflow name for default exports */ workflowFileMap: Map; /** - * Auth service namespace used to expand a string-literal `authInvoker` + * Auth service namespace used to expand a string-literal `invoker` * (e.g. `"kiosk"`) to the `{ namespace, machineUserName }` form expected by * the runtime. Undefined when no Auth service is configured. */ @@ -40,7 +40,7 @@ export function normalizeFilePath(filePath: string): string { * Build trigger context from workflow configuration * Scans workflow files to collect workflow and job mappings * @param workflowConfig - Workflow file loading configuration - * @param authNamespace - Auth service namespace (optional, used for string-literal authInvoker expansion) + * @param authNamespace - Auth service namespace (optional, used for string-literal invoker expansion) * @returns Trigger context built from workflow sources */ export async function buildTriggerContext( diff --git a/packages/sdk/src/cli/shared/type-generator.ts b/packages/sdk/src/cli/shared/type-generator.ts index 85d6f2295..19cf946c0 100644 --- a/packages/sdk/src/cli/shared/type-generator.ts +++ b/packages/sdk/src/cli/shared/type-generator.ts @@ -41,7 +41,7 @@ export function extractAttributesFromConfig(config: AppConfig): ExtractedAttribu * @param attributeMap - Attribute map configuration * @param attributeList - Attribute list configuration * @param env - Environment configuration - * @param machineUserNames - Registered machine user names (used to narrow `authInvoker` strings) + * @param machineUserNames - Registered machine user names (used to narrow `invoker` strings) * @param idpNames - Registered IdP names (used to narrow `idpUser*Trigger({ idp })` strings) * @returns Generated type definition source */ diff --git a/packages/sdk/src/configure/services/executor/executor.test.ts b/packages/sdk/src/configure/services/executor/executor.test.ts index c2160bc9b..a67d3450b 100644 --- a/packages/sdk/src/configure/services/executor/executor.test.ts +++ b/packages/sdk/src/configure/services/executor/executor.test.ts @@ -1365,7 +1365,7 @@ describe("workflowTarget", () => { }); }); - test("can specify authInvoker", () => { + test("can specify invoker", () => { createExecutor({ name: "test", trigger: scheduleTrigger({ cron: "0 12 * * *" }), @@ -1373,7 +1373,7 @@ describe("workflowTarget", () => { kind: "workflow", workflow: testWorkflow, args: { orderId: "test-id" }, - authInvoker: "admin", + invoker: "admin", }, }); }); diff --git a/packages/sdk/src/configure/services/executor/executor.ts b/packages/sdk/src/configure/services/executor/executor.ts index 2870652c0..32f7c7dd3 100644 --- a/packages/sdk/src/configure/services/executor/executor.ts +++ b/packages/sdk/src/configure/services/executor/executor.ts @@ -30,7 +30,7 @@ type Executor, O> = O extends { kind: "workflow"; workflow: W; args?: WorkflowInput | ((args: TriggerArgs) => WorkflowInput); - authInvoker?: MachineUserName; + invoker?: MachineUserName; }; } : ExecutorBase & { diff --git a/packages/sdk/src/configure/services/executor/operation.ts b/packages/sdk/src/configure/services/executor/operation.ts index 0c5c49c87..f00a277d5 100644 --- a/packages/sdk/src/configure/services/executor/operation.ts +++ b/packages/sdk/src/configure/services/executor/operation.ts @@ -10,18 +10,18 @@ import type { import type { Client } from "@urql/core"; /** Function-based executor operation. The body receives the trigger args and the `invoker`. */ -export type FunctionOperation = Omit & { +export type FunctionOperation = Omit & { body: (args: Args & { invoker: TailorPrincipal | null }) => void | Promise; - authInvoker?: MachineUserName; + invoker?: MachineUserName; }; type UrqlOperationArgs = Parameters; /** GraphQL-based executor operation. Executes a GraphQL query or mutation. */ -export type GqlOperation = Omit & { +export type GqlOperation = Omit & { query: UrqlOperationArgs[0]; variables?: (args: Args) => UrqlOperationArgs[1]; - authInvoker?: MachineUserName; + invoker?: MachineUserName; }; type RequestHeader = @@ -290,11 +290,11 @@ type WorkflowInput = Parameters[0]; /** Workflow-triggering executor operation. Triggers a workflow in response to an event. */ export type WorkflowOperation = Omit< ParserWorkflowOperation, - "workflowName" | "args" | "authInvoker" + "workflowName" | "args" | "invoker" > & { workflow: W; args?: WorkflowInput | ((args: Args) => WorkflowInput); - authInvoker?: MachineUserName; + invoker?: MachineUserName; }; export type Operation = diff --git a/packages/sdk/src/configure/services/resolver/resolver.test.ts b/packages/sdk/src/configure/services/resolver/resolver.test.ts index f2902ea44..0d0d39bd5 100644 --- a/packages/sdk/src/configure/services/resolver/resolver.test.ts +++ b/packages/sdk/src/configure/services/resolver/resolver.test.ts @@ -487,21 +487,21 @@ describe("createResolver", () => { expect(typeof resolver.body).toBe("function"); }); - test("creates resolver with authInvoker", () => { + test("creates resolver with invoker", () => { const outputType = t.object({ result: t.string(), }); const resolver = createResolver({ - name: "withAuthInvoker", + name: "withInvoker", operation: "query", output: outputType, body: () => ({ result: "ok" }), - authInvoker: "batch-user", + invoker: "batch-user", }); - expect(resolver.name).toBe("withAuthInvoker"); - expect(resolver.authInvoker).toBe("batch-user"); + expect(resolver.name).toBe("withInvoker"); + expect(resolver.invoker).toBe("batch-user"); }); test("creates minimal resolver without optional fields", () => { diff --git a/packages/sdk/src/configure/services/resolver/resolver.ts b/packages/sdk/src/configure/services/resolver/resolver.ts index 3a08c6ad4..86db81f1b 100644 --- a/packages/sdk/src/configure/services/resolver/resolver.ts +++ b/packages/sdk/src/configure/services/resolver/resolver.ts @@ -34,19 +34,19 @@ type NormalizedOutput | undefined, Output extends TailorAnyField | Record, -> = Omit & +> = Omit & Readonly<{ input?: Input; output: NormalizedOutput; body: (context: Context) => OutputType | Promise>; - authInvoker?: MachineUserName; + invoker?: MachineUserName; }>; /** * Create a resolver definition for the Tailor SDK. * * The `body` function receives a context with `input` (typed from `config.input`), - * `caller`, `invoker` (reflects `authInvoker` delegation), and `env`. + * `caller`, `invoker` (reflects configured machine-user delegation), and `env`. * The return value of `body` must match the `output` type. * * `output` accepts either a single TailorField (e.g. `t.string()`) or a @@ -85,12 +85,12 @@ export function createResolver< Input extends Record | undefined = undefined, Output extends TailorAnyField | Record = TailorAnyField, >( - config: Omit & + config: Omit & Readonly<{ input?: Input; output: Output; body: (context: Context) => OutputType | Promise>; - authInvoker?: MachineUserName; + invoker?: MachineUserName; }>, ): ResolverReturn { // Check if output is already a TailorField using duck typing. diff --git a/packages/sdk/src/configure/services/workflow/registry.ts b/packages/sdk/src/configure/services/workflow/registry.ts index ffb8ab0e2..5aa71c942 100644 --- a/packages/sdk/src/configure/services/workflow/registry.ts +++ b/packages/sdk/src/configure/services/workflow/registry.ts @@ -84,7 +84,11 @@ export function dispatchTriggerJob(name: string, args?: unknown): unknown { export function dispatchTriggerWorkflow( name: string, args?: unknown, - options?: unknown, + options?: { invoker?: unknown }, ): Promise { - return requirePlatformWorkflow().triggerWorkflow(name, args, options); + const workflow = requirePlatformWorkflow(); + if (options?.invoker === undefined) { + return workflow.triggerWorkflow(name, args); + } + return workflow.triggerWorkflow(name, args, { authInvoker: options.invoker }); } diff --git a/packages/sdk/src/configure/services/workflow/workflow.ts b/packages/sdk/src/configure/services/workflow/workflow.ts index 9b2628627..f6ed8d155 100644 --- a/packages/sdk/src/configure/services/workflow/workflow.ts +++ b/packages/sdk/src/configure/services/workflow/workflow.ts @@ -23,7 +23,7 @@ export interface Workflow = WorkflowJob
[0], - options?: { authInvoker: MachineUserName }, + options?: { invoker: MachineUserName }, ) => Promise; } diff --git a/packages/sdk/src/parser/service/executor/schema.ts b/packages/sdk/src/parser/service/executor/schema.ts index d5fd47d7e..e4d1b281b 100644 --- a/packages/sdk/src/parser/service/executor/schema.ts +++ b/packages/sdk/src/parser/service/executor/schema.ts @@ -87,7 +87,7 @@ export const TriggerSchema = z.discriminatedUnion("kind", [ export const FunctionOperationSchema = z.object({ kind: z.enum(["function", "jobFunction"]), body: functionSchema.describe("Function implementation"), - authInvoker: AuthInvokerSchema.optional().describe("Auth invoker for the function execution"), + invoker: AuthInvokerSchema.optional().describe("Invoker for the function execution"), }); export const GqlOperationSchema = z.object({ @@ -95,7 +95,7 @@ export const GqlOperationSchema = z.object({ appName: z.string().optional().describe("Target application name for the GraphQL query"), query: z.preprocess((val) => String(val), z.string().describe("GraphQL query string")), variables: functionSchema.optional().describe("Function to compute GraphQL variables"), - authInvoker: AuthInvokerSchema.optional().describe("Auth invoker for the GraphQL execution"), + invoker: AuthInvokerSchema.optional().describe("Invoker for the GraphQL execution"), }); export const WebhookOperationSchema = z.object({ @@ -130,7 +130,7 @@ export const WorkflowOperationSchema = z.preprocess( .union([z.record(z.string(), z.unknown()), functionSchema]) .optional() .describe("Arguments to pass to the workflow"), - authInvoker: AuthInvokerSchema.optional().describe("Auth invoker for the workflow execution"), + invoker: AuthInvokerSchema.optional().describe("Invoker for the workflow execution"), }), ); diff --git a/packages/sdk/src/parser/service/resolver/schema.ts b/packages/sdk/src/parser/service/resolver/schema.ts index e8091a2c2..b071714d9 100644 --- a/packages/sdk/src/parser/service/resolver/schema.ts +++ b/packages/sdk/src/parser/service/resolver/schema.ts @@ -15,5 +15,5 @@ export const ResolverSchema = z.object({ body: functionSchema.describe("Resolver implementation function"), output: TailorFieldSchema.describe("Output field definition"), publishEvents: z.boolean().optional().describe("Enable publishing events from this resolver"), - authInvoker: AuthInvokerSchema.optional().describe("Machine user to execute this resolver as"), + invoker: AuthInvokerSchema.optional().describe("Machine user to execute this resolver as"), }); diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index 60497d344..90d0ebb25 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -47,8 +47,8 @@ import type { UserQuery as IdpUserQuery, } from "./idp"; import type { - AuthInvoker as WorkflowAuthInvoker, - TriggerWorkflowOptions as WorkflowTriggerWorkflowOptions, + Invoker as WorkflowInvoker, + PlatformTriggerWorkflowOptions as WorkflowTriggerWorkflowOptions, } from "./workflow"; declare global { @@ -79,7 +79,7 @@ declare global { } namespace workflow { - type AuthInvoker = WorkflowAuthInvoker; + type Invoker = WorkflowInvoker; type TriggerWorkflowOptions = WorkflowTriggerWorkflowOptions; } diff --git a/packages/sdk/src/runtime/workflow.test.ts b/packages/sdk/src/runtime/workflow.test.ts index 92b270489..d1abf7ed4 100644 --- a/packages/sdk/src/runtime/workflow.test.ts +++ b/packages/sdk/src/runtime/workflow.test.ts @@ -31,7 +31,7 @@ describe("@tailor-platform/sdk/runtime/workflow", () => { "my-workflow", { a: 1 }, { - authInvoker: { namespace: "ns", machineUserName: "mu" }, + invoker: { namespace: "ns", machineUserName: "mu" }, }, ); diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index e6b2bc9bf..48d42e40d 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -16,7 +16,7 @@ * Specifies the machine user that should be used to execute the workflow. * This allows workflows to run with specific authentication context. */ -export interface AuthInvoker { +export interface Invoker { /** The namespace where the machine user is defined */ namespace: string; /** The name of the machine user to use for workflow execution */ @@ -25,8 +25,12 @@ export interface AuthInvoker { /** Options for {@link triggerWorkflow}. */ export interface TriggerWorkflowOptions { - /** Optional authentication invoker to specify which machine user should execute the workflow */ - authInvoker?: AuthInvoker; + /** Optional invoker to specify which machine user should execute the workflow */ + invoker?: Invoker; +} + +export interface PlatformTriggerWorkflowOptions { + authInvoker?: Invoker; } /** @@ -42,13 +46,13 @@ 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`) + * @param options - Optional platform trigger options * @returns The execution ID of the triggered workflow */ triggerWorkflow( workflowName: string, args?: any, - options?: TriggerWorkflowOptions, + options?: PlatformTriggerWorkflowOptions, ): Promise; /** @@ -82,11 +86,21 @@ const api = (): TailorWorkflowAPI => /** * See {@link TailorWorkflowAPI.triggerWorkflow}. - * @param args - Forwarded to {@link TailorWorkflowAPI.triggerWorkflow} + * @param workflowName - Workflow name as defined in tailor.config + * @param args - Arguments forwarded to the workflow's main job + * @param options - Optional trigger options * @returns The execution ID of the triggered workflow */ -export const triggerWorkflow: TailorWorkflowAPI["triggerWorkflow"] = (...args) => - api().triggerWorkflow(...args); +export function triggerWorkflow( + workflowName: string, + args?: any, + options?: TriggerWorkflowOptions, +): Promise { + if (options?.invoker === undefined) { + return api().triggerWorkflow(workflowName, args); + } + return api().triggerWorkflow(workflowName, args, { authInvoker: options.invoker }); +} /** * See {@link TailorWorkflowAPI.triggerJobFunction}. diff --git a/packages/sdk/src/types/executor.generated.ts b/packages/sdk/src/types/executor.generated.ts index 551aca559..61b872588 100644 --- a/packages/sdk/src/types/executor.generated.ts +++ b/packages/sdk/src/types/executor.generated.ts @@ -103,8 +103,8 @@ export type FunctionOperation = { kind: "function" | "jobFunction"; /** Function implementation */ body: Function; - /** Auth invoker for the function execution */ - authInvoker?: + /** Invoker for the function execution */ + invoker?: | string | { namespace: string; @@ -121,8 +121,8 @@ export type GqlOperationInput = { appName?: string | undefined; /** Function to compute GraphQL variables */ variables?: Function | undefined; - /** Auth invoker for the GraphQL execution */ - authInvoker?: + /** Invoker for the GraphQL execution */ + invoker?: | string | { namespace: string; @@ -138,8 +138,8 @@ export type GqlOperation = { appName?: string | undefined; /** Function to compute GraphQL variables */ variables?: Function | undefined; - /** Auth invoker for the GraphQL execution */ - authInvoker?: + /** Invoker for the GraphQL execution */ + invoker?: | string | { namespace: string; @@ -179,7 +179,7 @@ export type WorkflowOperation = { [x: string]: unknown; } | undefined; - authInvoker?: + invoker?: | string | { namespace: string; @@ -303,7 +303,7 @@ export type Executor = { [x: string]: unknown; } | undefined; - authInvoker?: + invoker?: | string | { namespace: string; @@ -314,7 +314,7 @@ export type Executor = { | { kind: "function" | "jobFunction"; body: Function; - authInvoker?: + invoker?: | string | { namespace: string; @@ -327,7 +327,7 @@ export type Executor = { query: string; appName?: string | undefined; variables?: Function | undefined; - authInvoker?: + invoker?: | string | { namespace: string; diff --git a/packages/sdk/src/types/resolver.generated.ts b/packages/sdk/src/types/resolver.generated.ts index de57d6451..922b99eab 100644 --- a/packages/sdk/src/types/resolver.generated.ts +++ b/packages/sdk/src/types/resolver.generated.ts @@ -56,7 +56,7 @@ export type Resolver = { /** Enable publishing events from this resolver */ publishEvents?: boolean | undefined; /** Machine user to execute this resolver as */ - authInvoker?: + invoker?: | string | { namespace: string; From d94cfd2d6b5ac6eed057584039555d5d51c351e2 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 19 Jun 2026 02:10:55 +0900 Subject: [PATCH 150/618] fix: migrate auth invoker option in codemod --- .../v2/auth-invoker-unwrap/codemod.yaml | 2 +- .../auth-invoker-unwrap/scripts/transform.ts | 24 +++++++++---------- .../non-literal-arg-untouched/expected.ts | 9 +++++++ .../tests/plain-string-option/expected.ts | 3 +++ .../tests/plain-string-option/input.ts | 3 +++ packages/sdk-codemod/src/registry.ts | 6 ++--- 6 files changed, 30 insertions(+), 17 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/codemod.yaml b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/codemod.yaml index 42ed8e210..dfbf7f58f 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/codemod.yaml +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/codemod.yaml @@ -1,6 +1,6 @@ name: "@tailor-platform/auth-invoker-unwrap" version: "1.0.0" -description: 'Replace `authInvoker: auth.invoker("name")` with `invoker: "name"` and drop the no-longer-needed `auth` import' +description: 'Rename `authInvoker` options to `invoker`, replace `auth.invoker("name")` with the bare string `"name"`, and drop the no-longer-needed `auth` import' engine: jssg language: typescript since: "1.0.0" diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts index a2ef61ccd..9eb22a20e 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts @@ -1,10 +1,10 @@ import { parse, Lang } from "@ast-grep/napi"; import type { Edit, SgNode } from "@ast-grep/napi"; -const QUICK_FILTER_NEEDLE = "auth.invoker"; +const QUICK_FILTER_NEEDLES = ["auth.invoker", "authInvoker"]; function quickFilter(source: string): boolean { - return source.includes(QUICK_FILTER_NEEDLE); + return QUICK_FILTER_NEEDLES.some((needle) => source.includes(needle)); } function isInsideImportStatement(node: SgNode): boolean { @@ -141,7 +141,7 @@ function findAuthImports(root: SgNode): SgNode[] { /** * Replace `auth.invoker("name")` calls with the bare `"name"` string literal - * and rename matching `authInvoker:` option keys to `invoker:`. + * and rename `authInvoker:` option keys to `invoker:`. * If no other `auth` references remain after the rewrite, drop the `auth` * specifier (or the entire import line when `auth` was its sole specifier). * @@ -159,24 +159,22 @@ export default function transform(source: string, _filePath: string): string | n const root = parse(lang, source).root(); const calls = findInvokerCalls(root); - if (calls.length === 0) return null; - const edits: Edit[] = calls.map((c) => c.callNode.replace(c.argText)); - const remaining = countRemainingAuthRefs( - root, - calls.map((c) => c.range), - ); - if (remaining === 0) { + if ( + calls.length > 0 && + countRemainingAuthRefs( + root, + calls.map((c) => c.range), + ) === 0 + ) { for (const importStmt of findAuthImports(root)) { const edit = buildAuthImportRemovalEdit(source, importStmt); if (edit) edits.push(edit); } } - if (edits.length === 0) return null; - - let result = root.commitEdits(edits); + let result = edits.length === 0 ? source : root.commitEdits(edits); result = result.replace(/\bauthInvoker(\s*):/g, "invoker$1:"); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts new file mode 100644 index 000000000..c474f8c54 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts @@ -0,0 +1,9 @@ +import { auth } from "../tailor.config"; + +const machineUserName = "kiosk"; + +export const cfg = { + // The argument is not a literal string, so the call is left intact and the + // `auth` import stays. + invoker: auth.invoker(machineUserName), +}; diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/expected.ts new file mode 100644 index 000000000..8e0f911aa --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/expected.ts @@ -0,0 +1,3 @@ +export const cfg = { + invoker: "kiosk", +}; diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/input.ts new file mode 100644 index 000000000..2c285f859 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/input.ts @@ -0,0 +1,3 @@ +export const cfg = { + authInvoker: "kiosk", +}; diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index f5ac23441..b7c365edb 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -87,15 +87,15 @@ const allCodemods: CodemodPackage[] = [ id: "v2/auth-invoker-unwrap", name: 'auth.invoker("name") → invoker: "name"', description: - 'Replace `authInvoker: auth.invoker("name")` with `invoker: "name"` and drop the `auth` import when no other reference remains. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle.', + 'Rename `authInvoker` options to `invoker`, replace `auth.invoker("name")` with the bare `"name"` string, and drop the `auth` import when no other reference remains. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle.', since: "1.0.0", until: "2.0.0", scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js", - suspiciousPatterns: ["auth.invoker"], + suspiciousPatterns: ["auth.invoker", "authInvoker"], prompt: [ "In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the", "machine user name passed directly as a string. The codemod already rewrote the", - 'string-literal form authInvoker: auth.invoker("name") to invoker: "name". These files still contain', + 'string-literal form authInvoker: auth.invoker("name") to invoker: "name" and renamed plain authInvoker options. These files still contain', "auth.invoker(...) because the argument is not a plain string literal (a variable,", "template literal, function call, or property access).", "", From 9025455965b0450df420383d02d987b8a59754eb Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 19 Jun 2026 02:18:44 +0900 Subject: [PATCH 151/618] fix: migrate shorthand invoker options --- .../v2/auth-invoker-unwrap/scripts/transform.ts | 12 ++++++++++++ .../tests/shorthand-option/expected.ts | 5 +++++ .../tests/shorthand-option/input.ts | 5 +++++ 3 files changed, 22 insertions(+) create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts index 9eb22a20e..baadef04d 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts @@ -139,6 +139,15 @@ function findAuthImports(root: SgNode): SgNode[] { }); } +function findAuthInvokerShorthands(root: SgNode): SgNode[] { + return root.findAll({ + rule: { + kind: "shorthand_property_identifier", + regex: "^authInvoker$", + }, + }); +} + /** * Replace `auth.invoker("name")` calls with the bare `"name"` string literal * and rename `authInvoker:` option keys to `invoker:`. @@ -160,6 +169,9 @@ export default function transform(source: string, _filePath: string): string | n const calls = findInvokerCalls(root); const edits: Edit[] = calls.map((c) => c.callNode.replace(c.argText)); + edits.push( + ...findAuthInvokerShorthands(root).map((node) => node.replace("invoker: authInvoker")), + ); if ( calls.length > 0 && diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts new file mode 100644 index 000000000..ad3086d7b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts @@ -0,0 +1,5 @@ +const authInvoker = "kiosk"; + +export const cfg = { + invoker: authInvoker, +}; diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts new file mode 100644 index 000000000..2595bb878 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts @@ -0,0 +1,5 @@ +const authInvoker = "kiosk"; + +export const cfg = { + authInvoker, +}; From 72057dbf2b8d43809fe44adb66edce93c56f7230 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 19 Jun 2026 02:28:01 +0900 Subject: [PATCH 152/618] fix: narrow auth invoker codemod review signals --- packages/sdk-codemod/src/registry.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index b7c365edb..08102bb0a 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -91,13 +91,12 @@ const allCodemods: CodemodPackage[] = [ since: "1.0.0", until: "2.0.0", scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js", - suspiciousPatterns: ["auth.invoker", "authInvoker"], + suspiciousPatterns: ["auth.invoker", "authInvoker:", "authInvoker :"], prompt: [ "In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the", "machine user name passed directly as a string. The codemod already rewrote the", 'string-literal form authInvoker: auth.invoker("name") to invoker: "name" and renamed plain authInvoker options. These files still contain', - "auth.invoker(...) because the argument is not a plain string literal (a variable,", - "template literal, function call, or property access).", + "auth.invoker(...) calls or authInvoker option keys that need manual review.", "", "For each remaining auth.invoker() call:", "1. Replace the whole call with as-is (e.g. auth.invoker(name) becomes name).", From eeec0f3b291a83947d6b9e463377b0dbef3c7da7 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 19 Jun 2026 02:31:16 +0900 Subject: [PATCH 153/618] fix: avoid string edits in auth invoker codemod --- .../auth-invoker-unwrap/scripts/transform.ts | 28 +++++++++++++++++-- .../tests/comments-and-types/expected.ts | 9 ++++++ .../tests/comments-and-types/input.ts | 9 ++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts index baadef04d..4a24ec094 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts @@ -148,6 +148,31 @@ function findAuthInvokerShorthands(root: SgNode): SgNode[] { }); } +function sameRange(a: SgNode, b: SgNode): boolean { + const ar = a.range(); + const br = b.range(); + return ar.start.index === br.start.index && ar.end.index === br.end.index; +} + +function findAuthInvokerPropertyKeys(root: SgNode): SgNode[] { + return root + .findAll({ + rule: { + kind: "property_identifier", + regex: "^authInvoker$", + }, + }) + .filter((node) => { + const parent = node.parent(); + if (!parent) return false; + if (parent.kind() === "pair") { + const key = parent.field("key"); + return key ? sameRange(key, node) : false; + } + return parent.kind() === "property_signature"; + }); +} + /** * Replace `auth.invoker("name")` calls with the bare `"name"` string literal * and rename `authInvoker:` option keys to `invoker:`. @@ -169,6 +194,7 @@ export default function transform(source: string, _filePath: string): string | n const calls = findInvokerCalls(root); const edits: Edit[] = calls.map((c) => c.callNode.replace(c.argText)); + edits.push(...findAuthInvokerPropertyKeys(root).map((node) => node.replace("invoker"))); edits.push( ...findAuthInvokerShorthands(root).map((node) => node.replace("invoker: authInvoker")), ); @@ -188,8 +214,6 @@ export default function transform(source: string, _filePath: string): string | n let result = edits.length === 0 ? source : root.commitEdits(edits); - result = result.replace(/\bauthInvoker(\s*):/g, "invoker$1:"); - // Normalize: drop the leading blank line that an import removal at the top // of the file leaves behind, and collapse runs of 3+ newlines. result = result.replace(/^[\t ]*\n+/, "").replace(/\n{3,}/g, "\n\n"); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/expected.ts new file mode 100644 index 000000000..1d552bdb2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/expected.ts @@ -0,0 +1,9 @@ +export interface Options { + invoker?: string; +} + +export const cfg = { + message: "authInvoker: keep this string", + // authInvoker: keep this comment + invoker: "kiosk", +}; diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/input.ts new file mode 100644 index 000000000..06940bb6e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/input.ts @@ -0,0 +1,9 @@ +export interface Options { + authInvoker?: string; +} + +export const cfg = { + message: "authInvoker: keep this string", + // authInvoker: keep this comment + authInvoker: "kiosk", +}; From f3f6507fd662c10c82148a981273c33c5d1d0649 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 19 Jun 2026 13:35:21 +0900 Subject: [PATCH 154/618] fix: migrate quoted invoker option keys --- .../auth-invoker-unwrap/scripts/transform.ts | 27 +++++++++++++++++++ .../tests/quoted-option/expected.ts | 9 +++++++ .../tests/quoted-option/input.ts | 9 +++++++ packages/sdk-codemod/src/registry.ts | 10 ++++++- 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts index 4a24ec094..5daac197b 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts @@ -173,6 +173,30 @@ function findAuthInvokerPropertyKeys(root: SgNode): SgNode[] { }); } +function findQuotedAuthInvokerPropertyKeys(root: SgNode): SgNode[] { + return root + .findAll({ + rule: { + kind: "string", + regex: "^['\"]authInvoker['\"]$", + }, + }) + .filter((node) => { + const parent = node.parent(); + if (!parent) return false; + if (parent.kind() === "pair") { + const key = parent.field("key"); + return key ? sameRange(key, node) : false; + } + return parent.kind() === "property_signature"; + }); +} + +function renameQuotedKey(node: SgNode): string { + const quote = node.text().startsWith("'") ? "'" : '"'; + return `${quote}invoker${quote}`; +} + /** * Replace `auth.invoker("name")` calls with the bare `"name"` string literal * and rename `authInvoker:` option keys to `invoker:`. @@ -195,6 +219,9 @@ export default function transform(source: string, _filePath: string): string | n const calls = findInvokerCalls(root); const edits: Edit[] = calls.map((c) => c.callNode.replace(c.argText)); edits.push(...findAuthInvokerPropertyKeys(root).map((node) => node.replace("invoker"))); + edits.push( + ...findQuotedAuthInvokerPropertyKeys(root).map((node) => node.replace(renameQuotedKey(node))), + ); edits.push( ...findAuthInvokerShorthands(root).map((node) => node.replace("invoker: authInvoker")), ); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts new file mode 100644 index 000000000..84c0271e9 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts @@ -0,0 +1,9 @@ +export interface Options { + "invoker"?: string; + 'invoker': string; +} + +export const cfg = { + "invoker": "kiosk", + 'invoker': "manager", +}; diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts new file mode 100644 index 000000000..0c6b3574b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts @@ -0,0 +1,9 @@ +export interface Options { + "authInvoker"?: string; + 'authInvoker': string; +} + +export const cfg = { + "authInvoker": "kiosk", + 'authInvoker': "manager", +}; diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 08102bb0a..45e6b0cc8 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -91,7 +91,15 @@ const allCodemods: CodemodPackage[] = [ since: "1.0.0", until: "2.0.0", scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js", - suspiciousPatterns: ["auth.invoker", "authInvoker:", "authInvoker :"], + suspiciousPatterns: [ + "auth.invoker", + "authInvoker:", + "authInvoker :", + '"authInvoker":', + '"authInvoker" :', + "'authInvoker':", + "'authInvoker' :", + ], prompt: [ "In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the", "machine user name passed directly as a string. The codemod already rewrote the", From 6a0b480631dcaeaa393f51db052b5b2a5ac94b0b Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 19 Jun 2026 13:36:11 +0900 Subject: [PATCH 155/618] fix: remove legacy workflow test env fallback --- .changeset/workflow-trigger-dispatch.md | 2 + packages/sdk/docs/testing.md | 4 ++ .../src/configure/services/workflow/job.ts | 2 - .../services/workflow/test-env-key.ts | 28 +--------- packages/sdk/src/utils/test/index.ts | 1 - packages/sdk/src/vitest/mock.test.ts | 55 ++++--------------- 6 files changed, 18 insertions(+), 74 deletions(-) diff --git a/.changeset/workflow-trigger-dispatch.md b/.changeset/workflow-trigger-dispatch.md index 553133b4b..f331e2a86 100644 --- a/.changeset/workflow-trigger-dispatch.md +++ b/.changeset/workflow-trigger-dispatch.md @@ -3,3 +3,5 @@ --- Align workflow job `.trigger()` with the platform runtime. Job triggers now require a mocked workflow runtime in tests instead of running job bodies locally, and `trigger()` returns the job result directly instead of a Promise wrapper. Use `mockWorkflow()` to mock trigger results in tests, or `runWorkflowLocally()` for full-chain local workflow tests. + +The deprecated `WORKFLOW_TEST_ENV_KEY` / `TAILOR_TEST_WORKFLOW_ENV` workflow test env fallback has been removed. Use `mockWorkflow().setEnv(...)` to configure env for local workflow tests, or pass `{ env }` to `runWorkflowLocally(...)` for a single run. diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index 21fc563a3..41c253fd9 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -173,6 +173,8 @@ wf.enqueueResult({ valid: true }); wf.enqueueResults({ valid: true }, { txnId: "txn-1" }); ``` +Use `wf.setEnv(...)` when locally-run workflow job bodies need configuration values. Per-run `runWorkflowLocally(..., { env })` options take precedence over the mock's env. + ### SecretManager Mock ```typescript @@ -737,6 +739,8 @@ describe("order-fulfillment workflow", () => { Pass `{ env }` as the third argument when job bodies need configuration values during the local run. +If you already acquired `mockWorkflow()`, you can also call `wf.setEnv(...)` to reuse the same env across local workflow runs. + Like the platform runtime, the local runner re-runs the orchestrator body once per `.trigger()` call (N triggers means N+1 passes), so any side effects outside the trigger results fire on every pass. Keep the body deterministic and move repeatable side effects into the triggered jobs. This helper is still a local runner. Use E2E tests when you need to verify deployed workflow scheduling, suspension, or replay behavior. diff --git a/packages/sdk/src/configure/services/workflow/job.ts b/packages/sdk/src/configure/services/workflow/job.ts index 23f8eeb39..2f9aa0759 100644 --- a/packages/sdk/src/configure/services/workflow/job.ts +++ b/packages/sdk/src/configure/services/workflow/job.ts @@ -54,8 +54,6 @@ export interface WorkflowJob Output | Promise; } -export { WORKFLOW_TEST_ENV_KEY } from "./test-env-key"; - interface CreateWorkflowJobConfig { readonly name: Name; readonly body: JobBody; diff --git a/packages/sdk/src/configure/services/workflow/test-env-key.ts b/packages/sdk/src/configure/services/workflow/test-env-key.ts index a9caa4086..4dbaa0982 100644 --- a/packages/sdk/src/configure/services/workflow/test-env-key.ts +++ b/packages/sdk/src/configure/services/workflow/test-env-key.ts @@ -58,13 +58,6 @@ export function withWorkflowTestInvoker(invoker: TailorPrincipal | null, run: return workflowInvokerStorage().run(invoker, run); } -/** - * Env-var fallback read by `runWorkflowLocally()` when `mockWorkflow().setEnv()` is unset. - * @deprecated Use `mockWorkflow().setEnv()` from `@tailor-platform/sdk/vitest`. - * @internal - */ -export const WORKFLOW_TEST_ENV_KEY = "TAILOR_TEST_WORKFLOW_ENV"; - type RuntimeInvoker = { id: string; type: "user" | "machine_user"; @@ -92,28 +85,11 @@ function readRuntimeInvoker(): TailorPrincipal | null { }; } -// env from `mockWorkflow().setEnv()`, else the deprecated env-var. Shallow-copied -// to isolate against cross-trigger mutation. +// Shallow-copied to isolate against cross-trigger mutation. export function buildJobContext(): { env: TailorEnv; invoker: TailorPrincipal | null } { const storedInvoker = invokerStorage?.getStore(); const invoker = storedInvoker === undefined ? readRuntimeInvoker() : storedInvoker; const fromGlobal = readWorkflowTestEnv(); if (fromGlobal !== undefined) return { env: { ...fromGlobal }, invoker }; - const raw = process.env[WORKFLOW_TEST_ENV_KEY]; - if (!raw) return { env: {} as TailorEnv, invoker }; - let parsed: unknown; - try { - parsed = JSON.parse(raw); - } catch (cause) { - throw new Error( - `Invalid JSON in ${WORKFLOW_TEST_ENV_KEY}; provide valid JSON or use mockWorkflow().setEnv().`, - { cause }, - ); - } - if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { - throw new Error( - `${WORKFLOW_TEST_ENV_KEY} must be a JSON object; provide a record or use mockWorkflow().setEnv().`, - ); - } - return { env: { ...(parsed as TailorEnv) }, invoker }; + return { env: {} as TailorEnv, invoker }; } diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index cbcb239d5..91890f957 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -3,7 +3,6 @@ import type { TailorDBType } from "@/configure/services/tailordb/schema"; import type { TailorField } from "@/configure/types/type"; import type { StandardSchemaV1 } from "@standard-schema/spec"; -export { WORKFLOW_TEST_ENV_KEY } from "@/configure/services/workflow/job"; export { setupTailordbMock, setupTailorErrorsMock, diff --git a/packages/sdk/src/vitest/mock.test.ts b/packages/sdk/src/vitest/mock.test.ts index d1e109f5f..07c0be0c0 100644 --- a/packages/sdk/src/vitest/mock.test.ts +++ b/packages/sdk/src/vitest/mock.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { createWorkflowJob, WORKFLOW_TEST_ENV_KEY } from "../configure/services/workflow/job"; +import { createWorkflowJob } from "../configure/services/workflow/job"; import { createWorkflow } from "../configure/services/workflow/workflow"; import { mockTailordb, @@ -211,54 +211,19 @@ describe("mock", () => { expect(await runWorkflowLocally(workflow)).toEqual({}); }); - describe("backward-compat: deprecated WORKFLOW_TEST_ENV_KEY env-var", () => { - test("setEnv takes priority over the env-var", async () => { - using wf = mockWorkflow(); - const captureEnv = createWorkflowJob({ - name: "capture-env-compat-priority", - body: (_input: undefined, ctx) => ctx.env, - }); - const workflow = createWorkflow({ - name: "capture-env-compat-priority-workflow", - mainJob: captureEnv, - }); - - vi.stubEnv(WORKFLOW_TEST_ENV_KEY, JSON.stringify({ STAGE: "fallback" })); - wf.setEnv({ STAGE: "from-setenv" }); - - expect(await runWorkflowLocally(workflow)).toEqual({ STAGE: "from-setenv" }); + test("ignores the legacy TAILOR_TEST_WORKFLOW_ENV env-var", async () => { + const captureEnv = createWorkflowJob({ + name: "capture-env-ignore-legacy-env", + body: (_input: undefined, ctx) => ctx.env, }); - - test("env-var is used when setEnv has not been called", async () => { - const captureEnv = createWorkflowJob({ - name: "capture-env-compat-fallback", - body: (_input: undefined, ctx) => ctx.env, - }); - const workflow = createWorkflow({ - name: "capture-env-compat-fallback-workflow", - mainJob: captureEnv, - }); - - vi.stubEnv(WORKFLOW_TEST_ENV_KEY, JSON.stringify({ STAGE: "from-env-var" })); - - expect(await runWorkflowLocally(workflow)).toEqual({ STAGE: "from-env-var" }); + const workflow = createWorkflow({ + name: "capture-env-ignore-legacy-env-workflow", + mainJob: captureEnv, }); - test("throws when the env-var is valid JSON but not an object", async () => { - using _wf = mockWorkflow(); - const captureEnv = createWorkflowJob({ - name: "capture-env-compat-nonobject", - body: (_input: undefined, ctx) => ctx.env, - }); - const workflow = createWorkflow({ - name: "capture-env-compat-nonobject-workflow", - mainJob: captureEnv, - }); - - vi.stubEnv(WORKFLOW_TEST_ENV_KEY, "42"); + vi.stubEnv("TAILOR_TEST_WORKFLOW_ENV", JSON.stringify({ STAGE: "from-env-var" })); - await expect(runWorkflowLocally(workflow)).rejects.toThrow(/must be a JSON object/); - }); + expect(await runWorkflowLocally(workflow)).toEqual({}); }); }); From 6a0a5eda681a16609075635a4d25e2aaf5b17107 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 19 Jun 2026 13:50:13 +0900 Subject: [PATCH 156/618] fix: preserve platform auth invoker payloads --- .../auth-invoker-unwrap/scripts/transform.ts | 118 ++++++++++++++---- .../tests/comments-and-types/expected.ts | 7 +- .../tests/comments-and-types/input.ts | 5 +- .../multi-import-drops-only-auth/expected.ts | 8 +- .../multi-import-drops-only-auth/input.ts | 8 +- .../tests/multi-import-keeps-auth/expected.ts | 8 +- .../tests/multi-import-keeps-auth/input.ts | 8 +- .../non-literal-arg-untouched/expected.ts | 7 +- .../tests/non-literal-arg-untouched/input.ts | 7 +- .../tests/plain-string-option/expected.ts | 5 +- .../tests/plain-string-option/input.ts | 5 +- .../tests/platform-payload-untouched/input.ts | 9 ++ .../tests/quoted-option/expected.ts | 16 +-- .../tests/quoted-option/input.ts | 16 +-- .../tests/shorthand-option/expected.ts | 7 +- .../tests/shorthand-option/input.ts | 7 +- packages/sdk-codemod/src/registry.ts | 23 ++-- 17 files changed, 187 insertions(+), 77 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/platform-payload-untouched/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts index 5daac197b..e4cf19b9b 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts @@ -140,12 +140,14 @@ function findAuthImports(root: SgNode): SgNode[] { } function findAuthInvokerShorthands(root: SgNode): SgNode[] { - return root.findAll({ - rule: { - kind: "shorthand_property_identifier", - regex: "^authInvoker$", - }, - }); + return root + .findAll({ + rule: { + kind: "shorthand_property_identifier", + regex: "^authInvoker$", + }, + }) + .filter(isSupportedInvokerOptionKey); } function sameRange(a: SgNode, b: SgNode): boolean { @@ -154,6 +156,88 @@ function sameRange(a: SgNode, b: SgNode): boolean { return ar.start.index === br.start.index && ar.end.index === br.end.index; } +function keyText(node: SgNode | null): string | null { + if (!node) return null; + return node.text().replace(/^['"]|['"]$/g, ""); +} + +function expressionArguments(args: SgNode): SgNode[] { + return args.children().filter((child) => !["(", ")", ","].includes(child.kind())); +} + +function argumentCallForObject(objectNode: SgNode): { call: SgNode; index: number } | null { + const args = objectNode.parent(); + const call = args?.parent(); + if (args?.kind() !== "arguments" || call?.kind() !== "call_expression") return null; + const index = expressionArguments(args).findIndex((arg) => sameRange(arg, objectNode)); + return index === -1 ? null : { call, index }; +} + +function calleeText(call: SgNode): string { + return call.field("function")?.text() ?? ""; +} + +function isCreateCallOptionObject(objectNode: SgNode, functionName: string): boolean { + const callInfo = argumentCallForObject(objectNode); + return ( + callInfo?.index === 0 && + callInfo.call.field("function")?.kind() === "identifier" && + calleeText(callInfo.call) === functionName + ); +} + +function isWorkflowTriggerOptionsObject(objectNode: SgNode): boolean { + const callInfo = argumentCallForObject(objectNode); + if (callInfo?.index !== 1) return false; + const fn = callInfo.call.field("function"); + return fn?.kind() === "member_expression" && calleeText(callInfo.call).endsWith(".trigger"); +} + +function isExecutorOperationObject(objectNode: SgNode): boolean { + const operationPair = objectNode.parent(); + if (operationPair?.kind() !== "pair" || keyText(operationPair.field("key")) !== "operation") { + return false; + } + const configObject = operationPair.parent(); + return ( + configObject?.kind() === "object" && isCreateCallOptionObject(configObject, "createExecutor") + ); +} + +function isSupportedInvokerOptionObject(objectNode: SgNode): boolean { + return ( + isCreateCallOptionObject(objectNode, "createResolver") || + isCreateCallOptionObject(objectNode, "startWorkflow") || + isWorkflowTriggerOptionsObject(objectNode) || + isExecutorOperationObject(objectNode) + ); +} + +function optionObjectForPairKey(node: SgNode): SgNode | null { + const parent = node.parent(); + if (!parent || parent.kind() !== "pair") return null; + const key = parent.field("key"); + if (!key || !sameRange(key, node)) return null; + const objectNode = parent.parent(); + return objectNode?.kind() === "object" ? objectNode : null; +} + +function isSupportedInvokerOptionKey(node: SgNode): boolean { + const objectNode = optionObjectForPairKey(node) ?? node.parent(); + return objectNode?.kind() === "object" && isSupportedInvokerOptionObject(objectNode); +} + +function isSupportedInvokerValueCall(node: SgNode): boolean { + const pair = node.parent(); + if (pair?.kind() !== "pair") return false; + const value = pair.field("value"); + if (!value || !sameRange(value, node)) return false; + const key = keyText(pair.field("key")); + if (key !== "authInvoker" && key !== "invoker") return false; + const objectNode = pair.parent(); + return objectNode?.kind() === "object" && isSupportedInvokerOptionObject(objectNode); +} + function findAuthInvokerPropertyKeys(root: SgNode): SgNode[] { return root .findAll({ @@ -162,15 +246,7 @@ function findAuthInvokerPropertyKeys(root: SgNode): SgNode[] { regex: "^authInvoker$", }, }) - .filter((node) => { - const parent = node.parent(); - if (!parent) return false; - if (parent.kind() === "pair") { - const key = parent.field("key"); - return key ? sameRange(key, node) : false; - } - return parent.kind() === "property_signature"; - }); + .filter(isSupportedInvokerOptionKey); } function findQuotedAuthInvokerPropertyKeys(root: SgNode): SgNode[] { @@ -181,15 +257,7 @@ function findQuotedAuthInvokerPropertyKeys(root: SgNode): SgNode[] { regex: "^['\"]authInvoker['\"]$", }, }) - .filter((node) => { - const parent = node.parent(); - if (!parent) return false; - if (parent.kind() === "pair") { - const key = parent.field("key"); - return key ? sameRange(key, node) : false; - } - return parent.kind() === "property_signature"; - }); + .filter(isSupportedInvokerOptionKey); } function renameQuotedKey(node: SgNode): string { @@ -216,7 +284,7 @@ export default function transform(source: string, _filePath: string): string | n const lang = source.includes("") ? Lang.Tsx : Lang.TypeScript; const root = parse(lang, source).root(); - const calls = findInvokerCalls(root); + const calls = findInvokerCalls(root).filter((c) => isSupportedInvokerValueCall(c.callNode)); const edits: Edit[] = calls.map((c) => c.callNode.replace(c.argText)); edits.push(...findAuthInvokerPropertyKeys(root).map((node) => node.replace("invoker"))); edits.push( diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/expected.ts index 1d552bdb2..422239484 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/expected.ts @@ -1,9 +1,10 @@ export interface Options { - invoker?: string; + authInvoker?: string; } -export const cfg = { +startWorkflow({ + workflow, message: "authInvoker: keep this string", // authInvoker: keep this comment invoker: "kiosk", -}; +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/input.ts index 06940bb6e..1254b3554 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/comments-and-types/input.ts @@ -2,8 +2,9 @@ export interface Options { authInvoker?: string; } -export const cfg = { +startWorkflow({ + workflow, message: "authInvoker: keep this string", // authInvoker: keep this comment authInvoker: "kiosk", -}; +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts index 5b3794146..8d7d81e0f 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts @@ -1,6 +1,8 @@ import { db } from "../tailor.config"; -export const cfg = { +createResolver({ + name: "orders", + operation: "query", invoker: "kiosk", - table: db.type("Order"), -}; + body: () => db.type("Order"), +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/input.ts index b9f79afd2..6a340cdae 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/input.ts @@ -1,6 +1,8 @@ import { auth, db } from "../tailor.config"; -export const cfg = { +createResolver({ + name: "orders", + operation: "query", authInvoker: auth.invoker("kiosk"), - table: db.type("Order"), -}; + body: () => db.type("Order"), +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts index e227da334..be541bbe7 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts @@ -1,8 +1,10 @@ import { auth, db } from "../tailor.config"; -export const cfg = { +createResolver({ + name: "orders", + operation: "query", invoker: "kiosk", // `auth` is still referenced below, so the import must be preserved. ownerType: auth.machineUser, - table: db.type("Order"), -}; + body: () => db.type("Order"), +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/input.ts index 2381e1fd6..5be04d7f4 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/input.ts @@ -1,8 +1,10 @@ import { auth, db } from "../tailor.config"; -export const cfg = { +createResolver({ + name: "orders", + operation: "query", authInvoker: auth.invoker("kiosk"), // `auth` is still referenced below, so the import must be preserved. ownerType: auth.machineUser, - table: db.type("Order"), -}; + body: () => db.type("Order"), +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts index c474f8c54..6867d8e3a 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts @@ -2,8 +2,11 @@ import { auth } from "../tailor.config"; const machineUserName = "kiosk"; -export const cfg = { +workflow.trigger( + { orderId }, + { // The argument is not a literal string, so the call is left intact and the // `auth` import stays. invoker: auth.invoker(machineUserName), -}; + }, +); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts index feee54fe2..7f6c03a20 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts @@ -2,8 +2,11 @@ import { auth } from "../tailor.config"; const machineUserName = "kiosk"; -export const cfg = { +workflow.trigger( + { orderId }, + { // The argument is not a literal string, so the call is left intact and the // `auth` import stays. authInvoker: auth.invoker(machineUserName), -}; + }, +); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/expected.ts index 8e0f911aa..0cbc9b716 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/expected.ts @@ -1,3 +1,4 @@ -export const cfg = { +startWorkflow({ + workflow, invoker: "kiosk", -}; +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/input.ts index 2c285f859..10df309cf 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/plain-string-option/input.ts @@ -1,3 +1,4 @@ -export const cfg = { +startWorkflow({ + workflow, authInvoker: "kiosk", -}; +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/platform-payload-untouched/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/platform-payload-untouched/input.ts new file mode 100644 index 000000000..69c0aef29 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/platform-payload-untouched/input.ts @@ -0,0 +1,9 @@ +import { auth } from "../tailor.config"; + +tailor.workflow.triggerWorkflow("daily", {}, { + authInvoker: { namespace, machineUserName }, +}); + +tailor.workflow.triggerWorkflow("daily", {}, { + authInvoker: auth.invoker("kiosk"), +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts index 84c0271e9..f2485b931 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts @@ -1,9 +1,11 @@ -export interface Options { - "invoker"?: string; - 'invoker': string; -} - -export const cfg = { +startWorkflow({ + workflow, "invoker": "kiosk", +}); + +workflow.trigger( + { orderId }, + { 'invoker': "manager", -}; + }, +); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts index 0c6b3574b..2d3386306 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts @@ -1,9 +1,11 @@ -export interface Options { - "authInvoker"?: string; - 'authInvoker': string; -} - -export const cfg = { +startWorkflow({ + workflow, "authInvoker": "kiosk", +}); + +workflow.trigger( + { orderId }, + { 'authInvoker': "manager", -}; + }, +); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts index ad3086d7b..5e4ce84ca 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts @@ -1,5 +1,8 @@ const authInvoker = "kiosk"; -export const cfg = { +workflow.trigger( + { orderId }, + { invoker: authInvoker, -}; + }, +); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts index 2595bb878..e25f748c2 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts @@ -1,5 +1,8 @@ const authInvoker = "kiosk"; -export const cfg = { +workflow.trigger( + { orderId }, + { authInvoker, -}; + }, +); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 45e6b0cc8..9c82e4d2b 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -87,7 +87,7 @@ const allCodemods: CodemodPackage[] = [ id: "v2/auth-invoker-unwrap", name: 'auth.invoker("name") → invoker: "name"', description: - 'Rename `authInvoker` options to `invoker`, replace `auth.invoker("name")` with the bare `"name"` string, and drop the `auth` import when no other reference remains. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle.', + 'Rename supported SDK `authInvoker` options to `invoker`, replace `auth.invoker("name")` there with the bare `"name"` string, and drop the `auth` import when no other reference remains. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle.', since: "1.0.0", until: "2.0.0", scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js", @@ -95,27 +95,32 @@ const allCodemods: CodemodPackage[] = [ "auth.invoker", "authInvoker:", "authInvoker :", + "authInvoker?", '"authInvoker":', '"authInvoker" :', + '"authInvoker"?', "'authInvoker':", "'authInvoker' :", + "'authInvoker'?", ], prompt: [ "In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the", "machine user name passed directly as a string. The codemod already rewrote the", - 'string-literal form authInvoker: auth.invoker("name") to invoker: "name" and renamed plain authInvoker options. These files still contain', - "auth.invoker(...) calls or authInvoker option keys that need manual review.", + 'supported SDK option form authInvoker: auth.invoker("name") to invoker: "name" and renamed supported authInvoker option keys. These files still contain', + "auth.invoker(...) calls or authInvoker keys that need manual review.", "", "For each remaining auth.invoker() call:", - "1. Replace the whole call with as-is (e.g. auth.invoker(name) becomes name).", - "2. Make sure evaluates to the machine user name (a string); adjust it if it", - " resolves to an object or an auth config value instead.", - "3. Rename any remaining authInvoker option key to invoker.", - "4. After removing every auth.invoker usage in a file, delete the now-unused auth", + "1. Replace the whole call with only where the target option expects a", + " machine user name string; platform/runtime authInvoker payloads still expect", + " the object form.", + "2. Rename remaining authInvoker option keys to invoker only for SDK resolver,", + " executor, workflow.trigger(), or startWorkflow() options. Keep platform/runtime", + " payload keys such as tailor.workflow.triggerWorkflow(..., { authInvoker: ... }).", + "3. After removing every auth.invoker usage in a file, delete the now-unused auth", " import (keeping it pulls Node-only config modules into runtime bundles); leave", " the import if auth is still referenced elsewhere.", "", - "Do not change behavior beyond removing the auth.invoker() indirection.", + "Do not change behavior beyond the SDK option rename and auth.invoker() removal.", ].join("\n"), }, { From 41809c75ca0f52d0872e55be095e0b73d026c622 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 19 Jun 2026 13:50:38 +0900 Subject: [PATCH 157/618] chore: add workflow env fallback changeset --- .changeset/remove-workflow-test-env-fallback.md | 5 +++++ .changeset/workflow-trigger-dispatch.md | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 .changeset/remove-workflow-test-env-fallback.md diff --git a/.changeset/remove-workflow-test-env-fallback.md b/.changeset/remove-workflow-test-env-fallback.md new file mode 100644 index 000000000..830daa5c0 --- /dev/null +++ b/.changeset/remove-workflow-test-env-fallback.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Remove the deprecated workflow test env fallback. `WORKFLOW_TEST_ENV_KEY` is no longer exported, and `TAILOR_TEST_WORKFLOW_ENV` is no longer read when running workflows locally. Use `mockWorkflow().setEnv(...)` or pass `{ env }` to `runWorkflowLocally(...)` instead. diff --git a/.changeset/workflow-trigger-dispatch.md b/.changeset/workflow-trigger-dispatch.md index f331e2a86..553133b4b 100644 --- a/.changeset/workflow-trigger-dispatch.md +++ b/.changeset/workflow-trigger-dispatch.md @@ -3,5 +3,3 @@ --- Align workflow job `.trigger()` with the platform runtime. Job triggers now require a mocked workflow runtime in tests instead of running job bodies locally, and `trigger()` returns the job result directly instead of a Promise wrapper. Use `mockWorkflow()` to mock trigger results in tests, or `runWorkflowLocally()` for full-chain local workflow tests. - -The deprecated `WORKFLOW_TEST_ENV_KEY` / `TAILOR_TEST_WORKFLOW_ENV` workflow test env fallback has been removed. Use `mockWorkflow().setEnv(...)` to configure env for local workflow tests, or pass `{ env }` to `runWorkflowLocally(...)` for a single run. From a7134610b7237234159f9b88aa742f5ffd649ae1 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 19 Jun 2026 14:03:11 +0900 Subject: [PATCH 158/618] fix: limit workflow trigger invoker codemod --- .../auth-invoker-unwrap/scripts/transform.ts | 97 +++++++++++++------ .../non-literal-arg-untouched/expected.ts | 1 + .../tests/non-literal-arg-untouched/input.ts | 1 + .../tests/platform-payload-untouched/input.ts | 4 + .../tests/quoted-option/expected.ts | 2 + .../tests/quoted-option/input.ts | 2 + .../tests/shorthand-option/expected.ts | 2 + .../tests/shorthand-option/input.ts | 2 + packages/sdk-codemod/src/registry.ts | 5 + 9 files changed, 89 insertions(+), 27 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts index e4cf19b9b..c096ac1de 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts @@ -139,17 +139,6 @@ function findAuthImports(root: SgNode): SgNode[] { }); } -function findAuthInvokerShorthands(root: SgNode): SgNode[] { - return root - .findAll({ - rule: { - kind: "shorthand_property_identifier", - regex: "^authInvoker$", - }, - }) - .filter(isSupportedInvokerOptionKey); -} - function sameRange(a: SgNode, b: SgNode): boolean { const ar = a.range(); const br = b.range(); @@ -177,6 +166,38 @@ function calleeText(call: SgNode): string { return call.field("function")?.text() ?? ""; } +interface TransformContext { + workflowTriggerReceivers: Set; +} + +function unquoteString(text: string): string { + return text.replace(/^['"]|['"]$/g, ""); +} + +function findWorkflowTriggerReceivers(root: SgNode): Set { + const out = new Set(); + for (const stmt of root.findAll({ rule: { kind: "import_statement" } })) { + const source = stmt.field("source"); + if (!source) continue; + const sourceText = unquoteString(source.text()); + if (!sourceText.startsWith(".") || !sourceText.toLowerCase().includes("workflow")) continue; + + for (const { localName } of iterateImportSpecs(stmt)) { + out.add(localName); + } + for (const clause of stmt.findAll({ rule: { kind: "import_clause" } })) { + for (const ident of clause.children().filter((child) => child.kind() === "identifier")) { + out.add(ident.text()); + } + } + for (const ns of stmt.findAll({ rule: { kind: "namespace_import" } })) { + const ident = ns.children().find((child) => child.kind() === "identifier"); + if (ident) out.add(ident.text()); + } + } + return out; +} + function isCreateCallOptionObject(objectNode: SgNode, functionName: string): boolean { const callInfo = argumentCallForObject(objectNode); return ( @@ -186,11 +207,15 @@ function isCreateCallOptionObject(objectNode: SgNode, functionName: string): boo ); } -function isWorkflowTriggerOptionsObject(objectNode: SgNode): boolean { +function isWorkflowTriggerOptionsObject(objectNode: SgNode, context: TransformContext): boolean { const callInfo = argumentCallForObject(objectNode); if (callInfo?.index !== 1) return false; const fn = callInfo.call.field("function"); - return fn?.kind() === "member_expression" && calleeText(callInfo.call).endsWith(".trigger"); + if (fn?.kind() !== "member_expression") return false; + const text = calleeText(callInfo.call); + if (!text.endsWith(".trigger")) return false; + const receiver = text.slice(0, -".trigger".length); + return context.workflowTriggerReceivers.has(receiver); } function isExecutorOperationObject(objectNode: SgNode): boolean { @@ -204,11 +229,11 @@ function isExecutorOperationObject(objectNode: SgNode): boolean { ); } -function isSupportedInvokerOptionObject(objectNode: SgNode): boolean { +function isSupportedInvokerOptionObject(objectNode: SgNode, context: TransformContext): boolean { return ( isCreateCallOptionObject(objectNode, "createResolver") || isCreateCallOptionObject(objectNode, "startWorkflow") || - isWorkflowTriggerOptionsObject(objectNode) || + isWorkflowTriggerOptionsObject(objectNode, context) || isExecutorOperationObject(objectNode) ); } @@ -222,12 +247,12 @@ function optionObjectForPairKey(node: SgNode): SgNode | null { return objectNode?.kind() === "object" ? objectNode : null; } -function isSupportedInvokerOptionKey(node: SgNode): boolean { +function isSupportedInvokerOptionKey(node: SgNode, context: TransformContext): boolean { const objectNode = optionObjectForPairKey(node) ?? node.parent(); - return objectNode?.kind() === "object" && isSupportedInvokerOptionObject(objectNode); + return objectNode?.kind() === "object" && isSupportedInvokerOptionObject(objectNode, context); } -function isSupportedInvokerValueCall(node: SgNode): boolean { +function isSupportedInvokerValueCall(node: SgNode, context: TransformContext): boolean { const pair = node.parent(); if (pair?.kind() !== "pair") return false; const value = pair.field("value"); @@ -235,10 +260,21 @@ function isSupportedInvokerValueCall(node: SgNode): boolean { const key = keyText(pair.field("key")); if (key !== "authInvoker" && key !== "invoker") return false; const objectNode = pair.parent(); - return objectNode?.kind() === "object" && isSupportedInvokerOptionObject(objectNode); + return objectNode?.kind() === "object" && isSupportedInvokerOptionObject(objectNode, context); } -function findAuthInvokerPropertyKeys(root: SgNode): SgNode[] { +function findAuthInvokerShorthands(root: SgNode, context: TransformContext): SgNode[] { + return root + .findAll({ + rule: { + kind: "shorthand_property_identifier", + regex: "^authInvoker$", + }, + }) + .filter((node) => isSupportedInvokerOptionKey(node, context)); +} + +function findAuthInvokerPropertyKeys(root: SgNode, context: TransformContext): SgNode[] { return root .findAll({ rule: { @@ -246,10 +282,10 @@ function findAuthInvokerPropertyKeys(root: SgNode): SgNode[] { regex: "^authInvoker$", }, }) - .filter(isSupportedInvokerOptionKey); + .filter((node) => isSupportedInvokerOptionKey(node, context)); } -function findQuotedAuthInvokerPropertyKeys(root: SgNode): SgNode[] { +function findQuotedAuthInvokerPropertyKeys(root: SgNode, context: TransformContext): SgNode[] { return root .findAll({ rule: { @@ -257,7 +293,7 @@ function findQuotedAuthInvokerPropertyKeys(root: SgNode): SgNode[] { regex: "^['\"]authInvoker['\"]$", }, }) - .filter(isSupportedInvokerOptionKey); + .filter((node) => isSupportedInvokerOptionKey(node, context)); } function renameQuotedKey(node: SgNode): string { @@ -283,15 +319,22 @@ export default function transform(source: string, _filePath: string): string | n const lang = source.includes("") ? Lang.Tsx : Lang.TypeScript; const root = parse(lang, source).root(); + const context: TransformContext = { + workflowTriggerReceivers: findWorkflowTriggerReceivers(root), + }; - const calls = findInvokerCalls(root).filter((c) => isSupportedInvokerValueCall(c.callNode)); + const calls = findInvokerCalls(root).filter((c) => + isSupportedInvokerValueCall(c.callNode, context), + ); const edits: Edit[] = calls.map((c) => c.callNode.replace(c.argText)); - edits.push(...findAuthInvokerPropertyKeys(root).map((node) => node.replace("invoker"))); + edits.push(...findAuthInvokerPropertyKeys(root, context).map((node) => node.replace("invoker"))); edits.push( - ...findQuotedAuthInvokerPropertyKeys(root).map((node) => node.replace(renameQuotedKey(node))), + ...findQuotedAuthInvokerPropertyKeys(root, context).map((node) => + node.replace(renameQuotedKey(node)), + ), ); edits.push( - ...findAuthInvokerShorthands(root).map((node) => node.replace("invoker: authInvoker")), + ...findAuthInvokerShorthands(root, context).map((node) => node.replace("invoker: authInvoker")), ); if ( diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts index 6867d8e3a..4c504eebd 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts @@ -1,4 +1,5 @@ import { auth } from "../tailor.config"; +import workflow from "../workflows/order-processing"; const machineUserName = "kiosk"; diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts index 7f6c03a20..2519ccad7 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts @@ -1,4 +1,5 @@ import { auth } from "../tailor.config"; +import workflow from "../workflows/order-processing"; const machineUserName = "kiosk"; diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/platform-payload-untouched/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/platform-payload-untouched/input.ts index 69c0aef29..e4d44c3e6 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/platform-payload-untouched/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/platform-payload-untouched/input.ts @@ -7,3 +7,7 @@ tailor.workflow.triggerWorkflow("daily", {}, { tailor.workflow.triggerWorkflow("daily", {}, { authInvoker: auth.invoker("kiosk"), }); + +paymentGateway.trigger("charge", { + authInvoker: "secret", +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts index f2485b931..12277ed7e 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts @@ -1,3 +1,5 @@ +import workflow from "../workflows/order-processing"; + startWorkflow({ workflow, "invoker": "kiosk", diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts index 2d3386306..5d25f8854 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts @@ -1,3 +1,5 @@ +import workflow from "../workflows/order-processing"; + startWorkflow({ workflow, "authInvoker": "kiosk", diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts index 5e4ce84ca..4d0d90072 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts @@ -1,3 +1,5 @@ +import workflow from "../workflows/order-processing"; + const authInvoker = "kiosk"; workflow.trigger( diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts index e25f748c2..33620ee84 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts @@ -1,3 +1,5 @@ +import workflow from "../workflows/order-processing"; + const authInvoker = "kiosk"; workflow.trigger( diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 9c82e4d2b..ea7211fb2 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -96,6 +96,11 @@ const allCodemods: CodemodPackage[] = [ "authInvoker:", "authInvoker :", "authInvoker?", + "{ authInvoker", + ", authInvoker", + "\n authInvoker", + "\n authInvoker", + "\n authInvoker", '"authInvoker":', '"authInvoker" :', '"authInvoker"?', From 7d61541ef6f26485dcf570efe042357bd3786417 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 19 Jun 2026 14:13:47 +0900 Subject: [PATCH 159/618] fix: leave workflow trigger invoker migration manual --- .../auth-invoker-unwrap/scripts/transform.ts | 81 ++++--------------- .../tests/basic-resolver/expected.ts | 21 ----- .../non-literal-arg-untouched/expected.ts | 9 +-- .../tests/non-literal-arg-untouched/input.ts | 9 +-- .../tests/platform-payload-untouched/input.ts | 4 + .../tests/quoted-option/expected.ts | 9 --- .../tests/quoted-option/input.ts | 9 --- .../tests/shorthand-option/expected.ts | 10 +-- .../tests/shorthand-option/input.ts | 10 +-- packages/sdk-codemod/src/registry.ts | 4 +- 10 files changed, 33 insertions(+), 133 deletions(-) delete mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/expected.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts index c096ac1de..379742507 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts @@ -166,38 +166,6 @@ function calleeText(call: SgNode): string { return call.field("function")?.text() ?? ""; } -interface TransformContext { - workflowTriggerReceivers: Set; -} - -function unquoteString(text: string): string { - return text.replace(/^['"]|['"]$/g, ""); -} - -function findWorkflowTriggerReceivers(root: SgNode): Set { - const out = new Set(); - for (const stmt of root.findAll({ rule: { kind: "import_statement" } })) { - const source = stmt.field("source"); - if (!source) continue; - const sourceText = unquoteString(source.text()); - if (!sourceText.startsWith(".") || !sourceText.toLowerCase().includes("workflow")) continue; - - for (const { localName } of iterateImportSpecs(stmt)) { - out.add(localName); - } - for (const clause of stmt.findAll({ rule: { kind: "import_clause" } })) { - for (const ident of clause.children().filter((child) => child.kind() === "identifier")) { - out.add(ident.text()); - } - } - for (const ns of stmt.findAll({ rule: { kind: "namespace_import" } })) { - const ident = ns.children().find((child) => child.kind() === "identifier"); - if (ident) out.add(ident.text()); - } - } - return out; -} - function isCreateCallOptionObject(objectNode: SgNode, functionName: string): boolean { const callInfo = argumentCallForObject(objectNode); return ( @@ -207,17 +175,6 @@ function isCreateCallOptionObject(objectNode: SgNode, functionName: string): boo ); } -function isWorkflowTriggerOptionsObject(objectNode: SgNode, context: TransformContext): boolean { - const callInfo = argumentCallForObject(objectNode); - if (callInfo?.index !== 1) return false; - const fn = callInfo.call.field("function"); - if (fn?.kind() !== "member_expression") return false; - const text = calleeText(callInfo.call); - if (!text.endsWith(".trigger")) return false; - const receiver = text.slice(0, -".trigger".length); - return context.workflowTriggerReceivers.has(receiver); -} - function isExecutorOperationObject(objectNode: SgNode): boolean { const operationPair = objectNode.parent(); if (operationPair?.kind() !== "pair" || keyText(operationPair.field("key")) !== "operation") { @@ -229,11 +186,10 @@ function isExecutorOperationObject(objectNode: SgNode): boolean { ); } -function isSupportedInvokerOptionObject(objectNode: SgNode, context: TransformContext): boolean { +function isSupportedInvokerOptionObject(objectNode: SgNode): boolean { return ( isCreateCallOptionObject(objectNode, "createResolver") || isCreateCallOptionObject(objectNode, "startWorkflow") || - isWorkflowTriggerOptionsObject(objectNode, context) || isExecutorOperationObject(objectNode) ); } @@ -247,12 +203,12 @@ function optionObjectForPairKey(node: SgNode): SgNode | null { return objectNode?.kind() === "object" ? objectNode : null; } -function isSupportedInvokerOptionKey(node: SgNode, context: TransformContext): boolean { +function isSupportedInvokerOptionKey(node: SgNode): boolean { const objectNode = optionObjectForPairKey(node) ?? node.parent(); - return objectNode?.kind() === "object" && isSupportedInvokerOptionObject(objectNode, context); + return objectNode?.kind() === "object" && isSupportedInvokerOptionObject(objectNode); } -function isSupportedInvokerValueCall(node: SgNode, context: TransformContext): boolean { +function isSupportedInvokerValueCall(node: SgNode): boolean { const pair = node.parent(); if (pair?.kind() !== "pair") return false; const value = pair.field("value"); @@ -260,10 +216,10 @@ function isSupportedInvokerValueCall(node: SgNode, context: TransformContext): b const key = keyText(pair.field("key")); if (key !== "authInvoker" && key !== "invoker") return false; const objectNode = pair.parent(); - return objectNode?.kind() === "object" && isSupportedInvokerOptionObject(objectNode, context); + return objectNode?.kind() === "object" && isSupportedInvokerOptionObject(objectNode); } -function findAuthInvokerShorthands(root: SgNode, context: TransformContext): SgNode[] { +function findAuthInvokerShorthands(root: SgNode): SgNode[] { return root .findAll({ rule: { @@ -271,10 +227,10 @@ function findAuthInvokerShorthands(root: SgNode, context: TransformContext): SgN regex: "^authInvoker$", }, }) - .filter((node) => isSupportedInvokerOptionKey(node, context)); + .filter(isSupportedInvokerOptionKey); } -function findAuthInvokerPropertyKeys(root: SgNode, context: TransformContext): SgNode[] { +function findAuthInvokerPropertyKeys(root: SgNode): SgNode[] { return root .findAll({ rule: { @@ -282,10 +238,10 @@ function findAuthInvokerPropertyKeys(root: SgNode, context: TransformContext): S regex: "^authInvoker$", }, }) - .filter((node) => isSupportedInvokerOptionKey(node, context)); + .filter(isSupportedInvokerOptionKey); } -function findQuotedAuthInvokerPropertyKeys(root: SgNode, context: TransformContext): SgNode[] { +function findQuotedAuthInvokerPropertyKeys(root: SgNode): SgNode[] { return root .findAll({ rule: { @@ -293,7 +249,7 @@ function findQuotedAuthInvokerPropertyKeys(root: SgNode, context: TransformConte regex: "^['\"]authInvoker['\"]$", }, }) - .filter((node) => isSupportedInvokerOptionKey(node, context)); + .filter(isSupportedInvokerOptionKey); } function renameQuotedKey(node: SgNode): string { @@ -319,22 +275,15 @@ export default function transform(source: string, _filePath: string): string | n const lang = source.includes("") ? Lang.Tsx : Lang.TypeScript; const root = parse(lang, source).root(); - const context: TransformContext = { - workflowTriggerReceivers: findWorkflowTriggerReceivers(root), - }; - const calls = findInvokerCalls(root).filter((c) => - isSupportedInvokerValueCall(c.callNode, context), - ); + const calls = findInvokerCalls(root).filter((c) => isSupportedInvokerValueCall(c.callNode)); const edits: Edit[] = calls.map((c) => c.callNode.replace(c.argText)); - edits.push(...findAuthInvokerPropertyKeys(root, context).map((node) => node.replace("invoker"))); + edits.push(...findAuthInvokerPropertyKeys(root).map((node) => node.replace("invoker"))); edits.push( - ...findQuotedAuthInvokerPropertyKeys(root, context).map((node) => - node.replace(renameQuotedKey(node)), - ), + ...findQuotedAuthInvokerPropertyKeys(root).map((node) => node.replace(renameQuotedKey(node))), ); edits.push( - ...findAuthInvokerShorthands(root, context).map((node) => node.replace("invoker: authInvoker")), + ...findAuthInvokerShorthands(root).map((node) => node.replace("invoker: authInvoker")), ); if ( diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/expected.ts deleted file mode 100644 index 76eb3e77a..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/expected.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createResolver, t } from "@tailor-platform/sdk"; -import orderProcessingWorkflow from "../workflows/order-processing"; - -export default createResolver({ - name: "triggerWorkflow", - type: "Mutation", - input: { - orderId: t.string().description("Order ID"), - customerId: t.string().description("Customer ID for the order"), - }, - body: async ({ input }) => { - const workflowRunId = await orderProcessingWorkflow.trigger( - { - orderId: input.orderId, - customerId: input.customerId, - }, - { invoker: "manager-machine-user" }, - ); - return workflowRunId; - }, -}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts index 4c504eebd..0e98f3a86 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/expected.ts @@ -1,13 +1,10 @@ import { auth } from "../tailor.config"; -import workflow from "../workflows/order-processing"; const machineUserName = "kiosk"; -workflow.trigger( - { orderId }, - { +startWorkflow({ + workflow, // The argument is not a literal string, so the call is left intact and the // `auth` import stays. invoker: auth.invoker(machineUserName), - }, -); +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts index 2519ccad7..bb14e5378 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts @@ -1,13 +1,10 @@ import { auth } from "../tailor.config"; -import workflow from "../workflows/order-processing"; const machineUserName = "kiosk"; -workflow.trigger( - { orderId }, - { +startWorkflow({ + workflow, // The argument is not a literal string, so the call is left intact and the // `auth` import stays. authInvoker: auth.invoker(machineUserName), - }, -); +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/platform-payload-untouched/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/platform-payload-untouched/input.ts index e4d44c3e6..50dafc3f0 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/platform-payload-untouched/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/platform-payload-untouched/input.ts @@ -11,3 +11,7 @@ tailor.workflow.triggerWorkflow("daily", {}, { paymentGateway.trigger("charge", { authInvoker: "secret", }); + +paymentGateway.trigger("charge", { + authInvoker: "secret", +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts index 12277ed7e..a9b8b9ca9 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/expected.ts @@ -1,13 +1,4 @@ -import workflow from "../workflows/order-processing"; - startWorkflow({ workflow, "invoker": "kiosk", }); - -workflow.trigger( - { orderId }, - { - 'invoker': "manager", - }, -); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts index 5d25f8854..3be988273 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/quoted-option/input.ts @@ -1,13 +1,4 @@ -import workflow from "../workflows/order-processing"; - startWorkflow({ workflow, "authInvoker": "kiosk", }); - -workflow.trigger( - { orderId }, - { - 'authInvoker': "manager", - }, -); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts index 4d0d90072..b91bf847d 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/expected.ts @@ -1,10 +1,6 @@ -import workflow from "../workflows/order-processing"; - const authInvoker = "kiosk"; -workflow.trigger( - { orderId }, - { +startWorkflow({ + workflow, invoker: authInvoker, - }, -); +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts index 33620ee84..39b2e41f8 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/shorthand-option/input.ts @@ -1,10 +1,6 @@ -import workflow from "../workflows/order-processing"; - const authInvoker = "kiosk"; -workflow.trigger( - { orderId }, - { +startWorkflow({ + workflow, authInvoker, - }, -); +}); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index ea7211fb2..09dfb45f8 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -87,7 +87,7 @@ const allCodemods: CodemodPackage[] = [ id: "v2/auth-invoker-unwrap", name: 'auth.invoker("name") → invoker: "name"', description: - 'Rename supported SDK `authInvoker` options to `invoker`, replace `auth.invoker("name")` there with the bare `"name"` string, and drop the `auth` import when no other reference remains. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle.', + 'Rename statically identified SDK `authInvoker` options to `invoker`, replace `auth.invoker("name")` there with the bare `"name"` string, and drop the `auth` import when no other reference remains. Ambiguous workflow `.trigger()` calls are left for manual review. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle.', since: "1.0.0", until: "2.0.0", scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js", @@ -111,7 +111,7 @@ const allCodemods: CodemodPackage[] = [ prompt: [ "In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the", "machine user name passed directly as a string. The codemod already rewrote the", - 'supported SDK option form authInvoker: auth.invoker("name") to invoker: "name" and renamed supported authInvoker option keys. These files still contain', + 'statically identified SDK option form authInvoker: auth.invoker("name") to invoker: "name" and renamed supported authInvoker option keys. These files still contain', "auth.invoker(...) calls or authInvoker keys that need manual review.", "", "For each remaining auth.invoker() call:", From e31b4969fe9b72cec4868925cca3573b2b334691 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 19 Jun 2026 14:35:34 +0900 Subject: [PATCH 160/618] fix: reject legacy auth invoker schema keys --- .../src/parser/service/auth/legacy-invoker.ts | 6 ++ .../parser/service/executor/schema.test.ts | 87 ++++++++++++++++++- .../sdk/src/parser/service/executor/schema.ts | 4 + .../parser/service/resolver/schema.test.ts | 43 +++++++++ .../sdk/src/parser/service/resolver/schema.ts | 2 + packages/sdk/src/types/executor.generated.ts | 7 ++ packages/sdk/src/types/resolver.generated.ts | 1 + 7 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 packages/sdk/src/parser/service/auth/legacy-invoker.ts create mode 100644 packages/sdk/src/parser/service/resolver/schema.test.ts diff --git a/packages/sdk/src/parser/service/auth/legacy-invoker.ts b/packages/sdk/src/parser/service/auth/legacy-invoker.ts new file mode 100644 index 000000000..38c494e7f --- /dev/null +++ b/packages/sdk/src/parser/service/auth/legacy-invoker.ts @@ -0,0 +1,6 @@ +import { z } from "zod"; + +// Prevent legacy config keys from being silently stripped by z.object(). +export const legacyAuthInvokerOption = z + .never({ error: "`authInvoker` was renamed to `invoker`; use `invoker` instead." }) + .optional(); diff --git a/packages/sdk/src/parser/service/executor/schema.test.ts b/packages/sdk/src/parser/service/executor/schema.test.ts index 526eaa795..25a4422c3 100644 --- a/packages/sdk/src/parser/service/executor/schema.test.ts +++ b/packages/sdk/src/parser/service/executor/schema.test.ts @@ -1,5 +1,10 @@ import { describe, expect, test } from "vitest"; -import { ExecutorSchema, GqlOperationSchema, WorkflowOperationSchema } from "./schema"; +import { + ExecutorSchema, + FunctionOperationSchema, + GqlOperationSchema, + WorkflowOperationSchema, +} from "./schema"; function expectParseSuccess( result: { success: true; data: T } | { success: false; error: unknown }, @@ -11,6 +16,43 @@ function expectParseSuccess( return result.data; } +function expectParseFailure( + result: { success: true; data: T } | { success: false; error: { issues: unknown[] } }, +): { issues: unknown[] } { + expect(result.success).toBe(false); + if (result.success) { + throw new Error("Expected schema parsing to fail"); + } + return result.error; +} + +function expectLegacyAuthInvokerRejected( + result: { success: true; data: T } | { success: false; error: { issues: unknown[] } }, +) { + const error = expectParseFailure(result); + expect(error.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + path: ["authInvoker"], + }), + ]), + ); +} + +describe("FunctionOperationSchema", () => { + test("rejects legacy authInvoker option", () => { + expect.hasAssertions(); + + expectLegacyAuthInvokerRejected( + FunctionOperationSchema.safeParse({ + kind: "function", + body: () => {}, + authInvoker: "admin", + }), + ); + }); +}); + describe("GqlOperationSchema", () => { test("converts query to string", () => { const documentNode = { @@ -37,6 +79,18 @@ describe("GqlOperationSchema", () => { const data = expectParseSuccess(result); expect(data.query).toBe("query { users { id } }"); }); + + test("rejects legacy authInvoker option", () => { + expect.hasAssertions(); + + expectLegacyAuthInvokerRejected( + GqlOperationSchema.safeParse({ + kind: "graphql", + query: "query { users { id } }", + authInvoker: "admin", + }), + ); + }); }); describe("WorkflowOperationSchema", () => { @@ -62,6 +116,18 @@ describe("WorkflowOperationSchema", () => { const data = expectParseSuccess(result); expect(data.workflowName).toBe("my-workflow"); }); + + test("rejects legacy authInvoker option", () => { + expect.hasAssertions(); + + expectLegacyAuthInvokerRejected( + WorkflowOperationSchema.safeParse({ + kind: "workflow", + workflowName: "my-workflow", + authInvoker: "admin", + }), + ); + }); }); describe("ExecutorSchema", () => { @@ -112,4 +178,23 @@ describe("ExecutorSchema", () => { } expect(data.operation.query).toBe("mutation { createUser { id } }"); }); + + test("rejects legacy authInvoker option on operations", () => { + expect.hasAssertions(); + + expectParseFailure( + ExecutorSchema.safeParse({ + name: "test-executor", + trigger: { + kind: "schedule", + cron: "0 12 * * *", + }, + operation: { + kind: "function", + body: () => {}, + authInvoker: "admin", + }, + }), + ); + }); }); diff --git a/packages/sdk/src/parser/service/executor/schema.ts b/packages/sdk/src/parser/service/executor/schema.ts index e4d1b281b..d9c33d835 100644 --- a/packages/sdk/src/parser/service/executor/schema.ts +++ b/packages/sdk/src/parser/service/executor/schema.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { AuthInvokerSchema } from "../auth"; +import { legacyAuthInvokerOption } from "../auth/legacy-invoker"; import { functionSchema } from "../common"; export const TailorDBTriggerSchema = z.object({ @@ -88,6 +89,7 @@ export const FunctionOperationSchema = z.object({ kind: z.enum(["function", "jobFunction"]), body: functionSchema.describe("Function implementation"), invoker: AuthInvokerSchema.optional().describe("Invoker for the function execution"), + authInvoker: legacyAuthInvokerOption, }); export const GqlOperationSchema = z.object({ @@ -96,6 +98,7 @@ export const GqlOperationSchema = z.object({ query: z.preprocess((val) => String(val), z.string().describe("GraphQL query string")), variables: functionSchema.optional().describe("Function to compute GraphQL variables"), invoker: AuthInvokerSchema.optional().describe("Invoker for the GraphQL execution"), + authInvoker: legacyAuthInvokerOption, }); export const WebhookOperationSchema = z.object({ @@ -131,6 +134,7 @@ export const WorkflowOperationSchema = z.preprocess( .optional() .describe("Arguments to pass to the workflow"), invoker: AuthInvokerSchema.optional().describe("Invoker for the workflow execution"), + authInvoker: legacyAuthInvokerOption, }), ); diff --git a/packages/sdk/src/parser/service/resolver/schema.test.ts b/packages/sdk/src/parser/service/resolver/schema.test.ts new file mode 100644 index 000000000..011d13f2f --- /dev/null +++ b/packages/sdk/src/parser/service/resolver/schema.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, test } from "vitest"; +import { ResolverSchema } from "./schema"; +import type { ZodError } from "zod"; + +function expectParseFailure( + result: { success: true; data: T } | { success: false; error: ZodError }, +): ZodError { + expect(result.success).toBe(false); + if (result.success) { + throw new Error("Expected schema parsing to fail"); + } + return result.error; +} + +describe("ResolverSchema", () => { + const validResolver = { + operation: "query", + name: "getUser", + body: () => {}, + output: { + type: "string", + fields: {}, + metadata: {}, + }, + }; + + test("rejects legacy authInvoker option", () => { + const error = expectParseFailure( + ResolverSchema.safeParse({ + ...validResolver, + authInvoker: "admin", + }), + ); + + expect(error.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + path: ["authInvoker"], + }), + ]), + ); + }); +}); diff --git a/packages/sdk/src/parser/service/resolver/schema.ts b/packages/sdk/src/parser/service/resolver/schema.ts index b071714d9..bab8d664a 100644 --- a/packages/sdk/src/parser/service/resolver/schema.ts +++ b/packages/sdk/src/parser/service/resolver/schema.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { legacyAuthInvokerOption } from "@/parser/service/auth/legacy-invoker"; import { AuthInvokerSchema } from "@/parser/service/auth/schema"; import { TailorFieldSchema } from "@/parser/service/field/schema"; import { functionSchema } from "../common"; @@ -16,4 +17,5 @@ export const ResolverSchema = z.object({ output: TailorFieldSchema.describe("Output field definition"), publishEvents: z.boolean().optional().describe("Enable publishing events from this resolver"), invoker: AuthInvokerSchema.optional().describe("Machine user to execute this resolver as"), + authInvoker: legacyAuthInvokerOption, }); diff --git a/packages/sdk/src/types/executor.generated.ts b/packages/sdk/src/types/executor.generated.ts index 61b872588..da0cce571 100644 --- a/packages/sdk/src/types/executor.generated.ts +++ b/packages/sdk/src/types/executor.generated.ts @@ -111,6 +111,7 @@ export type FunctionOperation = { machineUserName: string; } | undefined; + authInvoker?: undefined; }; export type FunctionOperationInput = FunctionOperation; @@ -129,6 +130,7 @@ export type GqlOperationInput = { machineUserName: string; } | undefined; + authInvoker?: undefined; }; export type GqlOperation = { @@ -146,6 +148,7 @@ export type GqlOperation = { machineUserName: string; } | undefined; + authInvoker?: undefined; }; export type WebhookOperation = { @@ -186,6 +189,7 @@ export type WorkflowOperation = { machineUserName: string; } | undefined; + authInvoker?: undefined; }; export type ExecutorInput = { @@ -310,6 +314,7 @@ export type Executor = { machineUserName: string; } | undefined; + authInvoker?: undefined; } | { kind: "function" | "jobFunction"; @@ -321,6 +326,7 @@ export type Executor = { machineUserName: string; } | undefined; + authInvoker?: undefined; } | { kind: "graphql"; @@ -334,6 +340,7 @@ export type Executor = { machineUserName: string; } | undefined; + authInvoker?: undefined; } | { kind: "webhook"; diff --git a/packages/sdk/src/types/resolver.generated.ts b/packages/sdk/src/types/resolver.generated.ts index 922b99eab..a778d0697 100644 --- a/packages/sdk/src/types/resolver.generated.ts +++ b/packages/sdk/src/types/resolver.generated.ts @@ -63,5 +63,6 @@ export type Resolver = { machineUserName: string; } | undefined; + authInvoker?: undefined; }; export type ResolverInput = Resolver; From ff538825f8110155cd1668bb1a0c9e9cf34c6a9d Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 19 Jun 2026 15:53:22 +0900 Subject: [PATCH 161/618] fix(codemod): fold runtime-globals import into tailordb-namespace; clean up prompt trim and test cast --- packages/sdk-codemod/src/migration-doc.test.ts | 6 ++---- packages/sdk-codemod/src/migration-doc.ts | 2 +- packages/sdk-codemod/src/registry.ts | 8 ++++++-- packages/sdk/docs/migration/v2.md | 6 +++++- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/sdk-codemod/src/migration-doc.test.ts b/packages/sdk-codemod/src/migration-doc.test.ts index 893ee123b..4fa76f9e5 100644 --- a/packages/sdk-codemod/src/migration-doc.test.ts +++ b/packages/sdk-codemod/src/migration-doc.test.ts @@ -27,9 +27,7 @@ describe("automationLevel", () => { }); test("no scriptPath is Manual", () => { - expect(automationLevel(makeCodemod({ scriptPath: undefined as unknown as string }))).toBe( - "Manual", - ); + expect(automationLevel(makeCodemod({ scriptPath: undefined }))).toBe("Manual"); }); }); @@ -66,7 +64,7 @@ describe("renderMigrationDoc", () => { makeCodemod({ name: "Keyring storage", description: "Tokens move to the OS keyring.", - scriptPath: undefined as unknown as string, + scriptPath: undefined, notice: true, }), ]); diff --git a/packages/sdk-codemod/src/migration-doc.ts b/packages/sdk-codemod/src/migration-doc.ts index 0ddaf7d9e..3d8da9a6f 100644 --- a/packages/sdk-codemod/src/migration-doc.ts +++ b/packages/sdk-codemod/src/migration-doc.ts @@ -55,7 +55,7 @@ function renderEntry(codemod: CodemodPackage): string { `${summary}`, "", "```text", - codemod.prompt.trim(), + codemod.prompt.trimEnd(), "```", "", "
", diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 4b8d42fc8..cc72798ef 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -212,7 +212,7 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/tailordb-namespace", name: "Tailordb → tailordb (lowercase ambient namespace)", description: - "Rewrite references to the removed capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the lowercase `tailordb.*` namespace exposed by `@tailor-platform/sdk/runtime/globals`.", + 'Rewrite references to the removed capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the lowercase `tailordb.*` namespace exposed by `@tailor-platform/sdk/runtime/globals`. Because v2 no longer activates ambient declarations automatically, each file that contains `tailordb.*` references after the rewrite must also add `import "@tailor-platform/sdk/runtime/globals"`.', since: "1.0.0", until: "2.0.0", scriptPath: "v2/tailordb-namespace/scripts/transform.js", @@ -220,7 +220,8 @@ export const allCodemods: CodemodPackage[] = [ examples: [ { before: 'const command: Tailordb.CommandType = "SELECT";', - after: 'const command: tailordb.CommandType = "SELECT";', + after: + 'import "@tailor-platform/sdk/runtime/globals";\nconst command: tailordb.CommandType = "SELECT";', }, ], prompt: [ @@ -229,6 +230,9 @@ export const allCodemods: CodemodPackage[] = [ "the known members (QueryResult, CommandType, Client). Rewrite any other remaining", "Tailordb.* reference to its tailordb.* equivalent (and confirm the member still", "exists on the lowercase namespace).", + 'Also add `import "@tailor-platform/sdk/runtime/globals"` at the top of each file', + "that contains any tailordb.* type reference — v2 no longer activates ambient", + "declarations automatically on SDK import.", ].join("\n"), }, { diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index d1451c8d0..039230c53 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -256,7 +256,7 @@ Do not change behavior beyond removing the auth.invoker() indirection. **Migration:** Partially automatic -Rewrite references to the removed capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the lowercase `tailordb.*` namespace exposed by `@tailor-platform/sdk/runtime/globals`. +Rewrite references to the removed capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the lowercase `tailordb.*` namespace exposed by `@tailor-platform/sdk/runtime/globals`. Because v2 no longer activates ambient declarations automatically, each file that contains `tailordb.*` references after the rewrite must also add `import "@tailor-platform/sdk/runtime/globals"`. Before: @@ -267,6 +267,7 @@ const command: Tailordb.CommandType = "SELECT"; After: ```ts +import "@tailor-platform/sdk/runtime/globals"; const command: tailordb.CommandType = "SELECT"; ``` @@ -279,6 +280,9 @@ tailordb.* namespace from @tailor-platform/sdk/runtime/globals. The codemod rewr the known members (QueryResult, CommandType, Client). Rewrite any other remaining Tailordb.* reference to its tailordb.* equivalent (and confirm the member still exists on the lowercase namespace). +Also add `import "@tailor-platform/sdk/runtime/globals"` at the top of each file +that contains any tailordb.* type reference — v2 no longer activates ambient +declarations automatically on SDK import. ```
From cc89b428342f51d83efa45757f01e3cfddda8936 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 19 Jun 2026 16:09:23 +0900 Subject: [PATCH 162/618] fix(codemod): tighten test assertions, fix manual entry scriptPath, clean up stale NOTE --- packages/sdk-codemod/src/migration-doc.test.ts | 2 +- packages/sdk-codemod/src/migration-doc.ts | 10 ++++------ packages/sdk-codemod/src/runner.test.ts | 5 ++--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/sdk-codemod/src/migration-doc.test.ts b/packages/sdk-codemod/src/migration-doc.test.ts index 4fa76f9e5..783ecf56a 100644 --- a/packages/sdk-codemod/src/migration-doc.test.ts +++ b/packages/sdk-codemod/src/migration-doc.test.ts @@ -55,7 +55,7 @@ describe("renderMigrationDoc", () => { test("omits the prompt block for Automatic entries", () => { const doc = renderMigrationDoc([makeCodemod({ name: "Auto", description: "Auto change." })]); expect(doc).toContain("**Migration:** Automatic"); - expect(doc).not.toContain("How to finish"); + expect(doc).not.toContain("
"); }); test("renders notices in a separate section without a migration label", () => { diff --git a/packages/sdk-codemod/src/migration-doc.ts b/packages/sdk-codemod/src/migration-doc.ts index 3d8da9a6f..b12be0743 100644 --- a/packages/sdk-codemod/src/migration-doc.ts +++ b/packages/sdk-codemod/src/migration-doc.ts @@ -78,12 +78,10 @@ function renderNotice(codemod: CodemodPackage): string { * @returns The migration guide as Markdown */ export function renderMigrationDoc(codemods: CodemodPackage[]): string { - // NOTE: This generator (and the registry it reads) is shaped around the v1->v2 - // migration: the title, the `v2.md` output path, and the `v2/*` rule ids / - // `until: "2.0.0"` ranges are all hardcoded. Before this lands on `main`, - // either clean up the v2-specific scaffolding or generalize it to be - // version-agnostic (parameterize the target version, output path, and rule - // namespace) so it can serve future major migrations too. + // NOTE: This generator is currently v1→v2-specific: the title, `v2.md` output + // path, and `v2/*` rule ids / `until: "2.0.0"` ranges are all hardcoded. + // Supporting future major migrations would require parameterizing the target + // version, output path, and rule namespace. const header = [ "# Migrating to v2", "", diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index eb3e2609e..cf6b7560b 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -23,7 +23,7 @@ async function createTestProject( function makeCodemod( id: string, - scriptPath: string, + scriptPath?: string, filePatterns?: string[], legacyPatterns?: Array, extra?: Pick, @@ -421,8 +421,7 @@ describe("runCodemods", () => { const result = await runCodemods( [ { - // No scriptPath: a manual entry that ships only a prompt. - codemod: makeCodemod("test/manual", "unused.ts", ["**/*.ts"], undefined, { + codemod: makeCodemod("test/manual", undefined, ["**/*.ts"], undefined, { prompt: "Do the manual change.", }), scriptPath: undefined, From cadec9c3f103df26f2c410125bd8a6eedd21b3c9 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 19 Jun 2026 16:17:32 +0900 Subject: [PATCH 163/618] docs: add codemod:docs commands to AGENTS.md Agent Instructions --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index d7a1a107f..11440a6d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,6 +49,8 @@ When editing files matching these globs, read and follow the linked rule documen - `pnpm agent:rules:update` - Regenerate `AGENTS.md`'s path-scoped rule index and `.claude/rules/*.md` - `pnpm agent:rules:check` - Verify generated agent rule files are up to date +- `pnpm codemod:docs:update` - Regenerate `packages/sdk/docs/migration/v2.md` from the codemod registry +- `pnpm codemod:docs:check` - Verify migration docs are up to date ### CLI From dcf66a1e648f5287eaea9ea330eb4ad726a4d363 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 20 Jun 2026 22:04:18 +0900 Subject: [PATCH 164/618] fix(sdk-codemod): rewrite apply commands in source files --- .changeset/apply-deploy-source-strings.md | 5 +++++ .../apply-to-deploy/tests/source-fixture/expected.ts | 7 +++++++ .../v2/apply-to-deploy/tests/source-fixture/input.ts | 7 +++++++ packages/sdk-codemod/src/registry.test.ts | 10 ++++++++++ packages/sdk-codemod/src/registry.ts | 7 ++++++- 5 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 .changeset/apply-deploy-source-strings.md create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-fixture/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-fixture/input.ts diff --git a/.changeset/apply-deploy-source-strings.md b/.changeset/apply-deploy-source-strings.md new file mode 100644 index 000000000..bf47fb101 --- /dev/null +++ b/.changeset/apply-deploy-source-strings.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Rewrite `tailor-sdk apply` to `tailor-sdk deploy` in source files that contain embedded CLI command strings. diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-fixture/expected.ts b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-fixture/expected.ts new file mode 100644 index 000000000..7da5667d7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-fixture/expected.ts @@ -0,0 +1,7 @@ +import { expect, test } from "vitest"; + +test("keeps collected scripts", () => { + expect(JSON.stringify({ scripts: { deploy: "tailor-sdk deploy", seed: "node seed.mjs" } })).toBe( + '{"scripts":{"deploy":"tailor-sdk deploy","seed":"node seed.mjs"}}', + ); +}); diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-fixture/input.ts b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-fixture/input.ts new file mode 100644 index 000000000..d9b3dd8c0 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-fixture/input.ts @@ -0,0 +1,7 @@ +import { expect, test } from "vitest"; + +test("keeps collected scripts", () => { + expect(JSON.stringify({ scripts: { deploy: "tailor-sdk apply", seed: "node seed.mjs" } })).toBe( + '{"scripts":{"deploy":"tailor-sdk apply","seed":"node seed.mjs"}}', + ); +}); diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 23da4125f..4edc5f25e 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -24,4 +24,14 @@ describe("getApplicableCodemods", () => { expect(() => getApplicableCodemods("invalid", "2.0.0")).toThrow("Invalid fromVersion"); expect(() => getApplicableCodemods("1.0.0", "invalid")).toThrow("Invalid toVersion"); }); + + test("apply-to-deploy scans source files with embedded CLI strings", () => { + const applyToDeploy = getApplicableCodemods("1.67.1", "2.0.0").find( + (codemod) => codemod.id === "v2/apply-to-deploy", + ); + + expect(applyToDeploy?.filePatterns).toEqual( + expect.arrayContaining(["**/*.{ts,tsx,mts,cts,js,mjs,cjs}"]), + ); + }); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index cc72798ef..eedaac1dc 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -142,7 +142,12 @@ export const allCodemods: CodemodPackage[] = [ since: "1.0.0", until: "2.0.0", scriptPath: "v2/apply-to-deploy/scripts/transform.js", - filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], + filePatterns: [ + "**/package.json", + "**/*.{sh,bash,zsh,yml,yaml}", + "**/*.{ts,tsx,mts,cts,js,mjs,cjs}", + "**/*.md", + ], examples: [ { lang: "sh", From 26f54da63b70ebff694b94aa4352081dc02f26ee Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 20 Jun 2026 22:12:11 +0900 Subject: [PATCH 165/618] fix(sdk-codemod): avoid cross-line apply rewrites --- .../codemods/v2/apply-to-deploy/scripts/transform.ts | 10 ++++++++++ .../tests/source-comment-no-match/input.ts | 2 ++ 2 files changed, 12 insertions(+) create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-comment-no-match/input.ts diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts index 09b860926..cc3305361 100644 --- a/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts @@ -12,6 +12,7 @@ const VALUE_GLOBAL_ARG = "(?:--env-file|--env-file-if-exists|-e)"; const GLOBAL_ARG_PATTERN = `(?:(?:\\s+${BOOLEAN_GLOBAL_ARG})|(?:\\s+${VALUE_GLOBAL_ARG}(?:=${ARG_VALUE}|\\s+${ARG_VALUE})))*`; const TAILOR_BINARY = `(? `${match.slice(0, -"apply".length)}deploy`); @@ -24,6 +25,14 @@ function transformText(source: string): string | null { return updated === source ? null : updated; } +function transformSourceText(source: string): string | null { + const updated = source + .split(/(\r\n|\n|\r)/) + .map((part, index) => (index % 2 === 0 ? replaceApply(part) : part)) + .join(""); + return updated === source ? null : updated; +} + function transformPackageJson(source: string): string | null { let parsed: Record; try { @@ -64,5 +73,6 @@ export default function transform(source: string, filePath: string): string | nu const ext = path.extname(filePath).toLowerCase(); if (ext === ".json") return transformPackageJson(source); + if (SOURCE_EXTENSIONS.has(ext)) return transformSourceText(source); return transformText(source); } diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-comment-no-match/input.ts b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-comment-no-match/input.ts new file mode 100644 index 000000000..b5bd01540 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-comment-no-match/input.ts @@ -0,0 +1,2 @@ +// tailor-sdk +apply(config); From 56f77b78cf1db54ca488a2ca1cf86874b4bbe394 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 20 Jun 2026 22:17:39 +0900 Subject: [PATCH 166/618] fix(sdk-codemod): include jsx source files in apply rewrite --- .../codemods/v2/apply-to-deploy/scripts/transform.ts | 2 +- .../apply-to-deploy/tests/source-jsx-comment-no-match/input.jsx | 2 ++ packages/sdk-codemod/src/registry.test.ts | 2 +- packages/sdk-codemod/src/registry.ts | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-jsx-comment-no-match/input.jsx diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts index cc3305361..a262d71c2 100644 --- a/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts @@ -12,7 +12,7 @@ const VALUE_GLOBAL_ARG = "(?:--env-file|--env-file-if-exists|-e)"; const GLOBAL_ARG_PATTERN = `(?:(?:\\s+${BOOLEAN_GLOBAL_ARG})|(?:\\s+${VALUE_GLOBAL_ARG}(?:=${ARG_VALUE}|\\s+${ARG_VALUE})))*`; const TAILOR_BINARY = `(? `${match.slice(0, -"apply".length)}deploy`); diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-jsx-comment-no-match/input.jsx b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-jsx-comment-no-match/input.jsx new file mode 100644 index 000000000..b5bd01540 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/source-jsx-comment-no-match/input.jsx @@ -0,0 +1,2 @@ +// tailor-sdk +apply(config); diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 4edc5f25e..dfcdc27b7 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -31,7 +31,7 @@ describe("getApplicableCodemods", () => { ); expect(applyToDeploy?.filePatterns).toEqual( - expect.arrayContaining(["**/*.{ts,tsx,mts,cts,js,mjs,cjs}"]), + expect.arrayContaining(["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"]), ); }); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index eedaac1dc..0ae28edb2 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -145,7 +145,7 @@ export const allCodemods: CodemodPackage[] = [ filePatterns: [ "**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", - "**/*.{ts,tsx,mts,cts,js,mjs,cjs}", + "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}", "**/*.md", ], examples: [ From a6497649be2786b3f6e410c8aa98c4247a599258 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 20 Jun 2026 22:31:46 +0900 Subject: [PATCH 167/618] fix(sdk-codemod): reduce residual pattern false positives --- .changeset/codemod-residual-matching.md | 5 + packages/sdk-codemod/src/runner.test.ts | 125 +++++++++++++++++++ packages/sdk-codemod/src/runner.ts | 153 +++++++++++++++++++++++- packages/sdk-codemod/src/types.ts | 17 +-- 4 files changed, 289 insertions(+), 11 deletions(-) create mode 100644 .changeset/codemod-residual-matching.md diff --git a/.changeset/codemod-residual-matching.md b/.changeset/codemod-residual-matching.md new file mode 100644 index 000000000..d62d97729 --- /dev/null +++ b/.changeset/codemod-residual-matching.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Reduce false-positive v2 codemod warnings and LLM-review prompts from source comments, string literals, and identifier substring matches. diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index cf6b7560b..1e862ae76 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -363,6 +363,71 @@ describe("runCodemods", () => { ]); }); + test("ignores source comments, strings, and identifier substrings for legacy warnings", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "createContext.ts"), + [ + "// Matches SDK's unauthenticatedTailorUser.id", + 'const note = "TailorUser";', + "const unauthenticatedTailorUserId = caller?.id;", + ].join("\n"), + "utf-8", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod( + "test/principal", + partialTransformPath, + ["**/*.ts"], + ["TailorUser", "unauthenticatedTailorUser"], + ), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toEqual([]); + }); + + test("keeps legacy warnings for source identifiers", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "resolver.ts"), + [ + 'import type { TailorUser } from "@tailor-platform/sdk";', + "const fallback = unauthenticatedTailorUser;", + ].join("\n"), + "utf-8", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod( + "test/principal", + partialTransformPath, + ["**/*.ts"], + ["TailorUser", "unauthenticatedTailorUser"], + ), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toEqual([ + "resolver.ts: contains TailorUser, unauthenticatedTailorUser but was not migrated automatically (rule: test/principal). Manual migration may be needed.", + ]); + }); + test("flags files matching a suspicious pattern for LLM review", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-test-")); tmpDir = dir; @@ -413,6 +478,66 @@ describe("runCodemods", () => { expect(result.llmReviews).toEqual([]); }); + test("ignores source comments and strings for LLM review patterns", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "a.ts"), + [ + "// executeScript({ arg: payload });", + 'const name = "executeScript";', + "const template = `executeScript`;", + ].join("\n"), + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/llm", partialTransformPath, ["**/*.ts"], undefined, { + suspiciousPatterns: ["executeScript"], + prompt: "Rewrite remaining executeScript usages by hand.", + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.llmReviews).toEqual([]); + }); + + test("keeps LLM review patterns inside template substitutions", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "a.ts"), + "const message = `${executeScript({ arg: payload })}`;\n", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/llm", partialTransformPath, ["**/*.ts"], undefined, { + suspiciousPatterns: ["executeScript"], + prompt: "Rewrite remaining executeScript usages by hand.", + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.llmReviews).toEqual([ + { + codemodId: "test/llm", + prompt: "Rewrite remaining executeScript usages by hand.", + files: ["a.ts"], + }, + ]); + }); + test("emits a blanket LLM review for a codemod-less manual entry", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-manual-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 4cf064660..9ff152add 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -50,6 +50,7 @@ const DEFAULT_FILE_PATTERNS = ["**/*.{ts,tsx,mts,cts}"]; /** Directory names always excluded from file scanning. */ const EXCLUDE_DIRS = new Set(["node_modules", "dist", ".git"]); const ALLOWED_DOT_DIRS = new Set([".github", ".circleci"]); +const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]); function shouldSkipDirectory(name: string): boolean { return EXCLUDE_DIRS.has(name) || (name.startsWith(".") && !ALLOWED_DOT_DIRS.has(name)); @@ -131,12 +132,155 @@ interface LoadedTransform { prompt?: string; } +function maskSourceNonCode(content: string): string { + let output = ""; + let i = 0; + let state: "code" | "lineComment" | "blockComment" | "single" | "double" | "template" = "code"; + let templateExpressionDepth = 0; + const mask = (char: string): string => (char === "\n" || char === "\r" ? char : " "); + + while (i < content.length) { + const char = content[i]!; + const next = content[i + 1]; + + if (state === "code") { + if (char === "/" && next === "/") { + output += " "; + i += 2; + state = "lineComment"; + continue; + } + if (char === "/" && next === "*") { + output += " "; + i += 2; + state = "blockComment"; + continue; + } + if (char === "'") { + output += " "; + i += 1; + state = "single"; + continue; + } + if (char === '"') { + output += " "; + i += 1; + state = "double"; + continue; + } + if (char === "`") { + output += " "; + i += 1; + state = "template"; + continue; + } + if (templateExpressionDepth > 0 && char === "{") { + output += char; + i += 1; + templateExpressionDepth += 1; + continue; + } + if (templateExpressionDepth > 0 && char === "}") { + output += char; + i += 1; + templateExpressionDepth -= 1; + if (templateExpressionDepth === 0) state = "template"; + continue; + } + output += char; + i += 1; + continue; + } + + if (state === "lineComment") { + output += mask(char); + i += 1; + if (char === "\n" || char === "\r") state = "code"; + continue; + } + + if (state === "blockComment") { + if (char === "*" && next === "/") { + output += " "; + i += 2; + state = "code"; + continue; + } + output += mask(char); + i += 1; + continue; + } + + if (state === "template") { + if (char === "$" && next === "{") { + output += " "; + i += 2; + state = "code"; + templateExpressionDepth += 1; + continue; + } + output += mask(char); + i += 1; + if (char === "\\") { + if (i < content.length) { + output += mask(content[i]!); + i += 1; + } + continue; + } + if (char === "`") state = "code"; + continue; + } + + output += mask(char); + i += 1; + if (char === "\\") { + if (i < content.length) { + output += mask(content[i]!); + i += 1; + } + continue; + } + if ((state === "single" && char === "'") || (state === "double" && char === '"')) { + state = "code"; + } else if (char === "\n" || char === "\r") { + state = "code"; + } + } + + return output; +} + +function contentForResidualMatching(relative: string, content: string): string { + const ext = path.extname(relative).toLowerCase(); + return SOURCE_EXTENSIONS.has(ext) ? maskSourceNonCode(content) : content; +} + +function isIdentifierChar(char: string | undefined): boolean { + return char != null && /^[A-Za-z0-9_$]$/.test(char); +} + +function includesResidualPattern(content: string, pattern: string): boolean { + const checkLeft = isIdentifierChar(pattern[0]); + const checkRight = isIdentifierChar(pattern.at(-1)); + let index = content.indexOf(pattern); + while (index !== -1) { + const before = index > 0 ? content[index - 1] : undefined; + const after = content[index + pattern.length]; + if ((!checkLeft || !isIdentifierChar(before)) && (!checkRight || !isIdentifierChar(after))) { + return true; + } + index = content.indexOf(pattern, index + 1); + } + return false; +} + /** Resolve a legacy pattern against content, returning its label when matched. */ function matchLegacyPattern(content: string, pattern: string | string[]): string | null { if (typeof pattern === "string") { - return content.includes(pattern) ? pattern : null; + return includesResidualPattern(content, pattern) ? pattern : null; } - return pattern.every((p) => content.includes(p)) ? pattern.join(" + ") : null; + return pattern.every((p) => includesResidualPattern(content, p)) ? pattern.join(" + ") : null; } function legacyPatternWarnings( @@ -226,11 +370,12 @@ export async function runCodemods( } } - warnings.push(...legacyPatternWarnings(relative, current, matchedTransforms)); + const residualContent = contentForResidualMatching(relative, current); + warnings.push(...legacyPatternWarnings(relative, residualContent, matchedTransforms)); for (const lt of matchedTransforms) { if (!lt.prompt || lt.suspiciousPatterns.length === 0) continue; - if (lt.suspiciousPatterns.some((p) => current.includes(p))) { + if (lt.suspiciousPatterns.some((p) => includesResidualPattern(residualContent, p))) { const files = suspiciousByCodemod.get(lt.id) ?? []; files.push(relative); suspiciousByCodemod.set(lt.id, files); diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index f96e437e9..77ad9a3e1 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -36,20 +36,23 @@ export interface CodemodPackage { filePatterns?: string[]; /** * Patterns to detect in post-transform file content for manual migration - * warnings. A plain string warns when that substring is present; a - * `string[]` group warns only when every substring in the group is present + * warnings. A plain string warns when that residual pattern is present; a + * `string[]` group warns only when every pattern in the group is present * (AND), letting a rule target a co-occurrence such as `executeScript` used - * together with `JSON.stringify`. + * together with `JSON.stringify`. In source files, comments and string + * literals are ignored, and identifier-like patterns must match token + * boundaries. */ legacyPatterns?: Array; /** - * Substrings that, when present in a file's post-transform content, mark it - * as a candidate for LLM-assisted review. Use this for migrations the + * Residual patterns that, when present in a file's post-transform content, + * mark it as a candidate for LLM-assisted review. Use this for migrations the * deterministic transform cannot safely complete on its own (e.g. a value * reached through a variable or a dynamic expression). Unlike * `legacyPatterns`, these do not need to be exhaustive: a broad signal such - * as the API name is enough to point an LLM at the right files. Has no effect - * unless `prompt` is also set. + * as the API name is enough to point an LLM at the right files. Source files + * use the same comment/string and token-boundary matching as + * `legacyPatterns`. Has no effect unless `prompt` is also set. */ suspiciousPatterns?: string[]; /** From 66a3c783543825dbcc0f3cec2657769cd18ae5ae Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 20 Jun 2026 22:39:29 +0900 Subject: [PATCH 168/618] fix(sdk-codemod): preserve nested template residual matches --- packages/sdk-codemod/src/runner.test.ts | 31 +++++++++++++++++++++++++ packages/sdk-codemod/src/runner.ts | 18 ++++++++------ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 1e862ae76..3a985c001 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -538,6 +538,37 @@ describe("runCodemods", () => { ]); }); + test("keeps LLM review patterns after nested template substitutions", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "a.ts"), + "const message = `${`prefix ${foo}`.toString(executeScript())}`;\n", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/llm", partialTransformPath, ["**/*.ts"], undefined, { + suspiciousPatterns: ["executeScript"], + prompt: "Rewrite remaining executeScript usages by hand.", + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.llmReviews).toEqual([ + { + codemodId: "test/llm", + prompt: "Rewrite remaining executeScript usages by hand.", + files: ["a.ts"], + }, + ]); + }); + test("emits a blanket LLM review for a codemod-less manual entry", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-manual-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 9ff152add..57b657c28 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -136,7 +136,7 @@ function maskSourceNonCode(content: string): string { let output = ""; let i = 0; let state: "code" | "lineComment" | "blockComment" | "single" | "double" | "template" = "code"; - let templateExpressionDepth = 0; + const templateExpressionStack: number[] = []; const mask = (char: string): string => (char === "\n" || char === "\r" ? char : " "); while (i < content.length) { @@ -174,17 +174,21 @@ function maskSourceNonCode(content: string): string { state = "template"; continue; } - if (templateExpressionDepth > 0 && char === "{") { + const topTemplateExpression = templateExpressionStack.length - 1; + if (topTemplateExpression >= 0 && char === "{") { output += char; i += 1; - templateExpressionDepth += 1; + templateExpressionStack[topTemplateExpression]! += 1; continue; } - if (templateExpressionDepth > 0 && char === "}") { + if (topTemplateExpression >= 0 && char === "}") { output += char; i += 1; - templateExpressionDepth -= 1; - if (templateExpressionDepth === 0) state = "template"; + templateExpressionStack[topTemplateExpression]! -= 1; + if (templateExpressionStack[topTemplateExpression] === 0) { + templateExpressionStack.pop(); + state = "template"; + } continue; } output += char; @@ -216,7 +220,7 @@ function maskSourceNonCode(content: string): string { output += " "; i += 2; state = "code"; - templateExpressionDepth += 1; + templateExpressionStack.push(1); continue; } output += mask(char); From 483fee7a038794e69900f60f6ac7062b791f6294 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 20 Jun 2026 22:47:45 +0900 Subject: [PATCH 169/618] fix(sdk-codemod): use syntax ranges for residual matching --- packages/sdk-codemod/src/runner.test.ts | 31 +++++ packages/sdk-codemod/src/runner.ts | 155 ++++++------------------ 2 files changed, 66 insertions(+), 120 deletions(-) diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 3a985c001..64518a0c9 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -569,6 +569,37 @@ describe("runCodemods", () => { ]); }); + test("keeps LLM review patterns after regex literals with escaped slashes", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "a.ts"), + "const re = /https?:\\/\\//; executeScript({ arg: payload });\n", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/llm", partialTransformPath, ["**/*.ts"], undefined, { + suspiciousPatterns: ["executeScript"], + prompt: "Rewrite remaining executeScript usages by hand.", + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.llmReviews).toEqual([ + { + codemodId: "test/llm", + prompt: "Rewrite remaining executeScript usages by hand.", + files: ["a.ts"], + }, + ]); + }); + test("emits a blanket LLM review for a codemod-less manual entry", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-manual-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 57b657c28..3aa13735d 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -1,10 +1,12 @@ import * as fs from "node:fs"; import * as url from "node:url"; +import { parse, Lang } from "@ast-grep/napi"; import chalk from "chalk"; import { structuredPatch } from "diff"; import * as path from "pathe"; import picomatch from "picomatch"; import type { CodemodPackage, LlmReview } from "./types"; +import type { SgNode } from "@ast-grep/napi"; /** * A transform function that receives source text and file path, @@ -51,6 +53,7 @@ const DEFAULT_FILE_PATTERNS = ["**/*.{ts,tsx,mts,cts}"]; const EXCLUDE_DIRS = new Set(["node_modules", "dist", ".git"]); const ALLOWED_DOT_DIRS = new Set([".github", ".circleci"]); const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]); +const MASKED_SOURCE_NODE_KINDS = new Set(["comment", "string", "regex", "string_fragment"]); function shouldSkipDirectory(name: string): boolean { return EXCLUDE_DIRS.has(name) || (name.startsWith(".") && !ALLOWED_DOT_DIRS.has(name)); @@ -132,132 +135,44 @@ interface LoadedTransform { prompt?: string; } -function maskSourceNonCode(content: string): string { - let output = ""; - let i = 0; - let state: "code" | "lineComment" | "blockComment" | "single" | "double" | "template" = "code"; - const templateExpressionStack: number[] = []; - const mask = (char: string): string => (char === "\n" || char === "\r" ? char : " "); - - while (i < content.length) { - const char = content[i]!; - const next = content[i + 1]; - - if (state === "code") { - if (char === "/" && next === "/") { - output += " "; - i += 2; - state = "lineComment"; - continue; - } - if (char === "/" && next === "*") { - output += " "; - i += 2; - state = "blockComment"; - continue; - } - if (char === "'") { - output += " "; - i += 1; - state = "single"; - continue; - } - if (char === '"') { - output += " "; - i += 1; - state = "double"; - continue; - } - if (char === "`") { - output += " "; - i += 1; - state = "template"; - continue; - } - const topTemplateExpression = templateExpressionStack.length - 1; - if (topTemplateExpression >= 0 && char === "{") { - output += char; - i += 1; - templateExpressionStack[topTemplateExpression]! += 1; - continue; - } - if (topTemplateExpression >= 0 && char === "}") { - output += char; - i += 1; - templateExpressionStack[topTemplateExpression]! -= 1; - if (templateExpressionStack[topTemplateExpression] === 0) { - templateExpressionStack.pop(); - state = "template"; - } - continue; - } - output += char; - i += 1; - continue; - } +function contentForResidualMatching(relative: string, content: string): string { + const ext = path.extname(relative).toLowerCase(); + return SOURCE_EXTENSIONS.has(ext) ? maskSourceNonCode(relative, content) : content; +} - if (state === "lineComment") { - output += mask(char); - i += 1; - if (char === "\n" || char === "\r") state = "code"; - continue; - } +function sourceLang(relative: string): Lang { + const ext = path.extname(relative).toLowerCase(); + return ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : Lang.TypeScript; +} - if (state === "blockComment") { - if (char === "*" && next === "/") { - output += " "; - i += 2; - state = "code"; - continue; - } - output += mask(char); - i += 1; - continue; - } +function collectMaskedRanges(node: SgNode, ranges: Array<[number, number]>): void { + if (MASKED_SOURCE_NODE_KINDS.has(node.kind())) { + const range = node.range(); + ranges.push([range.start.index, range.end.index]); + return; + } + for (const child of node.children()) { + collectMaskedRanges(child, ranges); + } +} - if (state === "template") { - if (char === "$" && next === "{") { - output += " "; - i += 2; - state = "code"; - templateExpressionStack.push(1); - continue; - } - output += mask(char); - i += 1; - if (char === "\\") { - if (i < content.length) { - output += mask(content[i]!); - i += 1; - } - continue; - } - if (char === "`") state = "code"; - continue; - } +function maskSourceNonCode(relative: string, content: string): string { + let ranges: Array<[number, number]> = []; + try { + const root = parse(sourceLang(relative), content).root(); + collectMaskedRanges(root, ranges); + } catch { + return content; + } - output += mask(char); - i += 1; - if (char === "\\") { - if (i < content.length) { - output += mask(content[i]!); - i += 1; - } - continue; - } - if ((state === "single" && char === "'") || (state === "double" && char === '"')) { - state = "code"; - } else if (char === "\n" || char === "\r") { - state = "code"; + ranges = ranges.toSorted(([a], [b]) => a - b); + const chars = content.split(""); + for (const [start, end] of ranges) { + for (let i = start; i < end && i < chars.length; i++) { + if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " "; } } - - return output; -} - -function contentForResidualMatching(relative: string, content: string): string { - const ext = path.extname(relative).toLowerCase(); - return SOURCE_EXTENSIONS.has(ext) ? maskSourceNonCode(content) : content; + return chars.join(""); } function isIdentifierChar(char: string | undefined): boolean { From 1c645488edbe72e22d7ebc5b5a3d37b907719ee4 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 20 Jun 2026 23:09:57 +0900 Subject: [PATCH 170/618] fix(sdk-codemod): type masked source kinds --- packages/sdk-codemod/src/runner.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 3aa13735d..5050cd6bf 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -53,7 +53,12 @@ const DEFAULT_FILE_PATTERNS = ["**/*.{ts,tsx,mts,cts}"]; const EXCLUDE_DIRS = new Set(["node_modules", "dist", ".git"]); const ALLOWED_DOT_DIRS = new Set([".github", ".circleci"]); const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]); -const MASKED_SOURCE_NODE_KINDS = new Set(["comment", "string", "regex", "string_fragment"]); +const MASKED_SOURCE_NODE_KINDS: ReadonlySet> = new Set([ + "comment", + "string", + "regex", + "string_fragment", +]); function shouldSkipDirectory(name: string): boolean { return EXCLUDE_DIRS.has(name) || (name.startsWith(".") && !ALLOWED_DOT_DIRS.has(name)); From ed3d338ce71d68904ef1fb83afbbd06a7e5f6973 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 20 Jun 2026 23:41:28 +0900 Subject: [PATCH 171/618] fix(sdk-codemod): add runtime globals import codemod --- .changeset/runtime-globals-import.md | 5 + .../v2/runtime-globals-opt-in/codemod.yaml | 7 + .../scripts/transform.ts | 1604 +++++++++++++++++ .../tests/abstract-class-local/input.ts | 6 + .../tests/already-imported/input.ts | 3 + .../array-destructuring-default/expected.ts | 5 + .../array-destructuring-default/input.ts | 3 + .../tests/bare-type-query/expected.ts | 5 + .../tests/bare-type-query/input.ts | 3 + .../bare-type-with-local-value/expected.ts | 12 + .../tests/bare-type-with-local-value/input.ts | 10 + .../tests/bare-value/expected.ts | 5 + .../tests/bare-value/input.ts | 3 + .../call-signature-parameter/expected.ts | 10 + .../tests/call-signature-parameter/input.ts | 8 + .../catch-block-local-and-global/expected.ts | 13 + .../catch-block-local-and-global/input.ts | 11 + .../tests/catch-local/input.ts | 5 + .../tests/catch-parameterless/expected.ts | 8 + .../tests/catch-parameterless/input.ts | 6 + .../tests/class-expression-local/input.ts | 7 + .../tests/comment-and-string/input.ts | 2 + .../tests/commonjs-declaration/expected.d.cts | 3 + .../tests/commonjs-declaration/input.d.cts | 1 + .../tests/commonjs-skipped/input.cjs | 1 + .../construct-signature-parameter/expected.ts | 10 + .../construct-signature-parameter/input.ts | 8 + .../tests/cts-namespace-value-local/input.cts | 7 + .../tests/cts-skipped/expected.cts | 5 + .../tests/cts-skipped/input.cts | 3 + .../expected.cts | 9 + .../cts-type-only-namespace-value/input.cts | 7 + .../declaration-file-skipped/expected.d.ts | 3 + .../tests/declaration-file-skipped/input.d.ts | 1 + .../input.d.ts | 7 + .../input.d.ts | 11 + .../input.d.ts | 9 + .../expected.d.ts | 11 + .../input.d.ts | 9 + .../expected.d.ts | 14 + .../input.d.ts | 12 + .../input.d.ts | 5 + .../input.d.ts | 5 + .../expected.d.ts | 9 + .../input.d.ts | 7 + .../input.d.ts | 7 + .../expected.d.ts | 9 + .../input.d.ts | 7 + .../tests/decorated-parameter-local/input.ts | 9 + .../destructure-key-not-binding/expected.ts | 5 + .../destructure-key-not-binding/input.ts | 3 + .../tests/destructuring-default/expected.ts | 5 + .../tests/destructuring-default/input.ts | 3 + .../expected.ts | 6 + .../directive-trailing-block-comment/input.ts | 5 + .../directive-trailing-comment/expected.ts | 6 + .../tests/directive-trailing-comment/input.ts | 5 + .../tests/enum-namespace-local/input.ts | 9 + .../tests/error-class-type/expected.ts | 5 + .../tests/error-class-type/input.ts | 3 + .../tests/error-class/expected.ts | 3 + .../tests/error-class/input.ts | 1 + .../export-as-namespace-local/input.d.ts | 2 + .../tests/for-await-local/input.ts | 7 + .../for-block-local-and-global/expected.ts | 11 + .../tests/for-block-local-and-global/input.ts | 9 + .../tests/for-local/input.ts | 3 + .../expected.ts | 9 + .../for-statement-local-and-global/input.ts | 7 + .../tests/for-statement-local/input.ts | 3 + .../expected.ts | 8 + .../function-body-var-default-global/input.ts | 6 + .../tests/function-body-var-local/input.ts | 8 + .../function-body-var-type-global/expected.ts | 8 + .../function-body-var-type-global/input.ts | 6 + .../function-expression-parameter/expected.ts | 9 + .../function-expression-parameter/input.ts | 7 + .../tests/function-type-parameter/expected.ts | 8 + .../tests/function-type-parameter/input.ts | 6 + .../expected.ts | 9 + .../generator-declaration-parameter/input.ts | 7 + .../tests/global-this-alias-chain/expected.ts | 8 + .../tests/global-this-alias-chain/input.ts | 6 + .../global-this-alias-comment/expected.ts | 7 + .../tests/global-this-alias-comment/input.ts | 5 + .../tests/global-this-alias-shadowed/input.ts | 7 + .../expected.ts | 7 + .../input.ts | 5 + .../tests/global-this-alias/expected.ts | 8 + .../tests/global-this-alias/input.ts | 6 + .../tests/global-this-bracket/expected.ts | 6 + .../tests/global-this-bracket/input.ts | 4 + .../input.ts | 6 + .../global-this-computed-key/expected.ts | 7 + .../tests/global-this-computed-key/input.ts | 5 + .../global-this-computed-local-key/input.ts | 5 + .../expected.ts | 9 + .../input.ts | 7 + .../input.ts | 3 + .../expected.ts | 5 + .../input.ts | 3 + .../global-this-destructuring/expected.ts | 7 + .../tests/global-this-destructuring/input.ts | 5 + .../input.ts | 8 + .../global-this-indexed-type-local/input.ts | 7 + .../global-this-indexed-type/expected.ts | 10 + .../tests/global-this-indexed-type/input.ts | 8 + .../global-this-local-shadow/expected.ts | 7 + .../tests/global-this-local-shadow/input.ts | 5 + .../global-this-shorthand-default/expected.ts | 6 + .../global-this-shorthand-default/input.ts | 4 + .../tests/global-this/expected.ts | 5 + .../tests/global-this/input.ts | 3 + .../tests/import-alias-local/input.ts | 9 + .../tests/import-equals-local/input.ts | 8 + .../import-trailing-block-comment/expected.ts | 7 + .../import-trailing-block-comment/input.ts | 6 + .../tests/infer-type-local/input.ts | 4 + .../invalid-reference-after-code/expected.ts | 6 + .../invalid-reference-after-code/input.ts | 4 + .../expected.ts | 8 + .../input.ts | 7 + .../tests/jsx-intrinsic-tag/input.tsx | 8 + .../tests/line-scoped-pragma/expected.ts | 4 + .../tests/line-scoped-pragma/input.ts | 2 + .../tests/local-binding/input.ts | 7 + .../tests/local-global-this/input.ts | 15 + .../local-parameter-and-global/expected.ts | 9 + .../tests/local-parameter-and-global/input.ts | 7 + .../tests/local-parameter-only/input.ts | 5 + .../tests/local-type-binding/input.ts | 5 + .../expected.ts | 6 + .../input.ts | 4 + .../method-signature-parameter/expected.ts | 10 + .../tests/method-signature-parameter/input.ts | 8 + .../input.ts | 9 + .../expected.ts | 11 + .../input.ts | 9 + .../tests/multiple-directives/expected.ts | 8 + .../tests/multiple-directives/input.ts | 7 + .../tests/named-expression-bindings/input.ts | 19 + .../tests/namespace-export-local/input.ts | 2 + .../tests/namespace-var-local/input.ts | 6 + .../namespace-var-scope-global/expected.ts | 10 + .../tests/namespace-var-scope-global/input.ts | 8 + .../namespace-with-local-class/expected.ts | 8 + .../tests/namespace-with-local-class/input.ts | 6 + .../tests/nested-destructuring-local/input.ts | 7 + .../input.ts | 9 + .../nested-import-scope-global/expected.ts | 12 + .../tests/nested-import-scope-global/input.ts | 10 + .../tests/nested-import-scope-local/input.ts | 6 + .../tests/nested-import/expected.ts | 9 + .../tests/nested-import/input.ts | 7 + .../tests/nested-parameter-local/input.ts | 5 + .../tests/non-null-access/expected.ts | 5 + .../tests/non-null-access/input.ts | 3 + .../tests/optional-chain/expected.ts | 5 + .../tests/optional-chain/input.ts | 3 + .../parameter-default-global/expected.ts | 7 + .../tests/parameter-default-global/input.ts | 5 + .../expected.ts | 7 + .../input.ts | 5 + .../expected.ts | 7 + .../input.ts | 5 + .../tests/parameter-property-local/input.ts | 10 + .../tests/parenthesized-access/expected.ts | 5 + .../tests/parenthesized-access/input.ts | 3 + .../tests/preserves-directives/expected.ts | 5 + .../tests/preserves-directives/input.ts | 4 + .../tests/qualified-namespace-local/input.ts | 7 + .../qualified-type-whitespace/expected.ts | 6 + .../tests/qualified-type-whitespace/input.ts | 4 + .../reference-before-directive/expected.cts | 7 + .../reference-before-directive/input.cts | 5 + .../tests/runtime-namespace-local/input.ts | 9 + .../shorthand-assignment-global/expected.ts | 6 + .../shorthand-assignment-global/input.ts | 4 + .../tests/shorthand-assignment-local/input.ts | 4 + .../shorthand-global-reference/expected.ts | 5 + .../tests/shorthand-global-reference/input.ts | 3 + .../tests/specifier-source-name/input.ts | 5 + .../tests/specifier-type-export/input.ts | 4 + .../static-block-var-scope-global/expected.ts | 12 + .../static-block-var-scope-global/input.ts | 10 + .../tests/subscript-access/expected.ts | 5 + .../tests/subscript-access/input.ts | 3 + .../switch-case-local-and-global/expected.ts | 12 + .../switch-case-local-and-global/input.ts | 10 + .../tests/tailor-type-reference/expected.ts | 5 + .../tests/tailor-type-reference/input.ts | 3 + .../tests/tailor-value/expected.ts | 9 + .../tests/tailor-value/input.ts | 8 + .../tailordb-type-annotation/expected.ts | 5 + .../tests/tailordb-type-annotation/input.ts | 3 + .../tests/tailordb-type/expected.ts | 5 + .../tests/tailordb-type/input.ts | 3 + .../tests/trailing-import-comment/expected.ts | 4 + .../tests/trailing-import-comment/input.ts | 3 + .../expected.ts | 3 + .../trailing-next-line-pragma-import/input.ts | 2 + .../type-binding-value-reference/expected.ts | 7 + .../type-binding-value-reference/input.ts | 5 + .../tests/type-level-binders/input.ts | 5 + .../tests/type-only-export-specifier/input.ts | 3 + .../tests/type-only-import/expected.ts | 6 + .../tests/type-only-import/input.ts | 5 + .../expected.ts | 8 + .../type-only-namespace-import-value/input.ts | 7 + .../tests/type-only-namespace-local/input.ts | 7 + .../expected.ts | 9 + .../type-only-namespace-value-global/input.ts | 7 + .../expected.ts | 11 + .../input.ts | 9 + .../tests/type-only-specifier/expected.ts | 6 + .../tests/type-only-specifier/input.ts | 5 + .../tests/unrelated-local-binding/expected.ts | 5 + .../tests/unrelated-local-binding/input.ts | 3 + .../tests/using-local/input.ts | 7 + .../value-binding-type-reference/expected.ts | 8 + .../value-binding-type-reference/input.ts | 6 + .../tests/value-import-local/input.ts | 5 + .../tests/value-import-type-local/input.ts | 8 + .../var-block-local-and-global/expected.ts | 13 + .../tests/var-block-local-and-global/input.ts | 11 + .../tests/var-block-local/input.ts | 9 + .../tests/var-comment-local/input.ts | 7 + .../tests/var-for-of-local/input.ts | 8 + .../tests/var-whitespace-local/input.ts | 5 + .../expected.ts | 6 + .../wrapped-global-this-destructure/input.ts | 4 + .../wrapped-global-this-member/expected.ts | 6 + .../tests/wrapped-global-this-member/input.ts | 4 + .../expected.ts | 7 + .../wrapped-global-this-string-key/input.ts | 5 + .../wrapped-global-this-subscript/expected.ts | 5 + .../wrapped-global-this-subscript/input.ts | 3 + packages/sdk-codemod/src/registry.test.ts | 8 +- packages/sdk-codemod/src/registry.ts | 10 +- packages/sdk-codemod/src/transform.test.ts | 64 +- packages/sdk-codemod/tsdown.config.ts | 2 + packages/sdk/docs/migration/v2.md | 23 +- 242 files changed, 3219 insertions(+), 9 deletions(-) create mode 100644 .changeset/runtime-globals-import.md create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/abstract-class-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/already-imported/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/class-expression-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/comment-and-string/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/expected.d.cts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/input.d.cts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-skipped/input.cjs create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-namespace-value-local/input.cts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/expected.cts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/input.cts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/expected.cts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/input.cts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/expected.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/input.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-class/input.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-namespace/input.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-local-member/input.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/expected.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/input.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/expected.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/input.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-local-member/input.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-value-member/input.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/expected.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/input.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-value-member/input.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/expected.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/input.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/decorated-parameter-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/enum-namespace-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/export-as-namespace-local/input.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-await-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-shadowed/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key-expression/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-local-key/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-local-alias/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-alias-shadowed/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-alias-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-equals-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/infer-type-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/jsx-intrinsic-tag/input.tsx create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-binding/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-global-this/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-only/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-binding/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-existing-runtime-member/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/named-expression-bindings/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-export-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-destructuring-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-global-this-destructure-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-parameter-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-property-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-namespace-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/expected.cts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/input.cts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/runtime-namespace-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-source-name/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-type-export/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-level-binders/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-export-specifier/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/using-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-type-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-comment-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-for-of-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-whitespace-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/input.ts diff --git a/.changeset/runtime-globals-import.md b/.changeset/runtime-globals-import.md new file mode 100644 index 000000000..a1d5625bb --- /dev/null +++ b/.changeset/runtime-globals-import.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Add explicit runtime globals imports to files that still reference ambient `tailor.*` or `tailordb.*` globals. diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/codemod.yaml b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/codemod.yaml new file mode 100644 index 000000000..e87b26dfe --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/runtime-globals-opt-in" +version: "1.0.0" +description: "Add the explicit runtime globals import for files that still use ambient `tailor.*` or `tailordb.*` references" +engine: jssg +language: typescript +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts new file mode 100644 index 000000000..82ef10490 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts @@ -0,0 +1,1604 @@ +import { parse, Lang } from "@ast-grep/napi"; +import type { SgNode } from "@ast-grep/napi"; + +const GLOBALS_IMPORT = 'import "@tailor-platform/sdk/runtime/globals";'; +const GLOBALS_IMPORT_PATH = "@tailor-platform/sdk/runtime/globals"; +const GLOBALS_REFERENCE = '/// '; +const GLOBAL_NAMES = new Set([ + "tailor", + "tailordb", + "TailorDBFileError", + "TailorErrorItem", + "TailorErrorMessage", + "TailorErrors", +]); +const TAILORDB_NAMESPACE_MEMBERS = new Set(["Client", "CommandType", "QueryResult"]); +const TAILOR_NAMESPACE_MEMBERS = new Map([ + ["context", new Set(["Invoker"])], + ["iconv", new Set(["Iconv"])], + [ + "idp", + new Set([ + "Client", + "ClientConfig", + "CreateUserInput", + "ListUsersOptions", + "ListUsersResponse", + "SendPasswordResetEmailInput", + "UpdateUserInput", + "User", + "UserQuery", + ]), + ], + ["workflow", new Set(["AuthInvoker", "TriggerWorkflowOptions"])], +]); +const GLOBAL_NAME_PATTERN = new RegExp(`\\b(?:${[...GLOBAL_NAMES].join("|")})\\b`); +const DECLARATION_PARENT_KINDS = new Set([ + "abstract_class_declaration", + "class_declaration", + "enum_declaration", + "function_declaration", + "generator_function_declaration", + "interface_declaration", + "internal_module", + "type_alias_declaration", +]); +const VALUE_NAMESPACE_MEMBER_KINDS = new Set([ + "abstract_class_declaration", + "class_declaration", + "enum_declaration", + "function_declaration", + "generator_function_declaration", + "lexical_declaration", + "variable_declaration", +]); +const PARAMETER_PARENT_KINDS = new Set([ + "optional_parameter", + "required_parameter", + "rest_pattern", +]); +const PARAMETER_PREFIX_KINDS = new Set([ + "accessibility_modifier", + "decorator", + "override", + "readonly", +]); +const FUNCTION_SCOPE_KINDS = new Set([ + "arrow_function", + "function", + "function_declaration", + "function_expression", + "function_type", + "generator_function_declaration", + "generator_function", + "method_signature", + "method_definition", + "call_signature", + "construct_signature", +]); +const MODULE_SCOPE_KINDS = new Set(["internal_module", "module"]); +const VAR_SCOPE_KINDS = new Set([ + ...FUNCTION_SCOPE_KINDS, + "class_static_block", + ...MODULE_SCOPE_KINDS, +]); +const TYPE_PARAMETER_SCOPE_KINDS = new Set([ + ...FUNCTION_SCOPE_KINDS, + "class_declaration", + "interface_declaration", + "type_alias_declaration", +]); +const ACCESS_EXPRESSION_KINDS = new Set(["member_expression", "subscript_expression"]); +const BARE_GLOBAL_REFERENCE_KINDS = new Set([ + "identifier", + "shorthand_property_identifier", + "shorthand_property_identifier_pattern", + "type_identifier", +]); +const BLOCK_SCOPE_KINDS = new Set(["program", "statement_block", "switch_body"]); +const CATCH_SCOPE_KINDS = new Set(["catch_clause"]); +const FOR_SCOPE_KINDS = new Set(["for_in_statement", "for_statement"]); +const IMPORT_STATEMENT_KINDS = new Set(["import_statement"]); +const WRAPPER_EXPRESSION_KINDS = new Set([ + "as_expression", + "non_null_expression", + "parenthesized_expression", + "satisfies_expression", + "type_assertion", +]); +const EXPRESSION_NAME_PARENT_KINDS = new Set([ + "class", + "function_expression", + "generator_function", +]); +const NON_INITIALIZER_AFTER_BINDING_KINDS = new Set(["comment", "type_annotation"]); +const LINE_SCOPED_PRAGMA_PATTERN = + /^(?:\/\/|\/\*)\s*(?:eslint-disable-next-line\b|oxlint-disable-next-line\b|@ts-(?:expect-error|ignore)\b|prettier-ignore\b|biome-ignore\b|rome-ignore\b|deno-lint-ignore\b|(?:c8|v8|istanbul|node:coverage|coverage)\s+ignore\s+next\b)/; + +type ReferenceNamespace = "namespace" | "type" | "value"; +type BindingNamespace = ReferenceNamespace | "all" | "both" | "type-namespace"; + +interface ScopedBinding { + name: string; + namespace: BindingNamespace; + node: SgNode; + scope: SgNode; +} + +function parseSource(source: string, filePath: string) { + return parse( + filePath.endsWith(".tsx") || filePath.endsWith(".jsx") ? Lang.Tsx : Lang.TypeScript, + source, + ); +} + +function firstNamedChild(node: SgNode): SgNode | undefined { + return node.children().find((child) => child.isNamed()); +} + +function isSameNode(a: SgNode | undefined, b: SgNode): boolean { + return a?.id() === b.id(); +} + +function hasChildKind(node: SgNode | null | undefined, kind: string): boolean { + return node?.children().some((child) => child.kind() === kind) === true; +} + +function importSpecifierLocalName(node: SgNode): string | null { + const identifiers = node.findAll({ rule: { kind: "identifier" } }); + return identifiers.at(-1)?.text() ?? null; +} + +function importSpecifierLocalNode(node: SgNode): SgNode | null { + const identifiers = node.findAll({ rule: { kind: "identifier" } }); + return identifiers.at(-1) ?? null; +} + +function importAliasLocalNode(node: SgNode): SgNode | null { + const local = firstNamedChild(node); + return local?.kind() === "identifier" ? local : null; +} + +function declaratorAncestor(node: SgNode): SgNode | null { + let current = node.parent(); + while (current) { + if (current.kind() === "variable_declarator") return current; + current = current.parent(); + } + return null; +} + +function bindingPatternName(node: SgNode, bindingPattern: SgNode, stop: SgNode): boolean { + let current: SgNode | null = node; + while (current) { + if (current.kind() === "assignment_pattern" || current.kind() === "object_assignment_pattern") { + const bindingTarget = firstNamedChild(current); + return bindingTarget != null && containsNode(bindingTarget, node); + } + if (current.kind() === "pair_pattern") { + const key = firstNamedChild(current); + return key != null && !containsNode(key, node); + } + if (isSameNode(current, bindingPattern)) return true; + if (isSameNode(current, stop)) return false; + current = current.parent(); + } + return false; +} + +function isInVariableBindingPattern(node: SgNode): boolean { + const declarator = declaratorAncestor(node); + if (!declarator) return false; + const bindingPattern = firstNamedChild(declarator); + if (!bindingPattern) return false; + + let current: SgNode | null = node; + while (current) { + if (isSameNode(current, bindingPattern)) return true; + if (isSameNode(current, declarator)) return false; + current = current.parent(); + } + return false; +} + +function isVariableBindingName(node: SgNode): boolean { + const declarator = declaratorAncestor(node); + if (!declarator || !isInVariableBindingPattern(node)) return false; + const bindingPattern = firstNamedChild(declarator); + return bindingPattern != null && bindingPatternName(node, bindingPattern, declarator); +} + +function parameterAncestor(node: SgNode): SgNode | null { + return nearestAncestorKind(node, PARAMETER_PARENT_KINDS); +} + +function parameterBindingPattern(parameter: SgNode): SgNode | undefined { + return parameter + .children() + .find((child) => child.isNamed() && !PARAMETER_PREFIX_KINDS.has(child.kind())); +} + +function isParameterBindingName(node: SgNode): boolean { + const parameter = parameterAncestor(node); + const bindingPattern = parameter ? parameterBindingPattern(parameter) : undefined; + return bindingPattern != null && bindingPatternName(node, bindingPattern, parameter); +} + +function isForBindingName(node: SgNode): boolean { + const parent = nearestAncestorKind(node, new Set(["for_in_statement"])); + if (parent == null || !/^for(?:\s+await)?\s*\(\s*(const|let|var)\s/.test(parent.text())) { + return false; + } + const bindingPattern = firstNamedChild(parent); + return bindingPattern != null && bindingPatternName(node, bindingPattern, parent); +} + +function isForVarBindingName(node: SgNode): boolean { + const parent = nearestAncestorKind(node, new Set(["for_in_statement"])); + return ( + parent != null && /^for(?:\s+await)?\s*\(\s*var\s/.test(parent.text()) && isForBindingName(node) + ); +} + +function isForStatementInitializerBinding(node: SgNode): boolean { + const declarator = declaratorAncestor(node); + const forStatement = nearestAncestorKind(node, FOR_SCOPE_KINDS); + const initializer = + forStatement?.kind() === "for_statement" ? firstNamedChild(forStatement) : undefined; + return declarator != null && initializer != null && containsNode(initializer, declarator); +} + +function isVarBindingName(node: SgNode): boolean { + const declaration = declaratorAncestor(node)?.parent(); + return declaration?.kind() === "variable_declaration" && /^var\b/.test(declaration.text()); +} + +function usingDeclarationAncestor(node: SgNode): SgNode | null { + const parent = node.parent(); + if (parent?.kind() !== "assignment_expression") return null; + if (!parent.children().some((child) => child.kind() === "using")) return null; + return isSameNode(firstNamedChild(parent), node) ? parent : null; +} + +function isUsingBindingName(node: SgNode): boolean { + return usingDeclarationAncestor(node) != null; +} + +function isCatchBindingName(node: SgNode): boolean { + const catchClause = nearestAncestorKind(node, CATCH_SCOPE_KINDS); + if (catchClause == null || !/\bcatch\s*\(/.test(catchClause.text())) return false; + const bindingPattern = catchClause ? firstNamedChild(catchClause) : undefined; + return bindingPattern != null && bindingPatternName(node, bindingPattern, catchClause); +} + +function isInferTypeBindingName(node: SgNode): boolean { + const parent = node.parent(); + return parent?.kind() === "infer_type" && isSameNode(firstNamedChild(parent), node); +} + +function isExportAsNamespaceName(node: SgNode): boolean { + const parent = node.parent(); + return ( + parent?.kind() === "export_statement" && + /^export\s+as\s+namespace\b/.test(parent.text()) && + isSameNode(firstNamedChild(parent), node) + ); +} + +function isBindingIdentifier(node: SgNode): boolean { + const parent = node.parent(); + if (!parent) return false; + + if (isVariableBindingName(node)) return true; + if (isParameterBindingName(node)) return true; + if (isForBindingName(node)) return true; + if (isCatchBindingName(node)) return true; + if (isQualifiedNamespaceBindingName(node)) return true; + if (isUsingBindingName(node)) return true; + if (isInferTypeBindingName(node)) return true; + if (isExportAsNamespaceName(node)) return true; + + const parentKind = parent.kind(); + if (parentKind === "import_alias") + return isSameNode(importAliasLocalNode(parent) ?? undefined, node); + if (parentKind === "import_specifier") return importSpecifierLocalName(parent) === node.text(); + if ( + parentKind === "import_clause" || + parentKind === "import_require_clause" || + parentKind === "namespace_import" + ) { + return true; + } + if (DECLARATION_PARENT_KINDS.has(parentKind)) return isSameNode(firstNamedChild(parent), node); + if (EXPRESSION_NAME_PARENT_KINDS.has(parentKind)) + return isSameNode(firstNamedChild(parent), node); + if (parentKind === "type_parameter" || parentKind === "mapped_type_clause") { + return isSameNode(firstNamedChild(parent), node); + } + if (parentKind === "index_signature") return isSameNode(firstNamedChild(parent), node); + + return false; +} + +function nameNodes(root: SgNode, names: Set): SgNode[] { + const nodes: SgNode[] = []; + const visit = (node: SgNode): void => { + if (node.isNamedLeaf() && names.has(node.text())) nodes.push(node); + for (const child of node.children()) visit(child); + }; + visit(root); + return nodes; +} + +function globalNameNodes(root: SgNode): SgNode[] { + return nameNodes(root, GLOBAL_NAMES); +} + +function nearestAncestorKind(node: SgNode, kinds: Set): SgNode | null { + let current = node.parent(); + while (current) { + if (kinds.has(current.kind())) return current; + current = current.parent(); + } + return null; +} + +function statementBlockScope(container: SgNode): SgNode { + return container.children().find((child) => child.kind() === "statement_block") ?? container; +} + +function varBindingScope(node: SgNode, root: SgNode): SgNode { + const container = nearestAncestorKind(node, VAR_SCOPE_KINDS); + if (container == null) return root; + return FUNCTION_SCOPE_KINDS.has(container.kind()) ? statementBlockScope(container) : container; +} + +function importBindingScope(node: SgNode, root: SgNode): SgNode { + return nearestAncestorKind(node, MODULE_SCOPE_KINDS) ?? root; +} + +function declareGlobalBlock(node: SgNode): SgNode | null { + let current = node.parent(); + while (current) { + if (current.kind() === "ambient_declaration" && hasChildKind(current, "global")) { + return current.children().find((child) => child.kind() === "statement_block") ?? null; + } + current = current.parent(); + } + return null; +} + +function hasDeclareGlobalBlock(root: SgNode): boolean { + let found = false; + const visit = (node: SgNode): void => { + if (found) return; + if (node.kind() === "ambient_declaration" && hasChildKind(node, "global")) { + found = true; + return; + } + for (const child of node.children()) visit(child); + }; + visit(root); + return found; +} + +function isDirectDeclareGlobalBinding(node: SgNode): boolean { + const block = declareGlobalBlock(node); + if (!block) return false; + + let current = node.parent(); + while (current) { + if (isSameNode(current, block)) return true; + if (current.kind() === "statement_block") return false; + current = current.parent(); + } + return false; +} + +function bindingScope(node: SgNode, root: SgNode): SgNode { + const parentKind = node.parent()?.kind(); + if (isVariableBindingName(node) && isVarBindingName(node)) { + return varBindingScope(node, root); + } + if (isForVarBindingName(node)) { + return varBindingScope(node, root); + } + if (isUsingBindingName(node)) return nearestAncestorKind(node, BLOCK_SCOPE_KINDS) ?? root; + if (isForStatementInitializerBinding(node)) { + return nearestAncestorKind(node, FOR_SCOPE_KINDS) ?? root; + } + if ( + parentKind === "import_clause" || + parentKind === "import_alias" || + parentKind === "import_require_clause" || + parentKind === "import_specifier" || + parentKind === "namespace_import" + ) { + return importBindingScope(node, root); + } + if (parentKind != null && EXPRESSION_NAME_PARENT_KINDS.has(parentKind)) { + return node.parent() ?? root; + } + if (parentKind === "type_parameter") { + return nearestAncestorKind(node, TYPE_PARAMETER_SCOPE_KINDS) ?? root; + } + if (parentKind === "infer_type") { + return nearestAncestorKind(node, new Set(["conditional_type"])) ?? node.parent() ?? root; + } + if (parentKind === "mapped_type_clause") { + return nearestAncestorKind(node, new Set(["index_signature"])) ?? root; + } + if (parentKind === "index_signature") return node.parent() ?? root; + if (isExportAsNamespaceName(node)) return root; + if (parentKind != null && PARAMETER_PARENT_KINDS.has(parentKind)) { + return nearestAncestorKind(node, FUNCTION_SCOPE_KINDS) ?? root; + } + const parameter = parameterAncestor(node); + if (parameter) return nearestAncestorKind(parameter, FUNCTION_SCOPE_KINDS) ?? root; + + if (isForBindingName(node)) return nearestAncestorKind(node, FOR_SCOPE_KINDS) ?? root; + if (isCatchBindingName(node)) return nearestAncestorKind(node, CATCH_SCOPE_KINDS) ?? root; + if (isDirectDeclareGlobalBinding(node)) return root; + + return nearestAncestorKind(node, BLOCK_SCOPE_KINDS) ?? root; +} + +function isTypeOnlyImport(node: SgNode): boolean { + const parent = node.parent(); + const importStatement = nearestAncestorKind(node, IMPORT_STATEMENT_KINDS); + return hasChildKind(parent, "type") || hasChildKind(importStatement, "type"); +} + +function hasRuntimeNamespaceMember(node: SgNode): boolean { + const runtimeKinds = new Set([ + "abstract_class_declaration", + "class_declaration", + "enum_declaration", + "export_assignment", + "function_declaration", + "generator_function_declaration", + "lexical_declaration", + "variable_declaration", + ]); + const visit = (current: SgNode): boolean => { + if (runtimeKinds.has(current.kind())) return true; + if (current.kind() === "internal_module") return hasRuntimeNamespaceMember(current); + if ( + current.kind() === "expression_statement" && + current.children().some((child) => child.isNamed()) + ) { + const namedChildren = current.children().filter((child) => child.isNamed()); + return namedChildren.every((child) => child.kind() === "internal_module") + ? namedChildren.some(visit) + : true; + } + return current.children().some(visit); + }; + return ( + node + .children() + .find((child) => child.kind() === "statement_block") + ?.children() + .some(visit) === true + ); +} + +function namespaceBindingModule(node: SgNode): SgNode | null { + const parent = node.parent(); + if (parent?.kind() === "internal_module") return parent; + if (parent?.kind() !== "nested_identifier") return null; + const namespace = parent.parent(); + if (namespace?.kind() !== "internal_module") return null; + return isSameNode(firstNamedChild(parent), node) ? namespace : null; +} + +function isQualifiedNamespaceBindingName(node: SgNode): boolean { + return node.parent()?.kind() === "nested_identifier" && namespaceBindingModule(node) != null; +} + +function isTypeOnlyNamespace(node: SgNode): boolean { + const namespace = namespaceBindingModule(node); + if (!namespace) return false; + if (namespace.parent()?.kind() === "ambient_declaration") return true; + return !hasRuntimeNamespaceMember(namespace); +} + +function initializerAfterBinding(node: SgNode, binding: SgNode): SgNode | null { + return ( + node + .children() + .find( + (child) => + child.isNamed() && + child.range().start.index > binding.range().end.index && + !NON_INITIALIZER_AFTER_BINDING_KINDS.has(child.kind()), + ) ?? null + ); +} + +function isUnshadowedGlobalThisExpression( + node: SgNode | undefined, + globalThisBindings: ScopedBinding[], + globalThisAliases: ScopedBinding[] = [], + aliasValueBindings: ScopedBinding[] = [], +): boolean { + return ( + (isGlobalThisExpression(node) && !isShadowedGlobalThisExpression(node, globalThisBindings)) || + isGlobalThisAliasExpression(node, globalThisAliases, aliasValueBindings) + ); +} + +function globalThisDestructuringBinding( + node: SgNode, + globalThisBindings: ScopedBinding[], + globalThisAliases: ScopedBinding[] = [], + aliasValueBindings: ScopedBinding[] = [], +): SgNode | null { + const declarator = declaratorAncestor(node); + const binding = declarator ? firstNamedChild(declarator) : undefined; + if ( + declarator && + binding && + containsNode(binding, node) && + isUnshadowedGlobalThisExpression( + initializerAfterBinding(declarator, binding) ?? undefined, + globalThisBindings, + globalThisAliases, + aliasValueBindings, + ) + ) { + return binding; + } + + const parameter = parameterAncestor(node); + const parameterBinding = parameter ? parameterBindingPattern(parameter) : undefined; + if ( + parameter && + parameterBinding && + containsNode(parameterBinding, node) && + isUnshadowedGlobalThisExpression( + initializerAfterBinding(parameter, parameterBinding) ?? undefined, + globalThisBindings, + globalThisAliases, + aliasValueBindings, + ) + ) { + return parameterBinding; + } + + const assignment = nearestAncestorKind(node, new Set(["assignment_expression"])); + const assignmentBinding = assignment ? firstNamedChild(assignment) : undefined; + if ( + assignment && + assignmentBinding && + containsNode(assignmentBinding, node) && + isUnshadowedGlobalThisExpression( + initializerAfterBinding(assignment, assignmentBinding) ?? undefined, + globalThisBindings, + globalThisAliases, + aliasValueBindings, + ) + ) { + return assignmentBinding; + } + + return null; +} + +function isGlobalThisDestructuredProperty( + node: SgNode, + globalThisBindings: ScopedBinding[], + globalThisAliases: ScopedBinding[] = [], + aliasValueBindings: ScopedBinding[] = [], +): boolean { + const binding = globalThisDestructuringBinding( + node, + globalThisBindings, + globalThisAliases, + aliasValueBindings, + ); + if (!binding) return false; + if (node.kind() === "shorthand_property_identifier_pattern") { + const parent = node.parent(); + return ( + isSameNode(parent, binding) || + ((parent?.kind() === "assignment_pattern" || + parent?.kind() === "object_assignment_pattern") && + isSameNode(parent.parent(), binding)) + ); + } + + let current = node.parent(); + while (current && !isSameNode(current, binding)) { + if (current.kind() === "object_assignment_pattern") { + const target = firstNamedChild(current); + return isSameNode(current.parent(), binding) && target != null && containsNode(target, node); + } + if (current.kind() === "pair_pattern") { + const key = firstNamedChild(current); + return ( + isSameNode(current.parent(), binding) && + key != null && + destructuredPropertyName(key) === node.text() && + containsNode(key, node) + ); + } + current = current.parent(); + } + return false; +} + +function destructuredPropertyName(key: SgNode): string | null { + if (key.kind() === "computed_property_name") { + const expression = firstNamedChild(key); + return expression ? staticLiteralPropertyName(expression) : null; + } + return staticPropertyName(key); +} + +function staticPropertyName(node: SgNode): string | null { + let current: SgNode | null = node; + while (current && WRAPPER_EXPRESSION_KINDS.has(current.kind())) { + current = firstNamedChild(current) ?? null; + } + if (!current) return null; + + const kind = current.kind(); + if ( + kind === "identifier" || + kind === "property_identifier" || + kind === "shorthand_property_identifier" || + kind === "shorthand_property_identifier_pattern" + ) { + return current.text(); + } + + const text = current.text(); + for (const name of GLOBAL_NAMES) { + if (text === `"${name}"` || text === `'${name}'` || text === `\`${name}\``) { + return name; + } + } + return null; +} + +function staticLiteralPropertyName(node: SgNode): string | null { + let current: SgNode | null = node; + while (current && WRAPPER_EXPRESSION_KINDS.has(current.kind())) { + current = firstNamedChild(current) ?? null; + } + if (!current) return null; + + const text = current.text(); + for (const name of GLOBAL_NAMES) { + if (text === `"${name}"` || text === `'${name}'` || text === `\`${name}\``) { + return name; + } + } + return null; +} + +function bindingNamespace(node: SgNode): BindingNamespace { + const parentKind = node.parent()?.kind(); + if (isQualifiedNamespaceBindingName(node)) { + return isTypeOnlyNamespace(node) ? "type-namespace" : "namespace"; + } + if ( + parentKind === "import_clause" || + parentKind === "import_alias" || + parentKind === "import_require_clause" || + parentKind === "import_specifier" || + parentKind === "namespace_import" + ) { + if (parentKind === "import_alias") return "all"; + if (parentKind === "import_require_clause" || parentKind === "namespace_import") { + return isTypeOnlyImport(node) ? "type-namespace" : "namespace"; + } + return isTypeOnlyImport(node) ? "type" : "both"; + } + if (parentKind === "type_alias_declaration" || parentKind === "interface_declaration") { + return "type"; + } + if (parentKind === "internal_module") { + return isTypeOnlyNamespace(node) ? "type-namespace" : "namespace"; + } + if (parentKind === "class") return "both"; + if (parentKind === "abstract_class_declaration" || parentKind === "class_declaration") { + return "both"; + } + if (parentKind === "enum_declaration") return "all"; + if ( + parentKind === "type_parameter" || + parentKind === "infer_type" || + parentKind === "mapped_type_clause" || + parentKind === "index_signature" + ) { + return "type"; + } + if (isExportAsNamespaceName(node)) return "namespace"; + return "value"; +} + +function localNameBindings(root: SgNode, names: Set): ScopedBinding[] { + return nameNodes(root, names) + .filter(isBindingIdentifier) + .map((node) => ({ + name: node.text(), + namespace: bindingNamespace(node), + node, + scope: bindingScope(node, root), + })); +} + +function localGlobalBindings(root: SgNode): ScopedBinding[] { + return localNameBindings(root, GLOBAL_NAMES); +} + +function localValueBindings(root: SgNode, name: string): ScopedBinding[] { + return localValueBindingsForNames(root, new Set([name])); +} + +function localValueBindingsForNames(root: SgNode, names: Set): ScopedBinding[] { + return localNameBindings(root, names).filter((binding) => + bindingShadowsReference(binding.namespace, "value"), + ); +} + +function isConstVariableDeclarator(node: SgNode): boolean { + const declaration = node.parent(); + return declaration?.kind() === "lexical_declaration" && /\bconst\b/.test(declaration.text()); +} + +function globalThisAliasBindings( + root: SgNode, + globalThisBindings: ScopedBinding[], +): ScopedBinding[] { + const aliases: ScopedBinding[] = []; + const visit = (node: SgNode): void => { + if (node.kind() === "variable_declarator" && isConstVariableDeclarator(node)) { + const binding = firstNamedChild(node); + const aliasValueBindings = + aliases.length === 0 + ? [] + : localValueBindingsForNames(root, new Set(aliases.map((alias) => alias.name))); + if ( + binding?.kind() === "identifier" && + isUnshadowedGlobalThisExpression( + initializerAfterBinding(node, binding) ?? undefined, + globalThisBindings, + aliases, + aliasValueBindings, + ) + ) { + aliases.push({ + name: binding.text(), + namespace: "value", + node: binding, + scope: bindingScope(binding, root), + }); + } + } + + for (const child of node.children()) visit(child); + }; + visit(root); + return aliases; +} + +function containsNode(ancestor: SgNode, node: SgNode): boolean { + let current: SgNode | null = node; + while (current) { + if (isSameNode(current, ancestor)) return true; + current = current.parent(); + } + return false; +} + +function isReExportSpecifier(node: SgNode): boolean { + const exportStatement = nearestAncestorKind(node, new Set(["export_statement"])); + return exportStatement?.children().some((child) => child.kind() === "from") === true; +} + +function isTypeOnlyExportSpecifier(node: SgNode): boolean { + const parent = node.parent(); + if (parent?.kind() !== "export_specifier") return false; + const exportStatement = nearestAncestorKind(node, new Set(["export_statement"])); + return hasChildKind(parent, "type") || hasChildKind(exportStatement, "type"); +} + +function isSpecifierNonReferenceName(node: SgNode): boolean { + const parent = node.parent(); + if (parent?.kind() === "namespace_export") return true; + if (parent?.kind() === "import_specifier") { + return !isSameNode(importSpecifierLocalNode(parent) ?? undefined, node); + } + if (parent?.kind() !== "export_specifier") return false; + + if (isReExportSpecifier(node)) return true; + + const identifiers = parent.findAll({ rule: { kind: "identifier" } }); + return identifiers.length > 1 && isSameNode(identifiers.at(-1), node); +} + +function isJsxTagName(node: SgNode): boolean { + const parent = node.parent(); + return ( + parent != null && + (parent.kind() === "jsx_closing_element" || + parent.kind() === "jsx_opening_element" || + parent.kind() === "jsx_self_closing_element") && + isSameNode(firstNamedChild(parent), node) + ); +} + +function globalThisSubscriptFromStringFragment( + node: SgNode, + globalThisAliases: ScopedBinding[] = [], + aliasValueBindings: ScopedBinding[] = [], +): SgNode | null { + if (node.kind() !== "string_fragment") return null; + let expression = node.parent(); + if (expression?.kind() !== "string" && expression?.kind() !== "template_string") return null; + if (expression.kind() === "template_string" && expression.text() !== `\`${node.text()}\``) { + return null; + } + while (expression.parent() && WRAPPER_EXPRESSION_KINDS.has(expression.parent()?.kind() ?? "")) { + expression = expression.parent(); + } + const subscript = expression.parent(); + if (subscript?.kind() !== "subscript_expression") return null; + return isGlobalThisLikeExpression( + firstNamedChild(subscript), + globalThisAliases, + aliasValueBindings, + ) + ? subscript + : null; +} + +function unwrapParenthesizedType(node: SgNode | undefined): SgNode | undefined { + let current = node; + while (current?.kind() === "parenthesized_type") { + current = firstNamedChild(current); + } + return current; +} + +function typeQueryExpression(node: SgNode | undefined): SgNode | undefined { + const query = unwrapParenthesizedType(node); + return query?.kind() === "type_query" ? firstNamedChild(query) : undefined; +} + +function globalThisLookupTypeFromStringFragment( + node: SgNode, + globalThisAliases: ScopedBinding[] = [], + aliasValueBindings: ScopedBinding[] = [], +): SgNode | null { + if (node.kind() !== "string_fragment") return null; + const stringLiteral = node.parent(); + if (stringLiteral?.kind() !== "string") return null; + const literalType = stringLiteral.parent(); + if (literalType?.kind() !== "literal_type") return null; + const lookupType = literalType.parent(); + if (lookupType?.kind() !== "lookup_type") return null; + + const targetType = firstNamedChild(lookupType); + if (targetType == null || isSameNode(targetType, literalType)) return null; + const expression = typeQueryExpression(targetType); + return expression != null && + isGlobalThisLikeExpression(expression, globalThisAliases, aliasValueBindings) + ? lookupType + : null; +} + +function identifierExpression(node: SgNode | undefined): SgNode | null { + if (node == null) return null; + if (node.kind() === "identifier") return node; + if (WRAPPER_EXPRESSION_KINDS.has(node.kind())) { + return identifierExpression(firstNamedChild(node)); + } + return null; +} + +function globalThisIdentifier(node: SgNode | undefined): SgNode | null { + const identifier = identifierExpression(node); + return identifier?.text() === "globalThis" ? identifier : null; +} + +function isGlobalThisExpression(node: SgNode | undefined): boolean { + return globalThisIdentifier(node) != null; +} + +function isReferenceToScopedBinding( + identifier: SgNode, + binding: ScopedBinding, + bindings: ScopedBinding[], +): boolean { + if (identifier.text() !== binding.name || !containsNode(binding.scope, identifier)) return false; + return !bindings.some( + (candidate) => + candidate.name === binding.name && + !isSameNode(candidate.node, binding.node) && + containsNode(candidate.scope, identifier) && + containsNode(binding.scope, candidate.node), + ); +} + +function isGlobalThisAliasExpression( + node: SgNode | undefined, + globalThisAliases: ScopedBinding[], + aliasValueBindings: ScopedBinding[], +): boolean { + const identifier = identifierExpression(node); + return ( + identifier != null && + globalThisAliases.some((alias) => + isReferenceToScopedBinding(identifier, alias, aliasValueBindings), + ) + ); +} + +function isGlobalThisLikeExpression( + node: SgNode | undefined, + globalThisAliases: ScopedBinding[] = [], + aliasValueBindings: ScopedBinding[] = [], +): boolean { + return ( + isGlobalThisExpression(node) || + isGlobalThisAliasExpression(node, globalThisAliases, aliasValueBindings) + ); +} + +function isShadowedGlobalThisExpression( + node: SgNode | undefined, + globalThisBindings: ScopedBinding[], +): boolean { + const identifier = globalThisIdentifier(node); + return ( + identifier != null && + globalThisBindings.some((binding) => containsNode(binding.scope, identifier)) + ); +} + +function globalReferenceAncestor( + node: SgNode, + globalThisAliases: ScopedBinding[] = [], + aliasValueBindings: ScopedBinding[] = [], +): SgNode | null { + if (!GLOBAL_NAMES.has(node.text())) return null; + if (isJsxTagName(node)) return null; + if (isSpecifierNonReferenceName(node)) return null; + const globalThisSubscript = globalThisSubscriptFromStringFragment( + node, + globalThisAliases, + aliasValueBindings, + ); + if (globalThisSubscript) return globalThisSubscript; + const globalThisLookupType = globalThisLookupTypeFromStringFragment( + node, + globalThisAliases, + aliasValueBindings, + ); + if (globalThisLookupType) return globalThisLookupType; + + let current = node.parent(); + while (current) { + const kind = current.kind(); + if (kind === "nested_identifier" || kind === "nested_type_identifier") { + const path = qualifiedPath(current); + if (path?.[0] !== node.text()) return null; + let reference = current; + while ( + reference.parent() && + (reference.parent()?.kind() === "nested_identifier" || + reference.parent()?.kind() === "nested_type_identifier") && + qualifiedPath(reference.parent()!)?.[0] === node.text() + ) { + reference = reference.parent()!; + } + return reference; + } + if (ACCESS_EXPRESSION_KINDS.has(kind)) { + const object = firstNamedChild(current); + if (isGlobalThisLikeExpression(object, globalThisAliases, aliasValueBindings)) return current; + if (object == null || !containsNode(object, node)) return null; + let reference = current; + while ( + reference.parent() && + ACCESS_EXPRESSION_KINDS.has(reference.parent()?.kind() ?? "") && + containsNode(firstNamedChild(reference.parent()!) ?? reference.parent()!, node) + ) { + reference = reference.parent()!; + } + return reference; + } + if (WRAPPER_EXPRESSION_KINDS.has(kind)) { + current = current.parent(); + continue; + } + return BARE_GLOBAL_REFERENCE_KINDS.has(node.kind()) && !isBindingIdentifier(node) ? node : null; + } + return BARE_GLOBAL_REFERENCE_KINDS.has(node.kind()) && !isBindingIdentifier(node) ? node : null; +} + +function globalThisReferenceObject(reference: SgNode): SgNode | undefined { + if (ACCESS_EXPRESSION_KINDS.has(reference.kind())) return firstNamedChild(reference); + if (reference.kind() === "lookup_type") return typeQueryExpression(firstNamedChild(reference)); + return undefined; +} + +function isGlobalReference( + node: SgNode, + globalThisAliases: ScopedBinding[] = [], + aliasValueBindings: ScopedBinding[] = [], +): boolean { + return globalReferenceAncestor(node, globalThisAliases, aliasValueBindings) != null; +} + +function referenceNamespace( + node: SgNode, + globalThisAliases: ScopedBinding[] = [], + aliasValueBindings: ScopedBinding[] = [], +): ReferenceNamespace { + const reference = globalReferenceAncestor(node, globalThisAliases, aliasValueBindings); + const referenceKind = reference?.kind(); + if (referenceKind === "nested_identifier" || referenceKind === "nested_type_identifier") { + return "namespace"; + } + if (isTypeOnlyExportSpecifier(node)) return "type"; + return referenceKind === "type_identifier" ? "type" : "value"; +} + +function runtimeNamespaceMemberPath( + node: SgNode, + globalThisAliases: ScopedBinding[] = [], + aliasValueBindings: ScopedBinding[] = [], +): string[] | null { + const reference = globalReferenceAncestor(node, globalThisAliases, aliasValueBindings); + const referenceKind = reference?.kind(); + if ( + referenceKind !== "nested_identifier" && + referenceKind !== "nested_type_identifier" && + referenceKind !== "member_expression" + ) { + return null; + } + + const parts = qualifiedPath(reference); + if (!parts) return null; + if (parts[0] !== node.text()) return null; + + if (parts[0] === "tailordb" && TAILORDB_NAMESPACE_MEMBERS.has(parts[1] ?? "")) { + return parts.slice(0, 2); + } + + const tailorMembers = parts[0] === "tailor" ? TAILOR_NAMESPACE_MEMBERS.get(parts[1] ?? "") : null; + if (tailorMembers?.has(parts[2] ?? "") === true) { + return parts.slice(0, 3); + } + + return null; +} + +function qualifiedPath(node: SgNode): string[] | null { + if ( + node.kind() === "identifier" || + node.kind() === "property_identifier" || + node.kind() === "type_identifier" + ) { + return [node.text()]; + } + if ( + node.kind() !== "member_expression" && + node.kind() !== "nested_identifier" && + node.kind() !== "nested_type_identifier" + ) { + return null; + } + + const parts: string[] = []; + for (const child of node.children()) { + if (!child.isNamed() || child.kind() === "comment") continue; + const childPath = qualifiedPath(child); + if (!childPath) return null; + parts.push(...childPath); + } + return parts.length > 0 ? parts : null; +} + +function dotPath(text: string): string[] { + return text.split(".").filter(Boolean); +} + +function startsWithPath(path: string[], prefix: string[]): boolean { + return prefix.length <= path.length && prefix.every((part, index) => path[index] === part); +} + +function namespaceBindingPath(node: SgNode): string[] | null { + const namespace = namespaceBindingModule(node); + const name = namespace ? firstNamedChild(namespace) : undefined; + if (name?.kind() !== "identifier" && name?.kind() !== "nested_identifier") return null; + return qualifiedPath(name) ?? dotPath(name.text()); +} + +function namespaceStatementBlock(node: SgNode): SgNode | null { + return node.children().find((child) => child.kind() === "statement_block") ?? null; +} + +function unwrapNamespaceMember(node: SgNode): SgNode | null { + if (node.kind() === "export_statement") { + return ( + node.children().find((child) => child.isNamed() && child.kind() !== "export_clause") ?? null + ); + } + if (node.kind() === "expression_statement") { + const namedChildren = node.children().filter((child) => child.isNamed()); + return namedChildren.length === 1 ? namedChildren[0] : null; + } + return node; +} + +function namespaceMemberPath(node: SgNode): string[] | null { + const member = unwrapNamespaceMember(node); + const name = member ? namespaceMemberName(member) : undefined; + if (!name) return null; + if (member?.kind() === "internal_module") return qualifiedPath(name) ?? dotPath(name.text()); + if (DECLARATION_PARENT_KINDS.has(member?.kind() ?? "")) return [name.text()]; + return null; +} + +function namespaceMemberName(member: SgNode): SgNode | null { + if (member.kind() === "lexical_declaration" || member.kind() === "variable_declaration") { + const declarator = firstNamedChild(member); + return declarator ? (firstNamedChild(declarator) ?? null) : null; + } + return firstNamedChild(member) ?? null; +} + +function namespaceValueMemberPath(node: SgNode): string[] | null { + const member = unwrapNamespaceMember(node); + const name = member ? namespaceMemberName(member) : undefined; + if (!member || !name) return null; + if (member.kind() === "internal_module") { + return hasRuntimeNamespaceMember(member) ? (qualifiedPath(name) ?? dotPath(name.text())) : null; + } + if (VALUE_NAMESPACE_MEMBER_KINDS.has(member.kind())) return [name.text()]; + return null; +} + +function namespaceDeclaresPath(namespace: SgNode, path: string[]): boolean { + if (path.length === 0) return true; + + const block = namespaceStatementBlock(namespace); + if (!block) return false; + + for (const child of block.children()) { + if (!child.isNamed()) continue; + const member = unwrapNamespaceMember(child); + if (!member) continue; + + const memberPath = namespaceMemberPath(member); + if (!memberPath || !startsWithPath(path, memberPath)) continue; + if (memberPath.length === path.length) return true; + if ( + member.kind() === "internal_module" && + namespaceDeclaresPath(member, path.slice(memberPath.length)) + ) { + return true; + } + } + + return false; +} + +function namespaceDeclaresValuePath(namespace: SgNode, path: string[]): boolean { + if (path.length === 0) return hasRuntimeNamespaceMember(namespace); + + const block = namespaceStatementBlock(namespace); + if (!block) return false; + + for (const child of block.children()) { + if (!child.isNamed()) continue; + const member = unwrapNamespaceMember(child); + if (!member) continue; + + const valuePath = namespaceValueMemberPath(member); + if (valuePath && startsWithPath(path, valuePath) && valuePath.length === path.length) { + return true; + } + + const memberPath = namespaceMemberPath(member); + if ( + member?.kind() === "internal_module" && + memberPath && + startsWithPath(path, memberPath) && + namespaceDeclaresValuePath(member, path.slice(memberPath.length)) + ) { + return true; + } + } + + return false; +} + +function namespaceBindingDeclaresPath(node: SgNode, path: string[]): boolean { + const namespace = namespaceBindingModule(node); + const namespacePath = namespaceBindingPath(node); + if (!namespace || !namespacePath || !startsWithPath(path, namespacePath)) return false; + return namespaceDeclaresPath(namespace, path.slice(namespacePath.length)); +} + +function namespaceBindingDeclaresValuePath(node: SgNode, path: string[]): boolean { + const namespace = namespaceBindingModule(node); + const namespacePath = namespaceBindingPath(node); + if (!namespace || !namespacePath || !startsWithPath(path, namespacePath)) return false; + return namespaceDeclaresValuePath(namespace, path.slice(namespacePath.length)); +} + +function isGlobalThisReference( + node: SgNode, + globalThisAliases: ScopedBinding[] = [], + aliasValueBindings: ScopedBinding[] = [], +): boolean { + const reference = globalReferenceAncestor(node, globalThisAliases, aliasValueBindings); + const object = reference ? globalThisReferenceObject(reference) : undefined; + return ( + object != null && isGlobalThisLikeExpression(object, globalThisAliases, aliasValueBindings) + ); +} + +function bindingShadowsReference( + binding: BindingNamespace, + reference: ReferenceNamespace, +): boolean { + if (binding === "all") return true; + if (binding === "both") return reference === "type" || reference === "value"; + if (binding === "namespace") return reference === "namespace" || reference === "value"; + if (binding === "type-namespace") return reference === "namespace"; + return binding === reference; +} + +function isShadowedGlobalThisReference( + node: SgNode, + globalThisBindings: ScopedBinding[], + globalThisAliases: ScopedBinding[] = [], + aliasValueBindings: ScopedBinding[] = [], +): boolean { + const reference = globalReferenceAncestor(node, globalThisAliases, aliasValueBindings); + const object = reference ? globalThisReferenceObject(reference) : undefined; + return object != null && isShadowedGlobalThisExpression(object, globalThisBindings); +} + +function isShadowedReference( + node: SgNode, + bindings: ScopedBinding[], + mergeRuntimeNamespaceAugmentations: boolean, + globalThisAliases: ScopedBinding[] = [], + aliasValueBindings: ScopedBinding[] = [], +): boolean { + if (isGlobalThisReference(node, globalThisAliases, aliasValueBindings)) return false; + const namespace = referenceNamespace(node, globalThisAliases, aliasValueBindings); + return bindings.some((binding) => { + if (binding.name !== node.text() || !containsNode(binding.scope, node)) return false; + const runtimePath = runtimeNamespaceMemberPath(node, globalThisAliases, aliasValueBindings); + if ( + mergeRuntimeNamespaceAugmentations && + binding.namespace === "type-namespace" && + runtimePath != null && + (namespace === "namespace" || namespace === "value") + ) { + if (namespace === "value") { + if (namespaceBindingDeclaresValuePath(binding.node, runtimePath)) return true; + } else if (namespaceBindingDeclaresPath(binding.node, runtimePath)) { + return true; + } + if (namespace === "namespace") return false; + } + return bindingShadowsReference(binding.namespace, namespace); + }); +} + +function unshadowedRuntimeGlobalReferences( + root: SgNode, + mergeRuntimeNamespaceAugmentations: boolean, +): SgNode[] { + const bindings = localGlobalBindings(root); + const globalThisBindings = localValueBindings(root, "globalThis"); + const globalThisAliases = globalThisAliasBindings(root, globalThisBindings); + const aliasValueBindings = localValueBindingsForNames( + root, + new Set(globalThisAliases.map((alias) => alias.name)), + ); + return globalNameNodes(root).filter( + (node) => + isGlobalThisDestructuredProperty( + node, + globalThisBindings, + globalThisAliases, + aliasValueBindings, + ) || + (isGlobalReference(node, globalThisAliases, aliasValueBindings) && + !isShadowedGlobalThisReference( + node, + globalThisBindings, + globalThisAliases, + aliasValueBindings, + ) && + !isShadowedReference( + node, + bindings, + mergeRuntimeNamespaceAugmentations, + globalThisAliases, + aliasValueBindings, + )), + ); +} + +function topLevelImports(root: SgNode): SgNode[] { + return root.children().filter((node) => node.kind() === "import_statement"); +} + +function hasGlobalsImport(root: SgNode): boolean { + return topLevelImports(root).some((node) => { + return ( + node.text().includes(`"${GLOBALS_IMPORT_PATH}"`) || + node.text().includes(`'${GLOBALS_IMPORT_PATH}'`) + ); + }); +} + +function hasGlobalsReference(source: string): boolean { + let pos = 0; + if (source.startsWith("#!")) { + const firstLineEnd = source.indexOf("\n"); + pos = firstLineEnd === -1 ? source.length : firstLineEnd + 1; + } + + while (pos < source.length) { + const rest = source.slice(pos); + const whitespace = rest.match(/^[ \t\r\n]+/); + if (whitespace) { + pos += whitespace[0].length; + continue; + } + + if (rest.startsWith("///")) { + const end = rest.indexOf("\n"); + const comment = end === -1 ? rest : rest.slice(0, end); + if ( + /^\/\/\/\s*/.test( + comment, + ) + ) { + return true; + } + pos += end === -1 ? rest.length : end + 1; + continue; + } + + if (rest.startsWith("//")) { + const end = rest.indexOf("\n"); + pos += end === -1 ? rest.length : end + 1; + continue; + } + + if (rest.startsWith("/*")) { + const end = rest.indexOf("*/"); + if (end === -1) return false; + pos += end + 2; + continue; + } + + return false; + } + + return false; +} + +function hasGlobalsOptIn(source: string, root: SgNode): boolean { + return hasGlobalsImport(root) || hasGlobalsReference(source); +} + +function isLineScopedPragmaComment(comment: string): boolean { + return LINE_SCOPED_PRAGMA_PATTERN.test(comment); +} + +function prologueEnd(source: string): number { + let pos = 0; + let consumedDirective = false; + let lastDirectiveEnd = 0; + if (source.startsWith("#!")) { + const firstLineEnd = source.indexOf("\n"); + pos = firstLineEnd === -1 ? source.length : firstLineEnd + 1; + lastDirectiveEnd = pos; + } + + while (pos < source.length) { + const rest = source.slice(pos); + const whitespace = rest.match(/^[ \t\r\n]+/); + if (whitespace) { + pos += whitespace[0].length; + continue; + } + + if (rest.startsWith("//")) { + const end = rest.indexOf("\n"); + const comment = end === -1 ? rest : rest.slice(0, end); + if (isLineScopedPragmaComment(comment)) return consumedDirective ? lastDirectiveEnd : pos; + pos += end === -1 ? rest.length : end + 1; + continue; + } + + if (rest.startsWith("/*")) { + const end = rest.indexOf("*/"); + if (end === -1) return pos; + const comment = rest.slice(0, end + 2); + if (isLineScopedPragmaComment(comment)) return consumedDirective ? lastDirectiveEnd : pos; + pos += end + 2; + continue; + } + + const directive = rest.match(/^(['"])(?:\\.|(?!\1).)*\1[ \t]*;?[ \t]*/); + if (!directive) return consumedDirective ? lastDirectiveEnd : pos; + + let directiveLength = directive[0].length; + while (directiveLength < rest.length) { + const trailing = rest.slice(directiveLength); + const spacing = trailing.match(/^[ \t]+/); + if (spacing) { + directiveLength += spacing[0].length; + continue; + } + + const lineComment = trailing.match(/^\/\/[^\r\n]*/); + if (lineComment) { + directiveLength += lineComment[0].length; + break; + } + + const blockComment = trailing.match(/^\/\*[\s\S]*?\*\//); + if (blockComment) { + directiveLength += blockComment[0].length; + continue; + } + + break; + } + + const afterDirective = rest.slice(directiveLength); + const lineEnd = afterDirective.match(/^\r?\n|^$/); + if (!lineEnd) return pos; + pos += directiveLength; + pos += lineEnd[0].length; + consumedDirective = true; + lastDirectiveEnd = pos; + } + + return consumedDirective ? lastDirectiveEnd : pos; +} + +function referenceDirectiveInsertPos(source: string): number { + let pos = 0; + if (source.startsWith("#!")) { + const firstLineEnd = source.indexOf("\n"); + pos = firstLineEnd === -1 ? source.length : firstLineEnd + 1; + } + + while (pos < source.length) { + const rest = source.slice(pos); + const whitespace = rest.match(/^[ \t\r\n]+/); + if (whitespace) { + pos += whitespace[0].length; + continue; + } + + if (rest.startsWith("//")) { + const end = rest.indexOf("\n"); + const comment = end === -1 ? rest : rest.slice(0, end); + if (isLineScopedPragmaComment(comment)) return pos; + pos += end === -1 ? rest.length : end + 1; + continue; + } + + if (rest.startsWith("/*")) { + const end = rest.indexOf("*/"); + if (end === -1) return pos; + const comment = rest.slice(0, end + 2); + if (isLineScopedPragmaComment(comment)) return pos; + pos += end + 2; + continue; + } + + return pos; + } + + return pos; +} + +function statementEndWithTrailingComments( + source: string, + statementStart: number, + pos: number, +): number { + let current = pos; + while (current < source.length) { + const rest = source.slice(current); + const spacing = rest.match(/^[ \t]+/); + if (spacing) { + current += spacing[0].length; + continue; + } + + if (rest.startsWith("//")) { + const end = rest.indexOf("\n"); + const comment = end === -1 ? rest : rest.slice(0, end); + if (isLineScopedPragmaComment(comment)) return statementStart; + return end === -1 ? source.length : current + end; + } + + if (rest.startsWith("/*")) { + const end = rest.indexOf("*/"); + if (end === -1) return current; + const comment = rest.slice(0, end + 2); + if (isLineScopedPragmaComment(comment)) return statementStart; + current += end + 2; + continue; + } + + break; + } + + const lineEnd = source.indexOf("\n", current); + return lineEnd === -1 ? source.length : lineEnd; +} + +function addGlobalsImport(source: string, root: SgNode): string { + const imports = topLevelImports(root); + const lastImport = imports.at(-1); + if (lastImport) { + const importEnd = lastImport.range().end.index; + const lineEnd = statementEndWithTrailingComments( + source, + lastImport.range().start.index, + importEnd, + ); + const insertPos = lineEnd === -1 ? source.length : lineEnd; + if (insertPos === lastImport.range().start.index) { + return `${source.slice(0, insertPos)}${GLOBALS_IMPORT}\n${source.slice(insertPos)}`; + } + return `${source.slice(0, insertPos)}\n${GLOBALS_IMPORT}${source.slice(insertPos)}`; + } + + const insertPos = prologueEnd(source); + const suffix = insertPos === 0 ? "\n\n" : "\n"; + return `${source.slice(0, insertPos)}${GLOBALS_IMPORT}${suffix}${source.slice(insertPos)}`; +} + +function addGlobalsReference(source: string): string { + const insertPos = referenceDirectiveInsertPos(source); + const suffix = insertPos === 0 ? "\n\n" : "\n"; + return `${source.slice(0, insertPos)}${GLOBALS_REFERENCE}${suffix}${source.slice(insertPos)}`; +} + +/** + * Add the explicit side-effect import required for ambient runtime globals in v2. + * @param source - File contents + * @param filePath - Absolute path to the file + * @returns Transformed source or null when no safe change is needed. + */ +export default function transform(source: string, filePath: string): string | null { + const isDeclarationFile = /\.d\.[cm]?ts$/.test(filePath); + const usesReferenceDirective = isDeclarationFile || filePath.endsWith(".cts"); + if (filePath.endsWith(".cjs")) return null; + if (!GLOBAL_NAME_PATTERN.test(source)) return null; + + const root = parseSource(source, filePath).root(); + const mergeRuntimeNamespaceAugmentations = usesReferenceDirective || hasDeclareGlobalBlock(root); + if (unshadowedRuntimeGlobalReferences(root, mergeRuntimeNamespaceAugmentations).length === 0) { + return null; + } + if (hasGlobalsOptIn(source, root)) return null; + if (usesReferenceDirective) return addGlobalsReference(source); + + return addGlobalsImport(source, root); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/abstract-class-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/abstract-class-local/input.ts new file mode 100644 index 000000000..627cc4cd8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/abstract-class-local/input.ts @@ -0,0 +1,6 @@ +abstract class TailorErrors extends Error {} + +type ErrorCtor = TailorErrors; + +export { TailorErrors }; +export type { ErrorCtor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/already-imported/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/already-imported/input.ts new file mode 100644 index 000000000..388fff3ee --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/already-imported/input.ts @@ -0,0 +1,3 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const client = new tailor.idp.Client(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/expected.ts new file mode 100644 index 000000000..d5f0f8765 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const [client = new tailor.idp.Client()] = opts; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/input.ts new file mode 100644 index 000000000..78ee6cdb5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/input.ts @@ -0,0 +1,3 @@ +const [client = new tailor.idp.Client()] = opts; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/expected.ts new file mode 100644 index 000000000..056bcbe09 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +type Runtime = keyof typeof tailor; + +export type { Runtime }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/input.ts new file mode 100644 index 000000000..6b1a9668a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/input.ts @@ -0,0 +1,3 @@ +type Runtime = keyof typeof tailor; + +export type { Runtime }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/expected.ts new file mode 100644 index 000000000..2a7edae04 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/expected.ts @@ -0,0 +1,12 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const TailorErrors = { + hasError() { + return false; + }, +}; + +type ErrorBag = TailorErrors; + +export { TailorErrors }; +export type { ErrorBag }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/input.ts new file mode 100644 index 000000000..ef3f75a94 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/input.ts @@ -0,0 +1,10 @@ +const TailorErrors = { + hasError() { + return false; + }, +}; + +type ErrorBag = TailorErrors; + +export { TailorErrors }; +export type { ErrorBag }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/expected.ts new file mode 100644 index 000000000..933f7c7d2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const runtime = tailor; + +export { runtime }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/input.ts new file mode 100644 index 000000000..da25597b8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/input.ts @@ -0,0 +1,3 @@ +const runtime = tailor; + +export { runtime }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/expected.ts new file mode 100644 index 000000000..8ffec9837 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/expected.ts @@ -0,0 +1,10 @@ +import "@tailor-platform/sdk/runtime/globals"; + +type Handler = { + (tailor: unknown): void; +}; + +const client = new tailor.idp.Client(); + +export { client }; +export type { Handler }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/input.ts new file mode 100644 index 000000000..54a8678c4 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/input.ts @@ -0,0 +1,8 @@ +type Handler = { + (tailor: unknown): void; +}; + +const client = new tailor.idp.Client(); + +export { client }; +export type { Handler }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/expected.ts new file mode 100644 index 000000000..0e19a3a73 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/expected.ts @@ -0,0 +1,13 @@ +import "@tailor-platform/sdk/runtime/globals"; + +try { + run(); +} catch (error) { + { + const tailor = localClient; + tailor.run(); + } + + const client = new tailor.idp.Client(); + use(client); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/input.ts new file mode 100644 index 000000000..35d64ad98 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/input.ts @@ -0,0 +1,11 @@ +try { + run(); +} catch (error) { + { + const tailor = localClient; + tailor.run(); + } + + const client = new tailor.idp.Client(); + use(client); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-local/input.ts new file mode 100644 index 000000000..f27da942f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-local/input.ts @@ -0,0 +1,5 @@ +try { + run(); +} catch (tailor) { + tailor.run(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/expected.ts new file mode 100644 index 000000000..e377017fa --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/expected.ts @@ -0,0 +1,8 @@ +import "@tailor-platform/sdk/runtime/globals"; + +try { + run(); +} catch { + const client = new tailor.idp.Client(); + use(client); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/input.ts new file mode 100644 index 000000000..7414196d3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/input.ts @@ -0,0 +1,6 @@ +try { + run(); +} catch { + const client = new tailor.idp.Client(); + use(client); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/class-expression-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/class-expression-local/input.ts new file mode 100644 index 000000000..dff45bcca --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/class-expression-local/input.ts @@ -0,0 +1,7 @@ +const RuntimeError = class TailorErrors extends Error { + value(): TailorErrors { + return this; + } +}; + +export { RuntimeError }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/comment-and-string/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/comment-and-string/input.ts new file mode 100644 index 000000000..cda85fb61 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/comment-and-string/input.ts @@ -0,0 +1,2 @@ +// tailor.idp.Client is documented here only. +const example = "tailordb.QueryResult"; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/expected.d.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/expected.d.cts new file mode 100644 index 000000000..e528ee417 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/expected.d.cts @@ -0,0 +1,3 @@ +/// + +type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/input.d.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/input.d.cts new file mode 100644 index 000000000..a817f947d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/input.d.cts @@ -0,0 +1 @@ +type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-skipped/input.cjs b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-skipped/input.cjs new file mode 100644 index 000000000..ad7487b36 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-skipped/input.cjs @@ -0,0 +1 @@ +const client = new tailor.idp.Client(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/expected.ts new file mode 100644 index 000000000..c9c27540d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/expected.ts @@ -0,0 +1,10 @@ +import "@tailor-platform/sdk/runtime/globals"; + +type Constructor = { + new (tailor: unknown): unknown; +}; + +const client = new tailor.idp.Client(); + +export { client }; +export type { Constructor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/input.ts new file mode 100644 index 000000000..96e743463 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/input.ts @@ -0,0 +1,8 @@ +type Constructor = { + new (tailor: unknown): unknown; +}; + +const client = new tailor.idp.Client(); + +export { client }; +export type { Constructor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-namespace-value-local/input.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-namespace-value-local/input.cts new file mode 100644 index 000000000..e757aa38e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-namespace-value-local/input.cts @@ -0,0 +1,7 @@ +namespace tailordb { + export class Client {} +} + +const C = tailordb.Client; + +export { C }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/expected.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/expected.cts new file mode 100644 index 000000000..3f5c0ef47 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/expected.cts @@ -0,0 +1,5 @@ +/// + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/input.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/input.cts new file mode 100644 index 000000000..2e76ceaac --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/input.cts @@ -0,0 +1,3 @@ +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/expected.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/expected.cts new file mode 100644 index 000000000..8a6ea5cd3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/expected.cts @@ -0,0 +1,9 @@ +/// + +namespace tailordb { + export interface Client {} +} + +const C = tailordb.Client; + +export { C }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/input.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/input.cts new file mode 100644 index 000000000..f46312655 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/input.cts @@ -0,0 +1,7 @@ +namespace tailordb { + export interface Client {} +} + +const C = tailordb.Client; + +export { C }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/expected.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/expected.d.ts new file mode 100644 index 000000000..e528ee417 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/expected.d.ts @@ -0,0 +1,3 @@ +/// + +type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/input.d.ts new file mode 100644 index 000000000..a817f947d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/input.d.ts @@ -0,0 +1 @@ +type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-class/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-class/input.d.ts new file mode 100644 index 000000000..0f2511f66 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-class/input.d.ts @@ -0,0 +1,7 @@ +export {}; + +declare global { + class TailorErrors extends Error {} +} + +type ErrorCtor = typeof TailorErrors; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-namespace/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-namespace/input.d.ts new file mode 100644 index 000000000..6efd20c5f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-namespace/input.d.ts @@ -0,0 +1,11 @@ +export {}; + +declare global { + namespace tailor { + namespace idp { + type User = string; + } + } +} + +type RuntimeUser = tailor.idp.User; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-local-member/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-local-member/input.d.ts new file mode 100644 index 000000000..df6cad4fd --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-local-member/input.d.ts @@ -0,0 +1,9 @@ +export {}; + +declare global { + namespace tailordb { + type Row = string; + } +} + +type Row = tailordb.Row; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/expected.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/expected.d.ts new file mode 100644 index 000000000..b517d0b9f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/expected.d.ts @@ -0,0 +1,11 @@ +/// + +export {}; + +declare global { + namespace tailordb { + type Row = string; + } +} + +type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/input.d.ts new file mode 100644 index 000000000..d2b7d36f2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/input.d.ts @@ -0,0 +1,9 @@ +export {}; + +declare global { + namespace tailordb { + type Row = string; + } +} + +type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/expected.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/expected.d.ts new file mode 100644 index 000000000..8b31b6270 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/expected.d.ts @@ -0,0 +1,14 @@ +/// + +declare namespace tailordb { + export type Row = string; +} + +declare namespace tailor { + namespace idp { + export type LocalUser = string; + } +} + +type Rows = tailordb.QueryResult; +type RuntimeUser = tailor.idp.User; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/input.d.ts new file mode 100644 index 000000000..68149881f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/input.d.ts @@ -0,0 +1,12 @@ +declare namespace tailordb { + export type Row = string; +} + +declare namespace tailor { + namespace idp { + export type LocalUser = string; + } +} + +type Rows = tailordb.QueryResult; +type RuntimeUser = tailor.idp.User; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-local-member/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-local-member/input.d.ts new file mode 100644 index 000000000..daf8a2eea --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-local-member/input.d.ts @@ -0,0 +1,5 @@ +declare namespace tailordb { + export type Row = string; +} + +type Row = tailordb.Row; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-value-member/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-value-member/input.d.ts new file mode 100644 index 000000000..e20a44ca1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-value-member/input.d.ts @@ -0,0 +1,5 @@ +declare namespace tailordb { + export class Client {} +} + +type ClientCtor = typeof tailordb.Client; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/expected.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/expected.d.ts new file mode 100644 index 000000000..bf27bcfc9 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/expected.d.ts @@ -0,0 +1,9 @@ +/// + +declare namespace tailor { + namespace idp { + export type LocalUser = string; + } +} + +type RuntimeUser = tailor.idp.User; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/input.d.ts new file mode 100644 index 000000000..7c3a7f9c2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/input.d.ts @@ -0,0 +1,7 @@ +declare namespace tailor { + namespace idp { + export type LocalUser = string; + } +} + +type RuntimeUser = tailor.idp.User; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-value-member/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-value-member/input.d.ts new file mode 100644 index 000000000..9d6873710 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-value-member/input.d.ts @@ -0,0 +1,7 @@ +declare namespace tailor { + namespace idp { + export class Client {} + } +} + +type ClientCtor = typeof tailor.idp.Client; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/expected.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/expected.d.ts new file mode 100644 index 000000000..398d9c725 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/expected.d.ts @@ -0,0 +1,9 @@ +/// + +declare namespace tailordb { + namespace Client { + export type Extra = string; + } +} + +type ClientCtor = typeof tailordb.Client; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/input.d.ts new file mode 100644 index 000000000..8fac4f9e4 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/input.d.ts @@ -0,0 +1,7 @@ +declare namespace tailordb { + namespace Client { + export type Extra = string; + } +} + +type ClientCtor = typeof tailordb.Client; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/decorated-parameter-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/decorated-parameter-local/input.ts new file mode 100644 index 000000000..5f478bf4b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/decorated-parameter-local/input.ts @@ -0,0 +1,9 @@ +class Client { + constructor(@inject tailor: string) { + this.value = tailor; + } + + value: string; +} + +export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/expected.ts new file mode 100644 index 000000000..3e2626354 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const { tailor: localTailor } = config; + +const client = new tailor.idp.Client(localTailor); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/input.ts new file mode 100644 index 000000000..5d2ac1743 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/input.ts @@ -0,0 +1,3 @@ +const { tailor: localTailor } = config; + +const client = new tailor.idp.Client(localTailor); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/expected.ts new file mode 100644 index 000000000..13168b5d9 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const { client = new tailor.idp.Client() } = opts; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/input.ts new file mode 100644 index 000000000..e2919364c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/input.ts @@ -0,0 +1,3 @@ +const { client = new tailor.idp.Client() } = opts; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/expected.ts new file mode 100644 index 000000000..d988d0192 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/expected.ts @@ -0,0 +1,6 @@ +"use client"; /* required by Next.js */ +import "@tailor-platform/sdk/runtime/globals"; + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/input.ts new file mode 100644 index 000000000..9efc3db19 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/input.ts @@ -0,0 +1,5 @@ +"use client"; /* required by Next.js */ + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/expected.ts new file mode 100644 index 000000000..820308bf9 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/expected.ts @@ -0,0 +1,6 @@ +"use client"; // required by Next.js +import "@tailor-platform/sdk/runtime/globals"; + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/input.ts new file mode 100644 index 000000000..d8159975b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/input.ts @@ -0,0 +1,5 @@ +"use client"; // required by Next.js + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/enum-namespace-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/enum-namespace-local/input.ts new file mode 100644 index 000000000..5a3ee7e53 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/enum-namespace-local/input.ts @@ -0,0 +1,9 @@ +enum tailordb { + Client, +} + +type Client = tailordb.Client; +const client = tailordb.Client; + +export { client, tailordb }; +export type { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/expected.ts new file mode 100644 index 000000000..c817d0681 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +type ErrorCtor = TailorErrors; + +export type { ErrorCtor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/input.ts new file mode 100644 index 000000000..cfbcc588a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/input.ts @@ -0,0 +1,3 @@ +type ErrorCtor = TailorErrors; + +export type { ErrorCtor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/expected.ts new file mode 100644 index 000000000..7e6dc6c3e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/expected.ts @@ -0,0 +1,3 @@ +import "@tailor-platform/sdk/runtime/globals"; + +throw new TailorErrors([]); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/input.ts new file mode 100644 index 000000000..e7f1f704c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/input.ts @@ -0,0 +1 @@ +throw new TailorErrors([]); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/export-as-namespace-local/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/export-as-namespace-local/input.d.ts new file mode 100644 index 000000000..9cf548494 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/export-as-namespace-local/input.d.ts @@ -0,0 +1,2 @@ +export as namespace tailor; +export as namespace tailordb; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-await-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-await-local/input.ts new file mode 100644 index 000000000..10945bdac --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-await-local/input.ts @@ -0,0 +1,7 @@ +async function run(clients: AsyncIterable<{ run(): void }>) { + for await (const tailor of clients) { + tailor.run(); + } +} + +export { run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/expected.ts new file mode 100644 index 000000000..1cc3aa0d5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/expected.ts @@ -0,0 +1,11 @@ +import "@tailor-platform/sdk/runtime/globals"; + +for (const item of items) { + { + const tailor = item; + tailor.run(); + } + + const client = new tailor.idp.Client(); + use(client); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/input.ts new file mode 100644 index 000000000..20af40be4 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/input.ts @@ -0,0 +1,9 @@ +for (const item of items) { + { + const tailor = item; + tailor.run(); + } + + const client = new tailor.idp.Client(); + use(client); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-local/input.ts new file mode 100644 index 000000000..75feeb96e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-local/input.ts @@ -0,0 +1,3 @@ +for (const tailor of clients) { + tailor.run(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/expected.ts new file mode 100644 index 000000000..efda4b316 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/expected.ts @@ -0,0 +1,9 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const client = new tailor.idp.Client(); + +for (let tailor = 0; tailor < 1; tailor++) { + console.log(tailor); +} + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/input.ts new file mode 100644 index 000000000..625f46157 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/input.ts @@ -0,0 +1,7 @@ +const client = new tailor.idp.Client(); + +for (let tailor = 0; tailor < 1; tailor++) { + console.log(tailor); +} + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local/input.ts new file mode 100644 index 000000000..7ee4288c4 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local/input.ts @@ -0,0 +1,3 @@ +for (let tailor = 0; tailor < 1; tailor++) { + tailor.toString(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/expected.ts new file mode 100644 index 000000000..87b0ce1b8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/expected.ts @@ -0,0 +1,8 @@ +import "@tailor-platform/sdk/runtime/globals"; + +function build(client = tailor) { + var tailor = localClient; + return client; +} + +export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/input.ts new file mode 100644 index 000000000..60a57f475 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/input.ts @@ -0,0 +1,6 @@ +function build(client = tailor) { + var tailor = localClient; + return client; +} + +export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-local/input.ts new file mode 100644 index 000000000..d13b1578b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-local/input.ts @@ -0,0 +1,8 @@ +function build() { + if (ready) { + var tailor = localClient; + } + return tailor.run(); +} + +export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/expected.ts new file mode 100644 index 000000000..ba2da0b36 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/expected.ts @@ -0,0 +1,8 @@ +import "@tailor-platform/sdk/runtime/globals"; + +function build(client: typeof tailor): typeof tailor { + var tailor = localClient; + return client; +} + +export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/input.ts new file mode 100644 index 000000000..25b2d0e3a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/input.ts @@ -0,0 +1,6 @@ +function build(client: typeof tailor): typeof tailor { + var tailor = localClient; + return client; +} + +export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/expected.ts new file mode 100644 index 000000000..58979a429 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/expected.ts @@ -0,0 +1,9 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const fn = function (tailor: unknown) { + return tailor; +}; + +const client = new tailor.idp.Client(); + +export { client, fn }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/input.ts new file mode 100644 index 000000000..aa4a25c0c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/input.ts @@ -0,0 +1,7 @@ +const fn = function (tailor: unknown) { + return tailor; +}; + +const client = new tailor.idp.Client(); + +export { client, fn }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/expected.ts new file mode 100644 index 000000000..2f6337d94 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/expected.ts @@ -0,0 +1,8 @@ +import "@tailor-platform/sdk/runtime/globals"; + +type Fn = (tailor: unknown) => unknown; + +const client = new tailor.idp.Client(); + +export type { Fn }; +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/input.ts new file mode 100644 index 000000000..c89b63f24 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/input.ts @@ -0,0 +1,6 @@ +type Fn = (tailor: unknown) => unknown; + +const client = new tailor.idp.Client(); + +export type { Fn }; +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/expected.ts new file mode 100644 index 000000000..2d8c37b00 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/expected.ts @@ -0,0 +1,9 @@ +import "@tailor-platform/sdk/runtime/globals"; + +function* inspect(tailor: unknown) { + yield tailor; +} + +const client = new tailor.idp.Client(); + +export { client, inspect }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/input.ts new file mode 100644 index 000000000..9410ed82d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/input.ts @@ -0,0 +1,7 @@ +function* inspect(tailor: unknown) { + yield tailor; +} + +const client = new tailor.idp.Client(); + +export { client, inspect }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/expected.ts new file mode 100644 index 000000000..1ecd70ec8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/expected.ts @@ -0,0 +1,8 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const runtime = globalThis; +const globals = runtime; + +const client = globals.tailor.idp.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/input.ts new file mode 100644 index 000000000..512aaf45b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/input.ts @@ -0,0 +1,6 @@ +const runtime = globalThis; +const globals = runtime; + +const client = globals.tailor.idp.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/expected.ts new file mode 100644 index 000000000..e357dbfa2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/expected.ts @@ -0,0 +1,7 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const runtime /* alias */ = /* value */ globalThis; + +const client = runtime.tailor.idp.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/input.ts new file mode 100644 index 000000000..64687f852 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/input.ts @@ -0,0 +1,5 @@ +const runtime /* alias */ = /* value */ globalThis; + +const client = runtime.tailor.idp.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-shadowed/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-shadowed/input.ts new file mode 100644 index 000000000..a47000a75 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-shadowed/input.ts @@ -0,0 +1,7 @@ +const runtime = globalThis; + +function read(runtime: { tailor: { idp: { Client: unknown } } }) { + return runtime.tailor.idp.Client; +} + +export { read }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/expected.ts new file mode 100644 index 000000000..6e73f6cb5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/expected.ts @@ -0,0 +1,7 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const runtime: typeof globalThis = globalThis; + +const client = runtime.tailor.idp.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/input.ts new file mode 100644 index 000000000..5b8e752cc --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/input.ts @@ -0,0 +1,5 @@ +const runtime: typeof globalThis = globalThis; + +const client = runtime.tailor.idp.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/expected.ts new file mode 100644 index 000000000..398552f1f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/expected.ts @@ -0,0 +1,8 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const runtime = globalThis; + +const client = runtime.tailor.idp.Client; +const database = runtime["tailordb"]; + +export { client, database }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/input.ts new file mode 100644 index 000000000..c0c2d3af1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/input.ts @@ -0,0 +1,6 @@ +const runtime = globalThis; + +const client = runtime.tailor.idp.Client; +const database = runtime["tailordb"]; + +export { client, database }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/expected.ts new file mode 100644 index 000000000..72ec92309 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/expected.ts @@ -0,0 +1,6 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const client = globalThis["tailor"].idp.Client; +const query = globalThis["tailordb"].QueryResult; + +export { client, query }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/input.ts new file mode 100644 index 000000000..797ce400a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/input.ts @@ -0,0 +1,4 @@ +const client = globalThis["tailor"].idp.Client; +const query = globalThis["tailordb"].QueryResult; + +export { client, query }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key-expression/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key-expression/input.ts new file mode 100644 index 000000000..c520822bc --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key-expression/input.ts @@ -0,0 +1,6 @@ +const config = { tailor: "custom" }; + +const { [config.tailor]: configuredValue } = globalThis; +const { [getKey("tailor")]: dynamicValue } = globalThis; + +export { configuredValue, dynamicValue }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/expected.ts new file mode 100644 index 000000000..7f16d1e86 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/expected.ts @@ -0,0 +1,7 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const { ["tailor"]: runtimeTailor } = globalThis; +const { [`tailordb`]: runtimeDb } = globalThis; +const { ["TailorErrors" as const]: TailorErrorsClass } = globalThis; + +export { runtimeDb, runtimeTailor, TailorErrorsClass }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/input.ts new file mode 100644 index 000000000..efcfe018d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/input.ts @@ -0,0 +1,5 @@ +const { ["tailor"]: runtimeTailor } = globalThis; +const { [`tailordb`]: runtimeDb } = globalThis; +const { ["TailorErrors" as const]: TailorErrorsClass } = globalThis; + +export { runtimeDb, runtimeTailor, TailorErrorsClass }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-local-key/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-local-key/input.ts new file mode 100644 index 000000000..03b160b76 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-local-key/input.ts @@ -0,0 +1,5 @@ +const tailor = "custom"; + +const { [tailor]: dynamicValue } = globalThis; + +export { dynamicValue }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/expected.ts new file mode 100644 index 000000000..d8eceb1f2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/expected.ts @@ -0,0 +1,9 @@ +import "@tailor-platform/sdk/runtime/globals"; + +let db; +let tailorClient; + +({ tailordb: db } = globalThis); +({ tailor: tailorClient } = globalThis); + +export { db, tailorClient }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/input.ts new file mode 100644 index 000000000..a194ff6d4 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/input.ts @@ -0,0 +1,7 @@ +let db; +let tailorClient; + +({ tailordb: db } = globalThis); +({ tailor: tailorClient } = globalThis); + +export { db, tailorClient }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-local-alias/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-local-alias/input.ts new file mode 100644 index 000000000..ab4fce9ae --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-local-alias/input.ts @@ -0,0 +1,3 @@ +const { x: tailor } = globalThis; + +export { tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/expected.ts new file mode 100644 index 000000000..daffaa101 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const { tailor }: typeof globalThis = globalThis; + +export { tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/input.ts new file mode 100644 index 000000000..296b62ed6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/input.ts @@ -0,0 +1,3 @@ +const { tailor }: typeof globalThis = globalThis; + +export { tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/expected.ts new file mode 100644 index 000000000..3d8afe268 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/expected.ts @@ -0,0 +1,7 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const { tailor } = globalThis; +const { tailordb: db } = globalThis; +const { TailorErrors = Error } = globalThis; + +export { db, tailor, TailorErrors }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/input.ts new file mode 100644 index 000000000..4c03bf765 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/input.ts @@ -0,0 +1,5 @@ +const { tailor } = globalThis; +const { tailordb: db } = globalThis; +const { TailorErrors = Error } = globalThis; + +export { db, tailor, TailorErrors }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-alias-shadowed/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-alias-shadowed/input.ts new file mode 100644 index 000000000..1fa86afc0 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-alias-shadowed/input.ts @@ -0,0 +1,8 @@ +const runtime = globalThis; + +function read(runtime: { tailor: unknown }) { + type TailorRuntime = (typeof runtime)["tailor"]; + return null as TailorRuntime; +} + +export { read }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-local/input.ts new file mode 100644 index 000000000..9578454e9 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-local/input.ts @@ -0,0 +1,7 @@ +const globalThis = { + tailor: {}, +}; + +type TailorRuntime = (typeof globalThis)["tailor"]; + +export type { TailorRuntime }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/expected.ts new file mode 100644 index 000000000..f0fe768c2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/expected.ts @@ -0,0 +1,10 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const runtime = globalThis; + +type TailorRuntime = (typeof globalThis)["tailor"]; +type TailordbRuntime = typeof globalThis["tailordb"]; +type AliasTailorRuntime = (typeof runtime)["tailor"]; +type ErrorCtor = typeof globalThis["TailorErrors"]; + +export type { AliasTailorRuntime, ErrorCtor, TailorRuntime, TailordbRuntime }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/input.ts new file mode 100644 index 000000000..864f9e91f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/input.ts @@ -0,0 +1,8 @@ +const runtime = globalThis; + +type TailorRuntime = (typeof globalThis)["tailor"]; +type TailordbRuntime = typeof globalThis["tailordb"]; +type AliasTailorRuntime = (typeof runtime)["tailor"]; +type ErrorCtor = typeof globalThis["TailorErrors"]; + +export type { AliasTailorRuntime, ErrorCtor, TailorRuntime, TailordbRuntime }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/expected.ts new file mode 100644 index 000000000..31df260ae --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/expected.ts @@ -0,0 +1,7 @@ +import "@tailor-platform/sdk/runtime/globals"; + +function run(tailor: unknown) { + return globalThis.tailor.idp.Client; +} + +export { run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/input.ts new file mode 100644 index 000000000..9ec12e4c8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/input.ts @@ -0,0 +1,5 @@ +function run(tailor: unknown) { + return globalThis.tailor.idp.Client; +} + +export { run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/expected.ts new file mode 100644 index 000000000..344291e7a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/expected.ts @@ -0,0 +1,6 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const { tailor = fallback } = globalThis; +const { tailordb = fallbackDb } = globalThis; + +export { tailor, tailordb }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/input.ts new file mode 100644 index 000000000..8b26d39ea --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/input.ts @@ -0,0 +1,4 @@ +const { tailor = fallback } = globalThis; +const { tailordb = fallbackDb } = globalThis; + +export { tailor, tailordb }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/expected.ts new file mode 100644 index 000000000..965412310 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const client = globalThis.tailor.idp.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/input.ts new file mode 100644 index 000000000..8b13d8def --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/input.ts @@ -0,0 +1,3 @@ +const client = globalThis.tailor.idp.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-alias-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-alias-local/input.ts new file mode 100644 index 000000000..6a6887e30 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-alias-local/input.ts @@ -0,0 +1,9 @@ +namespace RuntimeAliases { + import tailor = ExternalRuntime.tailor; + import tailordb = ExternalRuntime.tailordb; + + tailor.idp.Client; + tailordb.file; +} + +export { RuntimeAliases }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-equals-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-equals-local/input.ts new file mode 100644 index 000000000..2a360509a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-equals-local/input.ts @@ -0,0 +1,8 @@ +import tailor = require("pkg"); +import tailordb = require("other"); + +const client = tailor.idp.Client; +type Rows = tailordb.QueryResult; + +export { client }; +export type { Rows }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/expected.ts new file mode 100644 index 000000000..4c0754eb0 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/expected.ts @@ -0,0 +1,7 @@ +import value from "pkg"; /* keep this import grouped +with this comment */ +import "@tailor-platform/sdk/runtime/globals"; + +const Client = tailor.idp.Client; + +export { Client, value }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/input.ts new file mode 100644 index 000000000..31c41131e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/input.ts @@ -0,0 +1,6 @@ +import value from "pkg"; /* keep this import grouped +with this comment */ + +const Client = tailor.idp.Client; + +export { Client, value }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/infer-type-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/infer-type-local/input.ts new file mode 100644 index 000000000..4296b033a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/infer-type-local/input.ts @@ -0,0 +1,4 @@ +type Unwrap = T extends Promise ? tailor : never; +type ExtractDb = T extends { db: infer tailordb } ? tailordb : never; + +export type { ExtractDb, Unwrap }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/expected.ts new file mode 100644 index 000000000..be06a50d3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/expected.ts @@ -0,0 +1,6 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const Client = tailor.idp.Client; + +/// +export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/input.ts new file mode 100644 index 000000000..a64025845 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/input.ts @@ -0,0 +1,4 @@ +const Client = tailor.idp.Client; + +/// +export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/expected.ts new file mode 100644 index 000000000..0927e17ac --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/expected.ts @@ -0,0 +1,8 @@ +/* +/// +*/ + +import "@tailor-platform/sdk/runtime/globals"; +const Client = tailor.idp.Client; + +export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/input.ts new file mode 100644 index 000000000..0d7de1061 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/input.ts @@ -0,0 +1,7 @@ +/* +/// +*/ + +const Client = tailor.idp.Client; + +export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/jsx-intrinsic-tag/input.tsx b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/jsx-intrinsic-tag/input.tsx new file mode 100644 index 000000000..77b2eb1e6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/jsx-intrinsic-tag/input.tsx @@ -0,0 +1,8 @@ +const element = ( + <> + + + +); + +export { element }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/expected.ts new file mode 100644 index 000000000..21448ef91 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/expected.ts @@ -0,0 +1,4 @@ +import "@tailor-platform/sdk/runtime/globals"; + +// eslint-disable-next-line no-console +console.log(tailor.idp.Client); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/input.ts new file mode 100644 index 000000000..abe897b5f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/input.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-console +console.log(tailor.idp.Client); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-binding/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-binding/input.ts new file mode 100644 index 000000000..1fc617e98 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-binding/input.ts @@ -0,0 +1,7 @@ +const tailor = { + idp: { + Client: class Client {}, + }, +}; + +const client = new tailor.idp.Client(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-global-this/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-global-this/input.ts new file mode 100644 index 000000000..b9dd7ef17 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-global-this/input.ts @@ -0,0 +1,15 @@ +function read(globalThis: { + tailor: { idp: { Client: unknown } }; + tailordb: { file: unknown }; +}) { + return [globalThis.tailor.idp.Client, globalThis["tailordb"].file]; +} + +function destructure(globalThis: { runtime: { tailor: string } }) { + const { + runtime: { tailor }, + } = globalThis; + return tailor; +} + +export { destructure, read }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/expected.ts new file mode 100644 index 000000000..c4fac5b2a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/expected.ts @@ -0,0 +1,9 @@ +import "@tailor-platform/sdk/runtime/globals"; + +function inspect(tailor: { idp: { Client: new () => unknown } }) { + return new tailor.idp.Client(); +} + +const client = new tailor.idp.Client(); + +export { client, inspect }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/input.ts new file mode 100644 index 000000000..239cd8aa5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/input.ts @@ -0,0 +1,7 @@ +function inspect(tailor: { idp: { Client: new () => unknown } }) { + return new tailor.idp.Client(); +} + +const client = new tailor.idp.Client(); + +export { client, inspect }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-only/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-only/input.ts new file mode 100644 index 000000000..66faa2cd2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-only/input.ts @@ -0,0 +1,5 @@ +function inspect(tailor: { idp: { Client: new () => unknown } }) { + return new tailor.idp.Client(); +} + +export { inspect }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-binding/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-binding/input.ts new file mode 100644 index 000000000..f131a1602 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-binding/input.ts @@ -0,0 +1,5 @@ +class tailordb { + static QueryResult = class QueryResult {}; +} + +const result = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/expected.ts new file mode 100644 index 000000000..26e75da2a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/expected.ts @@ -0,0 +1,6 @@ +import "@tailor-platform/sdk/runtime/globals"; + +type tailor = {}; +type User = tailor.idp.User; + +export type { User }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/input.ts new file mode 100644 index 000000000..4de380c75 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/input.ts @@ -0,0 +1,4 @@ +type tailor = {}; +type User = tailor.idp.User; + +export type { User }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/expected.ts new file mode 100644 index 000000000..42ce80061 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/expected.ts @@ -0,0 +1,10 @@ +import "@tailor-platform/sdk/runtime/globals"; + +interface Handler { + handle(tailor: unknown): void; +} + +const client = new tailor.idp.Client(); + +export { client }; +export type { Handler }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/input.ts new file mode 100644 index 000000000..b524fb1ff --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/input.ts @@ -0,0 +1,8 @@ +interface Handler { + handle(tailor: unknown): void; +} + +const client = new tailor.idp.Client(); + +export { client }; +export type { Handler }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-existing-runtime-member/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-existing-runtime-member/input.ts new file mode 100644 index 000000000..539fc7aba --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-existing-runtime-member/input.ts @@ -0,0 +1,9 @@ +export {}; + +declare global { + namespace tailordb { + type QueryResult = T[]; + } +} + +type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/expected.ts new file mode 100644 index 000000000..bc37913e1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/expected.ts @@ -0,0 +1,11 @@ +import "@tailor-platform/sdk/runtime/globals"; + +export {}; + +declare global { + namespace tailordb { + type Row = string; + } +} + +type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/input.ts new file mode 100644 index 000000000..d2b7d36f2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/input.ts @@ -0,0 +1,9 @@ +export {}; + +declare global { + namespace tailordb { + type Row = string; + } +} + +type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/expected.ts new file mode 100644 index 000000000..b1abc913d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/expected.ts @@ -0,0 +1,8 @@ +"use client"; + +"use strict"; +import "@tailor-platform/sdk/runtime/globals"; + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/input.ts new file mode 100644 index 000000000..ae0848bd9 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/input.ts @@ -0,0 +1,7 @@ +"use client"; + +"use strict"; + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/named-expression-bindings/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/named-expression-bindings/input.ts new file mode 100644 index 000000000..2ba7b8ef8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/named-expression-bindings/input.ts @@ -0,0 +1,19 @@ +function* tailor() { + return tailor; +} + +const f = function tailordb() { + return tailordb; +}; + +const g = function* TailorErrors() { + return TailorErrors; +}; + +const C = class TailorErrorMessage { + method() { + return TailorErrorMessage; + } +}; + +export { C, f, g, tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-export-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-export-local/input.ts new file mode 100644 index 000000000..4517c4986 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-export-local/input.ts @@ -0,0 +1,2 @@ +export * as tailor from "pkg"; +export * as tailordb from "pkg"; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-local/input.ts new file mode 100644 index 000000000..23c13350b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-local/input.ts @@ -0,0 +1,6 @@ +namespace Local { + var tailor = { run() {} }; + tailor.run(); +} + +export { Local }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/expected.ts new file mode 100644 index 000000000..2f091fac6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/expected.ts @@ -0,0 +1,10 @@ +import "@tailor-platform/sdk/runtime/globals"; + +namespace Local { + var tailor = { value: 1 }; + tailor.value; +} + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/input.ts new file mode 100644 index 000000000..ed3b7e9ad --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/input.ts @@ -0,0 +1,8 @@ +namespace Local { + var tailor = { value: 1 }; + tailor.value; +} + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/expected.ts new file mode 100644 index 000000000..9b33b5a49 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/expected.ts @@ -0,0 +1,8 @@ +import "@tailor-platform/sdk/runtime/globals"; + +class tailordb {} + +type Rows = tailordb.QueryResult; + +export { tailordb }; +export type { Rows }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/input.ts new file mode 100644 index 000000000..c683e40f7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/input.ts @@ -0,0 +1,6 @@ +class tailordb {} + +type Rows = tailordb.QueryResult; + +export { tailordb }; +export type { Rows }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-destructuring-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-destructuring-local/input.ts new file mode 100644 index 000000000..c23388563 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-destructuring-local/input.ts @@ -0,0 +1,7 @@ +const { x: [tailor] } = obj; + +function run({ x: { tailordb } }: { x: { tailordb: { run(): void } } }) { + tailordb.run(); +} + +export { run, tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-global-this-destructure-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-global-this-destructure-local/input.ts new file mode 100644 index 000000000..e775a2028 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-global-this-destructure-local/input.ts @@ -0,0 +1,9 @@ +const { + runtime: { tailor }, +} = globalThis; + +const { + runtime: { tailordb: database }, +} = globalThis; + +export { database, tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/expected.ts new file mode 100644 index 000000000..f7df90279 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/expected.ts @@ -0,0 +1,12 @@ +import "@tailor-platform/sdk/runtime/globals"; + +declare module "pkg" { + import { tailor } from "dep"; + import * as tailordb from "dep"; +} + +const client = new tailor.idp.Client(); +type Rows = tailordb.QueryResult; + +export { client }; +export type { Rows }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/input.ts new file mode 100644 index 000000000..58c60bc4c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/input.ts @@ -0,0 +1,10 @@ +declare module "pkg" { + import { tailor } from "dep"; + import * as tailordb from "dep"; +} + +const client = new tailor.idp.Client(); +type Rows = tailordb.QueryResult; + +export { client }; +export type { Rows }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-local/input.ts new file mode 100644 index 000000000..2898c5359 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-local/input.ts @@ -0,0 +1,6 @@ +declare module "pkg" { + import { tailor } from "dep"; + export type Local = typeof tailor; +} + +export {}; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/expected.ts new file mode 100644 index 000000000..4083c7102 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/expected.ts @@ -0,0 +1,9 @@ +import "@tailor-platform/sdk/runtime/globals"; + +declare module "pkg" { + import type { Foo } from "dep"; + + export type Wrapped = Foo; +} + +const client = new tailor.idp.Client(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/input.ts new file mode 100644 index 000000000..876c4db6a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/input.ts @@ -0,0 +1,7 @@ +declare module "pkg" { + import type { Foo } from "dep"; + + export type Wrapped = Foo; +} + +const client = new tailor.idp.Client(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-parameter-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-parameter-local/input.ts new file mode 100644 index 000000000..d323b5f1b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-parameter-local/input.ts @@ -0,0 +1,5 @@ +function run({ tailor }: { tailor: { run(): void } }) { + tailor.run(); +} + +export { run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/expected.ts new file mode 100644 index 000000000..85195cd32 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const client = tailor!.idp.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/input.ts new file mode 100644 index 000000000..d8819ca29 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/input.ts @@ -0,0 +1,3 @@ +const client = tailor!.idp.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/expected.ts new file mode 100644 index 000000000..b5b02bc24 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const client = tailor?.idp.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/input.ts new file mode 100644 index 000000000..a8e624a32 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/input.ts @@ -0,0 +1,3 @@ +const client = tailor?.idp.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/expected.ts new file mode 100644 index 000000000..4d9c5406f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/expected.ts @@ -0,0 +1,7 @@ +import "@tailor-platform/sdk/runtime/globals"; + +function build(client = tailor) { + return client; +} + +export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/input.ts new file mode 100644 index 000000000..baa8725fb --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/input.ts @@ -0,0 +1,5 @@ +function build(client = tailor) { + return client; +} + +export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/expected.ts new file mode 100644 index 000000000..e51c1a89b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/expected.ts @@ -0,0 +1,7 @@ +import "@tailor-platform/sdk/runtime/globals"; + +function build({ tailor } = globalThis, { tailordb: db } = (globalThis as typeof globalThis)) { + return { db, tailor }; +} + +export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/input.ts new file mode 100644 index 000000000..4eaea1242 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/input.ts @@ -0,0 +1,5 @@ +function build({ tailor } = globalThis, { tailordb: db } = (globalThis as typeof globalThis)) { + return { db, tailor }; +} + +export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/expected.ts new file mode 100644 index 000000000..f7c97aff7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/expected.ts @@ -0,0 +1,7 @@ +import "@tailor-platform/sdk/runtime/globals"; + +function read({ tailor = fallback } = globalThis, { tailordb = fallbackDb } = globalThis) { + return { tailor, tailordb }; +} + +export { read }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/input.ts new file mode 100644 index 000000000..028f3738e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/input.ts @@ -0,0 +1,5 @@ +function read({ tailor = fallback } = globalThis, { tailordb = fallbackDb } = globalThis) { + return { tailor, tailordb }; +} + +export { read }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-property-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-property-local/input.ts new file mode 100644 index 000000000..10706b0d8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-property-local/input.ts @@ -0,0 +1,10 @@ +class Client { + constructor( + private tailor: { run(): void }, + public TailorErrors: string, + ) { + tailor.run(); + } +} + +export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/expected.ts new file mode 100644 index 000000000..bf0e87425 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const client = (tailor).idp.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/input.ts new file mode 100644 index 000000000..a087a7538 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/input.ts @@ -0,0 +1,3 @@ +const client = (tailor).idp.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/expected.ts new file mode 100644 index 000000000..3aa5c47be --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/expected.ts @@ -0,0 +1,5 @@ +// @ts-nocheck +"use client"; +import "@tailor-platform/sdk/runtime/globals"; + +const client = new tailor.idp.Client(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/input.ts new file mode 100644 index 000000000..415f59e48 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/input.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +"use client"; + +const client = new tailor.idp.Client(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-namespace-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-namespace-local/input.ts new file mode 100644 index 000000000..773337b31 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-namespace-local/input.ts @@ -0,0 +1,7 @@ +namespace tailor.idp { + export const Client = class {}; +} + +const Client = tailor.idp.Client; + +export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/expected.ts new file mode 100644 index 000000000..aecc646ad --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/expected.ts @@ -0,0 +1,6 @@ +import "@tailor-platform/sdk/runtime/globals"; + +type Rows = tailordb /* c */ . QueryResult; +type User = tailor . idp . User; + +export type { Rows, User }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/input.ts new file mode 100644 index 000000000..518288da7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/input.ts @@ -0,0 +1,4 @@ +type Rows = tailordb /* c */ . QueryResult; +type User = tailor . idp . User; + +export type { Rows, User }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/expected.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/expected.cts new file mode 100644 index 000000000..2fb66249a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/expected.cts @@ -0,0 +1,7 @@ +/// + +"use strict"; + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/input.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/input.cts new file mode 100644 index 000000000..e9e4fdcd7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/input.cts @@ -0,0 +1,5 @@ +"use strict"; + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/runtime-namespace-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/runtime-namespace-local/input.ts new file mode 100644 index 000000000..cb25a01d0 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/runtime-namespace-local/input.ts @@ -0,0 +1,9 @@ +namespace tailor { + export const idp = { + Client: class {}, + }; +} + +const Client = tailor.idp.Client; + +export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/expected.ts new file mode 100644 index 000000000..fb5579761 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/expected.ts @@ -0,0 +1,6 @@ +import "@tailor-platform/sdk/runtime/globals"; + +({ tailor } = source); +({ tailordb = fallback } = source); + +export {}; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/input.ts new file mode 100644 index 000000000..c663dcae0 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/input.ts @@ -0,0 +1,4 @@ +({ tailor } = source); +({ tailordb = fallback } = source); + +export {}; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-local/input.ts new file mode 100644 index 000000000..eee2c090e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-local/input.ts @@ -0,0 +1,4 @@ +let tailor; +({ tailor } = source); + +export { tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/expected.ts new file mode 100644 index 000000000..0797ab70b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const payload = { tailor }; + +export { payload }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/input.ts new file mode 100644 index 000000000..95944281f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/input.ts @@ -0,0 +1,3 @@ +const payload = { tailor }; + +export { payload }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-source-name/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-source-name/input.ts new file mode 100644 index 000000000..ee7df71e3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-source-name/input.ts @@ -0,0 +1,5 @@ +import { tailor as t } from "pkg"; + +export { tailor as x } from "pkg"; +export { foo as tailordb } from "pkg"; +export { t }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-type-export/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-type-export/input.ts new file mode 100644 index 000000000..26c15857e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-type-export/input.ts @@ -0,0 +1,4 @@ +import type { tailor } from "pkg"; + +export { type tailor }; +export { type tailor as Tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/expected.ts new file mode 100644 index 000000000..3ec8daf25 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/expected.ts @@ -0,0 +1,12 @@ +import "@tailor-platform/sdk/runtime/globals"; + +class Local { + static { + var tailor = { value: 1 }; + tailor.value; + } +} + +const client = new tailor.idp.Client(); + +export { Local, client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/input.ts new file mode 100644 index 000000000..2a8d12013 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/input.ts @@ -0,0 +1,10 @@ +class Local { + static { + var tailor = { value: 1 }; + tailor.value; + } +} + +const client = new tailor.idp.Client(); + +export { Local, client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/expected.ts new file mode 100644 index 000000000..f64bfd08f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const client = tailor["idp"].Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/input.ts new file mode 100644 index 000000000..cec16c2bc --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/input.ts @@ -0,0 +1,3 @@ +const client = tailor["idp"].Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/expected.ts new file mode 100644 index 000000000..c1a830975 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/expected.ts @@ -0,0 +1,12 @@ +import "@tailor-platform/sdk/runtime/globals"; + +switch (kind) { + case "local": + const tailor = localClient; + tailor.run(); + break; +} + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/input.ts new file mode 100644 index 000000000..c85561920 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/input.ts @@ -0,0 +1,10 @@ +switch (kind) { + case "local": + const tailor = localClient; + tailor.run(); + break; +} + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/expected.ts new file mode 100644 index 000000000..897002abe --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +type IdpUser = tailor.idp.User; + +export type Result = IdpUser; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/input.ts new file mode 100644 index 000000000..10551ac83 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/input.ts @@ -0,0 +1,3 @@ +type IdpUser = tailor.idp.User; + +export type Result = IdpUser; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/expected.ts new file mode 100644 index 000000000..a95fd09b5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/expected.ts @@ -0,0 +1,9 @@ +import { createResolver } from "@tailor-platform/sdk"; +import "@tailor-platform/sdk/runtime/globals"; + +export default createResolver({ + async handler() { + const client = new tailor.idp.Client(); + return client; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/input.ts new file mode 100644 index 000000000..225170d7d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/input.ts @@ -0,0 +1,8 @@ +import { createResolver } from "@tailor-platform/sdk"; + +export default createResolver({ + async handler() { + const client = new tailor.idp.Client(); + return client; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/expected.ts new file mode 100644 index 000000000..8aba357c0 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const command: tailordb.CommandType = "SELECT"; + +export { command }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/input.ts new file mode 100644 index 000000000..84679b35c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/input.ts @@ -0,0 +1,3 @@ +const command: tailordb.CommandType = "SELECT"; + +export { command }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/expected.ts new file mode 100644 index 000000000..0766d179c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +type Rows = tailordb.QueryResult; + +export type Result = Rows<{ id: string }>; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/input.ts new file mode 100644 index 000000000..7ddb064b2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/input.ts @@ -0,0 +1,3 @@ +type Rows = tailordb.QueryResult; + +export type Result = Rows<{ id: string }>; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/expected.ts new file mode 100644 index 000000000..4168dfa53 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/expected.ts @@ -0,0 +1,4 @@ +import value from "pkg"; // eslint-disable-line import/no-unresolved +import "@tailor-platform/sdk/runtime/globals"; + +const client = new tailor.idp.Client(value); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/input.ts new file mode 100644 index 000000000..00f60ddd5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/input.ts @@ -0,0 +1,3 @@ +import value from "pkg"; // eslint-disable-line import/no-unresolved + +const client = new tailor.idp.Client(value); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/expected.ts new file mode 100644 index 000000000..6bdc96024 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/expected.ts @@ -0,0 +1,3 @@ +import "@tailor-platform/sdk/runtime/globals"; +import value from "pkg"; // @ts-expect-error +const client = new tailor.idp.Client(value); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/input.ts new file mode 100644 index 000000000..fd097cc80 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/input.ts @@ -0,0 +1,2 @@ +import value from "pkg"; // @ts-expect-error +const client = new tailor.idp.Client(value); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/expected.ts new file mode 100644 index 000000000..eaccd5ea3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/expected.ts @@ -0,0 +1,7 @@ +import "@tailor-platform/sdk/runtime/globals"; + +type tailor = {}; + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/input.ts new file mode 100644 index 000000000..838a9acc4 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/input.ts @@ -0,0 +1,5 @@ +type tailor = {}; + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-level-binders/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-level-binders/input.ts new file mode 100644 index 000000000..cf46069a3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-level-binders/input.ts @@ -0,0 +1,5 @@ +type Box = tailor; +type Indexed = { [tailordb: string]: unknown }; +type Mapped = { [tailor in keyof Source]: Source[tailor] }; + +export type { Box, Indexed, Mapped }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-export-specifier/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-export-specifier/input.ts new file mode 100644 index 000000000..225240bb1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-export-specifier/input.ts @@ -0,0 +1,3 @@ +import type { tailor } from "pkg"; + +export type { tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/expected.ts new file mode 100644 index 000000000..f5ca69c44 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/expected.ts @@ -0,0 +1,6 @@ +import type { tailor } from "pkg"; +import "@tailor-platform/sdk/runtime/globals"; + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/input.ts new file mode 100644 index 000000000..98e11b871 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/input.ts @@ -0,0 +1,5 @@ +import type { tailor } from "pkg"; + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/expected.ts new file mode 100644 index 000000000..fdba63bab --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/expected.ts @@ -0,0 +1,8 @@ +import type * as tailor from "pkg"; +import type tailordb = require("other"); +import "@tailor-platform/sdk/runtime/globals"; + +const Client = tailor.idp.Client; +const Query = tailordb.Query; + +export { Client, Query }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/input.ts new file mode 100644 index 000000000..2200b678b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/input.ts @@ -0,0 +1,7 @@ +import type * as tailor from "pkg"; +import type tailordb = require("other"); + +const Client = tailor.idp.Client; +const Query = tailordb.Query; + +export { Client, Query }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-local/input.ts new file mode 100644 index 000000000..0ba78176b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-local/input.ts @@ -0,0 +1,7 @@ +namespace tailordb { + export type Row = string; +} + +type Row = tailordb.Row; + +export type { Row }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/expected.ts new file mode 100644 index 000000000..61d767d22 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/expected.ts @@ -0,0 +1,9 @@ +import "@tailor-platform/sdk/runtime/globals"; + +declare namespace tailor { + export type User = string; +} + +const Client = tailor.idp.Client; + +export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/input.ts new file mode 100644 index 000000000..0705bfc8e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/input.ts @@ -0,0 +1,7 @@ +declare namespace tailor { + export type User = string; +} + +const Client = tailor.idp.Client; + +export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/expected.ts new file mode 100644 index 000000000..033e0235f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/expected.ts @@ -0,0 +1,11 @@ +import "@tailor-platform/sdk/runtime/globals"; + +namespace tailor { + export namespace idp { + export type User = string; + } +} + +const Client = tailor.idp.Client; + +export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/input.ts new file mode 100644 index 000000000..7eaeea882 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/input.ts @@ -0,0 +1,9 @@ +namespace tailor { + export namespace idp { + export type User = string; + } +} + +const Client = tailor.idp.Client; + +export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/expected.ts new file mode 100644 index 000000000..2f0f2f429 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/expected.ts @@ -0,0 +1,6 @@ +import { type tailor } from "pkg"; +import "@tailor-platform/sdk/runtime/globals"; + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/input.ts new file mode 100644 index 000000000..ebb7cfa40 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/input.ts @@ -0,0 +1,5 @@ +import { type tailor } from "pkg"; + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/expected.ts new file mode 100644 index 000000000..7e9e9c044 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const tailor = {}; + +type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/input.ts new file mode 100644 index 000000000..e77f9d441 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/input.ts @@ -0,0 +1,3 @@ +const tailor = {}; + +type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/using-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/using-local/input.ts new file mode 100644 index 000000000..bd65e252c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/using-local/input.ts @@ -0,0 +1,7 @@ +export {}; + +using tailor = getClient(); +tailor.run(); + +await using tailordb = getDatabase(); +tailordb.run(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/expected.ts new file mode 100644 index 000000000..4840c8b16 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/expected.ts @@ -0,0 +1,8 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const tailordb = {}; + +type Rows = tailordb.QueryResult; + +export { tailordb }; +export type { Rows }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/input.ts new file mode 100644 index 000000000..a430a54e5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/input.ts @@ -0,0 +1,6 @@ +const tailordb = {}; + +type Rows = tailordb.QueryResult; + +export { tailordb }; +export type { Rows }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-local/input.ts new file mode 100644 index 000000000..58006a344 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-local/input.ts @@ -0,0 +1,5 @@ +import { tailor } from "pkg"; + +const client = new tailor.idp.Client(); + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-type-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-type-local/input.ts new file mode 100644 index 000000000..aacdd3c12 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-type-local/input.ts @@ -0,0 +1,8 @@ +import TailorErrors from "errors"; +import { TailorErrorItem, TailorErrorMessage } from "errors"; + +type Item = TailorErrorItem; +type Message = TailorErrorMessage; +type Errors = TailorErrors; + +export type { Errors, Item, Message }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/expected.ts new file mode 100644 index 000000000..5d4928f2c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/expected.ts @@ -0,0 +1,13 @@ +import "@tailor-platform/sdk/runtime/globals"; + +function run() { + { + var tailor = localClient; + } + + return tailor; +} + +const client = new tailor.idp.Client(); + +export { client, run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/input.ts new file mode 100644 index 000000000..ada9b41b4 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/input.ts @@ -0,0 +1,11 @@ +function run() { + { + var tailor = localClient; + } + + return tailor; +} + +const client = new tailor.idp.Client(); + +export { client, run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local/input.ts new file mode 100644 index 000000000..00f1c4bb1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local/input.ts @@ -0,0 +1,9 @@ +function run() { + { + var tailor = localClient; + } + + return tailor; +} + +export { run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-comment-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-comment-local/input.ts new file mode 100644 index 000000000..58c3191ca --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-comment-local/input.ts @@ -0,0 +1,7 @@ +{ + var/*comment*/tailordb = { Client: class {} }; +} + +const client = tailordb.Client; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-for-of-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-for-of-local/input.ts new file mode 100644 index 000000000..0aa5354dc --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-for-of-local/input.ts @@ -0,0 +1,8 @@ +function run(clients: unknown[]) { + for (var tailor of clients) { + void tailor; + } + return tailor; +} + +export { run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-whitespace-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-whitespace-local/input.ts new file mode 100644 index 000000000..0ee4c73b3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-whitespace-local/input.ts @@ -0,0 +1,5 @@ +{ + var tailor = { run() {} }; +} + +tailor.run(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/expected.ts new file mode 100644 index 000000000..595dadea8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/expected.ts @@ -0,0 +1,6 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const { tailor } = (globalThis); +({ tailordb } = (globalThis as typeof globalThis)); + +export { tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/input.ts new file mode 100644 index 000000000..e55c2f827 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/input.ts @@ -0,0 +1,4 @@ +const { tailor } = (globalThis); +({ tailordb } = (globalThis as typeof globalThis)); + +export { tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/expected.ts new file mode 100644 index 000000000..66f5ac7ca --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/expected.ts @@ -0,0 +1,6 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const client = (globalThis).tailor.idp.Client; +const errors = (globalThis as typeof globalThis).TailorErrors; + +export { client, errors }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/input.ts new file mode 100644 index 000000000..b701e0bae --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/input.ts @@ -0,0 +1,4 @@ +const client = (globalThis).tailor.idp.Client; +const errors = (globalThis as typeof globalThis).TailorErrors; + +export { client, errors }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/expected.ts new file mode 100644 index 000000000..686709121 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/expected.ts @@ -0,0 +1,7 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const client = globalThis["tailor" as const]; +const db = globalThis?.["tailordb" as const]; +const errors = globalThis[`TailorErrors`]; + +export { client, db, errors }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/input.ts new file mode 100644 index 000000000..c18124c8a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/input.ts @@ -0,0 +1,5 @@ +const client = globalThis["tailor" as const]; +const db = globalThis?.["tailordb" as const]; +const errors = globalThis[`TailorErrors`]; + +export { client, db, errors }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/expected.ts new file mode 100644 index 000000000..a469cd4f7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/expected.ts @@ -0,0 +1,5 @@ +import "@tailor-platform/sdk/runtime/globals"; + +const client = (globalThis)["tailor"]; + +export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/input.ts new file mode 100644 index 000000000..72b2f1424 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/input.ts @@ -0,0 +1,3 @@ +const client = (globalThis)["tailor"]; + +export { client }; diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 23da4125f..1a65e5c25 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { getApplicableCodemods } from "./registry"; +import { allCodemods, getApplicableCodemods } from "./registry"; describe("getApplicableCodemods", () => { test("returns codemods when upgrading across their version boundary", () => { @@ -24,4 +24,10 @@ describe("getApplicableCodemods", () => { expect(() => getApplicableCodemods("invalid", "2.0.0")).toThrow("Invalid fromVersion"); expect(() => getApplicableCodemods("1.0.0", "invalid")).toThrow("Invalid toVersion"); }); + + test("includes CommonJS TypeScript files in the runtime globals codemod", () => { + const codemod = allCodemods.find((entry) => entry.id === "v2/runtime-globals-opt-in"); + + expect(codemod?.filePatterns).toContain("**/*.{ts,tsx,mts,cts}"); + }); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index cc72798ef..bc5c17d46 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -294,7 +294,15 @@ export const allCodemods: CodemodPackage[] = [ 'Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Normal SDK development does not need them — use the SDK APIs and the typed wrappers from `@tailor-platform/sdk/runtime`. Only if you relied on the ambient globals directly, add `import "@tailor-platform/sdk/runtime/globals"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.)', since: "1.0.0", until: "2.0.0", - notice: true, + scriptPath: "v2/runtime-globals-opt-in/scripts/transform.js", + filePatterns: ["**/*.{ts,tsx,mts,cts}"], + examples: [ + { + before: "const client = new tailor.idp.Client();", + after: + 'import "@tailor-platform/sdk/runtime/globals";\nconst client = new tailor.idp.Client();', + }, + ], }, { id: "v2/workflow-trigger-dispatch", diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index 5ab9121fa..fb9ff2e19 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -38,9 +38,7 @@ async function discoverCases(codemodPath: string): Promise { } async function runFixtureCases(codemodPath: string): Promise { - const scriptPath = path.join(CODEMODS_DIR, codemodPath, "scripts/transform.ts"); - const mod = await import(scriptPath); - const transform = mod.default as TransformFn; + const transform = await loadTransform(codemodPath); const cases = await discoverCases(codemodPath); expect(cases.length, `expected at least one fixture under ${codemodPath}/tests`).toBeGreaterThan( @@ -58,6 +56,12 @@ async function runFixtureCases(codemodPath: string): Promise { } } +async function loadTransform(codemodPath: string): Promise { + const scriptPath = path.join(CODEMODS_DIR, codemodPath, "scripts/transform.ts"); + const mod = await import(scriptPath); + return mod.default as TransformFn; +} + describe("codemod transforms", () => { test("v2/define-generators-to-plugins transforms correctly", async () => { await expect(runFixtureCases("v2/define-generators-to-plugins")).resolves.toBeUndefined(); @@ -98,4 +102,58 @@ describe("codemod transforms", () => { test("v2/execute-script-arg transforms correctly", async () => { await expect(runFixtureCases("v2/execute-script-arg")).resolves.toBeUndefined(); }); + + test("v2/runtime-globals-opt-in transforms correctly", async () => { + await expect(runFixtureCases("v2/runtime-globals-opt-in")).resolves.toBeUndefined(); + }); + + test("v2/runtime-globals-opt-in recognizes multiline type-only syntax", async () => { + const transform = await loadTransform("v2/runtime-globals-opt-in"); + const importInput = [ + "import type", + '{ tailor } from "pkg";', + "", + "const client = tailor.idp.Client;", + "", + ].join("\n"); + + expect(await transform(importInput, "/tmp/input.ts")).toBe( + [ + "import type", + '{ tailor } from "pkg";', + 'import "@tailor-platform/sdk/runtime/globals";', + "", + "const client = tailor.idp.Client;", + "", + ].join("\n"), + ); + + const specifierInput = [ + "import { type", + 'tailor } from "pkg";', + "", + "const client = tailor.idp.Client;", + "", + ].join("\n"); + + expect(await transform(specifierInput, "/tmp/input.ts")).toBe( + [ + "import { type", + 'tailor } from "pkg";', + 'import "@tailor-platform/sdk/runtime/globals";', + "", + "const client = tailor.idp.Client;", + "", + ].join("\n"), + ); + + const exportInput = [ + 'import type { tailor } from "pkg";', + "export type", + "{ tailor };", + "", + ].join("\n"); + + expect(await transform(exportInput, "/tmp/input.ts")).toBeNull(); + }); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index aa8ede142..86971254f 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -31,6 +31,8 @@ export default defineConfig([ "codemods/v2/tailordb-namespace/scripts/transform.ts", "v2/execute-script-arg/scripts/transform": "codemods/v2/execute-script-arg/scripts/transform.ts", + "v2/runtime-globals-opt-in/scripts/transform": + "codemods/v2/runtime-globals-opt-in/scripts/transform.ts", }, format: ["esm"], target: "node18", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 039230c53..ad65bda2e 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -355,6 +355,25 @@ downloadStream and returns FileDownloadStreamResponse.
+## Ambient runtime globals are opt-in + +**Migration:** Automatic + +Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Normal SDK development does not need them — use the SDK APIs and the typed wrappers from `@tailor-platform/sdk/runtime`. Only if you relied on the ambient globals directly, add `import "@tailor-platform/sdk/runtime/globals"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.) + +Before: + +```ts +const client = new tailor.idp.Client(); +``` + +After: + +```ts +import "@tailor-platform/sdk/runtime/globals"; +const client = new tailor.idp.Client(); +``` + ## Workflow .trigger() and trigger tests **Migration:** Manual @@ -396,10 +415,6 @@ trigger result as the job output directly (no Promise wrapper to unwrap). These v2 changes alter runtime or CLI behavior; no source change is needed. -### Ambient runtime globals are opt-in - -Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Normal SDK development does not need them — use the SDK APIs and the typed wrappers from `@tailor-platform/sdk/runtime`. Only if you relied on the ambient globals directly, add `import "@tailor-platform/sdk/runtime/globals"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.) - ### CLI tokens stored in the OS keyring CLI login tokens are stored in the OS keyring by default when available, falling back to the platform config file when it is not. No source change is required; re-login if you need tokens moved into the keyring. From b46fc41f83c01da3341aa7ed0bbd544549611cd4 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 21 Jun 2026 11:47:08 +0900 Subject: [PATCH 172/618] fix(sdk-codemod): flag runtime globals opt-in --- .changeset/runtime-globals-import.md | 2 +- .../v2/runtime-globals-opt-in/codemod.yaml | 7 - .../scripts/transform.ts | 1604 ----------------- .../tests/abstract-class-local/input.ts | 6 - .../tests/already-imported/input.ts | 3 - .../array-destructuring-default/expected.ts | 5 - .../array-destructuring-default/input.ts | 3 - .../tests/bare-type-query/expected.ts | 5 - .../tests/bare-type-query/input.ts | 3 - .../bare-type-with-local-value/expected.ts | 12 - .../tests/bare-type-with-local-value/input.ts | 10 - .../tests/bare-value/expected.ts | 5 - .../tests/bare-value/input.ts | 3 - .../call-signature-parameter/expected.ts | 10 - .../tests/call-signature-parameter/input.ts | 8 - .../catch-block-local-and-global/expected.ts | 13 - .../catch-block-local-and-global/input.ts | 11 - .../tests/catch-local/input.ts | 5 - .../tests/catch-parameterless/expected.ts | 8 - .../tests/catch-parameterless/input.ts | 6 - .../tests/class-expression-local/input.ts | 7 - .../tests/comment-and-string/input.ts | 2 - .../tests/commonjs-declaration/expected.d.cts | 3 - .../tests/commonjs-declaration/input.d.cts | 1 - .../tests/commonjs-skipped/input.cjs | 1 - .../construct-signature-parameter/expected.ts | 10 - .../construct-signature-parameter/input.ts | 8 - .../tests/cts-namespace-value-local/input.cts | 7 - .../tests/cts-skipped/expected.cts | 5 - .../tests/cts-skipped/input.cts | 3 - .../expected.cts | 9 - .../cts-type-only-namespace-value/input.cts | 7 - .../declaration-file-skipped/expected.d.ts | 3 - .../tests/declaration-file-skipped/input.d.ts | 1 - .../input.d.ts | 7 - .../input.d.ts | 11 - .../input.d.ts | 9 - .../expected.d.ts | 11 - .../input.d.ts | 9 - .../expected.d.ts | 14 - .../input.d.ts | 12 - .../input.d.ts | 5 - .../input.d.ts | 5 - .../expected.d.ts | 9 - .../input.d.ts | 7 - .../input.d.ts | 7 - .../expected.d.ts | 9 - .../input.d.ts | 7 - .../tests/decorated-parameter-local/input.ts | 9 - .../destructure-key-not-binding/expected.ts | 5 - .../destructure-key-not-binding/input.ts | 3 - .../tests/destructuring-default/expected.ts | 5 - .../tests/destructuring-default/input.ts | 3 - .../expected.ts | 6 - .../directive-trailing-block-comment/input.ts | 5 - .../directive-trailing-comment/expected.ts | 6 - .../tests/directive-trailing-comment/input.ts | 5 - .../tests/enum-namespace-local/input.ts | 9 - .../tests/error-class-type/expected.ts | 5 - .../tests/error-class-type/input.ts | 3 - .../tests/error-class/expected.ts | 3 - .../tests/error-class/input.ts | 1 - .../export-as-namespace-local/input.d.ts | 2 - .../tests/for-await-local/input.ts | 7 - .../for-block-local-and-global/expected.ts | 11 - .../tests/for-block-local-and-global/input.ts | 9 - .../tests/for-local/input.ts | 3 - .../expected.ts | 9 - .../for-statement-local-and-global/input.ts | 7 - .../tests/for-statement-local/input.ts | 3 - .../expected.ts | 8 - .../function-body-var-default-global/input.ts | 6 - .../tests/function-body-var-local/input.ts | 8 - .../function-body-var-type-global/expected.ts | 8 - .../function-body-var-type-global/input.ts | 6 - .../function-expression-parameter/expected.ts | 9 - .../function-expression-parameter/input.ts | 7 - .../tests/function-type-parameter/expected.ts | 8 - .../tests/function-type-parameter/input.ts | 6 - .../expected.ts | 9 - .../generator-declaration-parameter/input.ts | 7 - .../tests/global-this-alias-chain/expected.ts | 8 - .../tests/global-this-alias-chain/input.ts | 6 - .../global-this-alias-comment/expected.ts | 7 - .../tests/global-this-alias-comment/input.ts | 5 - .../tests/global-this-alias-shadowed/input.ts | 7 - .../expected.ts | 7 - .../input.ts | 5 - .../tests/global-this-alias/expected.ts | 8 - .../tests/global-this-alias/input.ts | 6 - .../tests/global-this-bracket/expected.ts | 6 - .../tests/global-this-bracket/input.ts | 4 - .../input.ts | 6 - .../global-this-computed-key/expected.ts | 7 - .../tests/global-this-computed-key/input.ts | 5 - .../global-this-computed-local-key/input.ts | 5 - .../expected.ts | 9 - .../input.ts | 7 - .../input.ts | 3 - .../expected.ts | 5 - .../input.ts | 3 - .../global-this-destructuring/expected.ts | 7 - .../tests/global-this-destructuring/input.ts | 5 - .../input.ts | 8 - .../global-this-indexed-type-local/input.ts | 7 - .../global-this-indexed-type/expected.ts | 10 - .../tests/global-this-indexed-type/input.ts | 8 - .../global-this-local-shadow/expected.ts | 7 - .../tests/global-this-local-shadow/input.ts | 5 - .../global-this-shorthand-default/expected.ts | 6 - .../global-this-shorthand-default/input.ts | 4 - .../tests/global-this/expected.ts | 5 - .../tests/global-this/input.ts | 3 - .../tests/import-alias-local/input.ts | 9 - .../tests/import-equals-local/input.ts | 8 - .../import-trailing-block-comment/expected.ts | 7 - .../import-trailing-block-comment/input.ts | 6 - .../tests/infer-type-local/input.ts | 4 - .../invalid-reference-after-code/expected.ts | 6 - .../invalid-reference-after-code/input.ts | 4 - .../expected.ts | 8 - .../input.ts | 7 - .../tests/jsx-intrinsic-tag/input.tsx | 8 - .../tests/line-scoped-pragma/expected.ts | 4 - .../tests/line-scoped-pragma/input.ts | 2 - .../tests/local-binding/input.ts | 7 - .../tests/local-global-this/input.ts | 15 - .../local-parameter-and-global/expected.ts | 9 - .../tests/local-parameter-and-global/input.ts | 7 - .../tests/local-parameter-only/input.ts | 5 - .../tests/local-type-binding/input.ts | 5 - .../expected.ts | 6 - .../input.ts | 4 - .../method-signature-parameter/expected.ts | 10 - .../tests/method-signature-parameter/input.ts | 8 - .../input.ts | 9 - .../expected.ts | 11 - .../input.ts | 9 - .../tests/multiple-directives/expected.ts | 8 - .../tests/multiple-directives/input.ts | 7 - .../tests/named-expression-bindings/input.ts | 19 - .../tests/namespace-export-local/input.ts | 2 - .../tests/namespace-var-local/input.ts | 6 - .../namespace-var-scope-global/expected.ts | 10 - .../tests/namespace-var-scope-global/input.ts | 8 - .../namespace-with-local-class/expected.ts | 8 - .../tests/namespace-with-local-class/input.ts | 6 - .../tests/nested-destructuring-local/input.ts | 7 - .../input.ts | 9 - .../nested-import-scope-global/expected.ts | 12 - .../tests/nested-import-scope-global/input.ts | 10 - .../tests/nested-import-scope-local/input.ts | 6 - .../tests/nested-import/expected.ts | 9 - .../tests/nested-import/input.ts | 7 - .../tests/nested-parameter-local/input.ts | 5 - .../tests/non-null-access/expected.ts | 5 - .../tests/non-null-access/input.ts | 3 - .../tests/optional-chain/expected.ts | 5 - .../tests/optional-chain/input.ts | 3 - .../parameter-default-global/expected.ts | 7 - .../tests/parameter-default-global/input.ts | 5 - .../expected.ts | 7 - .../input.ts | 5 - .../expected.ts | 7 - .../input.ts | 5 - .../tests/parameter-property-local/input.ts | 10 - .../tests/parenthesized-access/expected.ts | 5 - .../tests/parenthesized-access/input.ts | 3 - .../tests/preserves-directives/expected.ts | 5 - .../tests/preserves-directives/input.ts | 4 - .../tests/qualified-namespace-local/input.ts | 7 - .../qualified-type-whitespace/expected.ts | 6 - .../tests/qualified-type-whitespace/input.ts | 4 - .../reference-before-directive/expected.cts | 7 - .../reference-before-directive/input.cts | 5 - .../tests/runtime-namespace-local/input.ts | 9 - .../shorthand-assignment-global/expected.ts | 6 - .../shorthand-assignment-global/input.ts | 4 - .../tests/shorthand-assignment-local/input.ts | 4 - .../shorthand-global-reference/expected.ts | 5 - .../tests/shorthand-global-reference/input.ts | 3 - .../tests/specifier-source-name/input.ts | 5 - .../tests/specifier-type-export/input.ts | 4 - .../static-block-var-scope-global/expected.ts | 12 - .../static-block-var-scope-global/input.ts | 10 - .../tests/subscript-access/expected.ts | 5 - .../tests/subscript-access/input.ts | 3 - .../switch-case-local-and-global/expected.ts | 12 - .../switch-case-local-and-global/input.ts | 10 - .../tests/tailor-type-reference/expected.ts | 5 - .../tests/tailor-type-reference/input.ts | 3 - .../tests/tailor-value/expected.ts | 9 - .../tests/tailor-value/input.ts | 8 - .../tailordb-type-annotation/expected.ts | 5 - .../tests/tailordb-type-annotation/input.ts | 3 - .../tests/tailordb-type/expected.ts | 5 - .../tests/tailordb-type/input.ts | 3 - .../tests/trailing-import-comment/expected.ts | 4 - .../tests/trailing-import-comment/input.ts | 3 - .../expected.ts | 3 - .../trailing-next-line-pragma-import/input.ts | 2 - .../type-binding-value-reference/expected.ts | 7 - .../type-binding-value-reference/input.ts | 5 - .../tests/type-level-binders/input.ts | 5 - .../tests/type-only-export-specifier/input.ts | 3 - .../tests/type-only-import/expected.ts | 6 - .../tests/type-only-import/input.ts | 5 - .../expected.ts | 8 - .../type-only-namespace-import-value/input.ts | 7 - .../tests/type-only-namespace-local/input.ts | 7 - .../expected.ts | 9 - .../type-only-namespace-value-global/input.ts | 7 - .../expected.ts | 11 - .../input.ts | 9 - .../tests/type-only-specifier/expected.ts | 6 - .../tests/type-only-specifier/input.ts | 5 - .../tests/unrelated-local-binding/expected.ts | 5 - .../tests/unrelated-local-binding/input.ts | 3 - .../tests/using-local/input.ts | 7 - .../value-binding-type-reference/expected.ts | 8 - .../value-binding-type-reference/input.ts | 6 - .../tests/value-import-local/input.ts | 5 - .../tests/value-import-type-local/input.ts | 8 - .../var-block-local-and-global/expected.ts | 13 - .../tests/var-block-local-and-global/input.ts | 11 - .../tests/var-block-local/input.ts | 9 - .../tests/var-comment-local/input.ts | 7 - .../tests/var-for-of-local/input.ts | 8 - .../tests/var-whitespace-local/input.ts | 5 - .../expected.ts | 6 - .../wrapped-global-this-destructure/input.ts | 4 - .../wrapped-global-this-member/expected.ts | 6 - .../tests/wrapped-global-this-member/input.ts | 4 - .../expected.ts | 7 - .../wrapped-global-this-string-key/input.ts | 5 - .../wrapped-global-this-subscript/expected.ts | 5 - .../wrapped-global-this-subscript/input.ts | 3 - packages/sdk-codemod/src/registry.test.ts | 4 +- packages/sdk-codemod/src/registry.ts | 30 +- packages/sdk-codemod/src/transform.test.ts | 64 +- packages/sdk-codemod/tsdown.config.ts | 2 - packages/sdk/docs/migration/v2.md | 21 +- 242 files changed, 56 insertions(+), 3183 deletions(-) delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/codemod.yaml delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/abstract-class-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/already-imported/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/class-expression-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/comment-and-string/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/expected.d.cts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/input.d.cts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-skipped/input.cjs delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-namespace-value-local/input.cts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/expected.cts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/input.cts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/expected.cts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/input.cts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/expected.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/input.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-class/input.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-namespace/input.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-local-member/input.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/expected.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/input.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/expected.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/input.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-local-member/input.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-value-member/input.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/expected.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/input.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-value-member/input.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/expected.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/input.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/decorated-parameter-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/enum-namespace-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/export-as-namespace-local/input.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-await-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-shadowed/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key-expression/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-local-key/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-local-alias/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-alias-shadowed/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-alias-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-equals-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/infer-type-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/jsx-intrinsic-tag/input.tsx delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-binding/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-global-this/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-only/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-binding/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-existing-runtime-member/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/named-expression-bindings/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-export-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-destructuring-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-global-this-destructure-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-parameter-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-property-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-namespace-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/expected.cts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/input.cts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/runtime-namespace-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-source-name/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-type-export/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-level-binders/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-export-specifier/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/using-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-type-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-comment-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-for-of-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-whitespace-local/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/input.ts diff --git a/.changeset/runtime-globals-import.md b/.changeset/runtime-globals-import.md index a1d5625bb..de9e7cfc7 100644 --- a/.changeset/runtime-globals-import.md +++ b/.changeset/runtime-globals-import.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk-codemod": patch --- -Add explicit runtime globals imports to files that still reference ambient `tailor.*` or `tailordb.*` globals. +Flag files that still reference ambient Tailor runtime globals so the v2 migration can opt them into `@tailor-platform/sdk/runtime/globals`. diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/codemod.yaml b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/codemod.yaml deleted file mode 100644 index e87b26dfe..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/codemod.yaml +++ /dev/null @@ -1,7 +0,0 @@ -name: "@tailor-platform/runtime-globals-opt-in" -version: "1.0.0" -description: "Add the explicit runtime globals import for files that still use ambient `tailor.*` or `tailordb.*` references" -engine: jssg -language: typescript -since: "1.0.0" -until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts deleted file mode 100644 index 82ef10490..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts +++ /dev/null @@ -1,1604 +0,0 @@ -import { parse, Lang } from "@ast-grep/napi"; -import type { SgNode } from "@ast-grep/napi"; - -const GLOBALS_IMPORT = 'import "@tailor-platform/sdk/runtime/globals";'; -const GLOBALS_IMPORT_PATH = "@tailor-platform/sdk/runtime/globals"; -const GLOBALS_REFERENCE = '/// '; -const GLOBAL_NAMES = new Set([ - "tailor", - "tailordb", - "TailorDBFileError", - "TailorErrorItem", - "TailorErrorMessage", - "TailorErrors", -]); -const TAILORDB_NAMESPACE_MEMBERS = new Set(["Client", "CommandType", "QueryResult"]); -const TAILOR_NAMESPACE_MEMBERS = new Map([ - ["context", new Set(["Invoker"])], - ["iconv", new Set(["Iconv"])], - [ - "idp", - new Set([ - "Client", - "ClientConfig", - "CreateUserInput", - "ListUsersOptions", - "ListUsersResponse", - "SendPasswordResetEmailInput", - "UpdateUserInput", - "User", - "UserQuery", - ]), - ], - ["workflow", new Set(["AuthInvoker", "TriggerWorkflowOptions"])], -]); -const GLOBAL_NAME_PATTERN = new RegExp(`\\b(?:${[...GLOBAL_NAMES].join("|")})\\b`); -const DECLARATION_PARENT_KINDS = new Set([ - "abstract_class_declaration", - "class_declaration", - "enum_declaration", - "function_declaration", - "generator_function_declaration", - "interface_declaration", - "internal_module", - "type_alias_declaration", -]); -const VALUE_NAMESPACE_MEMBER_KINDS = new Set([ - "abstract_class_declaration", - "class_declaration", - "enum_declaration", - "function_declaration", - "generator_function_declaration", - "lexical_declaration", - "variable_declaration", -]); -const PARAMETER_PARENT_KINDS = new Set([ - "optional_parameter", - "required_parameter", - "rest_pattern", -]); -const PARAMETER_PREFIX_KINDS = new Set([ - "accessibility_modifier", - "decorator", - "override", - "readonly", -]); -const FUNCTION_SCOPE_KINDS = new Set([ - "arrow_function", - "function", - "function_declaration", - "function_expression", - "function_type", - "generator_function_declaration", - "generator_function", - "method_signature", - "method_definition", - "call_signature", - "construct_signature", -]); -const MODULE_SCOPE_KINDS = new Set(["internal_module", "module"]); -const VAR_SCOPE_KINDS = new Set([ - ...FUNCTION_SCOPE_KINDS, - "class_static_block", - ...MODULE_SCOPE_KINDS, -]); -const TYPE_PARAMETER_SCOPE_KINDS = new Set([ - ...FUNCTION_SCOPE_KINDS, - "class_declaration", - "interface_declaration", - "type_alias_declaration", -]); -const ACCESS_EXPRESSION_KINDS = new Set(["member_expression", "subscript_expression"]); -const BARE_GLOBAL_REFERENCE_KINDS = new Set([ - "identifier", - "shorthand_property_identifier", - "shorthand_property_identifier_pattern", - "type_identifier", -]); -const BLOCK_SCOPE_KINDS = new Set(["program", "statement_block", "switch_body"]); -const CATCH_SCOPE_KINDS = new Set(["catch_clause"]); -const FOR_SCOPE_KINDS = new Set(["for_in_statement", "for_statement"]); -const IMPORT_STATEMENT_KINDS = new Set(["import_statement"]); -const WRAPPER_EXPRESSION_KINDS = new Set([ - "as_expression", - "non_null_expression", - "parenthesized_expression", - "satisfies_expression", - "type_assertion", -]); -const EXPRESSION_NAME_PARENT_KINDS = new Set([ - "class", - "function_expression", - "generator_function", -]); -const NON_INITIALIZER_AFTER_BINDING_KINDS = new Set(["comment", "type_annotation"]); -const LINE_SCOPED_PRAGMA_PATTERN = - /^(?:\/\/|\/\*)\s*(?:eslint-disable-next-line\b|oxlint-disable-next-line\b|@ts-(?:expect-error|ignore)\b|prettier-ignore\b|biome-ignore\b|rome-ignore\b|deno-lint-ignore\b|(?:c8|v8|istanbul|node:coverage|coverage)\s+ignore\s+next\b)/; - -type ReferenceNamespace = "namespace" | "type" | "value"; -type BindingNamespace = ReferenceNamespace | "all" | "both" | "type-namespace"; - -interface ScopedBinding { - name: string; - namespace: BindingNamespace; - node: SgNode; - scope: SgNode; -} - -function parseSource(source: string, filePath: string) { - return parse( - filePath.endsWith(".tsx") || filePath.endsWith(".jsx") ? Lang.Tsx : Lang.TypeScript, - source, - ); -} - -function firstNamedChild(node: SgNode): SgNode | undefined { - return node.children().find((child) => child.isNamed()); -} - -function isSameNode(a: SgNode | undefined, b: SgNode): boolean { - return a?.id() === b.id(); -} - -function hasChildKind(node: SgNode | null | undefined, kind: string): boolean { - return node?.children().some((child) => child.kind() === kind) === true; -} - -function importSpecifierLocalName(node: SgNode): string | null { - const identifiers = node.findAll({ rule: { kind: "identifier" } }); - return identifiers.at(-1)?.text() ?? null; -} - -function importSpecifierLocalNode(node: SgNode): SgNode | null { - const identifiers = node.findAll({ rule: { kind: "identifier" } }); - return identifiers.at(-1) ?? null; -} - -function importAliasLocalNode(node: SgNode): SgNode | null { - const local = firstNamedChild(node); - return local?.kind() === "identifier" ? local : null; -} - -function declaratorAncestor(node: SgNode): SgNode | null { - let current = node.parent(); - while (current) { - if (current.kind() === "variable_declarator") return current; - current = current.parent(); - } - return null; -} - -function bindingPatternName(node: SgNode, bindingPattern: SgNode, stop: SgNode): boolean { - let current: SgNode | null = node; - while (current) { - if (current.kind() === "assignment_pattern" || current.kind() === "object_assignment_pattern") { - const bindingTarget = firstNamedChild(current); - return bindingTarget != null && containsNode(bindingTarget, node); - } - if (current.kind() === "pair_pattern") { - const key = firstNamedChild(current); - return key != null && !containsNode(key, node); - } - if (isSameNode(current, bindingPattern)) return true; - if (isSameNode(current, stop)) return false; - current = current.parent(); - } - return false; -} - -function isInVariableBindingPattern(node: SgNode): boolean { - const declarator = declaratorAncestor(node); - if (!declarator) return false; - const bindingPattern = firstNamedChild(declarator); - if (!bindingPattern) return false; - - let current: SgNode | null = node; - while (current) { - if (isSameNode(current, bindingPattern)) return true; - if (isSameNode(current, declarator)) return false; - current = current.parent(); - } - return false; -} - -function isVariableBindingName(node: SgNode): boolean { - const declarator = declaratorAncestor(node); - if (!declarator || !isInVariableBindingPattern(node)) return false; - const bindingPattern = firstNamedChild(declarator); - return bindingPattern != null && bindingPatternName(node, bindingPattern, declarator); -} - -function parameterAncestor(node: SgNode): SgNode | null { - return nearestAncestorKind(node, PARAMETER_PARENT_KINDS); -} - -function parameterBindingPattern(parameter: SgNode): SgNode | undefined { - return parameter - .children() - .find((child) => child.isNamed() && !PARAMETER_PREFIX_KINDS.has(child.kind())); -} - -function isParameterBindingName(node: SgNode): boolean { - const parameter = parameterAncestor(node); - const bindingPattern = parameter ? parameterBindingPattern(parameter) : undefined; - return bindingPattern != null && bindingPatternName(node, bindingPattern, parameter); -} - -function isForBindingName(node: SgNode): boolean { - const parent = nearestAncestorKind(node, new Set(["for_in_statement"])); - if (parent == null || !/^for(?:\s+await)?\s*\(\s*(const|let|var)\s/.test(parent.text())) { - return false; - } - const bindingPattern = firstNamedChild(parent); - return bindingPattern != null && bindingPatternName(node, bindingPattern, parent); -} - -function isForVarBindingName(node: SgNode): boolean { - const parent = nearestAncestorKind(node, new Set(["for_in_statement"])); - return ( - parent != null && /^for(?:\s+await)?\s*\(\s*var\s/.test(parent.text()) && isForBindingName(node) - ); -} - -function isForStatementInitializerBinding(node: SgNode): boolean { - const declarator = declaratorAncestor(node); - const forStatement = nearestAncestorKind(node, FOR_SCOPE_KINDS); - const initializer = - forStatement?.kind() === "for_statement" ? firstNamedChild(forStatement) : undefined; - return declarator != null && initializer != null && containsNode(initializer, declarator); -} - -function isVarBindingName(node: SgNode): boolean { - const declaration = declaratorAncestor(node)?.parent(); - return declaration?.kind() === "variable_declaration" && /^var\b/.test(declaration.text()); -} - -function usingDeclarationAncestor(node: SgNode): SgNode | null { - const parent = node.parent(); - if (parent?.kind() !== "assignment_expression") return null; - if (!parent.children().some((child) => child.kind() === "using")) return null; - return isSameNode(firstNamedChild(parent), node) ? parent : null; -} - -function isUsingBindingName(node: SgNode): boolean { - return usingDeclarationAncestor(node) != null; -} - -function isCatchBindingName(node: SgNode): boolean { - const catchClause = nearestAncestorKind(node, CATCH_SCOPE_KINDS); - if (catchClause == null || !/\bcatch\s*\(/.test(catchClause.text())) return false; - const bindingPattern = catchClause ? firstNamedChild(catchClause) : undefined; - return bindingPattern != null && bindingPatternName(node, bindingPattern, catchClause); -} - -function isInferTypeBindingName(node: SgNode): boolean { - const parent = node.parent(); - return parent?.kind() === "infer_type" && isSameNode(firstNamedChild(parent), node); -} - -function isExportAsNamespaceName(node: SgNode): boolean { - const parent = node.parent(); - return ( - parent?.kind() === "export_statement" && - /^export\s+as\s+namespace\b/.test(parent.text()) && - isSameNode(firstNamedChild(parent), node) - ); -} - -function isBindingIdentifier(node: SgNode): boolean { - const parent = node.parent(); - if (!parent) return false; - - if (isVariableBindingName(node)) return true; - if (isParameterBindingName(node)) return true; - if (isForBindingName(node)) return true; - if (isCatchBindingName(node)) return true; - if (isQualifiedNamespaceBindingName(node)) return true; - if (isUsingBindingName(node)) return true; - if (isInferTypeBindingName(node)) return true; - if (isExportAsNamespaceName(node)) return true; - - const parentKind = parent.kind(); - if (parentKind === "import_alias") - return isSameNode(importAliasLocalNode(parent) ?? undefined, node); - if (parentKind === "import_specifier") return importSpecifierLocalName(parent) === node.text(); - if ( - parentKind === "import_clause" || - parentKind === "import_require_clause" || - parentKind === "namespace_import" - ) { - return true; - } - if (DECLARATION_PARENT_KINDS.has(parentKind)) return isSameNode(firstNamedChild(parent), node); - if (EXPRESSION_NAME_PARENT_KINDS.has(parentKind)) - return isSameNode(firstNamedChild(parent), node); - if (parentKind === "type_parameter" || parentKind === "mapped_type_clause") { - return isSameNode(firstNamedChild(parent), node); - } - if (parentKind === "index_signature") return isSameNode(firstNamedChild(parent), node); - - return false; -} - -function nameNodes(root: SgNode, names: Set): SgNode[] { - const nodes: SgNode[] = []; - const visit = (node: SgNode): void => { - if (node.isNamedLeaf() && names.has(node.text())) nodes.push(node); - for (const child of node.children()) visit(child); - }; - visit(root); - return nodes; -} - -function globalNameNodes(root: SgNode): SgNode[] { - return nameNodes(root, GLOBAL_NAMES); -} - -function nearestAncestorKind(node: SgNode, kinds: Set): SgNode | null { - let current = node.parent(); - while (current) { - if (kinds.has(current.kind())) return current; - current = current.parent(); - } - return null; -} - -function statementBlockScope(container: SgNode): SgNode { - return container.children().find((child) => child.kind() === "statement_block") ?? container; -} - -function varBindingScope(node: SgNode, root: SgNode): SgNode { - const container = nearestAncestorKind(node, VAR_SCOPE_KINDS); - if (container == null) return root; - return FUNCTION_SCOPE_KINDS.has(container.kind()) ? statementBlockScope(container) : container; -} - -function importBindingScope(node: SgNode, root: SgNode): SgNode { - return nearestAncestorKind(node, MODULE_SCOPE_KINDS) ?? root; -} - -function declareGlobalBlock(node: SgNode): SgNode | null { - let current = node.parent(); - while (current) { - if (current.kind() === "ambient_declaration" && hasChildKind(current, "global")) { - return current.children().find((child) => child.kind() === "statement_block") ?? null; - } - current = current.parent(); - } - return null; -} - -function hasDeclareGlobalBlock(root: SgNode): boolean { - let found = false; - const visit = (node: SgNode): void => { - if (found) return; - if (node.kind() === "ambient_declaration" && hasChildKind(node, "global")) { - found = true; - return; - } - for (const child of node.children()) visit(child); - }; - visit(root); - return found; -} - -function isDirectDeclareGlobalBinding(node: SgNode): boolean { - const block = declareGlobalBlock(node); - if (!block) return false; - - let current = node.parent(); - while (current) { - if (isSameNode(current, block)) return true; - if (current.kind() === "statement_block") return false; - current = current.parent(); - } - return false; -} - -function bindingScope(node: SgNode, root: SgNode): SgNode { - const parentKind = node.parent()?.kind(); - if (isVariableBindingName(node) && isVarBindingName(node)) { - return varBindingScope(node, root); - } - if (isForVarBindingName(node)) { - return varBindingScope(node, root); - } - if (isUsingBindingName(node)) return nearestAncestorKind(node, BLOCK_SCOPE_KINDS) ?? root; - if (isForStatementInitializerBinding(node)) { - return nearestAncestorKind(node, FOR_SCOPE_KINDS) ?? root; - } - if ( - parentKind === "import_clause" || - parentKind === "import_alias" || - parentKind === "import_require_clause" || - parentKind === "import_specifier" || - parentKind === "namespace_import" - ) { - return importBindingScope(node, root); - } - if (parentKind != null && EXPRESSION_NAME_PARENT_KINDS.has(parentKind)) { - return node.parent() ?? root; - } - if (parentKind === "type_parameter") { - return nearestAncestorKind(node, TYPE_PARAMETER_SCOPE_KINDS) ?? root; - } - if (parentKind === "infer_type") { - return nearestAncestorKind(node, new Set(["conditional_type"])) ?? node.parent() ?? root; - } - if (parentKind === "mapped_type_clause") { - return nearestAncestorKind(node, new Set(["index_signature"])) ?? root; - } - if (parentKind === "index_signature") return node.parent() ?? root; - if (isExportAsNamespaceName(node)) return root; - if (parentKind != null && PARAMETER_PARENT_KINDS.has(parentKind)) { - return nearestAncestorKind(node, FUNCTION_SCOPE_KINDS) ?? root; - } - const parameter = parameterAncestor(node); - if (parameter) return nearestAncestorKind(parameter, FUNCTION_SCOPE_KINDS) ?? root; - - if (isForBindingName(node)) return nearestAncestorKind(node, FOR_SCOPE_KINDS) ?? root; - if (isCatchBindingName(node)) return nearestAncestorKind(node, CATCH_SCOPE_KINDS) ?? root; - if (isDirectDeclareGlobalBinding(node)) return root; - - return nearestAncestorKind(node, BLOCK_SCOPE_KINDS) ?? root; -} - -function isTypeOnlyImport(node: SgNode): boolean { - const parent = node.parent(); - const importStatement = nearestAncestorKind(node, IMPORT_STATEMENT_KINDS); - return hasChildKind(parent, "type") || hasChildKind(importStatement, "type"); -} - -function hasRuntimeNamespaceMember(node: SgNode): boolean { - const runtimeKinds = new Set([ - "abstract_class_declaration", - "class_declaration", - "enum_declaration", - "export_assignment", - "function_declaration", - "generator_function_declaration", - "lexical_declaration", - "variable_declaration", - ]); - const visit = (current: SgNode): boolean => { - if (runtimeKinds.has(current.kind())) return true; - if (current.kind() === "internal_module") return hasRuntimeNamespaceMember(current); - if ( - current.kind() === "expression_statement" && - current.children().some((child) => child.isNamed()) - ) { - const namedChildren = current.children().filter((child) => child.isNamed()); - return namedChildren.every((child) => child.kind() === "internal_module") - ? namedChildren.some(visit) - : true; - } - return current.children().some(visit); - }; - return ( - node - .children() - .find((child) => child.kind() === "statement_block") - ?.children() - .some(visit) === true - ); -} - -function namespaceBindingModule(node: SgNode): SgNode | null { - const parent = node.parent(); - if (parent?.kind() === "internal_module") return parent; - if (parent?.kind() !== "nested_identifier") return null; - const namespace = parent.parent(); - if (namespace?.kind() !== "internal_module") return null; - return isSameNode(firstNamedChild(parent), node) ? namespace : null; -} - -function isQualifiedNamespaceBindingName(node: SgNode): boolean { - return node.parent()?.kind() === "nested_identifier" && namespaceBindingModule(node) != null; -} - -function isTypeOnlyNamespace(node: SgNode): boolean { - const namespace = namespaceBindingModule(node); - if (!namespace) return false; - if (namespace.parent()?.kind() === "ambient_declaration") return true; - return !hasRuntimeNamespaceMember(namespace); -} - -function initializerAfterBinding(node: SgNode, binding: SgNode): SgNode | null { - return ( - node - .children() - .find( - (child) => - child.isNamed() && - child.range().start.index > binding.range().end.index && - !NON_INITIALIZER_AFTER_BINDING_KINDS.has(child.kind()), - ) ?? null - ); -} - -function isUnshadowedGlobalThisExpression( - node: SgNode | undefined, - globalThisBindings: ScopedBinding[], - globalThisAliases: ScopedBinding[] = [], - aliasValueBindings: ScopedBinding[] = [], -): boolean { - return ( - (isGlobalThisExpression(node) && !isShadowedGlobalThisExpression(node, globalThisBindings)) || - isGlobalThisAliasExpression(node, globalThisAliases, aliasValueBindings) - ); -} - -function globalThisDestructuringBinding( - node: SgNode, - globalThisBindings: ScopedBinding[], - globalThisAliases: ScopedBinding[] = [], - aliasValueBindings: ScopedBinding[] = [], -): SgNode | null { - const declarator = declaratorAncestor(node); - const binding = declarator ? firstNamedChild(declarator) : undefined; - if ( - declarator && - binding && - containsNode(binding, node) && - isUnshadowedGlobalThisExpression( - initializerAfterBinding(declarator, binding) ?? undefined, - globalThisBindings, - globalThisAliases, - aliasValueBindings, - ) - ) { - return binding; - } - - const parameter = parameterAncestor(node); - const parameterBinding = parameter ? parameterBindingPattern(parameter) : undefined; - if ( - parameter && - parameterBinding && - containsNode(parameterBinding, node) && - isUnshadowedGlobalThisExpression( - initializerAfterBinding(parameter, parameterBinding) ?? undefined, - globalThisBindings, - globalThisAliases, - aliasValueBindings, - ) - ) { - return parameterBinding; - } - - const assignment = nearestAncestorKind(node, new Set(["assignment_expression"])); - const assignmentBinding = assignment ? firstNamedChild(assignment) : undefined; - if ( - assignment && - assignmentBinding && - containsNode(assignmentBinding, node) && - isUnshadowedGlobalThisExpression( - initializerAfterBinding(assignment, assignmentBinding) ?? undefined, - globalThisBindings, - globalThisAliases, - aliasValueBindings, - ) - ) { - return assignmentBinding; - } - - return null; -} - -function isGlobalThisDestructuredProperty( - node: SgNode, - globalThisBindings: ScopedBinding[], - globalThisAliases: ScopedBinding[] = [], - aliasValueBindings: ScopedBinding[] = [], -): boolean { - const binding = globalThisDestructuringBinding( - node, - globalThisBindings, - globalThisAliases, - aliasValueBindings, - ); - if (!binding) return false; - if (node.kind() === "shorthand_property_identifier_pattern") { - const parent = node.parent(); - return ( - isSameNode(parent, binding) || - ((parent?.kind() === "assignment_pattern" || - parent?.kind() === "object_assignment_pattern") && - isSameNode(parent.parent(), binding)) - ); - } - - let current = node.parent(); - while (current && !isSameNode(current, binding)) { - if (current.kind() === "object_assignment_pattern") { - const target = firstNamedChild(current); - return isSameNode(current.parent(), binding) && target != null && containsNode(target, node); - } - if (current.kind() === "pair_pattern") { - const key = firstNamedChild(current); - return ( - isSameNode(current.parent(), binding) && - key != null && - destructuredPropertyName(key) === node.text() && - containsNode(key, node) - ); - } - current = current.parent(); - } - return false; -} - -function destructuredPropertyName(key: SgNode): string | null { - if (key.kind() === "computed_property_name") { - const expression = firstNamedChild(key); - return expression ? staticLiteralPropertyName(expression) : null; - } - return staticPropertyName(key); -} - -function staticPropertyName(node: SgNode): string | null { - let current: SgNode | null = node; - while (current && WRAPPER_EXPRESSION_KINDS.has(current.kind())) { - current = firstNamedChild(current) ?? null; - } - if (!current) return null; - - const kind = current.kind(); - if ( - kind === "identifier" || - kind === "property_identifier" || - kind === "shorthand_property_identifier" || - kind === "shorthand_property_identifier_pattern" - ) { - return current.text(); - } - - const text = current.text(); - for (const name of GLOBAL_NAMES) { - if (text === `"${name}"` || text === `'${name}'` || text === `\`${name}\``) { - return name; - } - } - return null; -} - -function staticLiteralPropertyName(node: SgNode): string | null { - let current: SgNode | null = node; - while (current && WRAPPER_EXPRESSION_KINDS.has(current.kind())) { - current = firstNamedChild(current) ?? null; - } - if (!current) return null; - - const text = current.text(); - for (const name of GLOBAL_NAMES) { - if (text === `"${name}"` || text === `'${name}'` || text === `\`${name}\``) { - return name; - } - } - return null; -} - -function bindingNamespace(node: SgNode): BindingNamespace { - const parentKind = node.parent()?.kind(); - if (isQualifiedNamespaceBindingName(node)) { - return isTypeOnlyNamespace(node) ? "type-namespace" : "namespace"; - } - if ( - parentKind === "import_clause" || - parentKind === "import_alias" || - parentKind === "import_require_clause" || - parentKind === "import_specifier" || - parentKind === "namespace_import" - ) { - if (parentKind === "import_alias") return "all"; - if (parentKind === "import_require_clause" || parentKind === "namespace_import") { - return isTypeOnlyImport(node) ? "type-namespace" : "namespace"; - } - return isTypeOnlyImport(node) ? "type" : "both"; - } - if (parentKind === "type_alias_declaration" || parentKind === "interface_declaration") { - return "type"; - } - if (parentKind === "internal_module") { - return isTypeOnlyNamespace(node) ? "type-namespace" : "namespace"; - } - if (parentKind === "class") return "both"; - if (parentKind === "abstract_class_declaration" || parentKind === "class_declaration") { - return "both"; - } - if (parentKind === "enum_declaration") return "all"; - if ( - parentKind === "type_parameter" || - parentKind === "infer_type" || - parentKind === "mapped_type_clause" || - parentKind === "index_signature" - ) { - return "type"; - } - if (isExportAsNamespaceName(node)) return "namespace"; - return "value"; -} - -function localNameBindings(root: SgNode, names: Set): ScopedBinding[] { - return nameNodes(root, names) - .filter(isBindingIdentifier) - .map((node) => ({ - name: node.text(), - namespace: bindingNamespace(node), - node, - scope: bindingScope(node, root), - })); -} - -function localGlobalBindings(root: SgNode): ScopedBinding[] { - return localNameBindings(root, GLOBAL_NAMES); -} - -function localValueBindings(root: SgNode, name: string): ScopedBinding[] { - return localValueBindingsForNames(root, new Set([name])); -} - -function localValueBindingsForNames(root: SgNode, names: Set): ScopedBinding[] { - return localNameBindings(root, names).filter((binding) => - bindingShadowsReference(binding.namespace, "value"), - ); -} - -function isConstVariableDeclarator(node: SgNode): boolean { - const declaration = node.parent(); - return declaration?.kind() === "lexical_declaration" && /\bconst\b/.test(declaration.text()); -} - -function globalThisAliasBindings( - root: SgNode, - globalThisBindings: ScopedBinding[], -): ScopedBinding[] { - const aliases: ScopedBinding[] = []; - const visit = (node: SgNode): void => { - if (node.kind() === "variable_declarator" && isConstVariableDeclarator(node)) { - const binding = firstNamedChild(node); - const aliasValueBindings = - aliases.length === 0 - ? [] - : localValueBindingsForNames(root, new Set(aliases.map((alias) => alias.name))); - if ( - binding?.kind() === "identifier" && - isUnshadowedGlobalThisExpression( - initializerAfterBinding(node, binding) ?? undefined, - globalThisBindings, - aliases, - aliasValueBindings, - ) - ) { - aliases.push({ - name: binding.text(), - namespace: "value", - node: binding, - scope: bindingScope(binding, root), - }); - } - } - - for (const child of node.children()) visit(child); - }; - visit(root); - return aliases; -} - -function containsNode(ancestor: SgNode, node: SgNode): boolean { - let current: SgNode | null = node; - while (current) { - if (isSameNode(current, ancestor)) return true; - current = current.parent(); - } - return false; -} - -function isReExportSpecifier(node: SgNode): boolean { - const exportStatement = nearestAncestorKind(node, new Set(["export_statement"])); - return exportStatement?.children().some((child) => child.kind() === "from") === true; -} - -function isTypeOnlyExportSpecifier(node: SgNode): boolean { - const parent = node.parent(); - if (parent?.kind() !== "export_specifier") return false; - const exportStatement = nearestAncestorKind(node, new Set(["export_statement"])); - return hasChildKind(parent, "type") || hasChildKind(exportStatement, "type"); -} - -function isSpecifierNonReferenceName(node: SgNode): boolean { - const parent = node.parent(); - if (parent?.kind() === "namespace_export") return true; - if (parent?.kind() === "import_specifier") { - return !isSameNode(importSpecifierLocalNode(parent) ?? undefined, node); - } - if (parent?.kind() !== "export_specifier") return false; - - if (isReExportSpecifier(node)) return true; - - const identifiers = parent.findAll({ rule: { kind: "identifier" } }); - return identifiers.length > 1 && isSameNode(identifiers.at(-1), node); -} - -function isJsxTagName(node: SgNode): boolean { - const parent = node.parent(); - return ( - parent != null && - (parent.kind() === "jsx_closing_element" || - parent.kind() === "jsx_opening_element" || - parent.kind() === "jsx_self_closing_element") && - isSameNode(firstNamedChild(parent), node) - ); -} - -function globalThisSubscriptFromStringFragment( - node: SgNode, - globalThisAliases: ScopedBinding[] = [], - aliasValueBindings: ScopedBinding[] = [], -): SgNode | null { - if (node.kind() !== "string_fragment") return null; - let expression = node.parent(); - if (expression?.kind() !== "string" && expression?.kind() !== "template_string") return null; - if (expression.kind() === "template_string" && expression.text() !== `\`${node.text()}\``) { - return null; - } - while (expression.parent() && WRAPPER_EXPRESSION_KINDS.has(expression.parent()?.kind() ?? "")) { - expression = expression.parent(); - } - const subscript = expression.parent(); - if (subscript?.kind() !== "subscript_expression") return null; - return isGlobalThisLikeExpression( - firstNamedChild(subscript), - globalThisAliases, - aliasValueBindings, - ) - ? subscript - : null; -} - -function unwrapParenthesizedType(node: SgNode | undefined): SgNode | undefined { - let current = node; - while (current?.kind() === "parenthesized_type") { - current = firstNamedChild(current); - } - return current; -} - -function typeQueryExpression(node: SgNode | undefined): SgNode | undefined { - const query = unwrapParenthesizedType(node); - return query?.kind() === "type_query" ? firstNamedChild(query) : undefined; -} - -function globalThisLookupTypeFromStringFragment( - node: SgNode, - globalThisAliases: ScopedBinding[] = [], - aliasValueBindings: ScopedBinding[] = [], -): SgNode | null { - if (node.kind() !== "string_fragment") return null; - const stringLiteral = node.parent(); - if (stringLiteral?.kind() !== "string") return null; - const literalType = stringLiteral.parent(); - if (literalType?.kind() !== "literal_type") return null; - const lookupType = literalType.parent(); - if (lookupType?.kind() !== "lookup_type") return null; - - const targetType = firstNamedChild(lookupType); - if (targetType == null || isSameNode(targetType, literalType)) return null; - const expression = typeQueryExpression(targetType); - return expression != null && - isGlobalThisLikeExpression(expression, globalThisAliases, aliasValueBindings) - ? lookupType - : null; -} - -function identifierExpression(node: SgNode | undefined): SgNode | null { - if (node == null) return null; - if (node.kind() === "identifier") return node; - if (WRAPPER_EXPRESSION_KINDS.has(node.kind())) { - return identifierExpression(firstNamedChild(node)); - } - return null; -} - -function globalThisIdentifier(node: SgNode | undefined): SgNode | null { - const identifier = identifierExpression(node); - return identifier?.text() === "globalThis" ? identifier : null; -} - -function isGlobalThisExpression(node: SgNode | undefined): boolean { - return globalThisIdentifier(node) != null; -} - -function isReferenceToScopedBinding( - identifier: SgNode, - binding: ScopedBinding, - bindings: ScopedBinding[], -): boolean { - if (identifier.text() !== binding.name || !containsNode(binding.scope, identifier)) return false; - return !bindings.some( - (candidate) => - candidate.name === binding.name && - !isSameNode(candidate.node, binding.node) && - containsNode(candidate.scope, identifier) && - containsNode(binding.scope, candidate.node), - ); -} - -function isGlobalThisAliasExpression( - node: SgNode | undefined, - globalThisAliases: ScopedBinding[], - aliasValueBindings: ScopedBinding[], -): boolean { - const identifier = identifierExpression(node); - return ( - identifier != null && - globalThisAliases.some((alias) => - isReferenceToScopedBinding(identifier, alias, aliasValueBindings), - ) - ); -} - -function isGlobalThisLikeExpression( - node: SgNode | undefined, - globalThisAliases: ScopedBinding[] = [], - aliasValueBindings: ScopedBinding[] = [], -): boolean { - return ( - isGlobalThisExpression(node) || - isGlobalThisAliasExpression(node, globalThisAliases, aliasValueBindings) - ); -} - -function isShadowedGlobalThisExpression( - node: SgNode | undefined, - globalThisBindings: ScopedBinding[], -): boolean { - const identifier = globalThisIdentifier(node); - return ( - identifier != null && - globalThisBindings.some((binding) => containsNode(binding.scope, identifier)) - ); -} - -function globalReferenceAncestor( - node: SgNode, - globalThisAliases: ScopedBinding[] = [], - aliasValueBindings: ScopedBinding[] = [], -): SgNode | null { - if (!GLOBAL_NAMES.has(node.text())) return null; - if (isJsxTagName(node)) return null; - if (isSpecifierNonReferenceName(node)) return null; - const globalThisSubscript = globalThisSubscriptFromStringFragment( - node, - globalThisAliases, - aliasValueBindings, - ); - if (globalThisSubscript) return globalThisSubscript; - const globalThisLookupType = globalThisLookupTypeFromStringFragment( - node, - globalThisAliases, - aliasValueBindings, - ); - if (globalThisLookupType) return globalThisLookupType; - - let current = node.parent(); - while (current) { - const kind = current.kind(); - if (kind === "nested_identifier" || kind === "nested_type_identifier") { - const path = qualifiedPath(current); - if (path?.[0] !== node.text()) return null; - let reference = current; - while ( - reference.parent() && - (reference.parent()?.kind() === "nested_identifier" || - reference.parent()?.kind() === "nested_type_identifier") && - qualifiedPath(reference.parent()!)?.[0] === node.text() - ) { - reference = reference.parent()!; - } - return reference; - } - if (ACCESS_EXPRESSION_KINDS.has(kind)) { - const object = firstNamedChild(current); - if (isGlobalThisLikeExpression(object, globalThisAliases, aliasValueBindings)) return current; - if (object == null || !containsNode(object, node)) return null; - let reference = current; - while ( - reference.parent() && - ACCESS_EXPRESSION_KINDS.has(reference.parent()?.kind() ?? "") && - containsNode(firstNamedChild(reference.parent()!) ?? reference.parent()!, node) - ) { - reference = reference.parent()!; - } - return reference; - } - if (WRAPPER_EXPRESSION_KINDS.has(kind)) { - current = current.parent(); - continue; - } - return BARE_GLOBAL_REFERENCE_KINDS.has(node.kind()) && !isBindingIdentifier(node) ? node : null; - } - return BARE_GLOBAL_REFERENCE_KINDS.has(node.kind()) && !isBindingIdentifier(node) ? node : null; -} - -function globalThisReferenceObject(reference: SgNode): SgNode | undefined { - if (ACCESS_EXPRESSION_KINDS.has(reference.kind())) return firstNamedChild(reference); - if (reference.kind() === "lookup_type") return typeQueryExpression(firstNamedChild(reference)); - return undefined; -} - -function isGlobalReference( - node: SgNode, - globalThisAliases: ScopedBinding[] = [], - aliasValueBindings: ScopedBinding[] = [], -): boolean { - return globalReferenceAncestor(node, globalThisAliases, aliasValueBindings) != null; -} - -function referenceNamespace( - node: SgNode, - globalThisAliases: ScopedBinding[] = [], - aliasValueBindings: ScopedBinding[] = [], -): ReferenceNamespace { - const reference = globalReferenceAncestor(node, globalThisAliases, aliasValueBindings); - const referenceKind = reference?.kind(); - if (referenceKind === "nested_identifier" || referenceKind === "nested_type_identifier") { - return "namespace"; - } - if (isTypeOnlyExportSpecifier(node)) return "type"; - return referenceKind === "type_identifier" ? "type" : "value"; -} - -function runtimeNamespaceMemberPath( - node: SgNode, - globalThisAliases: ScopedBinding[] = [], - aliasValueBindings: ScopedBinding[] = [], -): string[] | null { - const reference = globalReferenceAncestor(node, globalThisAliases, aliasValueBindings); - const referenceKind = reference?.kind(); - if ( - referenceKind !== "nested_identifier" && - referenceKind !== "nested_type_identifier" && - referenceKind !== "member_expression" - ) { - return null; - } - - const parts = qualifiedPath(reference); - if (!parts) return null; - if (parts[0] !== node.text()) return null; - - if (parts[0] === "tailordb" && TAILORDB_NAMESPACE_MEMBERS.has(parts[1] ?? "")) { - return parts.slice(0, 2); - } - - const tailorMembers = parts[0] === "tailor" ? TAILOR_NAMESPACE_MEMBERS.get(parts[1] ?? "") : null; - if (tailorMembers?.has(parts[2] ?? "") === true) { - return parts.slice(0, 3); - } - - return null; -} - -function qualifiedPath(node: SgNode): string[] | null { - if ( - node.kind() === "identifier" || - node.kind() === "property_identifier" || - node.kind() === "type_identifier" - ) { - return [node.text()]; - } - if ( - node.kind() !== "member_expression" && - node.kind() !== "nested_identifier" && - node.kind() !== "nested_type_identifier" - ) { - return null; - } - - const parts: string[] = []; - for (const child of node.children()) { - if (!child.isNamed() || child.kind() === "comment") continue; - const childPath = qualifiedPath(child); - if (!childPath) return null; - parts.push(...childPath); - } - return parts.length > 0 ? parts : null; -} - -function dotPath(text: string): string[] { - return text.split(".").filter(Boolean); -} - -function startsWithPath(path: string[], prefix: string[]): boolean { - return prefix.length <= path.length && prefix.every((part, index) => path[index] === part); -} - -function namespaceBindingPath(node: SgNode): string[] | null { - const namespace = namespaceBindingModule(node); - const name = namespace ? firstNamedChild(namespace) : undefined; - if (name?.kind() !== "identifier" && name?.kind() !== "nested_identifier") return null; - return qualifiedPath(name) ?? dotPath(name.text()); -} - -function namespaceStatementBlock(node: SgNode): SgNode | null { - return node.children().find((child) => child.kind() === "statement_block") ?? null; -} - -function unwrapNamespaceMember(node: SgNode): SgNode | null { - if (node.kind() === "export_statement") { - return ( - node.children().find((child) => child.isNamed() && child.kind() !== "export_clause") ?? null - ); - } - if (node.kind() === "expression_statement") { - const namedChildren = node.children().filter((child) => child.isNamed()); - return namedChildren.length === 1 ? namedChildren[0] : null; - } - return node; -} - -function namespaceMemberPath(node: SgNode): string[] | null { - const member = unwrapNamespaceMember(node); - const name = member ? namespaceMemberName(member) : undefined; - if (!name) return null; - if (member?.kind() === "internal_module") return qualifiedPath(name) ?? dotPath(name.text()); - if (DECLARATION_PARENT_KINDS.has(member?.kind() ?? "")) return [name.text()]; - return null; -} - -function namespaceMemberName(member: SgNode): SgNode | null { - if (member.kind() === "lexical_declaration" || member.kind() === "variable_declaration") { - const declarator = firstNamedChild(member); - return declarator ? (firstNamedChild(declarator) ?? null) : null; - } - return firstNamedChild(member) ?? null; -} - -function namespaceValueMemberPath(node: SgNode): string[] | null { - const member = unwrapNamespaceMember(node); - const name = member ? namespaceMemberName(member) : undefined; - if (!member || !name) return null; - if (member.kind() === "internal_module") { - return hasRuntimeNamespaceMember(member) ? (qualifiedPath(name) ?? dotPath(name.text())) : null; - } - if (VALUE_NAMESPACE_MEMBER_KINDS.has(member.kind())) return [name.text()]; - return null; -} - -function namespaceDeclaresPath(namespace: SgNode, path: string[]): boolean { - if (path.length === 0) return true; - - const block = namespaceStatementBlock(namespace); - if (!block) return false; - - for (const child of block.children()) { - if (!child.isNamed()) continue; - const member = unwrapNamespaceMember(child); - if (!member) continue; - - const memberPath = namespaceMemberPath(member); - if (!memberPath || !startsWithPath(path, memberPath)) continue; - if (memberPath.length === path.length) return true; - if ( - member.kind() === "internal_module" && - namespaceDeclaresPath(member, path.slice(memberPath.length)) - ) { - return true; - } - } - - return false; -} - -function namespaceDeclaresValuePath(namespace: SgNode, path: string[]): boolean { - if (path.length === 0) return hasRuntimeNamespaceMember(namespace); - - const block = namespaceStatementBlock(namespace); - if (!block) return false; - - for (const child of block.children()) { - if (!child.isNamed()) continue; - const member = unwrapNamespaceMember(child); - if (!member) continue; - - const valuePath = namespaceValueMemberPath(member); - if (valuePath && startsWithPath(path, valuePath) && valuePath.length === path.length) { - return true; - } - - const memberPath = namespaceMemberPath(member); - if ( - member?.kind() === "internal_module" && - memberPath && - startsWithPath(path, memberPath) && - namespaceDeclaresValuePath(member, path.slice(memberPath.length)) - ) { - return true; - } - } - - return false; -} - -function namespaceBindingDeclaresPath(node: SgNode, path: string[]): boolean { - const namespace = namespaceBindingModule(node); - const namespacePath = namespaceBindingPath(node); - if (!namespace || !namespacePath || !startsWithPath(path, namespacePath)) return false; - return namespaceDeclaresPath(namespace, path.slice(namespacePath.length)); -} - -function namespaceBindingDeclaresValuePath(node: SgNode, path: string[]): boolean { - const namespace = namespaceBindingModule(node); - const namespacePath = namespaceBindingPath(node); - if (!namespace || !namespacePath || !startsWithPath(path, namespacePath)) return false; - return namespaceDeclaresValuePath(namespace, path.slice(namespacePath.length)); -} - -function isGlobalThisReference( - node: SgNode, - globalThisAliases: ScopedBinding[] = [], - aliasValueBindings: ScopedBinding[] = [], -): boolean { - const reference = globalReferenceAncestor(node, globalThisAliases, aliasValueBindings); - const object = reference ? globalThisReferenceObject(reference) : undefined; - return ( - object != null && isGlobalThisLikeExpression(object, globalThisAliases, aliasValueBindings) - ); -} - -function bindingShadowsReference( - binding: BindingNamespace, - reference: ReferenceNamespace, -): boolean { - if (binding === "all") return true; - if (binding === "both") return reference === "type" || reference === "value"; - if (binding === "namespace") return reference === "namespace" || reference === "value"; - if (binding === "type-namespace") return reference === "namespace"; - return binding === reference; -} - -function isShadowedGlobalThisReference( - node: SgNode, - globalThisBindings: ScopedBinding[], - globalThisAliases: ScopedBinding[] = [], - aliasValueBindings: ScopedBinding[] = [], -): boolean { - const reference = globalReferenceAncestor(node, globalThisAliases, aliasValueBindings); - const object = reference ? globalThisReferenceObject(reference) : undefined; - return object != null && isShadowedGlobalThisExpression(object, globalThisBindings); -} - -function isShadowedReference( - node: SgNode, - bindings: ScopedBinding[], - mergeRuntimeNamespaceAugmentations: boolean, - globalThisAliases: ScopedBinding[] = [], - aliasValueBindings: ScopedBinding[] = [], -): boolean { - if (isGlobalThisReference(node, globalThisAliases, aliasValueBindings)) return false; - const namespace = referenceNamespace(node, globalThisAliases, aliasValueBindings); - return bindings.some((binding) => { - if (binding.name !== node.text() || !containsNode(binding.scope, node)) return false; - const runtimePath = runtimeNamespaceMemberPath(node, globalThisAliases, aliasValueBindings); - if ( - mergeRuntimeNamespaceAugmentations && - binding.namespace === "type-namespace" && - runtimePath != null && - (namespace === "namespace" || namespace === "value") - ) { - if (namespace === "value") { - if (namespaceBindingDeclaresValuePath(binding.node, runtimePath)) return true; - } else if (namespaceBindingDeclaresPath(binding.node, runtimePath)) { - return true; - } - if (namespace === "namespace") return false; - } - return bindingShadowsReference(binding.namespace, namespace); - }); -} - -function unshadowedRuntimeGlobalReferences( - root: SgNode, - mergeRuntimeNamespaceAugmentations: boolean, -): SgNode[] { - const bindings = localGlobalBindings(root); - const globalThisBindings = localValueBindings(root, "globalThis"); - const globalThisAliases = globalThisAliasBindings(root, globalThisBindings); - const aliasValueBindings = localValueBindingsForNames( - root, - new Set(globalThisAliases.map((alias) => alias.name)), - ); - return globalNameNodes(root).filter( - (node) => - isGlobalThisDestructuredProperty( - node, - globalThisBindings, - globalThisAliases, - aliasValueBindings, - ) || - (isGlobalReference(node, globalThisAliases, aliasValueBindings) && - !isShadowedGlobalThisReference( - node, - globalThisBindings, - globalThisAliases, - aliasValueBindings, - ) && - !isShadowedReference( - node, - bindings, - mergeRuntimeNamespaceAugmentations, - globalThisAliases, - aliasValueBindings, - )), - ); -} - -function topLevelImports(root: SgNode): SgNode[] { - return root.children().filter((node) => node.kind() === "import_statement"); -} - -function hasGlobalsImport(root: SgNode): boolean { - return topLevelImports(root).some((node) => { - return ( - node.text().includes(`"${GLOBALS_IMPORT_PATH}"`) || - node.text().includes(`'${GLOBALS_IMPORT_PATH}'`) - ); - }); -} - -function hasGlobalsReference(source: string): boolean { - let pos = 0; - if (source.startsWith("#!")) { - const firstLineEnd = source.indexOf("\n"); - pos = firstLineEnd === -1 ? source.length : firstLineEnd + 1; - } - - while (pos < source.length) { - const rest = source.slice(pos); - const whitespace = rest.match(/^[ \t\r\n]+/); - if (whitespace) { - pos += whitespace[0].length; - continue; - } - - if (rest.startsWith("///")) { - const end = rest.indexOf("\n"); - const comment = end === -1 ? rest : rest.slice(0, end); - if ( - /^\/\/\/\s*/.test( - comment, - ) - ) { - return true; - } - pos += end === -1 ? rest.length : end + 1; - continue; - } - - if (rest.startsWith("//")) { - const end = rest.indexOf("\n"); - pos += end === -1 ? rest.length : end + 1; - continue; - } - - if (rest.startsWith("/*")) { - const end = rest.indexOf("*/"); - if (end === -1) return false; - pos += end + 2; - continue; - } - - return false; - } - - return false; -} - -function hasGlobalsOptIn(source: string, root: SgNode): boolean { - return hasGlobalsImport(root) || hasGlobalsReference(source); -} - -function isLineScopedPragmaComment(comment: string): boolean { - return LINE_SCOPED_PRAGMA_PATTERN.test(comment); -} - -function prologueEnd(source: string): number { - let pos = 0; - let consumedDirective = false; - let lastDirectiveEnd = 0; - if (source.startsWith("#!")) { - const firstLineEnd = source.indexOf("\n"); - pos = firstLineEnd === -1 ? source.length : firstLineEnd + 1; - lastDirectiveEnd = pos; - } - - while (pos < source.length) { - const rest = source.slice(pos); - const whitespace = rest.match(/^[ \t\r\n]+/); - if (whitespace) { - pos += whitespace[0].length; - continue; - } - - if (rest.startsWith("//")) { - const end = rest.indexOf("\n"); - const comment = end === -1 ? rest : rest.slice(0, end); - if (isLineScopedPragmaComment(comment)) return consumedDirective ? lastDirectiveEnd : pos; - pos += end === -1 ? rest.length : end + 1; - continue; - } - - if (rest.startsWith("/*")) { - const end = rest.indexOf("*/"); - if (end === -1) return pos; - const comment = rest.slice(0, end + 2); - if (isLineScopedPragmaComment(comment)) return consumedDirective ? lastDirectiveEnd : pos; - pos += end + 2; - continue; - } - - const directive = rest.match(/^(['"])(?:\\.|(?!\1).)*\1[ \t]*;?[ \t]*/); - if (!directive) return consumedDirective ? lastDirectiveEnd : pos; - - let directiveLength = directive[0].length; - while (directiveLength < rest.length) { - const trailing = rest.slice(directiveLength); - const spacing = trailing.match(/^[ \t]+/); - if (spacing) { - directiveLength += spacing[0].length; - continue; - } - - const lineComment = trailing.match(/^\/\/[^\r\n]*/); - if (lineComment) { - directiveLength += lineComment[0].length; - break; - } - - const blockComment = trailing.match(/^\/\*[\s\S]*?\*\//); - if (blockComment) { - directiveLength += blockComment[0].length; - continue; - } - - break; - } - - const afterDirective = rest.slice(directiveLength); - const lineEnd = afterDirective.match(/^\r?\n|^$/); - if (!lineEnd) return pos; - pos += directiveLength; - pos += lineEnd[0].length; - consumedDirective = true; - lastDirectiveEnd = pos; - } - - return consumedDirective ? lastDirectiveEnd : pos; -} - -function referenceDirectiveInsertPos(source: string): number { - let pos = 0; - if (source.startsWith("#!")) { - const firstLineEnd = source.indexOf("\n"); - pos = firstLineEnd === -1 ? source.length : firstLineEnd + 1; - } - - while (pos < source.length) { - const rest = source.slice(pos); - const whitespace = rest.match(/^[ \t\r\n]+/); - if (whitespace) { - pos += whitespace[0].length; - continue; - } - - if (rest.startsWith("//")) { - const end = rest.indexOf("\n"); - const comment = end === -1 ? rest : rest.slice(0, end); - if (isLineScopedPragmaComment(comment)) return pos; - pos += end === -1 ? rest.length : end + 1; - continue; - } - - if (rest.startsWith("/*")) { - const end = rest.indexOf("*/"); - if (end === -1) return pos; - const comment = rest.slice(0, end + 2); - if (isLineScopedPragmaComment(comment)) return pos; - pos += end + 2; - continue; - } - - return pos; - } - - return pos; -} - -function statementEndWithTrailingComments( - source: string, - statementStart: number, - pos: number, -): number { - let current = pos; - while (current < source.length) { - const rest = source.slice(current); - const spacing = rest.match(/^[ \t]+/); - if (spacing) { - current += spacing[0].length; - continue; - } - - if (rest.startsWith("//")) { - const end = rest.indexOf("\n"); - const comment = end === -1 ? rest : rest.slice(0, end); - if (isLineScopedPragmaComment(comment)) return statementStart; - return end === -1 ? source.length : current + end; - } - - if (rest.startsWith("/*")) { - const end = rest.indexOf("*/"); - if (end === -1) return current; - const comment = rest.slice(0, end + 2); - if (isLineScopedPragmaComment(comment)) return statementStart; - current += end + 2; - continue; - } - - break; - } - - const lineEnd = source.indexOf("\n", current); - return lineEnd === -1 ? source.length : lineEnd; -} - -function addGlobalsImport(source: string, root: SgNode): string { - const imports = topLevelImports(root); - const lastImport = imports.at(-1); - if (lastImport) { - const importEnd = lastImport.range().end.index; - const lineEnd = statementEndWithTrailingComments( - source, - lastImport.range().start.index, - importEnd, - ); - const insertPos = lineEnd === -1 ? source.length : lineEnd; - if (insertPos === lastImport.range().start.index) { - return `${source.slice(0, insertPos)}${GLOBALS_IMPORT}\n${source.slice(insertPos)}`; - } - return `${source.slice(0, insertPos)}\n${GLOBALS_IMPORT}${source.slice(insertPos)}`; - } - - const insertPos = prologueEnd(source); - const suffix = insertPos === 0 ? "\n\n" : "\n"; - return `${source.slice(0, insertPos)}${GLOBALS_IMPORT}${suffix}${source.slice(insertPos)}`; -} - -function addGlobalsReference(source: string): string { - const insertPos = referenceDirectiveInsertPos(source); - const suffix = insertPos === 0 ? "\n\n" : "\n"; - return `${source.slice(0, insertPos)}${GLOBALS_REFERENCE}${suffix}${source.slice(insertPos)}`; -} - -/** - * Add the explicit side-effect import required for ambient runtime globals in v2. - * @param source - File contents - * @param filePath - Absolute path to the file - * @returns Transformed source or null when no safe change is needed. - */ -export default function transform(source: string, filePath: string): string | null { - const isDeclarationFile = /\.d\.[cm]?ts$/.test(filePath); - const usesReferenceDirective = isDeclarationFile || filePath.endsWith(".cts"); - if (filePath.endsWith(".cjs")) return null; - if (!GLOBAL_NAME_PATTERN.test(source)) return null; - - const root = parseSource(source, filePath).root(); - const mergeRuntimeNamespaceAugmentations = usesReferenceDirective || hasDeclareGlobalBlock(root); - if (unshadowedRuntimeGlobalReferences(root, mergeRuntimeNamespaceAugmentations).length === 0) { - return null; - } - if (hasGlobalsOptIn(source, root)) return null; - if (usesReferenceDirective) return addGlobalsReference(source); - - return addGlobalsImport(source, root); -} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/abstract-class-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/abstract-class-local/input.ts deleted file mode 100644 index 627cc4cd8..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/abstract-class-local/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -abstract class TailorErrors extends Error {} - -type ErrorCtor = TailorErrors; - -export { TailorErrors }; -export type { ErrorCtor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/already-imported/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/already-imported/input.ts deleted file mode 100644 index 388fff3ee..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/already-imported/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const client = new tailor.idp.Client(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/expected.ts deleted file mode 100644 index d5f0f8765..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const [client = new tailor.idp.Client()] = opts; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/input.ts deleted file mode 100644 index 78ee6cdb5..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/array-destructuring-default/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const [client = new tailor.idp.Client()] = opts; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/expected.ts deleted file mode 100644 index 056bcbe09..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -type Runtime = keyof typeof tailor; - -export type { Runtime }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/input.ts deleted file mode 100644 index 6b1a9668a..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-query/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -type Runtime = keyof typeof tailor; - -export type { Runtime }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/expected.ts deleted file mode 100644 index 2a7edae04..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/expected.ts +++ /dev/null @@ -1,12 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const TailorErrors = { - hasError() { - return false; - }, -}; - -type ErrorBag = TailorErrors; - -export { TailorErrors }; -export type { ErrorBag }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/input.ts deleted file mode 100644 index ef3f75a94..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-type-with-local-value/input.ts +++ /dev/null @@ -1,10 +0,0 @@ -const TailorErrors = { - hasError() { - return false; - }, -}; - -type ErrorBag = TailorErrors; - -export { TailorErrors }; -export type { ErrorBag }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/expected.ts deleted file mode 100644 index 933f7c7d2..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const runtime = tailor; - -export { runtime }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/input.ts deleted file mode 100644 index da25597b8..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/bare-value/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const runtime = tailor; - -export { runtime }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/expected.ts deleted file mode 100644 index 8ffec9837..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/expected.ts +++ /dev/null @@ -1,10 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -type Handler = { - (tailor: unknown): void; -}; - -const client = new tailor.idp.Client(); - -export { client }; -export type { Handler }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/input.ts deleted file mode 100644 index 54a8678c4..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/call-signature-parameter/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -type Handler = { - (tailor: unknown): void; -}; - -const client = new tailor.idp.Client(); - -export { client }; -export type { Handler }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/expected.ts deleted file mode 100644 index 0e19a3a73..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/expected.ts +++ /dev/null @@ -1,13 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -try { - run(); -} catch (error) { - { - const tailor = localClient; - tailor.run(); - } - - const client = new tailor.idp.Client(); - use(client); -} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/input.ts deleted file mode 100644 index 35d64ad98..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-block-local-and-global/input.ts +++ /dev/null @@ -1,11 +0,0 @@ -try { - run(); -} catch (error) { - { - const tailor = localClient; - tailor.run(); - } - - const client = new tailor.idp.Client(); - use(client); -} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-local/input.ts deleted file mode 100644 index f27da942f..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-local/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -try { - run(); -} catch (tailor) { - tailor.run(); -} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/expected.ts deleted file mode 100644 index e377017fa..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -try { - run(); -} catch { - const client = new tailor.idp.Client(); - use(client); -} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/input.ts deleted file mode 100644 index 7414196d3..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-parameterless/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -try { - run(); -} catch { - const client = new tailor.idp.Client(); - use(client); -} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/class-expression-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/class-expression-local/input.ts deleted file mode 100644 index dff45bcca..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/class-expression-local/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -const RuntimeError = class TailorErrors extends Error { - value(): TailorErrors { - return this; - } -}; - -export { RuntimeError }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/comment-and-string/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/comment-and-string/input.ts deleted file mode 100644 index cda85fb61..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/comment-and-string/input.ts +++ /dev/null @@ -1,2 +0,0 @@ -// tailor.idp.Client is documented here only. -const example = "tailordb.QueryResult"; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/expected.d.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/expected.d.cts deleted file mode 100644 index e528ee417..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/expected.d.cts +++ /dev/null @@ -1,3 +0,0 @@ -/// - -type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/input.d.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/input.d.cts deleted file mode 100644 index a817f947d..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-declaration/input.d.cts +++ /dev/null @@ -1 +0,0 @@ -type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-skipped/input.cjs b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-skipped/input.cjs deleted file mode 100644 index ad7487b36..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/commonjs-skipped/input.cjs +++ /dev/null @@ -1 +0,0 @@ -const client = new tailor.idp.Client(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/expected.ts deleted file mode 100644 index c9c27540d..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/expected.ts +++ /dev/null @@ -1,10 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -type Constructor = { - new (tailor: unknown): unknown; -}; - -const client = new tailor.idp.Client(); - -export { client }; -export type { Constructor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/input.ts deleted file mode 100644 index 96e743463..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/construct-signature-parameter/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -type Constructor = { - new (tailor: unknown): unknown; -}; - -const client = new tailor.idp.Client(); - -export { client }; -export type { Constructor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-namespace-value-local/input.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-namespace-value-local/input.cts deleted file mode 100644 index e757aa38e..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-namespace-value-local/input.cts +++ /dev/null @@ -1,7 +0,0 @@ -namespace tailordb { - export class Client {} -} - -const C = tailordb.Client; - -export { C }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/expected.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/expected.cts deleted file mode 100644 index 3f5c0ef47..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/expected.cts +++ /dev/null @@ -1,5 +0,0 @@ -/// - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/input.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/input.cts deleted file mode 100644 index 2e76ceaac..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-skipped/input.cts +++ /dev/null @@ -1,3 +0,0 @@ -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/expected.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/expected.cts deleted file mode 100644 index 8a6ea5cd3..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/expected.cts +++ /dev/null @@ -1,9 +0,0 @@ -/// - -namespace tailordb { - export interface Client {} -} - -const C = tailordb.Client; - -export { C }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/input.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/input.cts deleted file mode 100644 index f46312655..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-type-only-namespace-value/input.cts +++ /dev/null @@ -1,7 +0,0 @@ -namespace tailordb { - export interface Client {} -} - -const C = tailordb.Client; - -export { C }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/expected.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/expected.d.ts deleted file mode 100644 index e528ee417..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/expected.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -/// - -type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/input.d.ts deleted file mode 100644 index a817f947d..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-file-skipped/input.d.ts +++ /dev/null @@ -1 +0,0 @@ -type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-class/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-class/input.d.ts deleted file mode 100644 index 0f2511f66..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-class/input.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export {}; - -declare global { - class TailorErrors extends Error {} -} - -type ErrorCtor = typeof TailorErrors; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-namespace/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-namespace/input.d.ts deleted file mode 100644 index 6efd20c5f..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-existing-namespace/input.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -export {}; - -declare global { - namespace tailor { - namespace idp { - type User = string; - } - } -} - -type RuntimeUser = tailor.idp.User; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-local-member/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-local-member/input.d.ts deleted file mode 100644 index df6cad4fd..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-local-member/input.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -export {}; - -declare global { - namespace tailordb { - type Row = string; - } -} - -type Row = tailordb.Row; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/expected.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/expected.d.ts deleted file mode 100644 index b517d0b9f..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/expected.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// - -export {}; - -declare global { - namespace tailordb { - type Row = string; - } -} - -type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/input.d.ts deleted file mode 100644 index d2b7d36f2..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-global-runtime-member/input.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -export {}; - -declare global { - namespace tailordb { - type Row = string; - } -} - -type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/expected.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/expected.d.ts deleted file mode 100644 index 8b31b6270..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/expected.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/// - -declare namespace tailordb { - export type Row = string; -} - -declare namespace tailor { - namespace idp { - export type LocalUser = string; - } -} - -type Rows = tailordb.QueryResult; -type RuntimeUser = tailor.idp.User; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/input.d.ts deleted file mode 100644 index 68149881f..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-augmentation/input.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare namespace tailordb { - export type Row = string; -} - -declare namespace tailor { - namespace idp { - export type LocalUser = string; - } -} - -type Rows = tailordb.QueryResult; -type RuntimeUser = tailor.idp.User; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-local-member/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-local-member/input.d.ts deleted file mode 100644 index daf8a2eea..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-local-member/input.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare namespace tailordb { - export type Row = string; -} - -type Row = tailordb.Row; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-value-member/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-value-member/input.d.ts deleted file mode 100644 index e20a44ca1..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-namespace-value-member/input.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare namespace tailordb { - export class Client {} -} - -type ClientCtor = typeof tailordb.Client; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/expected.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/expected.d.ts deleted file mode 100644 index bf27bcfc9..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/expected.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/// - -declare namespace tailor { - namespace idp { - export type LocalUser = string; - } -} - -type RuntimeUser = tailor.idp.User; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/input.d.ts deleted file mode 100644 index 7c3a7f9c2..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-augmentation/input.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare namespace tailor { - namespace idp { - export type LocalUser = string; - } -} - -type RuntimeUser = tailor.idp.User; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-value-member/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-value-member/input.d.ts deleted file mode 100644 index 9d6873710..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-nested-namespace-value-member/input.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare namespace tailor { - namespace idp { - export class Client {} - } -} - -type ClientCtor = typeof tailor.idp.Client; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/expected.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/expected.d.ts deleted file mode 100644 index 398d9c725..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/expected.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/// - -declare namespace tailordb { - namespace Client { - export type Extra = string; - } -} - -type ClientCtor = typeof tailordb.Client; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/input.d.ts deleted file mode 100644 index 8fac4f9e4..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/declaration-type-only-nested-namespace-value/input.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare namespace tailordb { - namespace Client { - export type Extra = string; - } -} - -type ClientCtor = typeof tailordb.Client; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/decorated-parameter-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/decorated-parameter-local/input.ts deleted file mode 100644 index 5f478bf4b..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/decorated-parameter-local/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -class Client { - constructor(@inject tailor: string) { - this.value = tailor; - } - - value: string; -} - -export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/expected.ts deleted file mode 100644 index 3e2626354..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const { tailor: localTailor } = config; - -const client = new tailor.idp.Client(localTailor); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/input.ts deleted file mode 100644 index 5d2ac1743..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructure-key-not-binding/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const { tailor: localTailor } = config; - -const client = new tailor.idp.Client(localTailor); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/expected.ts deleted file mode 100644 index 13168b5d9..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const { client = new tailor.idp.Client() } = opts; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/input.ts deleted file mode 100644 index e2919364c..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/destructuring-default/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const { client = new tailor.idp.Client() } = opts; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/expected.ts deleted file mode 100644 index d988d0192..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/expected.ts +++ /dev/null @@ -1,6 +0,0 @@ -"use client"; /* required by Next.js */ -import "@tailor-platform/sdk/runtime/globals"; - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/input.ts deleted file mode 100644 index 9efc3db19..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-block-comment/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -"use client"; /* required by Next.js */ - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/expected.ts deleted file mode 100644 index 820308bf9..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/expected.ts +++ /dev/null @@ -1,6 +0,0 @@ -"use client"; // required by Next.js -import "@tailor-platform/sdk/runtime/globals"; - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/input.ts deleted file mode 100644 index d8159975b..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-trailing-comment/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -"use client"; // required by Next.js - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/enum-namespace-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/enum-namespace-local/input.ts deleted file mode 100644 index 5a3ee7e53..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/enum-namespace-local/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -enum tailordb { - Client, -} - -type Client = tailordb.Client; -const client = tailordb.Client; - -export { client, tailordb }; -export type { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/expected.ts deleted file mode 100644 index c817d0681..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -type ErrorCtor = TailorErrors; - -export type { ErrorCtor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/input.ts deleted file mode 100644 index cfbcc588a..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class-type/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -type ErrorCtor = TailorErrors; - -export type { ErrorCtor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/expected.ts deleted file mode 100644 index 7e6dc6c3e..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/expected.ts +++ /dev/null @@ -1,3 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -throw new TailorErrors([]); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/input.ts deleted file mode 100644 index e7f1f704c..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/error-class/input.ts +++ /dev/null @@ -1 +0,0 @@ -throw new TailorErrors([]); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/export-as-namespace-local/input.d.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/export-as-namespace-local/input.d.ts deleted file mode 100644 index 9cf548494..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/export-as-namespace-local/input.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export as namespace tailor; -export as namespace tailordb; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-await-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-await-local/input.ts deleted file mode 100644 index 10945bdac..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-await-local/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -async function run(clients: AsyncIterable<{ run(): void }>) { - for await (const tailor of clients) { - tailor.run(); - } -} - -export { run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/expected.ts deleted file mode 100644 index 1cc3aa0d5..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/expected.ts +++ /dev/null @@ -1,11 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -for (const item of items) { - { - const tailor = item; - tailor.run(); - } - - const client = new tailor.idp.Client(); - use(client); -} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/input.ts deleted file mode 100644 index 20af40be4..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-block-local-and-global/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -for (const item of items) { - { - const tailor = item; - tailor.run(); - } - - const client = new tailor.idp.Client(); - use(client); -} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-local/input.ts deleted file mode 100644 index 75feeb96e..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-local/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -for (const tailor of clients) { - tailor.run(); -} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/expected.ts deleted file mode 100644 index efda4b316..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/expected.ts +++ /dev/null @@ -1,9 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const client = new tailor.idp.Client(); - -for (let tailor = 0; tailor < 1; tailor++) { - console.log(tailor); -} - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/input.ts deleted file mode 100644 index 625f46157..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local-and-global/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -const client = new tailor.idp.Client(); - -for (let tailor = 0; tailor < 1; tailor++) { - console.log(tailor); -} - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local/input.ts deleted file mode 100644 index 7ee4288c4..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-statement-local/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -for (let tailor = 0; tailor < 1; tailor++) { - tailor.toString(); -} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/expected.ts deleted file mode 100644 index 87b0ce1b8..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -function build(client = tailor) { - var tailor = localClient; - return client; -} - -export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/input.ts deleted file mode 100644 index 60a57f475..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-default-global/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -function build(client = tailor) { - var tailor = localClient; - return client; -} - -export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-local/input.ts deleted file mode 100644 index d13b1578b..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-local/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -function build() { - if (ready) { - var tailor = localClient; - } - return tailor.run(); -} - -export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/expected.ts deleted file mode 100644 index ba2da0b36..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -function build(client: typeof tailor): typeof tailor { - var tailor = localClient; - return client; -} - -export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/input.ts deleted file mode 100644 index 25b2d0e3a..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-body-var-type-global/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -function build(client: typeof tailor): typeof tailor { - var tailor = localClient; - return client; -} - -export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/expected.ts deleted file mode 100644 index 58979a429..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/expected.ts +++ /dev/null @@ -1,9 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const fn = function (tailor: unknown) { - return tailor; -}; - -const client = new tailor.idp.Client(); - -export { client, fn }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/input.ts deleted file mode 100644 index aa4a25c0c..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-parameter/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -const fn = function (tailor: unknown) { - return tailor; -}; - -const client = new tailor.idp.Client(); - -export { client, fn }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/expected.ts deleted file mode 100644 index 2f6337d94..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -type Fn = (tailor: unknown) => unknown; - -const client = new tailor.idp.Client(); - -export type { Fn }; -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/input.ts deleted file mode 100644 index c89b63f24..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-type-parameter/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -type Fn = (tailor: unknown) => unknown; - -const client = new tailor.idp.Client(); - -export type { Fn }; -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/expected.ts deleted file mode 100644 index 2d8c37b00..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/expected.ts +++ /dev/null @@ -1,9 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -function* inspect(tailor: unknown) { - yield tailor; -} - -const client = new tailor.idp.Client(); - -export { client, inspect }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/input.ts deleted file mode 100644 index 9410ed82d..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/generator-declaration-parameter/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -function* inspect(tailor: unknown) { - yield tailor; -} - -const client = new tailor.idp.Client(); - -export { client, inspect }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/expected.ts deleted file mode 100644 index 1ecd70ec8..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const runtime = globalThis; -const globals = runtime; - -const client = globals.tailor.idp.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/input.ts deleted file mode 100644 index 512aaf45b..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-chain/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -const runtime = globalThis; -const globals = runtime; - -const client = globals.tailor.idp.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/expected.ts deleted file mode 100644 index e357dbfa2..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const runtime /* alias */ = /* value */ globalThis; - -const client = runtime.tailor.idp.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/input.ts deleted file mode 100644 index 64687f852..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-comment/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -const runtime /* alias */ = /* value */ globalThis; - -const client = runtime.tailor.idp.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-shadowed/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-shadowed/input.ts deleted file mode 100644 index a47000a75..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-shadowed/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -const runtime = globalThis; - -function read(runtime: { tailor: { idp: { Client: unknown } } }) { - return runtime.tailor.idp.Client; -} - -export { read }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/expected.ts deleted file mode 100644 index 6e73f6cb5..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const runtime: typeof globalThis = globalThis; - -const client = runtime.tailor.idp.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/input.ts deleted file mode 100644 index 5b8e752cc..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias-type-annotation/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -const runtime: typeof globalThis = globalThis; - -const client = runtime.tailor.idp.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/expected.ts deleted file mode 100644 index 398552f1f..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const runtime = globalThis; - -const client = runtime.tailor.idp.Client; -const database = runtime["tailordb"]; - -export { client, database }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/input.ts deleted file mode 100644 index c0c2d3af1..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-alias/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -const runtime = globalThis; - -const client = runtime.tailor.idp.Client; -const database = runtime["tailordb"]; - -export { client, database }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/expected.ts deleted file mode 100644 index 72ec92309..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/expected.ts +++ /dev/null @@ -1,6 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const client = globalThis["tailor"].idp.Client; -const query = globalThis["tailordb"].QueryResult; - -export { client, query }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/input.ts deleted file mode 100644 index 797ce400a..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-bracket/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -const client = globalThis["tailor"].idp.Client; -const query = globalThis["tailordb"].QueryResult; - -export { client, query }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key-expression/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key-expression/input.ts deleted file mode 100644 index c520822bc..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key-expression/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -const config = { tailor: "custom" }; - -const { [config.tailor]: configuredValue } = globalThis; -const { [getKey("tailor")]: dynamicValue } = globalThis; - -export { configuredValue, dynamicValue }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/expected.ts deleted file mode 100644 index 7f16d1e86..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const { ["tailor"]: runtimeTailor } = globalThis; -const { [`tailordb`]: runtimeDb } = globalThis; -const { ["TailorErrors" as const]: TailorErrorsClass } = globalThis; - -export { runtimeDb, runtimeTailor, TailorErrorsClass }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/input.ts deleted file mode 100644 index efcfe018d..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-key/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -const { ["tailor"]: runtimeTailor } = globalThis; -const { [`tailordb`]: runtimeDb } = globalThis; -const { ["TailorErrors" as const]: TailorErrorsClass } = globalThis; - -export { runtimeDb, runtimeTailor, TailorErrorsClass }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-local-key/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-local-key/input.ts deleted file mode 100644 index 03b160b76..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-computed-local-key/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -const tailor = "custom"; - -const { [tailor]: dynamicValue } = globalThis; - -export { dynamicValue }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/expected.ts deleted file mode 100644 index d8eceb1f2..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/expected.ts +++ /dev/null @@ -1,9 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -let db; -let tailorClient; - -({ tailordb: db } = globalThis); -({ tailor: tailorClient } = globalThis); - -export { db, tailorClient }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/input.ts deleted file mode 100644 index a194ff6d4..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-assignment/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -let db; -let tailorClient; - -({ tailordb: db } = globalThis); -({ tailor: tailorClient } = globalThis); - -export { db, tailorClient }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-local-alias/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-local-alias/input.ts deleted file mode 100644 index ab4fce9ae..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-local-alias/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const { x: tailor } = globalThis; - -export { tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/expected.ts deleted file mode 100644 index daffaa101..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const { tailor }: typeof globalThis = globalThis; - -export { tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/input.ts deleted file mode 100644 index 296b62ed6..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring-type-annotation/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const { tailor }: typeof globalThis = globalThis; - -export { tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/expected.ts deleted file mode 100644 index 3d8afe268..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const { tailor } = globalThis; -const { tailordb: db } = globalThis; -const { TailorErrors = Error } = globalThis; - -export { db, tailor, TailorErrors }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/input.ts deleted file mode 100644 index 4c03bf765..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-destructuring/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -const { tailor } = globalThis; -const { tailordb: db } = globalThis; -const { TailorErrors = Error } = globalThis; - -export { db, tailor, TailorErrors }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-alias-shadowed/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-alias-shadowed/input.ts deleted file mode 100644 index 1fa86afc0..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-alias-shadowed/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -const runtime = globalThis; - -function read(runtime: { tailor: unknown }) { - type TailorRuntime = (typeof runtime)["tailor"]; - return null as TailorRuntime; -} - -export { read }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-local/input.ts deleted file mode 100644 index 9578454e9..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type-local/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -const globalThis = { - tailor: {}, -}; - -type TailorRuntime = (typeof globalThis)["tailor"]; - -export type { TailorRuntime }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/expected.ts deleted file mode 100644 index f0fe768c2..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/expected.ts +++ /dev/null @@ -1,10 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const runtime = globalThis; - -type TailorRuntime = (typeof globalThis)["tailor"]; -type TailordbRuntime = typeof globalThis["tailordb"]; -type AliasTailorRuntime = (typeof runtime)["tailor"]; -type ErrorCtor = typeof globalThis["TailorErrors"]; - -export type { AliasTailorRuntime, ErrorCtor, TailorRuntime, TailordbRuntime }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/input.ts deleted file mode 100644 index 864f9e91f..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-indexed-type/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -const runtime = globalThis; - -type TailorRuntime = (typeof globalThis)["tailor"]; -type TailordbRuntime = typeof globalThis["tailordb"]; -type AliasTailorRuntime = (typeof runtime)["tailor"]; -type ErrorCtor = typeof globalThis["TailorErrors"]; - -export type { AliasTailorRuntime, ErrorCtor, TailorRuntime, TailordbRuntime }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/expected.ts deleted file mode 100644 index 31df260ae..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -function run(tailor: unknown) { - return globalThis.tailor.idp.Client; -} - -export { run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/input.ts deleted file mode 100644 index 9ec12e4c8..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-local-shadow/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -function run(tailor: unknown) { - return globalThis.tailor.idp.Client; -} - -export { run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/expected.ts deleted file mode 100644 index 344291e7a..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/expected.ts +++ /dev/null @@ -1,6 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const { tailor = fallback } = globalThis; -const { tailordb = fallbackDb } = globalThis; - -export { tailor, tailordb }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/input.ts deleted file mode 100644 index 8b26d39ea..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this-shorthand-default/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -const { tailor = fallback } = globalThis; -const { tailordb = fallbackDb } = globalThis; - -export { tailor, tailordb }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/expected.ts deleted file mode 100644 index 965412310..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const client = globalThis.tailor.idp.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/input.ts deleted file mode 100644 index 8b13d8def..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/global-this/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const client = globalThis.tailor.idp.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-alias-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-alias-local/input.ts deleted file mode 100644 index 6a6887e30..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-alias-local/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -namespace RuntimeAliases { - import tailor = ExternalRuntime.tailor; - import tailordb = ExternalRuntime.tailordb; - - tailor.idp.Client; - tailordb.file; -} - -export { RuntimeAliases }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-equals-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-equals-local/input.ts deleted file mode 100644 index 2a360509a..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-equals-local/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -import tailor = require("pkg"); -import tailordb = require("other"); - -const client = tailor.idp.Client; -type Rows = tailordb.QueryResult; - -export { client }; -export type { Rows }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/expected.ts deleted file mode 100644 index 4c0754eb0..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import value from "pkg"; /* keep this import grouped -with this comment */ -import "@tailor-platform/sdk/runtime/globals"; - -const Client = tailor.idp.Client; - -export { Client, value }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/input.ts deleted file mode 100644 index 31c41131e..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-trailing-block-comment/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -import value from "pkg"; /* keep this import grouped -with this comment */ - -const Client = tailor.idp.Client; - -export { Client, value }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/infer-type-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/infer-type-local/input.ts deleted file mode 100644 index 4296b033a..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/infer-type-local/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -type Unwrap = T extends Promise ? tailor : never; -type ExtractDb = T extends { db: infer tailordb } ? tailordb : never; - -export type { ExtractDb, Unwrap }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/expected.ts deleted file mode 100644 index be06a50d3..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/expected.ts +++ /dev/null @@ -1,6 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const Client = tailor.idp.Client; - -/// -export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/input.ts deleted file mode 100644 index a64025845..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-after-code/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -const Client = tailor.idp.Client; - -/// -export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/expected.ts deleted file mode 100644 index 0927e17ac..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* -/// -*/ - -import "@tailor-platform/sdk/runtime/globals"; -const Client = tailor.idp.Client; - -export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/input.ts deleted file mode 100644 index 0d7de1061..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/invalid-reference-in-block-comment/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* -/// -*/ - -const Client = tailor.idp.Client; - -export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/jsx-intrinsic-tag/input.tsx b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/jsx-intrinsic-tag/input.tsx deleted file mode 100644 index 77b2eb1e6..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/jsx-intrinsic-tag/input.tsx +++ /dev/null @@ -1,8 +0,0 @@ -const element = ( - <> - - - -); - -export { element }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/expected.ts deleted file mode 100644 index 21448ef91..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/expected.ts +++ /dev/null @@ -1,4 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -// eslint-disable-next-line no-console -console.log(tailor.idp.Client); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/input.ts deleted file mode 100644 index abe897b5f..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/line-scoped-pragma/input.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-console -console.log(tailor.idp.Client); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-binding/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-binding/input.ts deleted file mode 100644 index 1fc617e98..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-binding/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -const tailor = { - idp: { - Client: class Client {}, - }, -}; - -const client = new tailor.idp.Client(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-global-this/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-global-this/input.ts deleted file mode 100644 index b9dd7ef17..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-global-this/input.ts +++ /dev/null @@ -1,15 +0,0 @@ -function read(globalThis: { - tailor: { idp: { Client: unknown } }; - tailordb: { file: unknown }; -}) { - return [globalThis.tailor.idp.Client, globalThis["tailordb"].file]; -} - -function destructure(globalThis: { runtime: { tailor: string } }) { - const { - runtime: { tailor }, - } = globalThis; - return tailor; -} - -export { destructure, read }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/expected.ts deleted file mode 100644 index c4fac5b2a..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/expected.ts +++ /dev/null @@ -1,9 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -function inspect(tailor: { idp: { Client: new () => unknown } }) { - return new tailor.idp.Client(); -} - -const client = new tailor.idp.Client(); - -export { client, inspect }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/input.ts deleted file mode 100644 index 239cd8aa5..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-and-global/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -function inspect(tailor: { idp: { Client: new () => unknown } }) { - return new tailor.idp.Client(); -} - -const client = new tailor.idp.Client(); - -export { client, inspect }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-only/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-only/input.ts deleted file mode 100644 index 66faa2cd2..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-parameter-only/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -function inspect(tailor: { idp: { Client: new () => unknown } }) { - return new tailor.idp.Client(); -} - -export { inspect }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-binding/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-binding/input.ts deleted file mode 100644 index f131a1602..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-binding/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -class tailordb { - static QueryResult = class QueryResult {}; -} - -const result = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/expected.ts deleted file mode 100644 index 26e75da2a..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/expected.ts +++ /dev/null @@ -1,6 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -type tailor = {}; -type User = tailor.idp.User; - -export type { User }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/input.ts deleted file mode 100644 index 4de380c75..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-type-name-namespace-reference/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -type tailor = {}; -type User = tailor.idp.User; - -export type { User }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/expected.ts deleted file mode 100644 index 42ce80061..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/expected.ts +++ /dev/null @@ -1,10 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -interface Handler { - handle(tailor: unknown): void; -} - -const client = new tailor.idp.Client(); - -export { client }; -export type { Handler }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/input.ts deleted file mode 100644 index b524fb1ff..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/method-signature-parameter/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -interface Handler { - handle(tailor: unknown): void; -} - -const client = new tailor.idp.Client(); - -export { client }; -export type { Handler }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-existing-runtime-member/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-existing-runtime-member/input.ts deleted file mode 100644 index 539fc7aba..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-existing-runtime-member/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -export {}; - -declare global { - namespace tailordb { - type QueryResult = T[]; - } -} - -type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/expected.ts deleted file mode 100644 index bc37913e1..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/expected.ts +++ /dev/null @@ -1,11 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -export {}; - -declare global { - namespace tailordb { - type Row = string; - } -} - -type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/input.ts deleted file mode 100644 index d2b7d36f2..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/module-declare-global-runtime-member/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -export {}; - -declare global { - namespace tailordb { - type Row = string; - } -} - -type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/expected.ts deleted file mode 100644 index b1abc913d..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -"use client"; - -"use strict"; -import "@tailor-platform/sdk/runtime/globals"; - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/input.ts deleted file mode 100644 index ae0848bd9..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/multiple-directives/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -"use client"; - -"use strict"; - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/named-expression-bindings/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/named-expression-bindings/input.ts deleted file mode 100644 index 2ba7b8ef8..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/named-expression-bindings/input.ts +++ /dev/null @@ -1,19 +0,0 @@ -function* tailor() { - return tailor; -} - -const f = function tailordb() { - return tailordb; -}; - -const g = function* TailorErrors() { - return TailorErrors; -}; - -const C = class TailorErrorMessage { - method() { - return TailorErrorMessage; - } -}; - -export { C, f, g, tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-export-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-export-local/input.ts deleted file mode 100644 index 4517c4986..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-export-local/input.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as tailor from "pkg"; -export * as tailordb from "pkg"; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-local/input.ts deleted file mode 100644 index 23c13350b..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-local/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -namespace Local { - var tailor = { run() {} }; - tailor.run(); -} - -export { Local }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/expected.ts deleted file mode 100644 index 2f091fac6..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/expected.ts +++ /dev/null @@ -1,10 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -namespace Local { - var tailor = { value: 1 }; - tailor.value; -} - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/input.ts deleted file mode 100644 index ed3b7e9ad..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-var-scope-global/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -namespace Local { - var tailor = { value: 1 }; - tailor.value; -} - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/expected.ts deleted file mode 100644 index 9b33b5a49..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -class tailordb {} - -type Rows = tailordb.QueryResult; - -export { tailordb }; -export type { Rows }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/input.ts deleted file mode 100644 index c683e40f7..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/namespace-with-local-class/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -class tailordb {} - -type Rows = tailordb.QueryResult; - -export { tailordb }; -export type { Rows }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-destructuring-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-destructuring-local/input.ts deleted file mode 100644 index c23388563..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-destructuring-local/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -const { x: [tailor] } = obj; - -function run({ x: { tailordb } }: { x: { tailordb: { run(): void } } }) { - tailordb.run(); -} - -export { run, tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-global-this-destructure-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-global-this-destructure-local/input.ts deleted file mode 100644 index e775a2028..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-global-this-destructure-local/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -const { - runtime: { tailor }, -} = globalThis; - -const { - runtime: { tailordb: database }, -} = globalThis; - -export { database, tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/expected.ts deleted file mode 100644 index f7df90279..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/expected.ts +++ /dev/null @@ -1,12 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -declare module "pkg" { - import { tailor } from "dep"; - import * as tailordb from "dep"; -} - -const client = new tailor.idp.Client(); -type Rows = tailordb.QueryResult; - -export { client }; -export type { Rows }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/input.ts deleted file mode 100644 index 58c60bc4c..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-global/input.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare module "pkg" { - import { tailor } from "dep"; - import * as tailordb from "dep"; -} - -const client = new tailor.idp.Client(); -type Rows = tailordb.QueryResult; - -export { client }; -export type { Rows }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-local/input.ts deleted file mode 100644 index 2898c5359..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import-scope-local/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare module "pkg" { - import { tailor } from "dep"; - export type Local = typeof tailor; -} - -export {}; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/expected.ts deleted file mode 100644 index 4083c7102..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/expected.ts +++ /dev/null @@ -1,9 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -declare module "pkg" { - import type { Foo } from "dep"; - - export type Wrapped = Foo; -} - -const client = new tailor.idp.Client(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/input.ts deleted file mode 100644 index 876c4db6a..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-import/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module "pkg" { - import type { Foo } from "dep"; - - export type Wrapped = Foo; -} - -const client = new tailor.idp.Client(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-parameter-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-parameter-local/input.ts deleted file mode 100644 index d323b5f1b..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-parameter-local/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -function run({ tailor }: { tailor: { run(): void } }) { - tailor.run(); -} - -export { run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/expected.ts deleted file mode 100644 index 85195cd32..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const client = tailor!.idp.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/input.ts deleted file mode 100644 index d8819ca29..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/non-null-access/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const client = tailor!.idp.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/expected.ts deleted file mode 100644 index b5b02bc24..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const client = tailor?.idp.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/input.ts deleted file mode 100644 index a8e624a32..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/optional-chain/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const client = tailor?.idp.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/expected.ts deleted file mode 100644 index 4d9c5406f..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -function build(client = tailor) { - return client; -} - -export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/input.ts deleted file mode 100644 index baa8725fb..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-default-global/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -function build(client = tailor) { - return client; -} - -export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/expected.ts deleted file mode 100644 index e51c1a89b..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -function build({ tailor } = globalThis, { tailordb: db } = (globalThis as typeof globalThis)) { - return { db, tailor }; -} - -export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/input.ts deleted file mode 100644 index 4eaea1242..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-destructure/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -function build({ tailor } = globalThis, { tailordb: db } = (globalThis as typeof globalThis)) { - return { db, tailor }; -} - -export { build }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/expected.ts deleted file mode 100644 index f7c97aff7..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -function read({ tailor = fallback } = globalThis, { tailordb = fallbackDb } = globalThis) { - return { tailor, tailordb }; -} - -export { read }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/input.ts deleted file mode 100644 index 028f3738e..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-global-this-shorthand-default/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -function read({ tailor = fallback } = globalThis, { tailordb = fallbackDb } = globalThis) { - return { tailor, tailordb }; -} - -export { read }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-property-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-property-local/input.ts deleted file mode 100644 index 10706b0d8..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parameter-property-local/input.ts +++ /dev/null @@ -1,10 +0,0 @@ -class Client { - constructor( - private tailor: { run(): void }, - public TailorErrors: string, - ) { - tailor.run(); - } -} - -export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/expected.ts deleted file mode 100644 index bf0e87425..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const client = (tailor).idp.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/input.ts deleted file mode 100644 index a087a7538..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/parenthesized-access/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const client = (tailor).idp.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/expected.ts deleted file mode 100644 index 3aa5c47be..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -"use client"; -import "@tailor-platform/sdk/runtime/globals"; - -const client = new tailor.idp.Client(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/input.ts deleted file mode 100644 index 415f59e48..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/preserves-directives/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -"use client"; - -const client = new tailor.idp.Client(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-namespace-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-namespace-local/input.ts deleted file mode 100644 index 773337b31..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-namespace-local/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -namespace tailor.idp { - export const Client = class {}; -} - -const Client = tailor.idp.Client; - -export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/expected.ts deleted file mode 100644 index aecc646ad..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/expected.ts +++ /dev/null @@ -1,6 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -type Rows = tailordb /* c */ . QueryResult; -type User = tailor . idp . User; - -export type { Rows, User }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/input.ts deleted file mode 100644 index 518288da7..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/qualified-type-whitespace/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -type Rows = tailordb /* c */ . QueryResult; -type User = tailor . idp . User; - -export type { Rows, User }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/expected.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/expected.cts deleted file mode 100644 index 2fb66249a..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/expected.cts +++ /dev/null @@ -1,7 +0,0 @@ -/// - -"use strict"; - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/input.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/input.cts deleted file mode 100644 index e9e4fdcd7..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/reference-before-directive/input.cts +++ /dev/null @@ -1,5 +0,0 @@ -"use strict"; - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/runtime-namespace-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/runtime-namespace-local/input.ts deleted file mode 100644 index cb25a01d0..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/runtime-namespace-local/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -namespace tailor { - export const idp = { - Client: class {}, - }; -} - -const Client = tailor.idp.Client; - -export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/expected.ts deleted file mode 100644 index fb5579761..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/expected.ts +++ /dev/null @@ -1,6 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -({ tailor } = source); -({ tailordb = fallback } = source); - -export {}; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/input.ts deleted file mode 100644 index c663dcae0..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-global/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -({ tailor } = source); -({ tailordb = fallback } = source); - -export {}; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-local/input.ts deleted file mode 100644 index eee2c090e..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-assignment-local/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -let tailor; -({ tailor } = source); - -export { tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/expected.ts deleted file mode 100644 index 0797ab70b..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const payload = { tailor }; - -export { payload }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/input.ts deleted file mode 100644 index 95944281f..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/shorthand-global-reference/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const payload = { tailor }; - -export { payload }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-source-name/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-source-name/input.ts deleted file mode 100644 index ee7df71e3..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-source-name/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { tailor as t } from "pkg"; - -export { tailor as x } from "pkg"; -export { foo as tailordb } from "pkg"; -export { t }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-type-export/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-type-export/input.ts deleted file mode 100644 index 26c15857e..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/specifier-type-export/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { tailor } from "pkg"; - -export { type tailor }; -export { type tailor as Tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/expected.ts deleted file mode 100644 index 3ec8daf25..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/expected.ts +++ /dev/null @@ -1,12 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -class Local { - static { - var tailor = { value: 1 }; - tailor.value; - } -} - -const client = new tailor.idp.Client(); - -export { Local, client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/input.ts deleted file mode 100644 index 2a8d12013..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/static-block-var-scope-global/input.ts +++ /dev/null @@ -1,10 +0,0 @@ -class Local { - static { - var tailor = { value: 1 }; - tailor.value; - } -} - -const client = new tailor.idp.Client(); - -export { Local, client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/expected.ts deleted file mode 100644 index f64bfd08f..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const client = tailor["idp"].Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/input.ts deleted file mode 100644 index cec16c2bc..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/subscript-access/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const client = tailor["idp"].Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/expected.ts deleted file mode 100644 index c1a830975..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/expected.ts +++ /dev/null @@ -1,12 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -switch (kind) { - case "local": - const tailor = localClient; - tailor.run(); - break; -} - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/input.ts deleted file mode 100644 index c85561920..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/switch-case-local-and-global/input.ts +++ /dev/null @@ -1,10 +0,0 @@ -switch (kind) { - case "local": - const tailor = localClient; - tailor.run(); - break; -} - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/expected.ts deleted file mode 100644 index 897002abe..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -type IdpUser = tailor.idp.User; - -export type Result = IdpUser; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/input.ts deleted file mode 100644 index 10551ac83..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-type-reference/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -type IdpUser = tailor.idp.User; - -export type Result = IdpUser; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/expected.ts deleted file mode 100644 index a95fd09b5..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/expected.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createResolver } from "@tailor-platform/sdk"; -import "@tailor-platform/sdk/runtime/globals"; - -export default createResolver({ - async handler() { - const client = new tailor.idp.Client(); - return client; - }, -}); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/input.ts deleted file mode 100644 index 225170d7d..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailor-value/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createResolver } from "@tailor-platform/sdk"; - -export default createResolver({ - async handler() { - const client = new tailor.idp.Client(); - return client; - }, -}); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/expected.ts deleted file mode 100644 index 8aba357c0..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const command: tailordb.CommandType = "SELECT"; - -export { command }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/input.ts deleted file mode 100644 index 84679b35c..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type-annotation/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const command: tailordb.CommandType = "SELECT"; - -export { command }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/expected.ts deleted file mode 100644 index 0766d179c..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -type Rows = tailordb.QueryResult; - -export type Result = Rows<{ id: string }>; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/input.ts deleted file mode 100644 index 7ddb064b2..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/tailordb-type/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -type Rows = tailordb.QueryResult; - -export type Result = Rows<{ id: string }>; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/expected.ts deleted file mode 100644 index 4168dfa53..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/expected.ts +++ /dev/null @@ -1,4 +0,0 @@ -import value from "pkg"; // eslint-disable-line import/no-unresolved -import "@tailor-platform/sdk/runtime/globals"; - -const client = new tailor.idp.Client(value); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/input.ts deleted file mode 100644 index 00f60ddd5..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-import-comment/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -import value from "pkg"; // eslint-disable-line import/no-unresolved - -const client = new tailor.idp.Client(value); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/expected.ts deleted file mode 100644 index 6bdc96024..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/expected.ts +++ /dev/null @@ -1,3 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; -import value from "pkg"; // @ts-expect-error -const client = new tailor.idp.Client(value); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/input.ts deleted file mode 100644 index fd097cc80..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/trailing-next-line-pragma-import/input.ts +++ /dev/null @@ -1,2 +0,0 @@ -import value from "pkg"; // @ts-expect-error -const client = new tailor.idp.Client(value); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/expected.ts deleted file mode 100644 index eaccd5ea3..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -type tailor = {}; - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/input.ts deleted file mode 100644 index 838a9acc4..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-binding-value-reference/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -type tailor = {}; - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-level-binders/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-level-binders/input.ts deleted file mode 100644 index cf46069a3..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-level-binders/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -type Box = tailor; -type Indexed = { [tailordb: string]: unknown }; -type Mapped = { [tailor in keyof Source]: Source[tailor] }; - -export type { Box, Indexed, Mapped }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-export-specifier/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-export-specifier/input.ts deleted file mode 100644 index 225240bb1..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-export-specifier/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { tailor } from "pkg"; - -export type { tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/expected.ts deleted file mode 100644 index f5ca69c44..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/expected.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { tailor } from "pkg"; -import "@tailor-platform/sdk/runtime/globals"; - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/input.ts deleted file mode 100644 index 98e11b871..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-import/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { tailor } from "pkg"; - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/expected.ts deleted file mode 100644 index fdba63bab..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type * as tailor from "pkg"; -import type tailordb = require("other"); -import "@tailor-platform/sdk/runtime/globals"; - -const Client = tailor.idp.Client; -const Query = tailordb.Query; - -export { Client, Query }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/input.ts deleted file mode 100644 index 2200b678b..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-import-value/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type * as tailor from "pkg"; -import type tailordb = require("other"); - -const Client = tailor.idp.Client; -const Query = tailordb.Query; - -export { Client, Query }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-local/input.ts deleted file mode 100644 index 0ba78176b..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-local/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -namespace tailordb { - export type Row = string; -} - -type Row = tailordb.Row; - -export type { Row }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/expected.ts deleted file mode 100644 index 61d767d22..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/expected.ts +++ /dev/null @@ -1,9 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -declare namespace tailor { - export type User = string; -} - -const Client = tailor.idp.Client; - -export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/input.ts deleted file mode 100644 index 0705bfc8e..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-namespace-value-global/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare namespace tailor { - export type User = string; -} - -const Client = tailor.idp.Client; - -export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/expected.ts deleted file mode 100644 index 033e0235f..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/expected.ts +++ /dev/null @@ -1,11 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -namespace tailor { - export namespace idp { - export type User = string; - } -} - -const Client = tailor.idp.Client; - -export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/input.ts deleted file mode 100644 index 7eaeea882..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-nested-namespace-value-global/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -namespace tailor { - export namespace idp { - export type User = string; - } -} - -const Client = tailor.idp.Client; - -export { Client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/expected.ts deleted file mode 100644 index 2f0f2f429..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/expected.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type tailor } from "pkg"; -import "@tailor-platform/sdk/runtime/globals"; - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/input.ts deleted file mode 100644 index ebb7cfa40..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-specifier/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { type tailor } from "pkg"; - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/expected.ts deleted file mode 100644 index 7e9e9c044..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const tailor = {}; - -type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/input.ts deleted file mode 100644 index e77f9d441..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/unrelated-local-binding/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const tailor = {}; - -type Rows = tailordb.QueryResult; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/using-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/using-local/input.ts deleted file mode 100644 index bd65e252c..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/using-local/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -export {}; - -using tailor = getClient(); -tailor.run(); - -await using tailordb = getDatabase(); -tailordb.run(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/expected.ts deleted file mode 100644 index 4840c8b16..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const tailordb = {}; - -type Rows = tailordb.QueryResult; - -export { tailordb }; -export type { Rows }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/input.ts deleted file mode 100644 index a430a54e5..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-binding-type-reference/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -const tailordb = {}; - -type Rows = tailordb.QueryResult; - -export { tailordb }; -export type { Rows }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-local/input.ts deleted file mode 100644 index 58006a344..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-local/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { tailor } from "pkg"; - -const client = new tailor.idp.Client(); - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-type-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-type-local/input.ts deleted file mode 100644 index aacdd3c12..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/value-import-type-local/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -import TailorErrors from "errors"; -import { TailorErrorItem, TailorErrorMessage } from "errors"; - -type Item = TailorErrorItem; -type Message = TailorErrorMessage; -type Errors = TailorErrors; - -export type { Errors, Item, Message }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/expected.ts deleted file mode 100644 index 5d4928f2c..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/expected.ts +++ /dev/null @@ -1,13 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -function run() { - { - var tailor = localClient; - } - - return tailor; -} - -const client = new tailor.idp.Client(); - -export { client, run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/input.ts deleted file mode 100644 index ada9b41b4..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local-and-global/input.ts +++ /dev/null @@ -1,11 +0,0 @@ -function run() { - { - var tailor = localClient; - } - - return tailor; -} - -const client = new tailor.idp.Client(); - -export { client, run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local/input.ts deleted file mode 100644 index 00f1c4bb1..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-block-local/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -function run() { - { - var tailor = localClient; - } - - return tailor; -} - -export { run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-comment-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-comment-local/input.ts deleted file mode 100644 index 58c3191ca..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-comment-local/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -{ - var/*comment*/tailordb = { Client: class {} }; -} - -const client = tailordb.Client; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-for-of-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-for-of-local/input.ts deleted file mode 100644 index 0aa5354dc..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-for-of-local/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -function run(clients: unknown[]) { - for (var tailor of clients) { - void tailor; - } - return tailor; -} - -export { run }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-whitespace-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-whitespace-local/input.ts deleted file mode 100644 index 0ee4c73b3..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/var-whitespace-local/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -{ - var tailor = { run() {} }; -} - -tailor.run(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/expected.ts deleted file mode 100644 index 595dadea8..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/expected.ts +++ /dev/null @@ -1,6 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const { tailor } = (globalThis); -({ tailordb } = (globalThis as typeof globalThis)); - -export { tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/input.ts deleted file mode 100644 index e55c2f827..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-destructure/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -const { tailor } = (globalThis); -({ tailordb } = (globalThis as typeof globalThis)); - -export { tailor }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/expected.ts deleted file mode 100644 index 66f5ac7ca..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/expected.ts +++ /dev/null @@ -1,6 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const client = (globalThis).tailor.idp.Client; -const errors = (globalThis as typeof globalThis).TailorErrors; - -export { client, errors }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/input.ts deleted file mode 100644 index b701e0bae..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-member/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -const client = (globalThis).tailor.idp.Client; -const errors = (globalThis as typeof globalThis).TailorErrors; - -export { client, errors }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/expected.ts deleted file mode 100644 index 686709121..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const client = globalThis["tailor" as const]; -const db = globalThis?.["tailordb" as const]; -const errors = globalThis[`TailorErrors`]; - -export { client, db, errors }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/input.ts deleted file mode 100644 index c18124c8a..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-string-key/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -const client = globalThis["tailor" as const]; -const db = globalThis?.["tailordb" as const]; -const errors = globalThis[`TailorErrors`]; - -export { client, db, errors }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/expected.ts deleted file mode 100644 index a469cd4f7..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "@tailor-platform/sdk/runtime/globals"; - -const client = (globalThis)["tailor"]; - -export { client }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/input.ts deleted file mode 100644 index 72b2f1424..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/wrapped-global-this-subscript/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -const client = (globalThis)["tailor"]; - -export { client }; diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 1a65e5c25..695e5f572 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -25,9 +25,11 @@ describe("getApplicableCodemods", () => { expect(() => getApplicableCodemods("1.0.0", "invalid")).toThrow("Invalid toVersion"); }); - test("includes CommonJS TypeScript files in the runtime globals codemod", () => { + test("flags CommonJS TypeScript files for runtime globals review", () => { const codemod = allCodemods.find((entry) => entry.id === "v2/runtime-globals-opt-in"); expect(codemod?.filePatterns).toContain("**/*.{ts,tsx,mts,cts}"); + expect(codemod?.suspiciousPatterns).toContain("tailor.idp"); + expect(codemod?.prompt).toContain("@tailor-platform/sdk/runtime/globals"); }); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index bc5c17d46..bbc9da18c 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -294,8 +294,23 @@ export const allCodemods: CodemodPackage[] = [ 'Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Normal SDK development does not need them — use the SDK APIs and the typed wrappers from `@tailor-platform/sdk/runtime`. Only if you relied on the ambient globals directly, add `import "@tailor-platform/sdk/runtime/globals"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.)', since: "1.0.0", until: "2.0.0", - scriptPath: "v2/runtime-globals-opt-in/scripts/transform.js", filePatterns: ["**/*.{ts,tsx,mts,cts}"], + suspiciousPatterns: [ + "tailor.context", + "tailor.iconv", + "tailor.idp", + "tailor.workflow", + "tailor[", + "tailordb.Client", + "tailordb.CommandType", + "tailordb.QueryResult", + "tailordb.file", + "tailordb[", + "TailorDBFileError", + "TailorErrorItem", + "TailorErrorMessage", + "TailorErrors", + ], examples: [ { before: "const client = new tailor.idp.Client();", @@ -303,6 +318,19 @@ export const allCodemods: CodemodPackage[] = [ 'import "@tailor-platform/sdk/runtime/globals";\nconst client = new tailor.idp.Client();', }, ], + prompt: [ + "The v2 SDK no longer enables ambient Tailor runtime globals from", + "`@tailor-platform/sdk`. For each flagged file that intentionally uses", + "`tailor.*`, `tailordb.*`, or Tailor runtime error globals, opt into the", + "global declarations by adding one of these:", + '- per-file: `import "@tailor-platform/sdk/runtime/globals";`', + '- project-wide: `"types": ["@tailor-platform/sdk/runtime/globals"]` in', + " the relevant tsconfig compilerOptions", + "", + "Leave files unchanged when the matching name is local, imported from another", + "module, or appears only in comments or strings. Prefer the typed runtime", + "wrappers from `@tailor-platform/sdk/runtime` for new code.", + ].join("\n"), }, { id: "v2/workflow-trigger-dispatch", diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index fb9ff2e19..5ab9121fa 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -38,7 +38,9 @@ async function discoverCases(codemodPath: string): Promise { } async function runFixtureCases(codemodPath: string): Promise { - const transform = await loadTransform(codemodPath); + const scriptPath = path.join(CODEMODS_DIR, codemodPath, "scripts/transform.ts"); + const mod = await import(scriptPath); + const transform = mod.default as TransformFn; const cases = await discoverCases(codemodPath); expect(cases.length, `expected at least one fixture under ${codemodPath}/tests`).toBeGreaterThan( @@ -56,12 +58,6 @@ async function runFixtureCases(codemodPath: string): Promise { } } -async function loadTransform(codemodPath: string): Promise { - const scriptPath = path.join(CODEMODS_DIR, codemodPath, "scripts/transform.ts"); - const mod = await import(scriptPath); - return mod.default as TransformFn; -} - describe("codemod transforms", () => { test("v2/define-generators-to-plugins transforms correctly", async () => { await expect(runFixtureCases("v2/define-generators-to-plugins")).resolves.toBeUndefined(); @@ -102,58 +98,4 @@ describe("codemod transforms", () => { test("v2/execute-script-arg transforms correctly", async () => { await expect(runFixtureCases("v2/execute-script-arg")).resolves.toBeUndefined(); }); - - test("v2/runtime-globals-opt-in transforms correctly", async () => { - await expect(runFixtureCases("v2/runtime-globals-opt-in")).resolves.toBeUndefined(); - }); - - test("v2/runtime-globals-opt-in recognizes multiline type-only syntax", async () => { - const transform = await loadTransform("v2/runtime-globals-opt-in"); - const importInput = [ - "import type", - '{ tailor } from "pkg";', - "", - "const client = tailor.idp.Client;", - "", - ].join("\n"); - - expect(await transform(importInput, "/tmp/input.ts")).toBe( - [ - "import type", - '{ tailor } from "pkg";', - 'import "@tailor-platform/sdk/runtime/globals";', - "", - "const client = tailor.idp.Client;", - "", - ].join("\n"), - ); - - const specifierInput = [ - "import { type", - 'tailor } from "pkg";', - "", - "const client = tailor.idp.Client;", - "", - ].join("\n"); - - expect(await transform(specifierInput, "/tmp/input.ts")).toBe( - [ - "import { type", - 'tailor } from "pkg";', - 'import "@tailor-platform/sdk/runtime/globals";', - "", - "const client = tailor.idp.Client;", - "", - ].join("\n"), - ); - - const exportInput = [ - 'import type { tailor } from "pkg";', - "export type", - "{ tailor };", - "", - ].join("\n"); - - expect(await transform(exportInput, "/tmp/input.ts")).toBeNull(); - }); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index 86971254f..aa8ede142 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -31,8 +31,6 @@ export default defineConfig([ "codemods/v2/tailordb-namespace/scripts/transform.ts", "v2/execute-script-arg/scripts/transform": "codemods/v2/execute-script-arg/scripts/transform.ts", - "v2/runtime-globals-opt-in/scripts/transform": - "codemods/v2/runtime-globals-opt-in/scripts/transform.ts", }, format: ["esm"], target: "node18", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index ad65bda2e..bae8e8beb 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -357,7 +357,7 @@ downloadStream and returns FileDownloadStreamResponse. ## Ambient runtime globals are opt-in -**Migration:** Automatic +**Migration:** Manual Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Normal SDK development does not need them — use the SDK APIs and the typed wrappers from `@tailor-platform/sdk/runtime`. Only if you relied on the ambient globals directly, add `import "@tailor-platform/sdk/runtime/globals"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.) @@ -374,6 +374,25 @@ import "@tailor-platform/sdk/runtime/globals"; const client = new tailor.idp.Client(); ``` +
+Prompt for an AI agent (to perform this migration) + +```text +The v2 SDK no longer enables ambient Tailor runtime globals from +`@tailor-platform/sdk`. For each flagged file that intentionally uses +`tailor.*`, `tailordb.*`, or Tailor runtime error globals, opt into the +global declarations by adding one of these: +- per-file: `import "@tailor-platform/sdk/runtime/globals";` +- project-wide: `"types": ["@tailor-platform/sdk/runtime/globals"]` in + the relevant tsconfig compilerOptions + +Leave files unchanged when the matching name is local, imported from another +module, or appears only in comments or strings. Prefer the typed runtime +wrappers from `@tailor-platform/sdk/runtime` for new code. +``` + +
+ ## Workflow .trigger() and trigger tests **Migration:** Manual From 8dbbc0d5f2b2d499011583d6d90f6c2b0913daf5 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 21 Jun 2026 21:34:57 +0900 Subject: [PATCH 173/618] fix(sdk-codemod): lead runtime globals migration with typed wrappers Present the `@tailor-platform/sdk/runtime` typed wrappers as the primary migration path for the runtime-globals-opt-in codemod, with the `runtime/globals` side-effect import demoted to a fallback for code that must keep the bare `tailor.*` names. Normal SDK development does not need the ambient globals. Claude-Session: https://claude.ai/code/session_016DxQAKYyszCsKoc1QiycXA --- packages/sdk-codemod/src/registry.test.ts | 9 +++++++ packages/sdk-codemod/src/registry.ts | 24 +++++++++++++---- packages/sdk/docs/migration/v2.md | 32 +++++++++++++++++++---- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 695e5f572..3c638e110 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -32,4 +32,13 @@ describe("getApplicableCodemods", () => { expect(codemod?.suspiciousPatterns).toContain("tailor.idp"); expect(codemod?.prompt).toContain("@tailor-platform/sdk/runtime/globals"); }); + + test("leads runtime globals migration with the typed wrappers", () => { + const codemod = allCodemods.find((entry) => entry.id === "v2/runtime-globals-opt-in"); + + expect(codemod?.prompt).toContain('import { idp } from "@tailor-platform/sdk/runtime"'); + expect(codemod?.examples?.[0]?.after).toContain( + 'import { idp } from "@tailor-platform/sdk/runtime"', + ); + }); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index bbc9da18c..0bcac9269 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -313,6 +313,15 @@ export const allCodemods: CodemodPackage[] = [ ], examples: [ { + caption: + "Preferred: switch to the typed wrappers from `@tailor-platform/sdk/runtime` and drop the ambient globals:", + before: "const client = new tailor.idp.Client();", + after: + 'import { idp } from "@tailor-platform/sdk/runtime";\nconst client = new idp.Client({ namespace: "my-namespace" });', + }, + { + caption: + "Fallback: only if you must keep referencing the bare `tailor.*` names, opt into the global declarations:", before: "const client = new tailor.idp.Client();", after: 'import "@tailor-platform/sdk/runtime/globals";\nconst client = new tailor.idp.Client();', @@ -320,16 +329,21 @@ export const allCodemods: CodemodPackage[] = [ ], prompt: [ "The v2 SDK no longer enables ambient Tailor runtime globals from", - "`@tailor-platform/sdk`. For each flagged file that intentionally uses", - "`tailor.*`, `tailordb.*`, or Tailor runtime error globals, opt into the", - "global declarations by adding one of these:", + "`@tailor-platform/sdk`. For each flagged file that uses `tailor.*`,", + "`tailordb.*`, or Tailor runtime error globals, prefer migrating to the", + "typed wrappers from `@tailor-platform/sdk/runtime` (e.g. replace", + '`new tailor.idp.Client()` with `import { idp } from "@tailor-platform/sdk/runtime"`', + "and `new idp.Client({ namespace })`). The wrappers are self-contained, so the", + "ambient globals are no longer needed.", + "", + "Only when the file must keep referencing the bare `tailor.*` names directly,", + "opt into the global declarations instead by adding one of these:", '- per-file: `import "@tailor-platform/sdk/runtime/globals";`', '- project-wide: `"types": ["@tailor-platform/sdk/runtime/globals"]` in', " the relevant tsconfig compilerOptions", "", "Leave files unchanged when the matching name is local, imported from another", - "module, or appears only in comments or strings. Prefer the typed runtime", - "wrappers from `@tailor-platform/sdk/runtime` for new code.", + "module, or appears only in comments or strings.", ].join("\n"), }, { diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index bae8e8beb..a059f1506 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -361,6 +361,23 @@ downloadStream and returns FileDownloadStreamResponse. Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Normal SDK development does not need them — use the SDK APIs and the typed wrappers from `@tailor-platform/sdk/runtime`. Only if you relied on the ambient globals directly, add `import "@tailor-platform/sdk/runtime/globals"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.) +Preferred: switch to the typed wrappers from `@tailor-platform/sdk/runtime` and drop the ambient globals: + +Before: + +```ts +const client = new tailor.idp.Client(); +``` + +After: + +```ts +import { idp } from "@tailor-platform/sdk/runtime"; +const client = new idp.Client({ namespace: "my-namespace" }); +``` + +Fallback: only if you must keep referencing the bare `tailor.*` names, opt into the global declarations: + Before: ```ts @@ -379,16 +396,21 @@ const client = new tailor.idp.Client(); ```text The v2 SDK no longer enables ambient Tailor runtime globals from -`@tailor-platform/sdk`. For each flagged file that intentionally uses -`tailor.*`, `tailordb.*`, or Tailor runtime error globals, opt into the -global declarations by adding one of these: +`@tailor-platform/sdk`. For each flagged file that uses `tailor.*`, +`tailordb.*`, or Tailor runtime error globals, prefer migrating to the +typed wrappers from `@tailor-platform/sdk/runtime` (e.g. replace +`new tailor.idp.Client()` with `import { idp } from "@tailor-platform/sdk/runtime"` +and `new idp.Client({ namespace })`). The wrappers are self-contained, so the +ambient globals are no longer needed. + +Only when the file must keep referencing the bare `tailor.*` names directly, +opt into the global declarations instead by adding one of these: - per-file: `import "@tailor-platform/sdk/runtime/globals";` - project-wide: `"types": ["@tailor-platform/sdk/runtime/globals"]` in the relevant tsconfig compilerOptions Leave files unchanged when the matching name is local, imported from another -module, or appears only in comments or strings. Prefer the typed runtime -wrappers from `@tailor-platform/sdk/runtime` for new code. +module, or appears only in comments or strings. ```
From e70c30d66d050191655275ceda4d63bcb490553a Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 22 Jun 2026 18:19:21 +0900 Subject: [PATCH 174/618] refactor(sdk-codemod): make collectMaskedRanges return a fresh array The helper took a ranges array by parameter and mutated it via push, which is inconsistent with the non-mutating array convention used elsewhere. Have it own a local accumulator and return it, then sort with toSorted at the call site. Claude-Session: https://claude.ai/code/session_01CDXjhLUUJwzssfLE2re9v2 --- packages/sdk-codemod/src/runner.ts | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 5050cd6bf..43a12b1c9 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -150,22 +150,27 @@ function sourceLang(relative: string): Lang { return ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : Lang.TypeScript; } -function collectMaskedRanges(node: SgNode, ranges: Array<[number, number]>): void { - if (MASKED_SOURCE_NODE_KINDS.has(node.kind())) { - const range = node.range(); - ranges.push([range.start.index, range.end.index]); - return; - } - for (const child of node.children()) { - collectMaskedRanges(child, ranges); - } +function collectMaskedRanges(root: SgNode): Array<[number, number]> { + const ranges: Array<[number, number]> = []; + const visit = (node: SgNode): void => { + if (MASKED_SOURCE_NODE_KINDS.has(node.kind())) { + const range = node.range(); + ranges.push([range.start.index, range.end.index]); + return; + } + for (const child of node.children()) { + visit(child); + } + }; + visit(root); + return ranges; } function maskSourceNonCode(relative: string, content: string): string { - let ranges: Array<[number, number]> = []; + let ranges: Array<[number, number]>; try { const root = parse(sourceLang(relative), content).root(); - collectMaskedRanges(root, ranges); + ranges = collectMaskedRanges(root); } catch { return content; } From 2d0689e8ac0079473294fab367799a5431c130f4 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 21 Jun 2026 12:28:23 +0900 Subject: [PATCH 175/618] fix(sdk-codemod): flag principal migration follow-ups --- .changeset/principal-followup-review.md | 5 +++++ packages/sdk-codemod/src/registry.test.ts | 8 ++++++++ packages/sdk-codemod/src/registry.ts | 12 ++++++++++++ packages/sdk/docs/migration/v2.md | 6 ++++++ 4 files changed, 31 insertions(+) create mode 100644 .changeset/principal-followup-review.md diff --git a/.changeset/principal-followup-review.md b/.changeset/principal-followup-review.md new file mode 100644 index 000000000..cd77e30a5 --- /dev/null +++ b/.changeset/principal-followup-review.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Flag files that need project-specific review after the v2 principal migration, including resolver helper adapters and nullable `caller` follow-ups. diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index dd8c5d5cc..cca2e523a 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -51,4 +51,12 @@ describe("getApplicableCodemods", () => { 'import { idp } from "@tailor-platform/sdk/runtime"', ); }); + + test("flags principal migration follow-ups for review", () => { + const codemod = allCodemods.find((entry) => entry.id === "v2/principal-unify"); + + expect(codemod?.suspiciousPatterns).toContain("context.user"); + expect(codemod?.suspiciousPatterns).toContain("caller?."); + expect(codemod?.prompt).toContain("anonymous callers"); + }); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 5423bb5bc..e30623ce8 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -112,6 +112,12 @@ export const allCodemods: CodemodPackage[] = [ "TailorInvoker", "unauthenticatedTailorUser", ], + suspiciousPatterns: [ + "caller?.", + "context.user", + "context.invoker ?? context.user", + "ResolverContext", + ], examples: [ { caption: "Type references unify under `TailorPrincipal`:", @@ -131,6 +137,12 @@ export const allCodemods: CodemodPackage[] = [ "- Replace member-access on the removed unauthenticatedTailorUser (e.g.", " unauthenticatedTailorUser.id); the codemod only replaced standalone references", " with null and left member access to surface a type error.", + "- Review helper adapters that still accept or read `context.user`; v2 resolver", + " context uses nullable `caller` and `invoker`, so project-specific helper", + " semantics for anonymous callers and command invokers must be chosen explicitly.", + "- Review `caller?.` values passed to APIs that require non-null values. If the", + " resolver requires authentication, throw or otherwise narrow before the call;", + " if anonymous callers are allowed, keep the nullable flow explicit.", "Use TailorPrincipal for the unified user/actor/invoker type.", ].join("\n"), }, diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index a059f1506..1b982e6bd 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -156,6 +156,12 @@ Finish the cases the codemod left for manual migration: - Replace member-access on the removed unauthenticatedTailorUser (e.g. unauthenticatedTailorUser.id); the codemod only replaced standalone references with null and left member access to surface a type error. +- Review helper adapters that still accept or read `context.user`; v2 resolver + context uses nullable `caller` and `invoker`, so project-specific helper + semantics for anonymous callers and command invokers must be chosen explicitly. +- Review `caller?.` values passed to APIs that require non-null values. If the + resolver requires authentication, throw or otherwise narrow before the call; + if anonymous callers are allowed, keep the nullable flow explicit. Use TailorPrincipal for the unified user/actor/invoker type. ``` From 4e3fa47d24e6bb1145eac13c355e976f2d594851 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 20 Jun 2026 23:28:44 +0900 Subject: [PATCH 176/618] fix(sdk-codemod): scope open download review prompts --- .changeset/open-download-review-scope.md | 5 +++++ packages/sdk-codemod/src/registry.test.ts | 10 ++++++++++ packages/sdk-codemod/src/registry.ts | 2 ++ 3 files changed, 17 insertions(+) create mode 100644 .changeset/open-download-review-scope.md diff --git a/.changeset/open-download-review-scope.md b/.changeset/open-download-review-scope.md new file mode 100644 index 000000000..baf39cabc --- /dev/null +++ b/.changeset/open-download-review-scope.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Limit the `openDownloadStream` migration review prompt to files that reference deprecated download stream APIs. diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index cca2e523a..f0552a36d 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -59,4 +59,14 @@ describe("getApplicableCodemods", () => { expect(codemod?.suspiciousPatterns).toContain("caller?."); expect(codemod?.prompt).toContain("anonymous callers"); }); + + test("open-download-stream review is scoped to deprecated API names", () => { + const openDownloadStream = getApplicableCodemods("1.67.1", "2.0.0").find( + (codemod) => codemod.id === "v2/open-download-stream", + ); + + expect(openDownloadStream?.suspiciousPatterns).toEqual( + expect.arrayContaining(["openDownloadStream", "openFileDownloadStream"]), + ); + }); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index e30623ce8..2c31acfef 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -291,6 +291,8 @@ export const allCodemods: CodemodPackage[] = [ since: "1.0.0", until: "2.0.0", // No scriptPath: this is a codemod-less ("manual") migration. + filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"], + suspiciousPatterns: ["openDownloadStream", "openFileDownloadStream"], examples: [ { before: "const res = await openDownloadStream(namespace, typeName, fieldName, recordId);", From ab10b1fea309ec5496e09bdca394d46d58603f5f Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 20 Jun 2026 23:05:10 +0900 Subject: [PATCH 177/618] fix(sdk-codemod): narrow executeScript review scope --- .changeset/execute-script-review-scope.md | 5 +++ packages/sdk-codemod/src/registry.ts | 6 +++- packages/sdk-codemod/src/runner.test.ts | 39 +++++++++++++++++++++++ packages/sdk-codemod/src/runner.ts | 10 +++--- packages/sdk-codemod/src/types.ts | 7 ++-- 5 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 .changeset/execute-script-review-scope.md diff --git a/.changeset/execute-script-review-scope.md b/.changeset/execute-script-review-scope.md new file mode 100644 index 000000000..395507f08 --- /dev/null +++ b/.changeset/execute-script-review-scope.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Reduce noisy `executeScript` LLM-review prompts by flagging files only when unresolved `arg` stringification remains likely. diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 2c31acfef..945d09d2d 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -261,7 +261,11 @@ export const allCodemods: CodemodPackage[] = [ until: "2.0.0", scriptPath: "v2/execute-script-arg/scripts/transform.js", filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"], - suspiciousPatterns: ["executeScript"], + suspiciousPatterns: [ + ["executeScript", "JSON.stringify", "arg:"], + ["executeScript", "JSON.stringify", '"arg"'], + ["executeScript", "JSON.stringify", "'arg'"], + ], prompt: [ "In Tailor SDK v2 the executeScript() arg option takes a JSON-serializable value", "and is serialized internally, so a pre-stringified argument double-encodes. The", diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index cf6b7560b..a8781f34c 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -392,6 +392,45 @@ describe("runCodemods", () => { ]); }); + test("AND-group suspicious pattern flags only when every substring co-occurs", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-and-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "unresolved.ts"), + "const serialized = JSON.stringify(payload);\nawait executeScript({ arg: serialized });\n", + ); + await fs.promises.writeFile( + path.join(dir, "already-plain.ts"), + "await executeScript({ arg: payload });\n", + ); + await fs.promises.writeFile( + path.join(dir, "non-arg-json.ts"), + "await executeScript({ code: JSON.stringify(meta) });\n", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/llm", partialTransformPath, ["**/*.ts"], undefined, { + suspiciousPatterns: [["executeScript", "JSON.stringify", "arg:"]], + prompt: "Rewrite remaining executeScript usages by hand.", + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.llmReviews).toEqual([ + { + codemodId: "test/llm", + prompt: "Rewrite remaining executeScript usages by hand.", + files: ["unresolved.ts"], + }, + ]); + }); + test("does not flag for LLM review without a prompt", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-noprompt-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 4cf064660..f6755a2b4 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -127,12 +127,12 @@ interface LoadedTransform { transform?: TransformFn; matches: (relativePath: string) => boolean; legacyPatterns: Array; - suspiciousPatterns: string[]; + suspiciousPatterns: Array; prompt?: string; } -/** Resolve a legacy pattern against content, returning its label when matched. */ -function matchLegacyPattern(content: string, pattern: string | string[]): string | null { +/** Resolve a residual pattern against content, returning its label when matched. */ +function matchResidualPattern(content: string, pattern: string | string[]): string | null { if (typeof pattern === "string") { return content.includes(pattern) ? pattern : null; } @@ -146,7 +146,7 @@ function legacyPatternWarnings( ): string[] { return transforms.flatMap((lt) => { const found = lt.legacyPatterns - .map((p) => matchLegacyPattern(content, p)) + .map((p) => matchResidualPattern(content, p)) .filter((label): label is string => label !== null); if (found.length === 0) return []; return [ @@ -230,7 +230,7 @@ export async function runCodemods( for (const lt of matchedTransforms) { if (!lt.prompt || lt.suspiciousPatterns.length === 0) continue; - if (lt.suspiciousPatterns.some((p) => current.includes(p))) { + if (lt.suspiciousPatterns.some((p) => matchResidualPattern(current, p) !== null)) { const files = suspiciousByCodemod.get(lt.id) ?? []; files.push(relative); suspiciousByCodemod.set(lt.id, files); diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index f96e437e9..4f878ff5d 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -43,15 +43,16 @@ export interface CodemodPackage { */ legacyPatterns?: Array; /** - * Substrings that, when present in a file's post-transform content, mark it + * Patterns that, when present in a file's post-transform content, mark it * as a candidate for LLM-assisted review. Use this for migrations the * deterministic transform cannot safely complete on its own (e.g. a value - * reached through a variable or a dynamic expression). Unlike + * reached through a variable or a dynamic expression). A `string[]` group + * matches only when every pattern in the group is present (AND). Unlike * `legacyPatterns`, these do not need to be exhaustive: a broad signal such * as the API name is enough to point an LLM at the right files. Has no effect * unless `prompt` is also set. */ - suspiciousPatterns?: string[]; + suspiciousPatterns?: Array; /** * Prompt that instructs an LLM how to finish the migration for files matched * by `suspiciousPatterns`. From f045a95e1e99dc92b2dbdbe1065dd59acde5c2e6 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 20 Jun 2026 23:12:13 +0900 Subject: [PATCH 178/618] fix(sdk-codemod): include arg assignment review patterns --- packages/sdk-codemod/src/registry.test.ts | 14 ++++++++++++++ packages/sdk-codemod/src/registry.ts | 2 ++ 2 files changed, 16 insertions(+) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index f0552a36d..0a9b2a9d2 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -25,6 +25,7 @@ describe("getApplicableCodemods", () => { expect(() => getApplicableCodemods("1.0.0", "invalid")).toThrow("Invalid toVersion"); }); +<<<<<<< HEAD test("apply-to-deploy scans source files with embedded CLI strings", () => { const applyToDeploy = getApplicableCodemods("1.67.1", "2.0.0").find( (codemod) => codemod.id === "v2/apply-to-deploy", @@ -49,6 +50,19 @@ describe("getApplicableCodemods", () => { expect(codemod?.prompt).toContain('import { idp } from "@tailor-platform/sdk/runtime"'); expect(codemod?.examples?.[0]?.after).toContain( 'import { idp } from "@tailor-platform/sdk/runtime"', +======= + test("execute-script-arg reviews unresolved arg stringification patterns", () => { + const executeScriptArg = getApplicableCodemods("1.67.1", "2.0.0").find( + (codemod) => codemod.id === "v2/execute-script-arg", + ); + + expect(executeScriptArg?.suspiciousPatterns).toEqual( + expect.arrayContaining([ + ["executeScript", "JSON.stringify", "arg:"], + ["executeScript", "JSON.stringify", "arg ="], + ["executeScript", "JSON.stringify", "arg="], + ]), +>>>>>>> 75828cd8c (fix(sdk-codemod): include arg assignment review patterns) ); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 945d09d2d..f882924d6 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -265,6 +265,8 @@ export const allCodemods: CodemodPackage[] = [ ["executeScript", "JSON.stringify", "arg:"], ["executeScript", "JSON.stringify", '"arg"'], ["executeScript", "JSON.stringify", "'arg'"], + ["executeScript", "JSON.stringify", "arg ="], + ["executeScript", "JSON.stringify", "arg="], ], prompt: [ "In Tailor SDK v2 the executeScript() arg option takes a JSON-serializable value", From 37b08bc6bcddc958d818d313987e0ca6ba86dfce Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 20 Jun 2026 23:19:55 +0900 Subject: [PATCH 179/618] fix(sdk-codemod): support regex review patterns --- packages/sdk-codemod/src/registry.test.ts | 24 +++++++++------- packages/sdk-codemod/src/registry.ts | 6 +--- packages/sdk-codemod/src/runner.test.ts | 35 +++++++++++++++++++++++ packages/sdk-codemod/src/runner.ts | 26 ++++++++++++----- packages/sdk-codemod/src/types.ts | 18 +++++++----- 5 files changed, 79 insertions(+), 30 deletions(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 0a9b2a9d2..9920e48bf 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -25,7 +25,6 @@ describe("getApplicableCodemods", () => { expect(() => getApplicableCodemods("1.0.0", "invalid")).toThrow("Invalid toVersion"); }); -<<<<<<< HEAD test("apply-to-deploy scans source files with embedded CLI strings", () => { const applyToDeploy = getApplicableCodemods("1.67.1", "2.0.0").find( (codemod) => codemod.id === "v2/apply-to-deploy", @@ -50,20 +49,23 @@ describe("getApplicableCodemods", () => { expect(codemod?.prompt).toContain('import { idp } from "@tailor-platform/sdk/runtime"'); expect(codemod?.examples?.[0]?.after).toContain( 'import { idp } from "@tailor-platform/sdk/runtime"', -======= + ); + }); + test("execute-script-arg reviews unresolved arg stringification patterns", () => { const executeScriptArg = getApplicableCodemods("1.67.1", "2.0.0").find( (codemod) => codemod.id === "v2/execute-script-arg", ); - - expect(executeScriptArg?.suspiciousPatterns).toEqual( - expect.arrayContaining([ - ["executeScript", "JSON.stringify", "arg:"], - ["executeScript", "JSON.stringify", "arg ="], - ["executeScript", "JSON.stringify", "arg="], - ]), ->>>>>>> 75828cd8c (fix(sdk-codemod): include arg assignment review patterns) - ); + const argPattern = executeScriptArg?.suspiciousPatterns?.find( + (pattern): pattern is [string, string, RegExp] => + Array.isArray(pattern) && pattern[2] instanceof RegExp, + )?.[2]; + + expect(argPattern?.test("arg: value")).toBe(true); + expect(argPattern?.test("arg : value")).toBe(true); + expect(argPattern?.test("arg = value")).toBe(true); + expect(argPattern?.test('"arg" : value')).toBe(true); + expect(argPattern?.test('["arg"] = value')).toBe(true); }); test("flags principal migration follow-ups for review", () => { diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index f882924d6..e3793e8fe 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -262,11 +262,7 @@ export const allCodemods: CodemodPackage[] = [ scriptPath: "v2/execute-script-arg/scripts/transform.js", filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"], suspiciousPatterns: [ - ["executeScript", "JSON.stringify", "arg:"], - ["executeScript", "JSON.stringify", '"arg"'], - ["executeScript", "JSON.stringify", "'arg'"], - ["executeScript", "JSON.stringify", "arg ="], - ["executeScript", "JSON.stringify", "arg="], + ["executeScript", "JSON.stringify", /\barg\s*[:=]|["']arg["']\s*(?::|\]\s*[:=])/], ], prompt: [ "In Tailor SDK v2 the executeScript() arg option takes a JSON-serializable value", diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index a8781f34c..6782c05f4 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -431,6 +431,41 @@ describe("runCodemods", () => { ]); }); + test("AND-group suspicious pattern supports regex members", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-regex-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "spaced-colon.ts"), + "const serialized = JSON.stringify(payload);\nawait executeScript({ arg : serialized });\n", + ); + await fs.promises.writeFile( + path.join(dir, "already-plain.ts"), + "await executeScript({ arg : payload });\n", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/llm", partialTransformPath, ["**/*.ts"], undefined, { + suspiciousPatterns: [["executeScript", "JSON.stringify", /\barg\s*:/]], + prompt: "Rewrite remaining executeScript usages by hand.", + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.llmReviews).toEqual([ + { + codemodId: "test/llm", + prompt: "Rewrite remaining executeScript usages by hand.", + files: ["spaced-colon.ts"], + }, + ]); + }); + test("does not flag for LLM review without a prompt", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-noprompt-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index f6755a2b4..8d09ee62f 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -4,7 +4,7 @@ import chalk from "chalk"; import { structuredPatch } from "diff"; import * as path from "pathe"; import picomatch from "picomatch"; -import type { CodemodPackage, LlmReview } from "./types"; +import type { CodemodPackage, CodemodPattern, CodemodPatternGroup, LlmReview } from "./types"; /** * A transform function that receives source text and file path, @@ -126,17 +126,29 @@ interface LoadedTransform { /** Undefined for codemod-less ("manual") entries that ship only guidance. */ transform?: TransformFn; matches: (relativePath: string) => boolean; - legacyPatterns: Array; - suspiciousPatterns: Array; + legacyPatterns: CodemodPatternGroup[]; + suspiciousPatterns: CodemodPatternGroup[]; prompt?: string; } +function matchesPattern(content: string, pattern: CodemodPattern): boolean { + if (typeof pattern === "string") return content.includes(pattern); + pattern.lastIndex = 0; + return pattern.test(content); +} + +function patternLabel(pattern: CodemodPattern): string { + return typeof pattern === "string" ? pattern : pattern.toString(); +} + /** Resolve a residual pattern against content, returning its label when matched. */ -function matchResidualPattern(content: string, pattern: string | string[]): string | null { - if (typeof pattern === "string") { - return content.includes(pattern) ? pattern : null; +function matchResidualPattern(content: string, pattern: CodemodPatternGroup): string | null { + if (!Array.isArray(pattern)) { + return matchesPattern(content, pattern) ? patternLabel(pattern) : null; } - return pattern.every((p) => content.includes(p)) ? pattern.join(" + ") : null; + return pattern.every((p) => matchesPattern(content, p)) + ? pattern.map((p) => patternLabel(p)).join(" + ") + : null; } function legacyPatternWarnings( diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index 4f878ff5d..8460f0bf5 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -10,6 +10,10 @@ export interface CodemodExample { lang?: string; } +export type CodemodPattern = string | RegExp; + +export type CodemodPatternGroup = CodemodPattern | CodemodPattern[]; + /** * Metadata for a codemod package. */ @@ -36,23 +40,23 @@ export interface CodemodPackage { filePatterns?: string[]; /** * Patterns to detect in post-transform file content for manual migration - * warnings. A plain string warns when that substring is present; a - * `string[]` group warns only when every substring in the group is present - * (AND), letting a rule target a co-occurrence such as `executeScript` used - * together with `JSON.stringify`. + * warnings. A plain string warns when that substring is present, a `RegExp` + * warns when it matches, and an array group warns only when every member is + * present (AND), letting a rule target a co-occurrence such as + * `executeScript` used together with `JSON.stringify`. */ - legacyPatterns?: Array; + legacyPatterns?: CodemodPatternGroup[]; /** * Patterns that, when present in a file's post-transform content, mark it * as a candidate for LLM-assisted review. Use this for migrations the * deterministic transform cannot safely complete on its own (e.g. a value - * reached through a variable or a dynamic expression). A `string[]` group + * reached through a variable or a dynamic expression). An array group * matches only when every pattern in the group is present (AND). Unlike * `legacyPatterns`, these do not need to be exhaustive: a broad signal such * as the API name is enough to point an LLM at the right files. Has no effect * unless `prompt` is also set. */ - suspiciousPatterns?: Array; + suspiciousPatterns?: CodemodPatternGroup[]; /** * Prompt that instructs an LLM how to finish the migration for files matched * by `suspiciousPatterns`. From 1011d9871e331646315439f4d85576da53fc57ed Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 23 Jun 2026 11:25:45 +0900 Subject: [PATCH 180/618] fix(sdk): reject unknown invoker options --- .../src/parser/service/auth/legacy-invoker.ts | 6 --- .../parser/service/executor/schema.test.ts | 26 +++++----- .../sdk/src/parser/service/executor/schema.ts | 52 ++++++++++--------- .../parser/service/resolver/schema.test.ts | 6 +-- .../sdk/src/parser/service/resolver/schema.ts | 24 ++++----- packages/sdk/src/types/executor.generated.ts | 7 --- packages/sdk/src/types/resolver.generated.ts | 1 - 7 files changed, 55 insertions(+), 67 deletions(-) delete mode 100644 packages/sdk/src/parser/service/auth/legacy-invoker.ts diff --git a/packages/sdk/src/parser/service/auth/legacy-invoker.ts b/packages/sdk/src/parser/service/auth/legacy-invoker.ts deleted file mode 100644 index 38c494e7f..000000000 --- a/packages/sdk/src/parser/service/auth/legacy-invoker.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { z } from "zod"; - -// Prevent legacy config keys from being silently stripped by z.object(). -export const legacyAuthInvokerOption = z - .never({ error: "`authInvoker` was renamed to `invoker`; use `invoker` instead." }) - .optional(); diff --git a/packages/sdk/src/parser/service/executor/schema.test.ts b/packages/sdk/src/parser/service/executor/schema.test.ts index 25a4422c3..199da969e 100644 --- a/packages/sdk/src/parser/service/executor/schema.test.ts +++ b/packages/sdk/src/parser/service/executor/schema.test.ts @@ -26,28 +26,28 @@ function expectParseFailure( return result.error; } -function expectLegacyAuthInvokerRejected( +function expectUnknownKeyRejected( result: { success: true; data: T } | { success: false; error: { issues: unknown[] } }, ) { const error = expectParseFailure(result); expect(error.issues).toEqual( expect.arrayContaining([ expect.objectContaining({ - path: ["authInvoker"], + code: "unrecognized_keys", }), ]), ); } describe("FunctionOperationSchema", () => { - test("rejects legacy authInvoker option", () => { + test("rejects unknown options", () => { expect.hasAssertions(); - expectLegacyAuthInvokerRejected( + expectUnknownKeyRejected( FunctionOperationSchema.safeParse({ kind: "function", body: () => {}, - authInvoker: "admin", + unknownOption: true, }), ); }); @@ -80,14 +80,14 @@ describe("GqlOperationSchema", () => { expect(data.query).toBe("query { users { id } }"); }); - test("rejects legacy authInvoker option", () => { + test("rejects unknown options", () => { expect.hasAssertions(); - expectLegacyAuthInvokerRejected( + expectUnknownKeyRejected( GqlOperationSchema.safeParse({ kind: "graphql", query: "query { users { id } }", - authInvoker: "admin", + unknownOption: true, }), ); }); @@ -117,14 +117,14 @@ describe("WorkflowOperationSchema", () => { expect(data.workflowName).toBe("my-workflow"); }); - test("rejects legacy authInvoker option", () => { + test("rejects unknown options", () => { expect.hasAssertions(); - expectLegacyAuthInvokerRejected( + expectUnknownKeyRejected( WorkflowOperationSchema.safeParse({ kind: "workflow", workflowName: "my-workflow", - authInvoker: "admin", + unknownOption: true, }), ); }); @@ -179,7 +179,7 @@ describe("ExecutorSchema", () => { expect(data.operation.query).toBe("mutation { createUser { id } }"); }); - test("rejects legacy authInvoker option on operations", () => { + test("rejects unknown options on operations", () => { expect.hasAssertions(); expectParseFailure( @@ -192,7 +192,7 @@ describe("ExecutorSchema", () => { operation: { kind: "function", body: () => {}, - authInvoker: "admin", + unknownOption: true, }, }), ); diff --git a/packages/sdk/src/parser/service/executor/schema.ts b/packages/sdk/src/parser/service/executor/schema.ts index d9c33d835..f07513562 100644 --- a/packages/sdk/src/parser/service/executor/schema.ts +++ b/packages/sdk/src/parser/service/executor/schema.ts @@ -1,6 +1,5 @@ import { z } from "zod"; import { AuthInvokerSchema } from "../auth"; -import { legacyAuthInvokerOption } from "../auth/legacy-invoker"; import { functionSchema } from "../common"; export const TailorDBTriggerSchema = z.object({ @@ -85,21 +84,23 @@ export const TriggerSchema = z.discriminatedUnion("kind", [ AuthAccessTokenTriggerSchema, ]); -export const FunctionOperationSchema = z.object({ - kind: z.enum(["function", "jobFunction"]), - body: functionSchema.describe("Function implementation"), - invoker: AuthInvokerSchema.optional().describe("Invoker for the function execution"), - authInvoker: legacyAuthInvokerOption, -}); +export const FunctionOperationSchema = z + .object({ + kind: z.enum(["function", "jobFunction"]), + body: functionSchema.describe("Function implementation"), + invoker: AuthInvokerSchema.optional().describe("Invoker for the function execution"), + }) + .strict(); -export const GqlOperationSchema = z.object({ - kind: z.literal("graphql"), - appName: z.string().optional().describe("Target application name for the GraphQL query"), - query: z.preprocess((val) => String(val), z.string().describe("GraphQL query string")), - variables: functionSchema.optional().describe("Function to compute GraphQL variables"), - invoker: AuthInvokerSchema.optional().describe("Invoker for the GraphQL execution"), - authInvoker: legacyAuthInvokerOption, -}); +export const GqlOperationSchema = z + .object({ + kind: z.literal("graphql"), + appName: z.string().optional().describe("Target application name for the GraphQL query"), + query: z.preprocess((val) => String(val), z.string().describe("GraphQL query string")), + variables: functionSchema.optional().describe("Function to compute GraphQL variables"), + invoker: AuthInvokerSchema.optional().describe("Invoker for the GraphQL execution"), + }) + .strict(); export const WebhookOperationSchema = z.object({ kind: z.literal("webhook"), @@ -126,16 +127,17 @@ export const WorkflowOperationSchema = z.preprocess( const { workflow, ...rest } = val as { workflow: { name: string } }; return { ...rest, workflowName: workflow.name }; }, - z.object({ - kind: z.literal("workflow"), - workflowName: z.string().describe("Name of the workflow to execute"), - args: z - .union([z.record(z.string(), z.unknown()), functionSchema]) - .optional() - .describe("Arguments to pass to the workflow"), - invoker: AuthInvokerSchema.optional().describe("Invoker for the workflow execution"), - authInvoker: legacyAuthInvokerOption, - }), + z + .object({ + kind: z.literal("workflow"), + workflowName: z.string().describe("Name of the workflow to execute"), + args: z + .union([z.record(z.string(), z.unknown()), functionSchema]) + .optional() + .describe("Arguments to pass to the workflow"), + invoker: AuthInvokerSchema.optional().describe("Invoker for the workflow execution"), + }) + .strict(), ); const OperationSchema = z.union([ diff --git a/packages/sdk/src/parser/service/resolver/schema.test.ts b/packages/sdk/src/parser/service/resolver/schema.test.ts index 011d13f2f..fe2c189d7 100644 --- a/packages/sdk/src/parser/service/resolver/schema.test.ts +++ b/packages/sdk/src/parser/service/resolver/schema.test.ts @@ -24,18 +24,18 @@ describe("ResolverSchema", () => { }, }; - test("rejects legacy authInvoker option", () => { + test("rejects unknown options", () => { const error = expectParseFailure( ResolverSchema.safeParse({ ...validResolver, - authInvoker: "admin", + unknownOption: true, }), ); expect(error.issues).toEqual( expect.arrayContaining([ expect.objectContaining({ - path: ["authInvoker"], + code: "unrecognized_keys", }), ]), ); diff --git a/packages/sdk/src/parser/service/resolver/schema.ts b/packages/sdk/src/parser/service/resolver/schema.ts index bab8d664a..df7784e84 100644 --- a/packages/sdk/src/parser/service/resolver/schema.ts +++ b/packages/sdk/src/parser/service/resolver/schema.ts @@ -1,5 +1,4 @@ import { z } from "zod"; -import { legacyAuthInvokerOption } from "@/parser/service/auth/legacy-invoker"; import { AuthInvokerSchema } from "@/parser/service/auth/schema"; import { TailorFieldSchema } from "@/parser/service/field/schema"; import { functionSchema } from "../common"; @@ -8,14 +7,15 @@ export const QueryTypeSchema = z .union([z.literal("query"), z.literal("mutation")]) .describe("GraphQL operation type"); -export const ResolverSchema = z.object({ - operation: QueryTypeSchema.describe("GraphQL operation type (query or mutation)"), - name: z.string().describe("Resolver name"), - description: z.string().optional().describe("Resolver description"), - input: z.record(z.string(), TailorFieldSchema).optional().describe("Input field definitions"), - body: functionSchema.describe("Resolver implementation function"), - output: TailorFieldSchema.describe("Output field definition"), - publishEvents: z.boolean().optional().describe("Enable publishing events from this resolver"), - invoker: AuthInvokerSchema.optional().describe("Machine user to execute this resolver as"), - authInvoker: legacyAuthInvokerOption, -}); +export const ResolverSchema = z + .object({ + operation: QueryTypeSchema.describe("GraphQL operation type (query or mutation)"), + name: z.string().describe("Resolver name"), + description: z.string().optional().describe("Resolver description"), + input: z.record(z.string(), TailorFieldSchema).optional().describe("Input field definitions"), + body: functionSchema.describe("Resolver implementation function"), + output: TailorFieldSchema.describe("Output field definition"), + publishEvents: z.boolean().optional().describe("Enable publishing events from this resolver"), + invoker: AuthInvokerSchema.optional().describe("Machine user to execute this resolver as"), + }) + .strict(); diff --git a/packages/sdk/src/types/executor.generated.ts b/packages/sdk/src/types/executor.generated.ts index da0cce571..61b872588 100644 --- a/packages/sdk/src/types/executor.generated.ts +++ b/packages/sdk/src/types/executor.generated.ts @@ -111,7 +111,6 @@ export type FunctionOperation = { machineUserName: string; } | undefined; - authInvoker?: undefined; }; export type FunctionOperationInput = FunctionOperation; @@ -130,7 +129,6 @@ export type GqlOperationInput = { machineUserName: string; } | undefined; - authInvoker?: undefined; }; export type GqlOperation = { @@ -148,7 +146,6 @@ export type GqlOperation = { machineUserName: string; } | undefined; - authInvoker?: undefined; }; export type WebhookOperation = { @@ -189,7 +186,6 @@ export type WorkflowOperation = { machineUserName: string; } | undefined; - authInvoker?: undefined; }; export type ExecutorInput = { @@ -314,7 +310,6 @@ export type Executor = { machineUserName: string; } | undefined; - authInvoker?: undefined; } | { kind: "function" | "jobFunction"; @@ -326,7 +321,6 @@ export type Executor = { machineUserName: string; } | undefined; - authInvoker?: undefined; } | { kind: "graphql"; @@ -340,7 +334,6 @@ export type Executor = { machineUserName: string; } | undefined; - authInvoker?: undefined; } | { kind: "webhook"; diff --git a/packages/sdk/src/types/resolver.generated.ts b/packages/sdk/src/types/resolver.generated.ts index a778d0697..922b99eab 100644 --- a/packages/sdk/src/types/resolver.generated.ts +++ b/packages/sdk/src/types/resolver.generated.ts @@ -63,6 +63,5 @@ export type Resolver = { machineUserName: string; } | undefined; - authInvoker?: undefined; }; export type ResolverInput = Resolver; From 6bb9e8ce367ab0ebd9a884f0ac8c112fd5055821 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 23 Jun 2026 12:12:59 +0900 Subject: [PATCH 181/618] refactor(sdk): reject unknown parser schema keys --- .../src/cli/commands/function/detect.test.ts | 21 ++ .../sdk/src/cli/commands/function/detect.ts | 3 +- .../sdk/src/cli/services/application.test.ts | 40 +++ packages/sdk/src/cli/services/application.ts | 29 +- .../src/cli/services/executor/loader.test.ts | 32 ++ .../sdk/src/cli/services/executor/loader.ts | 16 +- .../sdk/src/cli/services/executor/service.ts | 3 +- .../sdk/src/cli/services/tailordb/service.ts | 3 +- packages/sdk/src/parser/app-config/schema.ts | 42 +-- packages/sdk/src/parser/schema-strict.test.ts | 170 ++++++++++ .../src/parser/service/aigateway/schema.ts | 1 + .../parser/service/auth-connection/schema.ts | 24 +- .../sdk/src/parser/service/auth/schema.ts | 227 +++++++------ .../sdk/src/parser/service/executor/schema.ts | 185 ++++++----- .../sdk/src/parser/service/field/schema.ts | 48 ++- .../src/parser/service/http-adapter/schema.ts | 6 +- packages/sdk/src/parser/service/idp/schema.ts | 54 +-- .../sdk/src/parser/service/secrets/schema.ts | 10 +- .../parser/service/staticwebsite/schema.ts | 1 + .../service/tailordb/builder-helpers.ts | 30 ++ .../sdk/src/parser/service/tailordb/schema.ts | 311 ++++++++++-------- .../sdk/src/parser/service/workflow/schema.ts | 23 +- .../src/types/auth-connection.generated.ts | 9 +- packages/sdk/src/types/auth.generated.ts | 10 +- packages/sdk/src/types/field.generated.ts | 2 + packages/sdk/src/types/resolver.generated.ts | 1 + packages/sdk/src/utils/test/internal.ts | 3 +- 27 files changed, 883 insertions(+), 421 deletions(-) create mode 100644 packages/sdk/src/cli/services/executor/loader.test.ts create mode 100644 packages/sdk/src/parser/schema-strict.test.ts create mode 100644 packages/sdk/src/parser/service/tailordb/builder-helpers.ts diff --git a/packages/sdk/src/cli/commands/function/detect.test.ts b/packages/sdk/src/cli/commands/function/detect.test.ts index dc9e0a49b..9281b346a 100644 --- a/packages/sdk/src/cli/commands/function/detect.test.ts +++ b/packages/sdk/src/cli/commands/function/detect.test.ts @@ -64,6 +64,27 @@ export default { expect(result.name).toBe("my-executor"); }); + test("detects a function executor with trigger helper args", async () => { + const filePath = path.join(testDir, "executor-with-helper.mjs"); + fs.writeFileSync( + filePath, + ` +export default { + name: "my-executor", + trigger: { kind: "schedule", cron: "0 12 * * *", __args: [{ cron: "0 12 * * *" }] }, + operation: { + kind: "function", + body: (args) => {}, + }, +}; +`, + ); + + const result = await detectFunctionType({ filePath }); + expect(result.type).toBe("executor"); + expect(result.name).toBe("my-executor"); + }); + test("does not detect a non-function executor", async () => { const filePath = path.join(testDir, "gql-executor.mjs"); fs.writeFileSync( diff --git a/packages/sdk/src/cli/commands/function/detect.ts b/packages/sdk/src/cli/commands/function/detect.ts index 45fd9343e..5ae58dac1 100644 --- a/packages/sdk/src/cli/commands/function/detect.ts +++ b/packages/sdk/src/cli/commands/function/detect.ts @@ -7,6 +7,7 @@ import { pathToFileURL } from "node:url"; import * as path from "pathe"; +import { stripExecutorTriggerArgs } from "@/cli/services/executor/loader"; import { ExecutorSchema } from "@/parser/service/executor"; import { ResolverSchema } from "@/parser/service/resolver"; import { WorkflowJobSchema } from "@/parser/service/workflow"; @@ -76,7 +77,7 @@ export async function detectFunctionType( } // 2. Check executor (only function/jobFunction kinds) - const executorResult = ExecutorSchema.safeParse(module.default); + const executorResult = ExecutorSchema.safeParse(stripExecutorTriggerArgs(module.default)); if (executorResult.success) { const { operation } = executorResult.data; if (operation.kind === "function" || operation.kind === "jobFunction") { diff --git a/packages/sdk/src/cli/services/application.test.ts b/packages/sdk/src/cli/services/application.test.ts index 57ca2255b..582b07f5d 100644 --- a/packages/sdk/src/cli/services/application.test.ts +++ b/packages/sdk/src/cli/services/application.test.ts @@ -1,6 +1,8 @@ import { describe, expect, test } from "vitest"; import { defineConfig } from "@/configure/config"; import { defineAuth } from "@/configure/services/auth"; +import { defineIdp } from "@/configure/services/idp"; +import { defineStaticWebSite } from "@/configure/services/staticwebsite"; import { db } from "@/configure/services/tailordb/schema"; import { defineApplication } from "./application"; @@ -39,4 +41,42 @@ describe("defineAuth parse wiring", () => { expect(application.authService!.userProfile?.namespace).toBe("external-ns"); }); + + test("accepts defineIdp helper objects when parsing IdP services", () => { + const idp = defineIdp("my-idp", { + clients: ["default-client"], + }); + + const config = { + ...defineConfig({ + name: "testApp", + idp: [idp], + }), + path: "tailor.config.ts", + }; + + const application = defineApplication({ config }); + + expect(application.idpServices).toHaveLength(1); + expect(application.idpServices[0]?.name).toBe("my-idp"); + }); + + test("accepts defineStaticWebSite helper objects when parsing static websites", () => { + const website = defineStaticWebSite("my-site", { + description: "my website", + }); + + const config = { + ...defineConfig({ + name: "testApp", + staticWebsites: [website], + }), + path: "tailor.config.ts", + }; + + const application = defineApplication({ config }); + + expect(application.staticWebsiteServices).toHaveLength(1); + expect(application.staticWebsiteServices[0]?.name).toBe("my-site"); + }); }); diff --git a/packages/sdk/src/cli/services/application.ts b/packages/sdk/src/cli/services/application.ts index 32483bf15..619c8990e 100644 --- a/packages/sdk/src/cli/services/application.ts +++ b/packages/sdk/src/cli/services/application.ts @@ -155,6 +155,14 @@ type DefineIdpResult = { subgraphs: Array<{ Type: string; Name: string }>; }; +function stripIdpProviderHelper(idpConfig: IdPConfig): IdPConfig { + if ("external" in idpConfig) return idpConfig; + const configWithProvider = idpConfig as IdPConfig & { provider?: unknown }; + if (typeof configWithProvider.provider !== "function") return idpConfig; + const { provider: _provider, ...config } = configWithProvider; + return config as IdPConfig; +} + function defineIdp(config: readonly IdPConfig[] | undefined): DefineIdpResult { const idpServices: IdP[] = []; const subgraphs: Array<{ Type: string; Name: string }> = []; @@ -171,7 +179,7 @@ function defineIdp(config: readonly IdPConfig[] | undefined): DefineIdpResult { } idpNames.add(name); if (!("external" in idpConfig)) { - const idp = IdPSchema.parse(idpConfig); + const idp = IdPSchema.parse(stripIdpProviderHelper(idpConfig)); idpServices.push(idp); } subgraphs.push({ Type: "idp", Name: name }); @@ -185,6 +193,14 @@ type DefineAuthResult = { subgraphs: Array<{ Type: string; Name: string }>; }; +function stripAuthConnectionTokenHelper(config: AuthConfig): AuthConfig { + if ("external" in config) return config; + const configWithConnectionToken = config as AuthConfig & { getConnectionToken?: unknown }; + if (typeof configWithConnectionToken.getConnectionToken !== "function") return config; + const { getConnectionToken: _getConnectionToken, ...authConfig } = configWithConnectionToken; + return authConfig as AuthConfig; +} + function defineAuth( config: AuthConfig | undefined, tailorDBServices: ReadonlyArray, @@ -199,7 +215,7 @@ function defineAuth( let authService: AuthService | undefined; if (!("external" in config)) { authService = createAuthService( - AuthConfigSchema.parse(config), + AuthConfigSchema.parse(stripAuthConnectionTokenHelper(config)), tailorDBServices, externalTailorDBNamespaces, ); @@ -235,6 +251,13 @@ function defineHttpAdapterService( return createHttpAdapterService({ config }); } +function stripStaticWebsiteUrlHelper(config: StaticWebsiteInput): StaticWebsiteInput { + const configWithUrl = config as StaticWebsiteInput & { url?: unknown }; + if (configWithUrl.url !== `${config.name}:url`) return config; + const { url: _url, ...websiteConfig } = configWithUrl; + return websiteConfig; +} + function defineStaticWebsites( websites: readonly StaticWebsiteInput[] | undefined, ): StaticWebsite[] { @@ -242,7 +265,7 @@ function defineStaticWebsites( const websiteNames = new Set(); (websites ?? []).forEach((config) => { - const website = StaticWebsiteSchema.parse(config); + const website = StaticWebsiteSchema.parse(stripStaticWebsiteUrlHelper(config)); if (websiteNames.has(website.name)) { throw new Error(`Static website with name "${website.name}" already defined.`); } diff --git a/packages/sdk/src/cli/services/executor/loader.test.ts b/packages/sdk/src/cli/services/executor/loader.test.ts new file mode 100644 index 000000000..d402d0bd1 --- /dev/null +++ b/packages/sdk/src/cli/services/executor/loader.test.ts @@ -0,0 +1,32 @@ +import * as fs from "node:fs"; +import * as path from "pathe"; +import { describe, expect, test } from "vitest"; +import { tempCwd } from "@/cli/shared/test-helpers/temp-cwd"; +import { loadExecutor } from "./loader"; + +describe("loadExecutor", () => { + test("accepts executor trigger helper args", async () => { + using tmp = tempCwd("sdk-executor-loader-"); + const executorFile = path.join(tmp.dir, "executor.ts"); + fs.writeFileSync( + executorFile, + ` +import { createExecutor, scheduleTrigger } from "@tailor-platform/sdk"; + +export default createExecutor({ + name: "daily", + trigger: scheduleTrigger({ cron: "0 12 * * *" }), + operation: { + kind: "function", + body: async () => {}, + }, +}); +`, + ); + + const executor = await loadExecutor(executorFile); + + expect(executor).not.toBeNull(); + expect(executor?.trigger).not.toHaveProperty("__args"); + }); +}); diff --git a/packages/sdk/src/cli/services/executor/loader.ts b/packages/sdk/src/cli/services/executor/loader.ts index 41c492f17..53f71c088 100644 --- a/packages/sdk/src/cli/services/executor/loader.ts +++ b/packages/sdk/src/cli/services/executor/loader.ts @@ -2,6 +2,20 @@ import { pathToFileURL } from "node:url"; import { ExecutorSchema } from "@/parser/service/executor"; import type { Executor } from "@/types/executor.generated"; +export function stripExecutorTriggerArgs(executor: unknown): unknown { + if (executor === null || typeof executor !== "object") { + return executor; + } + + const trigger = (executor as { trigger?: unknown }).trigger; + if (trigger === null || typeof trigger !== "object" || !("__args" in trigger)) { + return executor; + } + + const { __args: _args, ...triggerConfig } = trigger as Record; + return { ...(executor as Record), trigger: triggerConfig }; +} + /** * Load and validate an executor definition from a file. * @param executorFilePath - Path to the executor file @@ -11,7 +25,7 @@ export async function loadExecutor(executorFilePath: string): Promise => { try { const executorModule = await import(pathToFileURL(executorFile).href); - const result = ExecutorSchema.safeParse(executorModule.default); + const result = ExecutorSchema.safeParse(stripExecutorTriggerArgs(executorModule.default)); if (result.success) { const relativePath = path.relative(process.cwd(), executorFile); logger.log( diff --git a/packages/sdk/src/cli/services/tailordb/service.ts b/packages/sdk/src/cli/services/tailordb/service.ts index 498117ee9..9b5f99880 100644 --- a/packages/sdk/src/cli/services/tailordb/service.ts +++ b/packages/sdk/src/cli/services/tailordb/service.ts @@ -4,6 +4,7 @@ import { resolveTSConfig } from "pkg-types"; import { loadFilesWithIgnores } from "@/cli/services/file-loader"; import { logger, styles } from "@/cli/shared/logger"; import { parseTypes, TailorDBTypeSchema } from "@/parser/service/tailordb"; +import { stripTailorDBTypeBuilderHelpers } from "@/parser/service/tailordb/builder-helpers"; import { findOmittedPermitRules } from "@/parser/service/tailordb/permission"; import { assertDefined } from "@/utils/assert"; import { isSdkBranded } from "@/utils/brand"; @@ -176,7 +177,7 @@ export function createTailorDBService(params: CreateTailorDBServiceParams): Tail for (const exportName of Object.keys(module)) { const exportedValue = module[exportName]; - const result = TailorDBTypeSchema.safeParse(exportedValue); + const result = TailorDBTypeSchema.safeParse(stripTailorDBTypeBuilderHelpers(exportedValue)); if (!result.success) { if (isSdkBranded(exportedValue, "tailordb-type")) { throw result.error; diff --git a/packages/sdk/src/parser/app-config/schema.ts b/packages/sdk/src/parser/app-config/schema.ts index 193a90e59..aa35f4127 100644 --- a/packages/sdk/src/parser/app-config/schema.ts +++ b/packages/sdk/src/parser/app-config/schema.ts @@ -22,23 +22,25 @@ const logLevelSchema = z * label-compatible prefix is added at the metadata boundary, so user-facing * configs only need to carry a UUID. */ -export const AppConfigSchema = z.object({ - id: z.uuid({ message: "'id' must be a UUID." }).optional(), - name: z.string().min(1, { message: "'name' must be a non-empty string." }), - env: z.record(z.string(), envValueSchema).optional(), - cors: z.array(z.string()).optional(), - allowedIpAddresses: z.array(z.string()).optional(), - disableIntrospection: z.boolean().optional(), - inlineSourcemap: z.boolean().optional(), - logLevel: logLevelSchema.optional(), - db: z.unknown().optional(), - resolver: z.unknown().optional(), - idp: z.unknown().optional(), - auth: z.unknown().optional(), - executor: z.unknown().optional(), - workflow: z.unknown().optional(), - httpAdapter: z.unknown().optional(), - staticWebsites: z.unknown().optional(), - aiGateways: z.unknown().optional(), - secrets: z.unknown().optional(), -}); +export const AppConfigSchema = z + .object({ + id: z.uuid({ message: "'id' must be a UUID." }).optional(), + name: z.string().min(1, { message: "'name' must be a non-empty string." }), + env: z.record(z.string(), envValueSchema).optional(), + cors: z.array(z.string()).optional(), + allowedIpAddresses: z.array(z.string()).optional(), + disableIntrospection: z.boolean().optional(), + inlineSourcemap: z.boolean().optional(), + logLevel: logLevelSchema.optional(), + db: z.unknown().optional(), + resolver: z.unknown().optional(), + idp: z.unknown().optional(), + auth: z.unknown().optional(), + executor: z.unknown().optional(), + workflow: z.unknown().optional(), + httpAdapter: z.unknown().optional(), + staticWebsites: z.unknown().optional(), + aiGateways: z.unknown().optional(), + secrets: z.unknown().optional(), + }) + .strict(); diff --git a/packages/sdk/src/parser/schema-strict.test.ts b/packages/sdk/src/parser/schema-strict.test.ts new file mode 100644 index 000000000..001e9b7bc --- /dev/null +++ b/packages/sdk/src/parser/schema-strict.test.ts @@ -0,0 +1,170 @@ +import { describe, expect, test } from "vitest"; +import { AppConfigSchema } from "./app-config/schema"; +import { PluginConfigSchema } from "./plugin-config/schema"; +import { AIGatewaySchema } from "./service/aigateway/schema"; +import { AuthConnectionConfigSchema } from "./service/auth-connection/schema"; +import { AuthConfigSchema } from "./service/auth/schema"; +import { ExecutorSchema } from "./service/executor/schema"; +import { TailorFieldSchema } from "./service/field/schema"; +import { IdPSchema } from "./service/idp/schema"; +import { ResolverSchema } from "./service/resolver/schema"; +import { SecretsSchema } from "./service/secrets/schema"; +import { StaticWebsiteSchema } from "./service/staticwebsite/schema"; +import { TailorDBServiceConfigSchema, TailorDBTypeSchema } from "./service/tailordb/schema"; +import { WorkflowSchema } from "./service/workflow/schema"; +import type { ZodType } from "zod"; + +type StrictSchemaCase = { + readonly name: string; + readonly schema: ZodType; + readonly value: Record; +}; + +function hasUnrecognizedKeyIssue(issues: readonly unknown[]): boolean { + return issues.some((issue) => { + if (typeof issue !== "object" || issue === null) return false; + if ("code" in issue && issue.code === "unrecognized_keys") return true; + if (!("errors" in issue) || !Array.isArray(issue.errors)) return false; + return issue.errors.some( + (variantIssues) => Array.isArray(variantIssues) && hasUnrecognizedKeyIssue(variantIssues), + ); + }); +} + +const strictSchemaCases: StrictSchemaCase[] = [ + { + name: "app config", + schema: AppConfigSchema, + value: { name: "my-app" }, + }, + { + name: "AI gateway", + schema: AIGatewaySchema, + value: { name: "my-gateway", authNamespace: "my-auth" }, + }, + { + name: "auth connection", + schema: AuthConnectionConfigSchema, + value: { + type: "oauth2", + providerUrl: "https://accounts.example.com", + issuerUrl: "https://accounts.example.com", + clientId: "client-id", + clientSecret: "client-secret", + }, + }, + { + name: "auth config", + schema: AuthConfigSchema, + value: { name: "my-auth" }, + }, + { + name: "executor", + schema: ExecutorSchema, + value: { + name: "my-executor", + trigger: { kind: "schedule", cron: "0 12 * * *" }, + operation: { kind: "function", body: () => {} }, + }, + }, + { + name: "IdP", + schema: IdPSchema, + value: { name: "my-idp", authorization: "loggedIn", clients: ["default-client"] }, + }, + { + name: "resolver", + schema: ResolverSchema, + value: { + operation: "query", + name: "getUser", + body: () => {}, + output: { type: "string", metadata: {}, fields: {} }, + }, + }, + { + name: "secrets", + schema: SecretsSchema, + value: { + vaults: { "my-vault": { secret: "value" } }, + options: { ignoreNullishValues: true }, + }, + }, + { + name: "static website", + schema: StaticWebsiteSchema, + value: { name: "my-site" }, + }, + { + name: "TailorDB service config", + schema: TailorDBServiceConfigSchema, + value: { files: ["tailordb/*.ts"] }, + }, + { + name: "TailorDB type", + schema: TailorDBTypeSchema, + value: { + name: "User", + fields: {}, + metadata: { + name: "User", + permissions: {}, + files: {}, + }, + }, + }, + { + name: "workflow", + schema: WorkflowSchema, + value: { + name: "my-workflow", + mainJob: { + name: "main", + trigger: () => {}, + body: () => {}, + }, + }, + }, +]; + +describe("parser schemas", () => { + test.each(strictSchemaCases)("rejects unknown keys for $name", ({ schema, value }) => { + const result = schema.safeParse({ ...value, unknownOption: true }); + + expect(result.success).toBe(false); + if (result.success) { + throw new Error("Expected schema parsing to fail"); + } + expect(hasUnrecognizedKeyIssue(result.error.issues)).toBe(true); + }); + + test("preserves plugin instance properties", () => { + const result = PluginConfigSchema.safeParse({ + id: "plugin", + description: "Plugin", + customProperty: true, + }); + + expect(result.success).toBe(true); + if (!result.success) { + throw new Error("Expected plugin config parsing to succeed"); + } + expect(result.data).toHaveProperty("customProperty", true); + }); + + test("accepts field builder properties", () => { + const result = TailorFieldSchema.safeParse({ + type: "string", + metadata: { + validate: [() => true, [() => true, "Invalid value"]], + }, + fields: {}, + builderProperty: true, + }); + + expect(result.success).toBe(true); + if (!result.success) { + throw new Error("Expected field parsing to succeed"); + } + }); +}); diff --git a/packages/sdk/src/parser/service/aigateway/schema.ts b/packages/sdk/src/parser/service/aigateway/schema.ts index 14c5d74a7..b624f94fd 100644 --- a/packages/sdk/src/parser/service/aigateway/schema.ts +++ b/packages/sdk/src/parser/service/aigateway/schema.ts @@ -20,4 +20,5 @@ export const AIGatewaySchema = z "Allowed CORS origins for browser-based clients. Each entry is `*`, `http(s)://*`, `http(s)://*.example.com`, or `http(s)://app.example.com`, optionally with `:port`. Empty list disables cross-origin access.", ), }) + .strict() .brand("AIGatewayConfig"); diff --git a/packages/sdk/src/parser/service/auth-connection/schema.ts b/packages/sdk/src/parser/service/auth-connection/schema.ts index c99092b48..08b5af17f 100644 --- a/packages/sdk/src/parser/service/auth-connection/schema.ts +++ b/packages/sdk/src/parser/service/auth-connection/schema.ts @@ -1,16 +1,16 @@ import { z } from "zod"; -export const AuthConnectionOAuth2ConfigSchema = z.object({ - providerUrl: z.string().describe("OAuth2 provider URL"), - issuerUrl: z.string().describe("OAuth2 issuer URL"), - clientId: z.string().describe("OAuth2 client ID"), - clientSecret: z.string().describe("OAuth2 client secret"), - authUrl: z.string().optional().describe("OAuth2 authorization endpoint override"), - tokenUrl: z.string().optional().describe("OAuth2 token endpoint override"), -}); - -export const AuthConnectionConfigSchema = z +export const AuthConnectionOAuth2ConfigSchema = z .object({ - type: z.literal("oauth2").describe("Connection type"), + providerUrl: z.string().describe("OAuth2 provider URL"), + issuerUrl: z.string().describe("OAuth2 issuer URL"), + clientId: z.string().describe("OAuth2 client ID"), + clientSecret: z.string().describe("OAuth2 client secret"), + authUrl: z.string().optional().describe("OAuth2 authorization endpoint override"), + tokenUrl: z.string().optional().describe("OAuth2 token endpoint override"), }) - .and(AuthConnectionOAuth2ConfigSchema); + .strict(); + +export const AuthConnectionConfigSchema = AuthConnectionOAuth2ConfigSchema.extend({ + type: z.literal("oauth2").describe("Connection type"), +}); diff --git a/packages/sdk/src/parser/service/auth/schema.ts b/packages/sdk/src/parser/service/auth/schema.ts index f521a3112..b244c6545 100644 --- a/packages/sdk/src/parser/service/auth/schema.ts +++ b/packages/sdk/src/parser/service/auth/schema.ts @@ -3,7 +3,7 @@ import { AuthConnectionConfigSchema } from "@/parser/service/auth-connection"; import { TailorFieldSchema } from "@/parser/service/field/schema"; import type { ValueOperand } from "@/configure/services/auth/types"; -export const AuthInvokerObjectSchema = z.object({ +export const AuthInvokerObjectSchema = z.strictObject({ namespace: z.string().describe("Auth namespace"), machineUserName: z.string().describe("Machine user name for authentication"), }); @@ -13,20 +13,24 @@ export const AuthInvokerSchema = z.union([ AuthInvokerObjectSchema, ]); -const secretValueSchema = z.object({ - vaultName: z.string().describe("Vault name containing the secret"), - secretKey: z.string().describe("Key of the secret in the vault"), -}); +const secretValueSchema = z + .object({ + vaultName: z.string().describe("Vault name containing the secret"), + secretKey: z.string().describe("Key of the secret in the vault"), + }) + .strict(); -export const OIDCSchema = z.object({ - name: z.string().describe("Identity provider name"), - kind: z.literal("OIDC"), - clientID: z.string().describe("OAuth2 client ID"), - clientSecret: secretValueSchema.describe("OAuth2 client secret"), - providerURL: z.string().describe("OIDC provider URL"), - issuerURL: z.string().optional().describe("OIDC issuer URL (defaults to providerURL)"), - usernameClaim: z.string().optional().describe("JWT claim to use as username"), -}); +export const OIDCSchema = z + .object({ + name: z.string().describe("Identity provider name"), + kind: z.literal("OIDC"), + clientID: z.string().describe("OAuth2 client ID"), + clientSecret: secretValueSchema.describe("OAuth2 client secret"), + providerURL: z.string().describe("OIDC provider URL"), + issuerURL: z.string().optional().describe("OIDC issuer URL (defaults to providerURL)"), + usernameClaim: z.string().optional().describe("JWT claim to use as username"), + }) + .strict(); export const SAMLSchema = z .object({ @@ -46,22 +50,25 @@ export const SAMLSchema = z .optional() .describe("URL to redirect to when SAML ACS receives a response with an empty RelayState."), }) + .strict() .refine((value) => { const hasMetadata = value.metadataURL !== undefined; const hasRaw = value.rawMetadata !== undefined; return hasMetadata !== hasRaw; }, "Provide either metadataURL or rawMetadata"); -export const IDTokenSchema = z.object({ - name: z.string().describe("Identity provider name"), - kind: z.literal("IDToken"), - providerURL: z.string().describe("ID token provider URL"), - issuerURL: z.string().optional().describe("ID token issuer URL"), - clientID: z.string().describe("Client ID for ID token validation"), - usernameClaim: z.string().optional().describe("JWT claim to use as username"), -}); +export const IDTokenSchema = z + .object({ + name: z.string().describe("Identity provider name"), + kind: z.literal("IDToken"), + providerURL: z.string().describe("ID token provider URL"), + issuerURL: z.string().optional().describe("ID token issuer URL"), + clientID: z.string().describe("Client ID for ID token validation"), + usernameClaim: z.string().optional().describe("JWT claim to use as username"), + }) + .strict(); -export const BuiltinIdPSchema = z.object({ +export const BuiltinIdPSchema = z.strictObject({ name: z.string().describe("Identity provider name"), kind: z.literal("BuiltInIdP"), namespace: z.string().describe("IdP namespace"), @@ -121,17 +128,20 @@ export const OAuth2ClientSchema = z .optional() .describe("Require DPoP (Demonstrating Proof-of-Possession) for token requests"), }) + .strict() .refine((data) => !(data.clientType === "browser" && data.requireDpop === true), { message: "requireDpop cannot be set to true for browser clients as they don't support DPoP", path: ["requireDpop"], }); -export const SCIMAuthorizationSchema = z.object({ - type: z.union([z.literal("oauth2"), z.literal("bearer")]).describe("SCIM authorization type"), - bearerSecret: secretValueSchema - .optional() - .describe("Bearer token secret (required for bearer type)"), -}); +export const SCIMAuthorizationSchema = z + .object({ + type: z.union([z.literal("oauth2"), z.literal("bearer")]).describe("SCIM authorization type"), + bearerSecret: secretValueSchema + .optional() + .describe("Bearer token secret (required for bearer type)"), + }) + .strict(); export const SCIMAttributeTypeSchema = z .union([ @@ -163,56 +173,66 @@ export const SCIMAttributeSchema = z.object({ }, }); -const SCIMSchemaSchema = z.object({ - name: z.string().describe("SCIM schema name"), - attributes: z.array(SCIMAttributeSchema).describe("Schema attributes"), -}); +const SCIMSchemaSchema = z + .object({ + name: z.string().describe("SCIM schema name"), + attributes: z.array(SCIMAttributeSchema).describe("Schema attributes"), + }) + .strict(); -export const SCIMAttributeMappingSchema = z.object({ +export const SCIMAttributeMappingSchema = z.strictObject({ tailorDBField: z.string().describe("TailorDB field name to map to"), scimPath: z.string().describe("SCIM attribute path"), }); -export const SCIMResourceSchema = z.object({ - name: z.string().describe("SCIM resource name"), - tailorDBNamespace: z.string().describe("TailorDB namespace for the resource"), - tailorDBType: z.string().describe("TailorDB type name for the resource"), - coreSchema: SCIMSchemaSchema.describe("Core SCIM schema definition"), - attributeMapping: z.array(SCIMAttributeMappingSchema).describe("Attribute mapping configuration"), -}); +export const SCIMResourceSchema = z + .object({ + name: z.string().describe("SCIM resource name"), + tailorDBNamespace: z.string().describe("TailorDB namespace for the resource"), + tailorDBType: z.string().describe("TailorDB type name for the resource"), + coreSchema: SCIMSchemaSchema.describe("Core SCIM schema definition"), + attributeMapping: z + .array(SCIMAttributeMappingSchema) + .describe("Attribute mapping configuration"), + }) + .strict(); -export const SCIMSchema = z.object({ - machineUserName: z.string().describe("Machine user name for SCIM operations"), - authorization: SCIMAuthorizationSchema.describe("SCIM authorization configuration"), - resources: z.array(SCIMResourceSchema).describe("SCIM resource definitions"), -}); +export const SCIMSchema = z + .object({ + machineUserName: z.string().describe("Machine user name for SCIM operations"), + authorization: SCIMAuthorizationSchema.describe("SCIM authorization configuration"), + resources: z.array(SCIMResourceSchema).describe("SCIM resource definitions"), + }) + .strict(); -export const TenantProviderSchema = z.object({ +export const TenantProviderSchema = z.strictObject({ namespace: z.string().describe("TailorDB namespace for the tenant type"), type: z.string().describe("TailorDB type name for tenants"), signatureField: z.string().describe("Field used as the tenant signature"), }); -const UserProfileSchema = z.object({ - namespace: z.string().optional().describe("TailorDB namespace where the user type is defined"), - // FIXME: improve TailorDBInstance schema validation - type: z.object({ - name: z.string(), - fields: z.any(), - metadata: z.any(), - hooks: z.any(), - validate: z.any(), - features: z.any(), - indexes: z.any(), - files: z.any(), - permission: z.any(), - gqlPermission: z.any(), - _output: z.any(), - }), - usernameField: z.string(), - attributes: z.record(z.string(), z.literal(true)).optional(), - attributeList: z.array(z.string()).optional(), -}); +const UserProfileSchema = z + .object({ + namespace: z.string().optional().describe("TailorDB namespace where the user type is defined"), + // FIXME: improve TailorDBInstance schema validation + type: z.object({ + name: z.string(), + fields: z.any(), + metadata: z.any(), + hooks: z.any(), + validate: z.any(), + features: z.any(), + indexes: z.any(), + files: z.any(), + permission: z.any(), + gqlPermission: z.any(), + _output: z.any(), + }), + usernameField: z.string(), + attributes: z.record(z.string(), z.literal(true)).optional(), + attributeList: z.array(z.string()).optional(), + }) + .strict(); const ValueOperandSchema: z.ZodType = z.union([ z.string(), @@ -221,41 +241,48 @@ const ValueOperandSchema: z.ZodType = z.union([ z.array(z.boolean()), ]); -const MachineUserSchema = z.object({ - attributes: z.record(z.string(), ValueOperandSchema).optional(), - attributeList: z.array(z.uuid()).optional(), -}); +const MachineUserSchema = z + .object({ + attributes: z.record(z.string(), ValueOperandSchema).optional(), + attributeList: z.array(z.uuid()).optional(), + }) + .strict(); -const BeforeLoginHookSchema = z.object({ - handler: z.function(), - invoker: z.string(), -}); +const BeforeLoginHookSchema = z + .object({ + handler: z.function(), + invoker: z.string(), + }) + .strict(); -const AuthConfigBaseSchema = z.object({ - name: z.string().describe("Auth service name"), - hooks: z - .object({ - beforeLogin: BeforeLoginHookSchema.optional().describe("Before login auth hook"), - }) - .optional() - .describe("Auth hooks"), - machineUsers: z - .record(z.string(), MachineUserSchema) - .optional() - .describe("Machine user definitions"), - oauth2Clients: z - .record(z.string(), OAuth2ClientSchema) - .optional() - .describe("OAuth2 client definitions"), - idProvider: IdProviderSchema.optional().describe("Identity provider configuration"), - scim: SCIMSchema.optional().describe("SCIM provisioning configuration"), - tenantProvider: TenantProviderSchema.optional().describe("Multi-tenant provider configuration"), - connections: z - .record(z.string(), AuthConnectionConfigSchema) - .optional() - .describe("Auth connection definitions for external OAuth2 providers"), - publishSessionEvents: z.boolean().optional().describe("Enable publishing session events"), -}); +const AuthConfigBaseSchema = z + .object({ + name: z.string().describe("Auth service name"), + hooks: z + .object({ + beforeLogin: BeforeLoginHookSchema.optional().describe("Before login auth hook"), + }) + .strict() + .optional() + .describe("Auth hooks"), + machineUsers: z + .record(z.string(), MachineUserSchema) + .optional() + .describe("Machine user definitions"), + oauth2Clients: z + .record(z.string(), OAuth2ClientSchema) + .optional() + .describe("OAuth2 client definitions"), + idProvider: IdProviderSchema.optional().describe("Identity provider configuration"), + scim: SCIMSchema.optional().describe("SCIM provisioning configuration"), + tenantProvider: TenantProviderSchema.optional().describe("Multi-tenant provider configuration"), + connections: z + .record(z.string(), AuthConnectionConfigSchema) + .optional() + .describe("Auth connection definitions for external OAuth2 providers"), + publishSessionEvents: z.boolean().optional().describe("Enable publishing session events"), + }) + .strict(); export const AuthConfigSchema = z .xor( diff --git a/packages/sdk/src/parser/service/executor/schema.ts b/packages/sdk/src/parser/service/executor/schema.ts index f07513562..02a232a05 100644 --- a/packages/sdk/src/parser/service/executor/schema.ts +++ b/packages/sdk/src/parser/service/executor/schema.ts @@ -2,78 +2,92 @@ import { z } from "zod"; import { AuthInvokerSchema } from "../auth"; import { functionSchema } from "../common"; -export const TailorDBTriggerSchema = z.object({ - kind: z.literal("tailordb").describe("TailorDB record event trigger"), - events: z - .array( - z.enum([ - "tailordb.type_record.created", - "tailordb.type_record.updated", - "tailordb.type_record.deleted", - ]), - ) - .min(1) - .transform((arr) => [...new Set(arr)]) - .describe("TailorDB event types to trigger on"), - typeName: z.string().describe("TailorDB type name to watch for events"), - condition: functionSchema.optional().describe("Condition function to filter events"), -}); +export const TailorDBTriggerSchema = z + .object({ + kind: z.literal("tailordb").describe("TailorDB record event trigger"), + events: z + .array( + z.enum([ + "tailordb.type_record.created", + "tailordb.type_record.updated", + "tailordb.type_record.deleted", + ]), + ) + .min(1) + .transform((arr) => [...new Set(arr)]) + .describe("TailorDB event types to trigger on"), + typeName: z.string().describe("TailorDB type name to watch for events"), + condition: functionSchema.optional().describe("Condition function to filter events"), + }) + .strict(); -export const ResolverExecutedTriggerSchema = z.object({ - kind: z.literal("resolverExecuted"), - resolverName: z.string().describe("Name of the resolver to trigger on"), - condition: functionSchema.optional().describe("Condition function to filter events"), -}); +export const ResolverExecutedTriggerSchema = z + .object({ + kind: z.literal("resolverExecuted"), + resolverName: z.string().describe("Name of the resolver to trigger on"), + condition: functionSchema.optional().describe("Condition function to filter events"), + }) + .strict(); -export const ScheduleTriggerSchema = z.object({ - kind: z.literal("schedule"), - cron: z.string().describe("CRON expression for the schedule"), - timezone: z - .string() - .optional() - .default("UTC") - .describe("Timezone for the CRON schedule (default: UTC)"), -}); +export const ScheduleTriggerSchema = z + .object({ + kind: z.literal("schedule"), + cron: z.string().describe("CRON expression for the schedule"), + timezone: z + .string() + .optional() + .default("UTC") + .describe("Timezone for the CRON schedule (default: UTC)"), + }) + .strict(); -export const IncomingWebhookTriggerResponseSchema = z.object({ - body: functionSchema.optional().describe("Function returning the response body"), - statusCode: z.number().int().optional().describe("HTTP status code for the response"), -}); +export const IncomingWebhookTriggerResponseSchema = z + .object({ + body: functionSchema.optional().describe("Function returning the response body"), + statusCode: z.number().int().optional().describe("HTTP status code for the response"), + }) + .strict(); -export const IncomingWebhookTriggerSchema = z.object({ - kind: z.literal("incomingWebhook"), - response: IncomingWebhookTriggerResponseSchema.optional().describe("Response configuration"), -}); +export const IncomingWebhookTriggerSchema = z + .object({ + kind: z.literal("incomingWebhook"), + response: IncomingWebhookTriggerResponseSchema.optional().describe("Response configuration"), + }) + .strict(); -export const IdpUserTriggerSchema = z.object({ - kind: z.literal("idpUser").describe("IdP user event trigger"), - events: z - .array(z.enum(["idp.user.created", "idp.user.updated", "idp.user.deleted"])) - .min(1) - .transform((arr) => [...new Set(arr)]) - .describe("IdP user event types to trigger on"), - idp: z - .string() - .optional() - .describe( - "IdP namespace name to subscribe to. If omitted, the project's only IdP is used; throws when multiple IdPs exist.", - ), -}); +export const IdpUserTriggerSchema = z + .object({ + kind: z.literal("idpUser").describe("IdP user event trigger"), + events: z + .array(z.enum(["idp.user.created", "idp.user.updated", "idp.user.deleted"])) + .min(1) + .transform((arr) => [...new Set(arr)]) + .describe("IdP user event types to trigger on"), + idp: z + .string() + .optional() + .describe( + "IdP namespace name to subscribe to. If omitted, the project's only IdP is used; throws when multiple IdPs exist.", + ), + }) + .strict(); -export const AuthAccessTokenTriggerSchema = z.object({ - kind: z.literal("authAccessToken").describe("Auth access token event trigger"), - events: z - .array( - z.enum([ - "auth.access_token.issued", - "auth.access_token.refreshed", - "auth.access_token.revoked", - ]), - ) - .min(1) - .transform((arr) => [...new Set(arr)]) - .describe("Auth access token event types to trigger on"), -}); +export const AuthAccessTokenTriggerSchema = z + .object({ + kind: z.literal("authAccessToken").describe("Auth access token event trigger"), + events: z + .array( + z.enum([ + "auth.access_token.issued", + "auth.access_token.refreshed", + "auth.access_token.revoked", + ]), + ) + .min(1) + .transform((arr) => [...new Set(arr)]) + .describe("Auth access token event types to trigger on"), + }) + .strict(); export const TriggerSchema = z.discriminatedUnion("kind", [ TailorDBTriggerSchema, @@ -102,15 +116,20 @@ export const GqlOperationSchema = z }) .strict(); -export const WebhookOperationSchema = z.object({ - kind: z.literal("webhook"), - url: functionSchema.describe("Function returning the webhook URL"), - requestBody: functionSchema.optional().describe("Function to compute the request body"), - headers: z - .record(z.string(), z.union([z.string(), z.object({ vault: z.string(), key: z.string() })])) - .optional() - .describe("HTTP headers for the webhook request"), -}); +export const WebhookOperationSchema = z + .object({ + kind: z.literal("webhook"), + url: functionSchema.describe("Function returning the webhook URL"), + requestBody: functionSchema.optional().describe("Function to compute the request body"), + headers: z + .record( + z.string(), + z.union([z.string(), z.object({ vault: z.string(), key: z.string() }).strict()]), + ) + .optional() + .describe("HTTP headers for the webhook request"), + }) + .strict(); export const WorkflowOperationSchema = z.preprocess( (val) => { @@ -147,10 +166,12 @@ const OperationSchema = z.union([ WorkflowOperationSchema, ]); -export const ExecutorSchema = z.object({ - name: z.string().describe("Executor name"), - description: z.string().optional().describe("Executor description"), - disabled: z.boolean().optional().default(false).describe("Whether the executor is disabled"), - trigger: TriggerSchema.describe("Event trigger configuration"), - operation: OperationSchema.describe("Operation to execute when triggered"), -}); +export const ExecutorSchema = z + .object({ + name: z.string().describe("Executor name"), + description: z.string().optional().describe("Executor description"), + disabled: z.boolean().optional().default(false).describe("Whether the executor is disabled"), + trigger: TriggerSchema.describe("Event trigger configuration"), + operation: OperationSchema.describe("Operation to execute when triggered"), + }) + .strict(); diff --git a/packages/sdk/src/parser/service/field/schema.ts b/packages/sdk/src/parser/service/field/schema.ts index 8686c02ae..7bca66aad 100644 --- a/packages/sdk/src/parser/service/field/schema.ts +++ b/packages/sdk/src/parser/service/field/schema.ts @@ -15,25 +15,37 @@ const TailorFieldTypeSchema = z.enum([ "nested", ]); -const AllowedValueSchema = z.object({ - value: z.string().describe("The allowed value"), - description: z.string().optional().describe("Description of the allowed value"), -}); +const AllowedValueSchema = z + .object({ + value: z.string().describe("The allowed value"), + description: z.string().optional().describe("Description of the allowed value"), + }) + .strict(); -const FieldMetadataSchema = z.object({ - required: z.boolean().optional().describe("Whether the field is required"), - array: z.boolean().optional().describe("Whether the field is an array"), - description: z.string().optional().describe("Field description"), - allowedValues: z.array(AllowedValueSchema).optional().describe("Allowed values for enum fields"), - hooks: z - .object({ - create: functionSchema.optional().describe("Hook function called on creation"), - update: functionSchema.optional().describe("Hook function called on update"), - }) - .optional() - .describe("Lifecycle hooks"), - typeName: z.string().optional().describe("Type name for nested or enum fields"), -}); +const FieldMetadataSchema = z + .object({ + required: z.boolean().optional().describe("Whether the field is required"), + array: z.boolean().optional().describe("Whether the field is an array"), + description: z.string().optional().describe("Field description"), + allowedValues: z + .array(AllowedValueSchema) + .optional() + .describe("Allowed values for enum fields"), + hooks: z + .object({ + create: functionSchema.optional().describe("Hook function called on creation"), + update: functionSchema.optional().describe("Hook function called on update"), + }) + .strict() + .optional() + .describe("Lifecycle hooks"), + validate: z + .array(z.union([functionSchema, z.tuple([functionSchema, z.string()])])) + .optional() + .describe("Validation functions for the field"), + typeName: z.string().optional().describe("Type name for nested or enum fields"), + }) + .strict(); export const TailorFieldSchema = z.object({ type: TailorFieldTypeSchema.describe("Field data type"), diff --git a/packages/sdk/src/parser/service/http-adapter/schema.ts b/packages/sdk/src/parser/service/http-adapter/schema.ts index f1f5deed5..1519dd38f 100644 --- a/packages/sdk/src/parser/service/http-adapter/schema.ts +++ b/packages/sdk/src/parser/service/http-adapter/schema.ts @@ -4,13 +4,14 @@ import { functionSchema } from "../common"; const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/; const inputHandlersSchema = z - .strictObject({ + .object({ get: functionSchema.optional().describe("Handler for GET requests"), post: functionSchema.optional().describe("Handler for POST requests"), put: functionSchema.optional().describe("Handler for PUT requests"), patch: functionSchema.optional().describe("Handler for PATCH requests"), delete: functionSchema.optional().describe("Handler for DELETE requests"), }) + .strict() .refine( // optional fields become undefined after zod parses them // oxlint-disable-next-line typescript/no-unnecessary-condition @@ -20,7 +21,7 @@ const inputHandlersSchema = z .describe("Per-method functions that transform HTTP requests to GraphQL requests"); export const HttpAdapterConfigSchema = z - .strictObject({ + .object({ name: z .string() .regex( @@ -44,4 +45,5 @@ export const HttpAdapterConfigSchema = z .optional() .describe("Function that transforms GraphQL response to HTTP response"), }) + .strict() .brand("HttpAdapterConfig"); diff --git a/packages/sdk/src/parser/service/idp/schema.ts b/packages/sdk/src/parser/service/idp/schema.ts index 43d6dc410..1e28c9152 100644 --- a/packages/sdk/src/parser/service/idp/schema.ts +++ b/packages/sdk/src/parser/service/idp/schema.ts @@ -36,16 +36,18 @@ function normalizeIdPGqlOperations( export const IdPGqlOperationsSchema = z .union([ z.literal("query"), - z.object({ - create: z.boolean().optional().describe("Enable _createUser mutation (default: true)"), - update: z.boolean().optional().describe("Enable _updateUser mutation (default: true)"), - delete: z.boolean().optional().describe("Enable _deleteUser mutation (default: true)"), - read: z.boolean().optional().describe("Enable _users and _user queries (default: true)"), - sendPasswordResetEmail: z - .boolean() - .optional() - .describe("Enable _sendPasswordResetEmail mutation (default: true)"), - }), + z + .object({ + create: z.boolean().optional().describe("Enable _createUser mutation (default: true)"), + update: z.boolean().optional().describe("Enable _updateUser mutation (default: true)"), + delete: z.boolean().optional().describe("Enable _deleteUser mutation (default: true)"), + read: z.boolean().optional().describe("Enable _users and _user queries (default: true)"), + sendPasswordResetEmail: z + .boolean() + .optional() + .describe("Enable _sendPasswordResetEmail mutation (default: true)"), + }) + .strict(), ]) .describe( "Configuration for GraphQL operations on IdP users.\nAll operations are enabled by default (undefined or true = enabled, false = disabled).", @@ -104,6 +106,7 @@ export const IdPUserAuthPolicySchema = z allowMicrosoftOauth: z.boolean().optional().describe("Enable Microsoft OAuth login"), disablePasswordAuth: z.boolean().optional().describe("Disable password-based authentication"), }) + .strict() .refine( (data) => data.passwordMinLength === undefined || @@ -186,6 +189,7 @@ export const IdPEmailConfigSchema = z .optional() .describe("Default subject for password reset emails"), }) + .strict() .describe("Namespace-level email configuration defaults"); const IdPPermissionOperandSchema = z.union([ @@ -193,10 +197,10 @@ const IdPPermissionOperandSchema = z.union([ z.boolean(), z.array(z.string()).readonly(), z.array(z.boolean()).readonly(), - z.object({ user: z.string() }), - z.object({ idpUser: z.enum(["id", "name", "disabled"]) }), - z.object({ oldIdpUser: z.enum(["id", "name", "disabled"]) }), - z.object({ newIdpUser: z.enum(["id", "name", "disabled"]) }), + z.object({ user: z.string() }).strict(), + z.object({ idpUser: z.enum(["id", "name", "disabled"]) }).strict(), + z.object({ oldIdpUser: z.enum(["id", "name", "disabled"]) }).strict(), + z.object({ newIdpUser: z.enum(["id", "name", "disabled"]) }).strict(), ]); const IdPPermissionOperatorSchema = z.enum(["=", "!=", "in", "not in"]); @@ -207,14 +211,16 @@ const IdPPermissionConditionSchema = z const IdPActionPermissionSchema = z.union([ // Object format: { conditions, description?, permit? } - z.object({ - conditions: z.union([ - IdPPermissionConditionSchema, - z.array(IdPPermissionConditionSchema).readonly(), - ]), - description: z.string().optional(), - permit: z.boolean().optional(), - }), + z + .object({ + conditions: z.union([ + IdPPermissionConditionSchema, + z.array(IdPPermissionConditionSchema).readonly(), + ]), + description: z.string().optional(), + permit: z.boolean().optional(), + }) + .strict(), // Single condition tuple: [operand, operator, operand] z .tuple([IdPPermissionOperandSchema, IdPPermissionOperatorSchema, IdPPermissionOperandSchema]) @@ -249,13 +255,14 @@ export const IdPPermissionSchema = z delete: z.array(IdPActionPermissionSchema).readonly(), sendPasswordResetEmail: z.array(IdPActionPermissionSchema).readonly(), }) + .strict() .describe("Per-operation permission policies for IdP users"); export const IdPSchema = z .object({ name: z.string().describe("IdP service name"), authorization: z - .union([z.literal("insecure"), z.literal("loggedIn"), z.object({ cel: z.string() })]) + .union([z.literal("insecure"), z.literal("loggedIn"), z.object({ cel: z.string() }).strict()]) .optional() .describe("Authorization mode for IdP API access"), clients: z.array(z.string()).describe("OAuth2 client names that can use this IdP"), @@ -278,4 +285,5 @@ export const IdPSchema = z "Per-operation permission policies for IdP users", ), }) + .strict() .brand("IdPConfig"); diff --git a/packages/sdk/src/parser/service/secrets/schema.ts b/packages/sdk/src/parser/service/secrets/schema.ts index 06d0f4f62..ec6991a65 100644 --- a/packages/sdk/src/parser/service/secrets/schema.ts +++ b/packages/sdk/src/parser/service/secrets/schema.ts @@ -4,9 +4,11 @@ const namePattern = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/; const nameSchema = z.string().regex(namePattern); const secretsVaultSchema = z.record(nameSchema, z.string().nullish()); -export const SecretsSchema = z.object({ +export const SecretsSchema = z.strictObject({ vaults: z.record(nameSchema, secretsVaultSchema), - options: z.object({ - ignoreNullishValues: z.boolean(), - }), + options: z + .object({ + ignoreNullishValues: z.boolean(), + }) + .strict(), }); diff --git a/packages/sdk/src/parser/service/staticwebsite/schema.ts b/packages/sdk/src/parser/service/staticwebsite/schema.ts index ee53f0de9..e79cfb91a 100644 --- a/packages/sdk/src/parser/service/staticwebsite/schema.ts +++ b/packages/sdk/src/parser/service/staticwebsite/schema.ts @@ -10,4 +10,5 @@ export const StaticWebsiteSchema = z .describe("IP addresses allowed to access the website"), customDomains: z.array(z.string()).optional().describe("Custom domains for the static website"), }) + .strict() .brand("StaticWebsiteConfig"); diff --git a/packages/sdk/src/parser/service/tailordb/builder-helpers.ts b/packages/sdk/src/parser/service/tailordb/builder-helpers.ts new file mode 100644 index 000000000..ac9ec8c70 --- /dev/null +++ b/packages/sdk/src/parser/service/tailordb/builder-helpers.ts @@ -0,0 +1,30 @@ +import { isSdkBranded } from "@/utils/brand"; + +const TAILORDB_TYPE_BUILDER_HELPER_KEYS = [ + "_output", + "_description", + "hooks", + "validate", + "features", + "indexes", + "files", + "permission", + "gqlPermission", + "description", + "pickFields", + "omitFields", + "plugins", + "plugin", +] as const; + +export function stripTailorDBTypeBuilderHelpers(type: unknown): unknown { + if (!isSdkBranded(type, "tailordb-type") || type === null || typeof type !== "object") { + return type; + } + + const config = { ...(type as Record) }; + for (const key of TAILORDB_TYPE_BUILDER_HELPER_KEYS) { + delete config[key]; + } + return config; +} diff --git a/packages/sdk/src/parser/service/tailordb/schema.ts b/packages/sdk/src/parser/service/tailordb/schema.ts index 3acfc855f..fcefb668c 100644 --- a/packages/sdk/src/parser/service/tailordb/schema.ts +++ b/packages/sdk/src/parser/service/tailordb/schema.ts @@ -25,15 +25,17 @@ function normalizeGqlOperations( export const GqlOperationsSchema = z .union([ z.literal("query"), - z.object({ - create: z.boolean().optional().describe("Enable create mutation (default: true)"), - update: z.boolean().optional().describe("Enable update mutation (default: true)"), - delete: z.boolean().optional().describe("Enable delete mutation (default: true)"), - read: z - .boolean() - .optional() - .describe("Enable read queries - get, list, aggregation (default: true)"), - }), + z + .object({ + create: z.boolean().optional().describe("Enable create mutation (default: true)"), + update: z.boolean().optional().describe("Enable update mutation (default: true)"), + delete: z.boolean().optional().describe("Enable delete mutation (default: true)"), + read: z + .boolean() + .optional() + .describe("Enable read queries - get, list, aggregation (default: true)"), + }) + .strict(), ]) .describe( "Configuration for GraphQL operations on a TailorDB type.\nAll operations are enabled by default (undefined or true = enabled, false = disabled).", @@ -54,65 +56,78 @@ const TailorFieldTypeSchema = z.enum([ "nested", ]); -const AllowedValueSchema = z.object({ - value: z.string(), - description: z.string().optional(), -}); +const AllowedValueSchema = z + .object({ + value: z.string(), + description: z.string().optional(), + }) + .strict(); -export const DBFieldMetadataSchema = z.object({ - required: z.boolean().optional().describe("Whether the field is required"), - array: z.boolean().optional().describe("Whether the field is an array"), - description: z.string().optional().describe("Field description"), - typeName: z.string().optional().describe("Type name for nested or enum fields"), - allowedValues: z.array(AllowedValueSchema).optional().describe("Allowed values for enum fields"), - index: z.boolean().optional().describe("Whether the field is indexed for faster queries"), - unique: z.boolean().optional().describe("Whether the field value must be unique"), - vector: z - .boolean() - .optional() - .describe("Whether the field is a vector field for similarity search"), - foreignKey: z.boolean().optional().describe("Whether the field is a foreign key"), - foreignKeyType: z.string().optional().describe("Target type name for foreign key relations"), - foreignKeyField: z.string().optional().describe("Target field name for foreign key relations"), - hooks: z - .object({ - create: functionSchema.optional().describe("Hook function called on record creation"), - update: functionSchema.optional().describe("Hook function called on record update"), - }) - .optional() - .describe("Lifecycle hooks for the field"), - validate: z - .array(z.union([functionSchema, z.tuple([functionSchema, z.string()])])) - .optional() - .describe("Validation functions for the field"), - serial: z - .object({ - start: z.number().describe("Starting value for the serial sequence"), - maxValue: z.number().optional().describe("Maximum value for the serial sequence"), - format: z.string().optional().describe("Format string for serial value (string type only)"), - }) - .optional() - .describe("Serial (auto-increment) configuration"), - scale: z - .number() - .int() - .min(0) - .max(12) - .optional() - .describe("Decimal scale (number of digits after decimal point, 0-12)"), -}); +export const DBFieldMetadataSchema = z + .object({ + required: z.boolean().optional().describe("Whether the field is required"), + array: z.boolean().optional().describe("Whether the field is an array"), + description: z.string().optional().describe("Field description"), + typeName: z.string().optional().describe("Type name for nested or enum fields"), + allowedValues: z + .array(AllowedValueSchema) + .optional() + .describe("Allowed values for enum fields"), + index: z.boolean().optional().describe("Whether the field is indexed for faster queries"), + unique: z.boolean().optional().describe("Whether the field value must be unique"), + vector: z + .boolean() + .optional() + .describe("Whether the field is a vector field for similarity search"), + foreignKey: z.boolean().optional().describe("Whether the field is a foreign key"), + foreignKeyType: z.string().optional().describe("Target type name for foreign key relations"), + foreignKeyField: z.string().optional().describe("Target field name for foreign key relations"), + hooks: z + .object({ + create: functionSchema.optional().describe("Hook function called on record creation"), + update: functionSchema.optional().describe("Hook function called on record update"), + }) + .strict() + .optional() + .describe("Lifecycle hooks for the field"), + validate: z + .array(z.union([functionSchema, z.tuple([functionSchema, z.string()])])) + .optional() + .describe("Validation functions for the field"), + serial: z + .object({ + start: z.number().describe("Starting value for the serial sequence"), + maxValue: z.number().optional().describe("Maximum value for the serial sequence"), + format: z.string().optional().describe("Format string for serial value (string type only)"), + }) + .strict() + .optional() + .describe("Serial (auto-increment) configuration"), + scale: z + .number() + .int() + .min(0) + .max(12) + .optional() + .describe("Decimal scale (number of digits after decimal point, 0-12)"), + }) + .strict(); const RelationTypeSchema = z.enum(relationTypesKeys); -export const RawRelationConfigSchema = z.object({ - type: RelationTypeSchema.describe("Relation cardinality type"), - toward: z.object({ - type: z.string().describe("Target type name, or 'self' for self-relations"), - as: z.string().optional().describe("Custom forward relation name"), - key: z.string().optional().describe("Target field to join on (default: 'id')"), - }), - backward: z.string().optional().describe("Backward relation name on the target type"), -}); +export const RawRelationConfigSchema = z + .object({ + type: RelationTypeSchema.describe("Relation cardinality type"), + toward: z + .object({ + type: z.string().describe("Target type name, or 'self' for self-relations"), + as: z.string().optional().describe("Custom forward relation name"), + key: z.string().optional().describe("Target field to join on (default: 'id')"), + }) + .strict(), + backward: z.string().optional().describe("Backward relation name on the target type"), + }) + .strict(); const TailorDBFieldSchema: z.ZodType = z.lazy(() => z.object({ @@ -127,20 +142,22 @@ const TailorDBFieldSchema: z.ZodType = z.lazy(() => * Schema for TailorDB type settings. * Normalizes gqlOperations from alias ("query") to object format. */ -export const TailorDBTypeSettingsSchema = z.object({ - pluralForm: z.string().optional().describe("Custom plural form of the type name for GraphQL"), - aggregation: z.boolean().optional().describe("Enable aggregation queries for this type"), - bulkUpsert: z.boolean().optional().describe("Enable bulk upsert mutation for this type"), - gqlOperations: GqlOperationsSchema.optional().describe( - 'Configure GraphQL operations for this type. Use "query" for read-only mode, or an object for granular control.', - ), - publishEvents: z - .boolean() - .optional() - .describe( - "Enable publishing events for this type.\nWhen enabled, record creation/update/deletion events are published.\nIf not specified, this is automatically set to true when an executor uses this type\nwith recordCreated/recordUpdated/recordDeleted triggers. If explicitly set to false\nwhile an executor uses this type, an error will be thrown during apply.", +export const TailorDBTypeSettingsSchema = z + .object({ + pluralForm: z.string().optional().describe("Custom plural form of the type name for GraphQL"), + aggregation: z.boolean().optional().describe("Enable aggregation queries for this type"), + bulkUpsert: z.boolean().optional().describe("Enable bulk upsert mutation for this type"), + gqlOperations: GqlOperationsSchema.optional().describe( + 'Configure GraphQL operations for this type. Use "query" for read-only mode, or an object for granular control.', ), -}); + publishEvents: z + .boolean() + .optional() + .describe( + "Enable publishing events for this type.\nWhen enabled, record creation/update/deletion events are published.\nIf not specified, this is automatically set to true when an executor uses this type\nwith recordCreated/recordUpdated/recordDeleted triggers. If explicitly set to false\nwhile an executor uses this type, an error will be thrown during apply.", + ), + }) + .strict(); export const GQL_PERMISSION_INVALID_OPERAND_MESSAGE = "operand is not supported in gqlPermission. Use permission() for record-level conditions."; @@ -169,9 +186,9 @@ const GqlPermissionOperandSchema = z.union( const RecordPermissionOperandSchema = z.union([ GqlPermissionOperandSchema, - z.object({ record: z.string() }), - z.object({ oldRecord: z.string() }), - z.object({ newRecord: z.string() }), + z.object({ record: z.string() }).strict(), + z.object({ oldRecord: z.string() }).strict(), + z.object({ newRecord: z.string() }).strict(), ]); const PermissionOperatorSchema = z.enum(["=", "!=", "in", "not in", "hasAny", "not hasAny"]); @@ -186,14 +203,16 @@ const GqlPermissionConditionSchema = z const ActionPermissionSchema = z.union([ // Object format: { conditions, description?, permit? } - z.object({ - conditions: z.union([ - RecordPermissionConditionSchema, - z.array(RecordPermissionConditionSchema).readonly(), - ]), - description: z.string().optional(), - permit: z.boolean().optional(), - }), + z + .object({ + conditions: z.union([ + RecordPermissionConditionSchema, + z.array(RecordPermissionConditionSchema).readonly(), + ]), + description: z.string().optional(), + permit: z.boolean().optional(), + }) + .strict(), // Single condition tuple: [operand, operator, operand] z .tuple([RecordPermissionOperandSchema, PermissionOperatorSchema, RecordPermissionOperandSchema]) @@ -229,61 +248,79 @@ const GqlPermissionActionSchema = z.enum([ "bulkUpsert", ]); -const GqlPermissionPolicySchema = z.object({ - conditions: z.array(GqlPermissionConditionSchema).readonly(), - actions: z.union([z.literal("all"), z.array(GqlPermissionActionSchema).readonly()]), - permit: z.boolean().optional(), - description: z.string().optional(), -}); +const GqlPermissionPolicySchema = z + .object({ + conditions: z.array(GqlPermissionConditionSchema).readonly(), + actions: z.union([z.literal("all"), z.array(GqlPermissionActionSchema).readonly()]), + permit: z.boolean().optional(), + description: z.string().optional(), + }) + .strict(); -export const RawPermissionsSchema = z.object({ - record: z - .object({ - create: z.array(ActionPermissionSchema).readonly(), - read: z.array(ActionPermissionSchema).readonly(), - update: z.array(ActionPermissionSchema).readonly(), - delete: z.array(ActionPermissionSchema).readonly(), - }) - .optional(), - gql: z.array(GqlPermissionPolicySchema).readonly().optional(), -}); +export const RawPermissionsSchema = z + .object({ + record: z + .object({ + create: z.array(ActionPermissionSchema).readonly(), + read: z.array(ActionPermissionSchema).readonly(), + update: z.array(ActionPermissionSchema).readonly(), + delete: z.array(ActionPermissionSchema).readonly(), + }) + .strict() + .optional(), + gql: z.array(GqlPermissionPolicySchema).readonly().optional(), + }) + .strict(); -export const TailorDBTypeSchema = z.object({ - name: z.string(), - fields: z.record(z.string(), TailorDBFieldSchema), - metadata: z.object({ +export const TailorDBTypeSchema = z + .object({ name: z.string(), - description: z.string().optional(), - settings: TailorDBTypeSettingsSchema.optional(), - permissions: RawPermissionsSchema, - files: z.record(z.string(), z.string()), - indexes: z - .record( - z.string(), - z.object({ - fields: z.array(z.string()), - unique: z.boolean().optional(), - }), - ) - .optional(), - }), -}); + fields: z.record(z.string(), TailorDBFieldSchema), + metadata: z + .object({ + name: z.string(), + description: z.string().optional(), + settings: TailorDBTypeSettingsSchema.optional(), + permissions: RawPermissionsSchema, + files: z.record(z.string(), z.string()), + indexes: z + .record( + z.string(), + z + .object({ + fields: z.array(z.string()), + unique: z.boolean().optional(), + }) + .strict(), + ) + .optional(), + }) + .strict(), + }) + .strict(); -const TailorDBMigrationConfigSchema = z.object({ - directory: z.string().describe("Directory containing migration files"), - machineUser: z.string().optional().describe("Machine user name for migration execution"), -}); +const TailorDBMigrationConfigSchema = z + .object({ + directory: z.string().describe("Directory containing migration files"), + machineUser: z.string().optional().describe("Machine user name for migration execution"), + }) + .strict(); /** * Schema for TailorDB service configuration. * Normalizes gqlOperations from alias ("query") to object format. */ -export const TailorDBServiceConfigSchema = z.object({ - files: z.array(z.string()).describe("Glob patterns for TailorDB type definition files"), - ignores: z.array(z.string()).optional().describe("Glob patterns to exclude from type discovery"), - erdSite: z.string().optional().describe("URL for the ERD (Entity Relationship Diagram) site"), - migration: TailorDBMigrationConfigSchema.optional().describe("Migration configuration"), - gqlOperations: GqlOperationsSchema.optional().describe( - "Default GraphQL operations for all types in this service", - ), -}); +export const TailorDBServiceConfigSchema = z + .object({ + files: z.array(z.string()).describe("Glob patterns for TailorDB type definition files"), + ignores: z + .array(z.string()) + .optional() + .describe("Glob patterns to exclude from type discovery"), + erdSite: z.string().optional().describe("URL for the ERD (Entity Relationship Diagram) site"), + migration: TailorDBMigrationConfigSchema.optional().describe("Migration configuration"), + gqlOperations: GqlOperationsSchema.optional().describe( + "Default GraphQL operations for all types in this service", + ), + }) + .strict(); diff --git a/packages/sdk/src/parser/service/workflow/schema.ts b/packages/sdk/src/parser/service/workflow/schema.ts index 3953bca7c..98ba3cc95 100644 --- a/packages/sdk/src/parser/service/workflow/schema.ts +++ b/packages/sdk/src/parser/service/workflow/schema.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { functionSchema } from "../common"; -export const WorkflowJobSchema = z.object({ +export const WorkflowJobSchema = z.strictObject({ name: z.string().describe("Job name (must be unique across the project)"), trigger: functionSchema.describe("Trigger function that initiates the job"), body: functionSchema.describe("Job implementation function"), @@ -42,6 +42,7 @@ export const RetryPolicySchema = z ), backoffMultiplier: z.number().min(1).describe("Backoff multiplier (>= 1)"), }) + .strict() .refine((data) => durationToSeconds(data.initialBackoff) <= durationToSeconds(data.maxBackoff), { message: "initialBackoff must be less than or equal to maxBackoff", path: ["initialBackoff"], @@ -51,7 +52,7 @@ export const RetryPolicySchema = z path: ["initialBackoff"], }); -export const ConcurrencyPolicySchema = z.object({ +export const ConcurrencyPolicySchema = z.strictObject({ maxConcurrentExecutions: z .number() .int() @@ -60,11 +61,13 @@ export const ConcurrencyPolicySchema = z.object({ .describe("Maximum number of concurrent executions (1-1000)"), }); -export const WorkflowSchema = z.object({ - name: z.string().describe("Workflow name"), - mainJob: WorkflowJobSchema.describe("Main job that starts the workflow"), - retryPolicy: RetryPolicySchema.optional().describe("Retry policy for the workflow"), - concurrencyPolicy: ConcurrencyPolicySchema.optional().describe( - "Concurrency policy for the workflow", - ), -}); +export const WorkflowSchema = z + .object({ + name: z.string().describe("Workflow name"), + mainJob: WorkflowJobSchema.describe("Main job that starts the workflow"), + retryPolicy: RetryPolicySchema.optional().describe("Retry policy for the workflow"), + concurrencyPolicy: ConcurrencyPolicySchema.optional().describe( + "Concurrency policy for the workflow", + ), + }) + .strict(); diff --git a/packages/sdk/src/types/auth-connection.generated.ts b/packages/sdk/src/types/auth-connection.generated.ts index 1b0034829..654eff112 100644 --- a/packages/sdk/src/types/auth-connection.generated.ts +++ b/packages/sdk/src/types/auth-connection.generated.ts @@ -17,12 +17,19 @@ export type AuthConnectionOAuth2Config = { export type AuthConnectionOAuth2ConfigInput = AuthConnectionOAuth2Config; export type AuthConnectionConfig = { - type: "oauth2"; + /** OAuth2 provider URL */ providerUrl: string; + /** OAuth2 issuer URL */ issuerUrl: string; + /** OAuth2 client ID */ clientId: string; + /** OAuth2 client secret */ clientSecret: string; + /** Connection type */ + type: "oauth2"; + /** OAuth2 authorization endpoint override */ authUrl?: string | undefined; + /** OAuth2 token endpoint override */ tokenUrl?: string | undefined; }; export type AuthConnectionConfigInput = AuthConnectionConfig; diff --git a/packages/sdk/src/types/auth.generated.ts b/packages/sdk/src/types/auth.generated.ts index ecd8f7bed..2fd445add 100644 --- a/packages/sdk/src/types/auth.generated.ts +++ b/packages/sdk/src/types/auth.generated.ts @@ -412,11 +412,11 @@ export type AuthConfigInput = connections?: | { [x: string]: { - type: "oauth2"; providerUrl: string; issuerUrl: string; clientId: string; clientSecret: string; + type: "oauth2"; authUrl?: string | undefined; tokenUrl?: string | undefined; }; @@ -482,6 +482,7 @@ export type AuthConfigInput = update?: Function | undefined; } | undefined; + validate?: (Function | [Function, string])[] | undefined; typeName?: string | undefined; }; fields: any; @@ -610,11 +611,11 @@ export type AuthConfigInput = connections?: | { [x: string]: { - type: "oauth2"; providerUrl: string; issuerUrl: string; clientId: string; clientSecret: string; + type: "oauth2"; authUrl?: string | undefined; tokenUrl?: string | undefined; }; @@ -760,11 +761,11 @@ export type AuthConfig = connections?: | { [x: string]: { - type: "oauth2"; providerUrl: string; issuerUrl: string; clientId: string; clientSecret: string; + type: "oauth2"; authUrl?: string | undefined; tokenUrl?: string | undefined; }; @@ -830,6 +831,7 @@ export type AuthConfig = update?: Function | undefined; } | undefined; + validate?: (Function | [Function, string])[] | undefined; typeName?: string | undefined; }; fields: any; @@ -968,11 +970,11 @@ export type AuthConfig = connections?: | { [x: string]: { - type: "oauth2"; providerUrl: string; issuerUrl: string; clientId: string; clientSecret: string; + type: "oauth2"; authUrl?: string | undefined; tokenUrl?: string | undefined; }; diff --git a/packages/sdk/src/types/field.generated.ts b/packages/sdk/src/types/field.generated.ts index 3653e9734..65d0dd2d8 100644 --- a/packages/sdk/src/types/field.generated.ts +++ b/packages/sdk/src/types/field.generated.ts @@ -31,6 +31,7 @@ export type TailorFieldInput = { update?: Function | undefined; } | undefined; + validate?: (Function | [Function, string])[] | undefined; typeName?: string | undefined; }; fields: { @@ -69,6 +70,7 @@ export type TailorField = { update?: Function | undefined; } | undefined; + validate?: (Function | [Function, string])[] | undefined; typeName?: string | undefined; }; fields: { diff --git a/packages/sdk/src/types/resolver.generated.ts b/packages/sdk/src/types/resolver.generated.ts index 922b99eab..f867c5d10 100644 --- a/packages/sdk/src/types/resolver.generated.ts +++ b/packages/sdk/src/types/resolver.generated.ts @@ -39,6 +39,7 @@ export type Resolver = { update?: Function | undefined; } | undefined; + validate?: (Function | [Function, string])[] | undefined; typeName?: string | undefined; }; fields: { diff --git a/packages/sdk/src/utils/test/internal.ts b/packages/sdk/src/utils/test/internal.ts index e40e8b0c3..fa6261d24 100644 --- a/packages/sdk/src/utils/test/internal.ts +++ b/packages/sdk/src/utils/test/internal.ts @@ -1,3 +1,4 @@ +import { stripTailorDBTypeBuilderHelpers } from "@/parser/service/tailordb/builder-helpers"; /** * Internal test utilities for SDK development. * These are NOT exported to library users. @@ -15,7 +16,7 @@ export function toSchemaOutput( // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accept any db.type() result for testing type: any, ): TailorDBTypeSchemaOutput { - const parsed = TailorDBTypeSchema.safeParse(type); + const parsed = TailorDBTypeSchema.safeParse(stripTailorDBTypeBuilderHelpers(type)); if (!parsed.success) { throw new Error(`Failed to parse type ${type.name}: ${parsed.error.message}`); } From 891e823902e3de3bb884390216219ab6d48da5ba Mon Sep 17 00:00:00 2001 From: "tailor-platform-pr-trigger[bot]" <247949890+tailor-platform-pr-trigger[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 03:13:24 +0000 Subject: [PATCH 182/618] Version Packages (next) --- .changeset/pre.json | 23 ++++++++--- packages/create-sdk/CHANGELOG.md | 24 ++++++++++++ packages/create-sdk/package.json | 2 +- packages/sdk-codemod/CHANGELOG.md | 65 +++++++++++++++++++++++++++++++ packages/sdk-codemod/package.json | 2 +- packages/sdk/CHANGELOG.md | 56 ++++++++++++++++++++++++++ packages/sdk/package.json | 2 +- 7 files changed, 166 insertions(+), 8 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index f9321bcdd..cdd1033bd 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -7,19 +7,32 @@ "@tailor-platform/sdk-codemod": "0.2.7" }, "changesets": [ + "apply-deploy-source-strings", + "codemod-llm-review", + "codemod-migration-docs", + "codemod-residual-matching", + "execute-script-json-arg", + "execute-script-review-scope", + "invoker-option-rename", + "keyring-default-storage", + "open-download-review-scope", + "principal-followup-review", + "principal-unify-codemod", "remove-auth-invoker-helper", "remove-define-generators", "remove-function-test-run-input-wrapper", + "remove-open-download-stream", "remove-runtime-globals-compatibility", "remove-tailor-sdk-skills-shim", "remove-v2-cli-aliases", - "renovate-1425", - "renovate-1429", - "renovate-1433", - "renovate-1448", - "reorganize-types-zod-isolation", + "remove-workflow-test-env-fallback", + "renovate-1516", + "renovate-1525", "require-function-log-content-hash", + "runtime-globals-import", "store-cli-users-by-subject", + "tailor-principal-type", + "timestamps-updated-at-create", "v2-baseline", "workflow-trigger-dispatch" ] diff --git a/packages/create-sdk/CHANGELOG.md b/packages/create-sdk/CHANGELOG.md index 0f4fb50b7..ecb3f4482 100644 --- a/packages/create-sdk/CHANGELOG.md +++ b/packages/create-sdk/CHANGELOG.md @@ -1,5 +1,29 @@ # @tailor-platform/create-sdk +## 2.0.0-next.2 +### Major Changes + + + +- [#1498](https://github.com/tailor-platform/sdk/pull/1498) [`83145db`](https://github.com/tailor-platform/sdk/commit/83145db9a0d243aa68c1b641c2b6026771a62188) Thanks [@dqn](https://github.com/dqn)! - Set `db.fields.timestamps()` `updatedAt` when records are created and make the generated field non-null. `createdAt` keeps its existing create-time behavior, while `updatedAt` keeps its update-time behavior and now also gets a create hook that preserves provided values and falls back to the current time. + + Update create-sdk templates so scaffolded projects use the new non-null `updatedAt` Kysely types and seed schemas. + + Existing TailorDB schemas that already use this helper will change `updatedAt` from optional to required. Backfill existing records that have `updatedAt: null` before applying the schema change. + +### Patch Changes + + + +- [#1509](https://github.com/tailor-platform/sdk/pull/1509) [`7cadaa7`](https://github.com/tailor-platform/sdk/commit/7cadaa7c4987b81130ca80ba80bc5d5b26276394) Thanks [@dqn](https://github.com/dqn)! - Rename resolver, executor, workflow trigger, and typed workflow start machine-user options from `authInvoker` to `invoker`. + + Update create-sdk templates and the v2 auth invoker codemod to generate the new `invoker` option. + + +- [#1484](https://github.com/tailor-platform/sdk/pull/1484) [`a376dc8`](https://github.com/tailor-platform/sdk/commit/a376dc8cd053d20744c90104e8b44ed2729ffe8c) Thanks [@dqn](https://github.com/dqn)! - Remove the deprecated `openDownloadStream` file streaming API. Use `downloadStream` for streamed file downloads. + + The generated file utilities now emit `downloadFileStream`, which calls `downloadStream` and returns `FileDownloadStreamResponse`, instead of the removed `openFileDownloadStream` helper. + ## 2.0.0-next.1 ## 2.0.0-next.0 diff --git a/packages/create-sdk/package.json b/packages/create-sdk/package.json index d6f1ba2b1..1afd3e216 100644 --- a/packages/create-sdk/package.json +++ b/packages/create-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/create-sdk", - "version": "2.0.0-next.1", + "version": "2.0.0-next.2", "description": "A CLI tool to quickly create a new Tailor Platform SDK project", "license": "MIT", "repository": { diff --git a/packages/sdk-codemod/CHANGELOG.md b/packages/sdk-codemod/CHANGELOG.md index 37127d7b8..1a3fa0447 100644 --- a/packages/sdk-codemod/CHANGELOG.md +++ b/packages/sdk-codemod/CHANGELOG.md @@ -1,5 +1,70 @@ # @tailor-platform/sdk-codemod +## 0.3.0-next.2 +### Minor Changes + + + +- [#1473](https://github.com/tailor-platform/sdk/pull/1473) [`7ddf3c7`](https://github.com/tailor-platform/sdk/commit/7ddf3c716adf85a66a75d554da7730b5406f84b1) Thanks [@dqn](https://github.com/dqn)! - Add the `v2/principal-unify` codemod so `tailor-sdk upgrade` can migrate SDK principal APIs to `TailorPrincipal`. + + +### Patch Changes + + + +- [#1515](https://github.com/tailor-platform/sdk/pull/1515) [`dcf66a1`](https://github.com/tailor-platform/sdk/commit/dcf66a1e648f5287eaea9ea330eb4ad726a4d363) Thanks [@dqn](https://github.com/dqn)! - Rewrite `tailor-sdk apply` to `tailor-sdk deploy` in source files that contain embedded CLI command strings. + + + +- [#1482](https://github.com/tailor-platform/sdk/pull/1482) [`8b5870e`](https://github.com/tailor-platform/sdk/commit/8b5870e85db1efec7647acb98226f8161e3d1583) Thanks [@toiroakr](https://github.com/toiroakr)! - Add LLM-assisted review support to the codemod runner. A codemod can declare `suspiciousPatterns` plus a `prompt`; after running, files whose post-transform content still matches a suspicious pattern are reported as `llmReviews` (in the JSON output and on stderr) together with the codemod's migration prompt. This surfaces the cases a deterministic transform cannot safely complete (e.g. a value reached through a variable) so they can be finished with an LLM. The `auth.invoker(...)` codemod adopts this for its non-literal-argument cases. + + + +- [#1495](https://github.com/tailor-platform/sdk/pull/1495) [`6234022`](https://github.com/tailor-platform/sdk/commit/6234022d7dc03813b8dade831b86f63a5f7a20e6) Thanks [@toiroakr](https://github.com/toiroakr)! - Generate the v2 migration guide (`packages/sdk/docs/migration/v2.md`) from the codemod registry, which is the single source of truth. Each entry renders its name, automation level (Automatic / Partially automatic / Manual), description, optional before/after `examples`, and — for changes the codemods cannot fully migrate on their own — the LLM/manual migration prompt. Run `pnpm codemod:docs:update` to regenerate and `pnpm codemod:docs:check` (wired into `pnpm check`) to verify it is in sync. + + `scriptPath` is now optional, so the registry can also describe codemod-less ("manual") migrations that ship only guidance (`examples` / `prompt` / `suspiciousPatterns`) with no automatic transform. A manual entry with a `prompt` but no scoping pattern is surfaced as a project-wide `llmReviews` entry at runtime. + + Add a `sdk-codemod list` command that prints every registered rule (id, name, kind, version range). + + +- [#1517](https://github.com/tailor-platform/sdk/pull/1517) [`a649764`](https://github.com/tailor-platform/sdk/commit/a6497649be2786b3f6e410c8aa98c4247a599258) Thanks [@dqn](https://github.com/dqn)! - Reduce false-positive v2 codemod warnings and LLM-review prompts from source comments, string literals, and identifier substring matches. + + + +- [#1476](https://github.com/tailor-platform/sdk/pull/1476) [`fa83075`](https://github.com/tailor-platform/sdk/commit/fa83075f5e0e91085c0ef0cb44b7058a28a79ec3) Thanks [@toiroakr](https://github.com/toiroakr)! - `executeScript` now takes its `arg` as a JSON-serializable value instead of a pre-serialized JSON string. Pass the value directly (e.g. `arg: { a: 1 }`) instead of `arg: JSON.stringify({ a: 1 })`. + + Add the `v2/execute-script-arg` codemod, which unwraps `JSON.stringify(...)` passed as the `executeScript` `arg` option. Indirect forms (a stringified value held in a variable, etc.) cannot be rewritten automatically and are surfaced as an LLM-assisted review task with a migration prompt. + + +- [#1518](https://github.com/tailor-platform/sdk/pull/1518) [`ab10b1f`](https://github.com/tailor-platform/sdk/commit/ab10b1fea309ec5496e09bdca394d46d58603f5f) Thanks [@dqn](https://github.com/dqn)! - Reduce noisy `executeScript` LLM-review prompts by flagging files only when unresolved `arg` stringification remains likely. + + + +- [#1509](https://github.com/tailor-platform/sdk/pull/1509) [`7cadaa7`](https://github.com/tailor-platform/sdk/commit/7cadaa7c4987b81130ca80ba80bc5d5b26276394) Thanks [@dqn](https://github.com/dqn)! - Rename resolver, executor, workflow trigger, and typed workflow start machine-user options from `authInvoker` to `invoker`. + + Update create-sdk templates and the v2 auth invoker codemod to generate the new `invoker` option. + + +- [#1519](https://github.com/tailor-platform/sdk/pull/1519) [`4e3fa47`](https://github.com/tailor-platform/sdk/commit/4e3fa47d24e6bb1145eac13c355e976f2d594851) Thanks [@dqn](https://github.com/dqn)! - Limit the `openDownloadStream` migration review prompt to files that reference deprecated download stream APIs. + + + +- [#1521](https://github.com/tailor-platform/sdk/pull/1521) [`2d0689e`](https://github.com/tailor-platform/sdk/commit/2d0689e8ac0079473294fab367799a5431c130f4) Thanks [@dqn](https://github.com/dqn)! - Flag files that need project-specific review after the v2 principal migration, including resolver helper adapters and nullable `caller` follow-ups. + + + +- [#1525](https://github.com/tailor-platform/sdk/pull/1525) [`425a19d`](https://github.com/tailor-platform/sdk/commit/425a19dd58da6e373b739d3b3e838c2ff3d1736a) Thanks [@renovate](https://github.com/apps/renovate)! - fix(deps): update dependency semver to v7.8.5 + + + +- [#1520](https://github.com/tailor-platform/sdk/pull/1520) [`ed3d338`](https://github.com/tailor-platform/sdk/commit/ed3d338ce71d68904ef1fb83afbbd06a7e5f6973) Thanks [@dqn](https://github.com/dqn)! - Flag files that still reference ambient Tailor runtime globals so the v2 migration can opt them into `@tailor-platform/sdk/runtime/globals`. + + + +- [#1439](https://github.com/tailor-platform/sdk/pull/1439) [`c5b10d2`](https://github.com/tailor-platform/sdk/commit/c5b10d2841ded08927285bce538c05220cde5e4c) Thanks [@dqn](https://github.com/dqn)! - Unify function principal context around `TailorPrincipal`. + + Resolver contexts now use `caller` and `invoker` as `TailorPrincipal | null`, workflow and executor invokers also use `TailorPrincipal | null`, and event executor `actor` uses `TailorPrincipal | null` with `id`/`type` fields. The legacy `TailorUser`, `TailorInvoker`, `TailorActor`, `TailorActorType`, and `unauthenticatedTailorUser` exports are removed. + ## 0.3.0-next.1 ### Patch Changes diff --git a/packages/sdk-codemod/package.json b/packages/sdk-codemod/package.json index eb8f26c03..3d112ad5a 100644 --- a/packages/sdk-codemod/package.json +++ b/packages/sdk-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk-codemod", - "version": "0.3.0-next.1", + "version": "0.3.0-next.2", "description": "Codemod runner for Tailor Platform SDK upgrades", "license": "MIT", "repository": { diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index a2a0a7e03..3ba593884 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,5 +1,61 @@ # @tailor-platform/sdk +## 2.0.0-next.2 +### Major Changes + + + +- [#1476](https://github.com/tailor-platform/sdk/pull/1476) [`fa83075`](https://github.com/tailor-platform/sdk/commit/fa83075f5e0e91085c0ef0cb44b7058a28a79ec3) Thanks [@toiroakr](https://github.com/toiroakr)! - `executeScript` now takes its `arg` as a JSON-serializable value instead of a pre-serialized JSON string. Pass the value directly (e.g. `arg: { a: 1 }`) instead of `arg: JSON.stringify({ a: 1 })`. + + Add the `v2/execute-script-arg` codemod, which unwraps `JSON.stringify(...)` passed as the `executeScript` `arg` option. Indirect forms (a stringified value held in a variable, etc.) cannot be rewritten automatically and are surfaced as an LLM-assisted review task with a migration prompt. + + +- [#1509](https://github.com/tailor-platform/sdk/pull/1509) [`7cadaa7`](https://github.com/tailor-platform/sdk/commit/7cadaa7c4987b81130ca80ba80bc5d5b26276394) Thanks [@dqn](https://github.com/dqn)! - Rename resolver, executor, workflow trigger, and typed workflow start machine-user options from `authInvoker` to `invoker`. + + Update create-sdk templates and the v2 auth invoker codemod to generate the new `invoker` option. + + +- [#1492](https://github.com/tailor-platform/sdk/pull/1492) [`41774d1`](https://github.com/tailor-platform/sdk/commit/41774d175d6e42ecce9fd0be458e1fea199fe78a) Thanks [@dqn](https://github.com/dqn)! - Store CLI login tokens in the OS keyring by default when available. If the keyring is unavailable, tokens are stored in the platform config file. + + + +- [#1484](https://github.com/tailor-platform/sdk/pull/1484) [`a376dc8`](https://github.com/tailor-platform/sdk/commit/a376dc8cd053d20744c90104e8b44ed2729ffe8c) Thanks [@dqn](https://github.com/dqn)! - Remove the deprecated `openDownloadStream` file streaming API. Use `downloadStream` for streamed file downloads. + + The generated file utilities now emit `downloadFileStream`, which calls `downloadStream` and returns `FileDownloadStreamResponse`, instead of the removed `openFileDownloadStream` helper. + + +- [#1510](https://github.com/tailor-platform/sdk/pull/1510) [`41809c7`](https://github.com/tailor-platform/sdk/commit/41809c75ca0f52d0872e55be095e0b73d026c622) Thanks [@dqn](https://github.com/dqn)! - Remove the deprecated workflow test env fallback. `WORKFLOW_TEST_ENV_KEY` is no longer exported, and `TAILOR_TEST_WORKFLOW_ENV` is no longer read when running workflows locally. Use `mockWorkflow().setEnv(...)` or pass `{ env }` to `runWorkflowLocally(...)` instead. + + + +- [#1439](https://github.com/tailor-platform/sdk/pull/1439) [`c5b10d2`](https://github.com/tailor-platform/sdk/commit/c5b10d2841ded08927285bce538c05220cde5e4c) Thanks [@dqn](https://github.com/dqn)! - Unify function principal context around `TailorPrincipal`. + + Resolver contexts now use `caller` and `invoker` as `TailorPrincipal | null`, workflow and executor invokers also use `TailorPrincipal | null`, and event executor `actor` uses `TailorPrincipal | null` with `id`/`type` fields. The legacy `TailorUser`, `TailorInvoker`, `TailorActor`, `TailorActorType`, and `unauthenticatedTailorUser` exports are removed. + + +- [#1498](https://github.com/tailor-platform/sdk/pull/1498) [`83145db`](https://github.com/tailor-platform/sdk/commit/83145db9a0d243aa68c1b641c2b6026771a62188) Thanks [@dqn](https://github.com/dqn)! - Set `db.fields.timestamps()` `updatedAt` when records are created and make the generated field non-null. `createdAt` keeps its existing create-time behavior, while `updatedAt` keeps its update-time behavior and now also gets a create hook that preserves provided values and falls back to the current time. + + Update create-sdk templates so scaffolded projects use the new non-null `updatedAt` Kysely types and seed schemas. + + Existing TailorDB schemas that already use this helper will change `updatedAt` from optional to required. Backfill existing records that have `updatedAt: null` before applying the schema change. + +### Patch Changes + + + +- [#1495](https://github.com/tailor-platform/sdk/pull/1495) [`6234022`](https://github.com/tailor-platform/sdk/commit/6234022d7dc03813b8dade831b86f63a5f7a20e6) Thanks [@toiroakr](https://github.com/toiroakr)! - Generate the v2 migration guide (`packages/sdk/docs/migration/v2.md`) from the codemod registry, which is the single source of truth. Each entry renders its name, automation level (Automatic / Partially automatic / Manual), description, optional before/after `examples`, and — for changes the codemods cannot fully migrate on their own — the LLM/manual migration prompt. Run `pnpm codemod:docs:update` to regenerate and `pnpm codemod:docs:check` (wired into `pnpm check`) to verify it is in sync. + + `scriptPath` is now optional, so the registry can also describe codemod-less ("manual") migrations that ship only guidance (`examples` / `prompt` / `suspiciousPatterns`) with no automatic transform. A manual entry with a `prompt` but no scoping pattern is surfaced as a project-wide `llmReviews` entry at runtime. + + Add a `sdk-codemod list` command that prints every registered rule (id, name, kind, version range). + + +- [#1516](https://github.com/tailor-platform/sdk/pull/1516) [`ec752bd`](https://github.com/tailor-platform/sdk/commit/ec752bd38f7dec817f7e3b4d2d5468e7320050e0) Thanks [@renovate](https://github.com/apps/renovate)! - fix(deps): update dependency undici to v8.5.0 [security] + + + +- [#1525](https://github.com/tailor-platform/sdk/pull/1525) [`425a19d`](https://github.com/tailor-platform/sdk/commit/425a19dd58da6e373b739d3b3e838c2ff3d1736a) Thanks [@renovate](https://github.com/apps/renovate)! - fix(deps): update dependency semver to v7.8.5 + ## 2.0.0-next.1 ### Major Changes diff --git a/packages/sdk/package.json b/packages/sdk/package.json index a19c59cc1..588d6c789 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk", - "version": "2.0.0-next.1", + "version": "2.0.0-next.2", "description": "Tailor Platform SDK - The SDK to work with Tailor Platform", "license": "MIT", "repository": { From 84e993a83d59a13f73fc3d57b7fdd847605ba45b Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 23 Jun 2026 12:16:16 +0900 Subject: [PATCH 183/618] fix(sdk): limit executor helper stripping --- packages/sdk/src/cli/commands/function/detect.test.ts | 8 +++++++- packages/sdk/src/cli/services/executor/loader.ts | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/cli/commands/function/detect.test.ts b/packages/sdk/src/cli/commands/function/detect.test.ts index 9281b346a..0d71cd7a3 100644 --- a/packages/sdk/src/cli/commands/function/detect.test.ts +++ b/packages/sdk/src/cli/commands/function/detect.test.ts @@ -69,7 +69,7 @@ export default { fs.writeFileSync( filePath, ` -export default { +const executor = { name: "my-executor", trigger: { kind: "schedule", cron: "0 12 * * *", __args: [{ cron: "0 12 * * *" }] }, operation: { @@ -77,6 +77,12 @@ export default { body: (args) => {}, }, }; + +Object.defineProperty(executor, Symbol.for("tailor-platform/sdk"), { + value: "executor", +}); + +export default executor; `, ); diff --git a/packages/sdk/src/cli/services/executor/loader.ts b/packages/sdk/src/cli/services/executor/loader.ts index 53f71c088..a0d3c9259 100644 --- a/packages/sdk/src/cli/services/executor/loader.ts +++ b/packages/sdk/src/cli/services/executor/loader.ts @@ -1,9 +1,10 @@ import { pathToFileURL } from "node:url"; import { ExecutorSchema } from "@/parser/service/executor"; +import { isSdkBranded } from "@/utils/brand"; import type { Executor } from "@/types/executor.generated"; export function stripExecutorTriggerArgs(executor: unknown): unknown { - if (executor === null || typeof executor !== "object") { + if (!isSdkBranded(executor, "executor") || executor === null || typeof executor !== "object") { return executor; } From 9ecb380acdc1b37578c23c628ab46958663b4001 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 23 Jun 2026 12:18:36 +0900 Subject: [PATCH 184/618] docs(sdk): add parser strict changeset --- .changeset/parser-schema-strict.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/parser-schema-strict.md diff --git a/.changeset/parser-schema-strict.md b/.changeset/parser-schema-strict.md new file mode 100644 index 000000000..f06690fcd --- /dev/null +++ b/.changeset/parser-schema-strict.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Reject unknown keys in SDK parser schemas instead of silently dropping them from application definitions. From 3a690999feb35f816c384f89be1e763d59f1d25f Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 23 Jun 2026 15:11:14 +0900 Subject: [PATCH 185/618] fix(sdk): strip workflow runtime trigger before parsing Claude-Session: https://claude.ai/code/session_01B94teUhmAVNzM9TTvX9Pf9 --- packages/sdk/src/cli/services/workflow/service.ts | 10 +++++++++- packages/sdk/src/parser/app-config/schema.test.ts | 15 +++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/cli/services/workflow/service.ts b/packages/sdk/src/cli/services/workflow/service.ts index 542d864d4..8e1b963a0 100644 --- a/packages/sdk/src/cli/services/workflow/service.ts +++ b/packages/sdk/src/cli/services/workflow/service.ts @@ -13,6 +13,14 @@ export interface CollectedJob { sourceFile: string; } +function stripRuntimeTrigger(workflow: unknown): unknown { + if (workflow === null || typeof workflow !== "object" || !("trigger" in workflow)) { + return workflow; + } + const { trigger: _trigger, ...rest } = workflow as Record; + return rest; +} + interface WorkflowLoadResult { workflows: Record; workflowSources: Array<{ workflow: Workflow; sourceFile: string }>; @@ -177,7 +185,7 @@ async function loadFileContent(filePath: string): Promise<{ for (const [exportName, exportValue] of Object.entries(module)) { // Check if it's a workflow (default export) if (exportName === "default") { - const workflowResult = WorkflowSchema.safeParse(exportValue); + const workflowResult = WorkflowSchema.safeParse(stripRuntimeTrigger(exportValue)); if (workflowResult.success) { workflow = workflowResult.data; } else if (isSdkBranded(exportValue, ["workflow", "workflow-job"])) { diff --git a/packages/sdk/src/parser/app-config/schema.test.ts b/packages/sdk/src/parser/app-config/schema.test.ts index 807261588..d11b94e41 100644 --- a/packages/sdk/src/parser/app-config/schema.test.ts +++ b/packages/sdk/src/parser/app-config/schema.test.ts @@ -47,12 +47,23 @@ describe("AppConfigSchema", () => { expect(result.success).toBe(true); }); - test("ignores unknown top-level fields without erroring", () => { + test("rejects unknown top-level fields", () => { const result = AppConfigSchema.safeParse({ name: "my-app", futureField: "ok", }); - expect(result.success).toBe(true); + expect(result.success).toBe(false); + if (result.success) { + throw new Error("Expected AppConfigSchema parsing to fail"); + } + expect(result.error.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "unrecognized_keys", + keys: ["futureField"], + }), + ]), + ); }); test("rejects when env value type is unsupported", () => { From 84d9aba843f14e8a7a43f0baff92dfc8afdf2821 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 23 Jun 2026 17:04:39 +0900 Subject: [PATCH 186/618] chore(sdk): replace tsx with amaro for TypeScript loading --- .changeset/remove-tsx.md | 13 +++++ packages/sdk/package.json | 8 +-- packages/sdk/postinstall.mjs | 2 +- packages/sdk/scripts/check-import-cycles.ts | 2 +- .../sdk/scripts/check-public-api-jsdoc.ts | 2 +- packages/sdk/scripts/check-zod-isolation.ts | 2 +- packages/sdk/src/cli/index.ts | 8 +-- packages/sdk/src/cli/lib.ts | 8 +-- packages/sdk/src/cli/ts-hook.mjs | 53 +++++++++++++++++++ packages/sdk/tsdown.config.ts | 1 + pnpm-lock.yaml | 12 +++-- 11 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 .changeset/remove-tsx.md create mode 100644 packages/sdk/src/cli/ts-hook.mjs diff --git a/.changeset/remove-tsx.md b/.changeset/remove-tsx.md new file mode 100644 index 000000000..32b522951 --- /dev/null +++ b/.changeset/remove-tsx.md @@ -0,0 +1,13 @@ +--- +"@tailor-platform/sdk": patch +--- + +chore: replace tsx with amaro for TypeScript loading + +Removes `tsx` (which pulled in esbuild's native binaries, ~10.5 MB) from +`dependencies` and replaces it with `amaro` (~3.8 MB, zero transitive deps). + +A small `ts-hook.mjs` provides the Node.js module hook with both a resolver +(`.ts` extension fallback) and a load hook (`amaro/transform` for full +TypeScript support including enums). Dev-only scripts now use +`node --experimental-strip-types` instead. diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 03d3e007b..147f865a5 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -145,9 +145,9 @@ "docs:update": "POLITTY_DOCS_UPDATE=true vitest run --project=unit* src/cli/docs.test.ts", "build": "tsdown && politty generate-worker --bin dist/cli/index.mjs --program tailor-sdk --shell zsh --verify", "lint": "oxlint --type-aware .", - "check:public-api-jsdoc": "tsx scripts/check-public-api-jsdoc.ts", - "check:zod-isolation": "tsx scripts/check-zod-isolation.ts", - "check:import-cycles": "tsx scripts/check-import-cycles.ts", + "check:public-api-jsdoc": "node --experimental-strip-types scripts/check-public-api-jsdoc.ts", + "check:zod-isolation": "node --experimental-strip-types scripts/check-zod-isolation.ts", + "check:import-cycles": "node --experimental-strip-types scripts/check-import-cycles.ts", "lint:fix": "oxlint --type-aware . --fix", "typecheck": "tsc --noEmit", "typecheck:go": "tsgo", @@ -182,6 +182,7 @@ "@toiroakr/lines-db": "0.9.2", "@toiroakr/read-multiline": "0.4.1", "@urql/core": "6.0.2", + "amaro": "1.1.10", "chalk": "5.6.2", "chokidar": "5.0.0", "confbox": "0.2.4", @@ -207,7 +208,6 @@ "std-env": "4.1.0", "table": "6.9.0", "ts-cron-validator": "1.1.5", - "tsx": "4.22.4", "type-fest": "5.7.0", "undici": "8.5.0", "xdg-basedir": "5.1.0", diff --git a/packages/sdk/postinstall.mjs b/packages/sdk/postinstall.mjs index 7ac8733d6..0f580dcb2 100644 --- a/packages/sdk/postinstall.mjs +++ b/packages/sdk/postinstall.mjs @@ -32,9 +32,9 @@ async function install() { } try { + register(new URL("./dist/cli/ts-hook.mjs", import.meta.url), import.meta.url); const configDir = dirname(configPath); process.chdir(configDir); - register("tsx", import.meta.url, { data: {} }); const { generateUserTypes, loadConfig } = await import( pathToFileURL(resolve(__dirname, "dist", "cli", "lib.mjs")).href diff --git a/packages/sdk/scripts/check-import-cycles.ts b/packages/sdk/scripts/check-import-cycles.ts index 24f9c572f..8526efb17 100644 --- a/packages/sdk/scripts/check-import-cycles.ts +++ b/packages/sdk/scripts/check-import-cycles.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env -S pnpm exec tsx +#!/usr/bin/env -S node --experimental-strip-types // Verify the src/ module graph is acyclic — including type-only edges. // // oxlint's import/no-cycle (ignoreTypes: false) already rejects cycles formed diff --git a/packages/sdk/scripts/check-public-api-jsdoc.ts b/packages/sdk/scripts/check-public-api-jsdoc.ts index 6871035d1..4c35999d8 100644 --- a/packages/sdk/scripts/check-public-api-jsdoc.ts +++ b/packages/sdk/scripts/check-public-api-jsdoc.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env -S pnpm exec tsx +#!/usr/bin/env -S node --experimental-strip-types // Verify every public API export has JSDoc. // // "Public API" is derived from package.json#exports — each `types` entry diff --git a/packages/sdk/scripts/check-zod-isolation.ts b/packages/sdk/scripts/check-zod-isolation.ts index e3c3b2b45..78c026e0a 100644 --- a/packages/sdk/scripts/check-zod-isolation.ts +++ b/packages/sdk/scripts/check-zod-isolation.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env -S pnpm exec tsx +#!/usr/bin/env -S node --experimental-strip-types // Verify zod stays isolated to the CLI entry point. // // zinfer exists so that user-facing entry points never depend on zod: diff --git a/packages/sdk/src/cli/index.ts b/packages/sdk/src/cli/index.ts index 17282b302..31db9423a 100644 --- a/packages/sdk/src/cli/index.ts +++ b/packages/sdk/src/cli/index.ts @@ -37,13 +37,9 @@ import { logger } from "./shared/logger"; import { readPackageJson } from "./shared/package-json"; import { isNativeTypeScriptRuntime } from "./shared/runtime"; -// Register tsx for TypeScript loading on Node.js. -// Bun and Deno handle TypeScript natively, so registration is skipped. -// tsx's own register() picks `module.registerHooks` on Node ≥ 24.11.1 / 25.1 / 26 -// (avoiding the DEP0205 deprecation) and falls back to `module.register` on older runtimes. if (!isNativeTypeScriptRuntime()) { - const { register } = await import("tsx/esm/api"); - register(); + const { register } = await import("node:module"); + register(new URL("./ts-hook.mjs", import.meta.url), import.meta.url); } // Runs before globalArgs effects load --env-file, so env file overrides for diff --git a/packages/sdk/src/cli/lib.ts b/packages/sdk/src/cli/lib.ts index 76de91b52..0af3e9ca9 100644 --- a/packages/sdk/src/cli/lib.ts +++ b/packages/sdk/src/cli/lib.ts @@ -1,13 +1,9 @@ // CLI API exports for programmatic usage import { isNativeTypeScriptRuntime } from "./shared/runtime"; -// Register tsx to handle TypeScript files when using CLI API programmatically. -// Bun and Deno handle TypeScript natively, so registration is skipped. -// tsx's own register() picks `module.registerHooks` on Node ≥ 24.11.1 / 25.1 / 26 -// (avoiding the DEP0205 deprecation) and falls back to `module.register` on older runtimes. if (!isNativeTypeScriptRuntime()) { - const { register } = await import("tsx/esm/api"); - register(); + const { register } = await import("node:module"); + register(new URL("./ts-hook.mjs", import.meta.url), import.meta.url); } export { deploy, deploy as apply } from "./commands/deploy/deploy"; diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs new file mode 100644 index 000000000..021a3019b --- /dev/null +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -0,0 +1,53 @@ +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +// Node.js module hook: TypeScript resolver + type stripper via amaro. +// Registered programmatically so the CLI can import user .ts files without +// requiring --experimental-strip-types at process startup. +import { transformSync } from "amaro"; + +const TS_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"]; +const JS_TO_TS = new Map([ + [".js", ".ts"], + [".jsx", ".tsx"], + [".mjs", ".mts"], + [".cjs", ".cts"], +]); + +export async function resolve(specifier, context, nextResolve) { + try { + return await nextResolve(specifier, context); + } catch (err) { + if (err.code !== "ERR_MODULE_NOT_FOUND") throw err; + if (!specifier.startsWith(".") && !specifier.startsWith("/")) throw err; + + for (const ext of TS_EXTENSIONS) { + try { + return await nextResolve(specifier + ext, context); + } catch { + /* try next */ + } + } + + for (const [jsExt, tsExt] of JS_TO_TS) { + if (specifier.endsWith(jsExt)) { + try { + return await nextResolve(specifier.slice(0, -jsExt.length) + tsExt, context); + } catch { + /* try next */ + } + } + } + + throw err; + } +} + +export async function load(url, context, nextLoad) { + if (TS_EXTENSIONS.some((ext) => url.endsWith(ext))) { + const filePath = fileURLToPath(url); + const source = await readFile(filePath, "utf-8"); + const { code } = transformSync(source, { mode: "transform", filename: filePath }); + return { format: "module", shortCircuit: true, source: `${code}\n//# sourceURL=${url}` }; + } + return nextLoad(url, context); +} diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index c4bfc9e1c..4e20a51df 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -83,5 +83,6 @@ export default defineConfig({ plugins, onSuccess: (config) => { copyErdViewerAssets(config.outDir); + cpSync("src/cli/ts-hook.mjs", path.join(config.outDir, "cli/ts-hook.mjs")); }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75e67fc95..7490ea16a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -434,6 +434,9 @@ importers: '@urql/core': specifier: 6.0.2 version: 6.0.2(graphql@16.14.2) + amaro: + specifier: 1.1.10 + version: 1.1.10 chalk: specifier: 5.6.2 version: 5.6.2 @@ -509,9 +512,6 @@ importers: ts-cron-validator: specifier: 1.1.5 version: 1.1.5 - tsx: - specifier: 4.22.4 - version: 4.22.4 type-fest: specifier: 5.7.0 version: 5.7.0 @@ -2419,6 +2419,10 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + amaro@1.1.10: + resolution: {integrity: sha512-ceFv+QA3SlhFsn0hu8Q8oyj36YZdIgoJFpyS2sGJGK2dyncwcMWuBlNzhXfc1oLWtbDWM2Ol2rrzOYa+HNyEjg==} + engines: {node: '>=22'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -5203,6 +5207,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + amaro@1.1.10: {} + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} From 9c17f649eee8acda6754e5caff0c04a92052c65b Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 24 Jun 2026 09:09:07 +0900 Subject: [PATCH 187/618] fix(sdk): drop .cts/.cjs from ESM hook and tighten inner catch to ERR_MODULE_NOT_FOUND --- packages/sdk/src/cli/ts-hook.mjs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 021a3019b..3630dac43 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -5,12 +5,11 @@ import { fileURLToPath } from "node:url"; // requiring --experimental-strip-types at process startup. import { transformSync } from "amaro"; -const TS_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"]; +const TS_EXTENSIONS = [".ts", ".tsx", ".mts"]; const JS_TO_TS = new Map([ [".js", ".ts"], [".jsx", ".tsx"], [".mjs", ".mts"], - [".cjs", ".cts"], ]); export async function resolve(specifier, context, nextResolve) { @@ -23,8 +22,8 @@ export async function resolve(specifier, context, nextResolve) { for (const ext of TS_EXTENSIONS) { try { return await nextResolve(specifier + ext, context); - } catch { - /* try next */ + } catch (e) { + if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; } } @@ -32,8 +31,8 @@ export async function resolve(specifier, context, nextResolve) { if (specifier.endsWith(jsExt)) { try { return await nextResolve(specifier.slice(0, -jsExt.length) + tsExt, context); - } catch { - /* try next */ + } catch (e) { + if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; } } } From 6a69a06a99ac05379f519b24a2f019fa3e5324cf Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 24 Jun 2026 11:02:41 +0900 Subject: [PATCH 188/618] chore(sdk): enable SWC compress in ts-hook.mjs transform --- packages/sdk/src/cli/ts-hook.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 3630dac43..7af765981 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -45,7 +45,12 @@ export async function load(url, context, nextLoad) { if (TS_EXTENSIONS.some((ext) => url.endsWith(ext))) { const filePath = fileURLToPath(url); const source = await readFile(filePath, "utf-8"); - const { code } = transformSync(source, { mode: "transform", filename: filePath }); + const { code } = transformSync(source, { + mode: "transform", + filename: filePath, + minify: true, + jsc: { minify: { compress: true, mangle: false } }, + }); return { format: "module", shortCircuit: true, source: `${code}\n//# sourceURL=${url}` }; } return nextLoad(url, context); From 1152667324ae3243ff02e791503b8f9124aff04a Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 24 Jun 2026 11:39:12 +0900 Subject: [PATCH 189/618] fix(sdk): use registerHooks() when available to avoid DEP0205 Prefer module.registerHooks() (Node >= 22.15.0) over module.register() to avoid the DEP0205 deprecation warning introduced in Node >= 24.11.1. Falls back to register() for Node 22 versions prior to 22.15.0. registerHooks() only supports synchronous hooks, so export resolveSync and loadSync from ts-hook.mjs alongside the existing async hooks. --- packages/sdk/knip.json | 1 + packages/sdk/postinstall.mjs | 9 ++++-- packages/sdk/src/cli/index.ts | 13 +++++++-- packages/sdk/src/cli/lib.ts | 13 +++++++-- packages/sdk/src/cli/ts-hook.d.mts | 4 +++ packages/sdk/src/cli/ts-hook.mjs | 46 ++++++++++++++++++++++++++++++ 6 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 packages/sdk/src/cli/ts-hook.d.mts diff --git a/packages/sdk/knip.json b/packages/sdk/knip.json index 467ac9e74..ac7efcc49 100644 --- a/packages/sdk/knip.json +++ b/packages/sdk/knip.json @@ -8,6 +8,7 @@ "eslint-rules/__tests__/fixtures/**", "src/cli/commands/deploy/__test_fixtures__/**", "src/cli/commands/tailordb/erd/viewer-assets/**", + "src/cli/ts-hook.d.mts", "src/types/*.ts", "src/vitest/integration/vitest.config.ts", "zinfer.config.ts" diff --git a/packages/sdk/postinstall.mjs b/packages/sdk/postinstall.mjs index 0f580dcb2..14231b857 100644 --- a/packages/sdk/postinstall.mjs +++ b/packages/sdk/postinstall.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { existsSync } from "node:fs"; -import { register } from "node:module"; +import { register, registerHooks } from "node:module"; import { dirname, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { findUpSync } from "find-up-simple"; @@ -32,7 +32,12 @@ async function install() { } try { - register(new URL("./dist/cli/ts-hook.mjs", import.meta.url), import.meta.url); + if (registerHooks) { + const { resolveSync, loadSync } = await import("./dist/cli/ts-hook.mjs"); + registerHooks({ resolve: resolveSync, load: loadSync }); + } else { + register(new URL("./dist/cli/ts-hook.mjs", import.meta.url), import.meta.url); + } const configDir = dirname(configPath); process.chdir(configDir); diff --git a/packages/sdk/src/cli/index.ts b/packages/sdk/src/cli/index.ts index 31db9423a..012d213a9 100644 --- a/packages/sdk/src/cli/index.ts +++ b/packages/sdk/src/cli/index.ts @@ -38,8 +38,17 @@ import { readPackageJson } from "./shared/package-json"; import { isNativeTypeScriptRuntime } from "./shared/runtime"; if (!isNativeTypeScriptRuntime()) { - const { register } = await import("node:module"); - register(new URL("./ts-hook.mjs", import.meta.url), import.meta.url); + const mod = await import("node:module"); + // registerHooks is available since Node 22.15.0; fall back to register() on older versions. + const registerHooks = (mod as unknown as Record).registerHooks as + | ((opts: { resolve?: unknown; load?: unknown }) => void) + | undefined; + if (registerHooks) { + const { resolveSync, loadSync } = await import("./ts-hook.mjs"); + registerHooks({ resolve: resolveSync, load: loadSync }); + } else { + mod.register(new URL("./ts-hook.mjs", import.meta.url), import.meta.url); + } } // Runs before globalArgs effects load --env-file, so env file overrides for diff --git a/packages/sdk/src/cli/lib.ts b/packages/sdk/src/cli/lib.ts index 0af3e9ca9..bf2a9e377 100644 --- a/packages/sdk/src/cli/lib.ts +++ b/packages/sdk/src/cli/lib.ts @@ -2,8 +2,17 @@ import { isNativeTypeScriptRuntime } from "./shared/runtime"; if (!isNativeTypeScriptRuntime()) { - const { register } = await import("node:module"); - register(new URL("./ts-hook.mjs", import.meta.url), import.meta.url); + const mod = await import("node:module"); + // registerHooks is available since Node 22.15.0; fall back to register() on older versions. + const registerHooks = (mod as unknown as Record).registerHooks as + | ((opts: { resolve?: unknown; load?: unknown }) => void) + | undefined; + if (registerHooks) { + const { resolveSync, loadSync } = await import("./ts-hook.mjs"); + registerHooks({ resolve: resolveSync, load: loadSync }); + } else { + mod.register(new URL("./ts-hook.mjs", import.meta.url), import.meta.url); + } } export { deploy, deploy as apply } from "./commands/deploy/deploy"; diff --git a/packages/sdk/src/cli/ts-hook.d.mts b/packages/sdk/src/cli/ts-hook.d.mts new file mode 100644 index 000000000..d8daefaa1 --- /dev/null +++ b/packages/sdk/src/cli/ts-hook.d.mts @@ -0,0 +1,4 @@ +export declare function resolve(...args: unknown[]): Promise; +export declare function load(...args: unknown[]): Promise; +export declare function resolveSync(...args: unknown[]): unknown; +export declare function loadSync(...args: unknown[]): unknown; diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 7af765981..b64d3f8a6 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -1,3 +1,4 @@ +import { readFileSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; // Node.js module hook: TypeScript resolver + type stripper via amaro. @@ -55,3 +56,48 @@ export async function load(url, context, nextLoad) { } return nextLoad(url, context); } + +// Sync hooks for module.registerHooks() (Node >= 22.15.0). +export function resolveSync(specifier, context, nextResolve) { + try { + return nextResolve(specifier, context); + } catch (err) { + if (err.code !== "ERR_MODULE_NOT_FOUND") throw err; + if (!specifier.startsWith(".") && !specifier.startsWith("/")) throw err; + + for (const ext of TS_EXTENSIONS) { + try { + return nextResolve(specifier + ext, context); + } catch (e) { + if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + } + } + + for (const [jsExt, tsExt] of JS_TO_TS) { + if (specifier.endsWith(jsExt)) { + try { + return nextResolve(specifier.slice(0, -jsExt.length) + tsExt, context); + } catch (e) { + if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + } + } + } + + throw err; + } +} + +export function loadSync(url, context, nextLoad) { + if (TS_EXTENSIONS.some((ext) => url.endsWith(ext))) { + const filePath = fileURLToPath(url); + const source = readFileSync(filePath, "utf-8"); + const { code } = transformSync(source, { + mode: "transform", + filename: filePath, + minify: true, + jsc: { minify: { compress: true, mangle: false } }, + }); + return { format: "module", shortCircuit: true, source: `${code}\n//# sourceURL=${url}` }; + } + return nextLoad(url, context); +} From 59e95b4c5020e7f23aa141f680573438f7f51898 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 24 Jun 2026 11:50:09 +0900 Subject: [PATCH 190/618] fix(sdk): remove undocumented amaro minify options; use namespace import in postinstall --- packages/sdk/postinstall.mjs | 5 +++-- packages/sdk/src/cli/ts-hook.mjs | 14 ++------------ 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/packages/sdk/postinstall.mjs b/packages/sdk/postinstall.mjs index 14231b857..fd8d6b7fa 100644 --- a/packages/sdk/postinstall.mjs +++ b/packages/sdk/postinstall.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { existsSync } from "node:fs"; -import { register, registerHooks } from "node:module"; +import * as nodeModule from "node:module"; import { dirname, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { findUpSync } from "find-up-simple"; @@ -32,11 +32,12 @@ async function install() { } try { + const registerHooks = nodeModule.registerHooks; if (registerHooks) { const { resolveSync, loadSync } = await import("./dist/cli/ts-hook.mjs"); registerHooks({ resolve: resolveSync, load: loadSync }); } else { - register(new URL("./dist/cli/ts-hook.mjs", import.meta.url), import.meta.url); + nodeModule.register(new URL("./dist/cli/ts-hook.mjs", import.meta.url), import.meta.url); } const configDir = dirname(configPath); process.chdir(configDir); diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index b64d3f8a6..e22bfddb5 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -46,12 +46,7 @@ export async function load(url, context, nextLoad) { if (TS_EXTENSIONS.some((ext) => url.endsWith(ext))) { const filePath = fileURLToPath(url); const source = await readFile(filePath, "utf-8"); - const { code } = transformSync(source, { - mode: "transform", - filename: filePath, - minify: true, - jsc: { minify: { compress: true, mangle: false } }, - }); + const { code } = transformSync(source, { mode: "transform", filename: filePath }); return { format: "module", shortCircuit: true, source: `${code}\n//# sourceURL=${url}` }; } return nextLoad(url, context); @@ -91,12 +86,7 @@ export function loadSync(url, context, nextLoad) { if (TS_EXTENSIONS.some((ext) => url.endsWith(ext))) { const filePath = fileURLToPath(url); const source = readFileSync(filePath, "utf-8"); - const { code } = transformSync(source, { - mode: "transform", - filename: filePath, - minify: true, - jsc: { minify: { compress: true, mangle: false } }, - }); + const { code } = transformSync(source, { mode: "transform", filename: filePath }); return { format: "module", shortCircuit: true, source: `${code}\n//# sourceURL=${url}` }; } return nextLoad(url, context); From d79415b76b02ddbbbdfa6f406f2444f0b832d9b2 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 24 Jun 2026 13:34:29 +0900 Subject: [PATCH 191/618] fix(sdk): use JSDoc cast in postinstall.mjs to avoid no-unnecessary-condition lint error --- packages/sdk/postinstall.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sdk/postinstall.mjs b/packages/sdk/postinstall.mjs index fd8d6b7fa..9fdff77fa 100644 --- a/packages/sdk/postinstall.mjs +++ b/packages/sdk/postinstall.mjs @@ -32,12 +32,13 @@ async function install() { } try { - const registerHooks = nodeModule.registerHooks; + const mod = /** @type {{ register: Function, registerHooks?: Function }} */ (nodeModule); + const registerHooks = mod.registerHooks; if (registerHooks) { const { resolveSync, loadSync } = await import("./dist/cli/ts-hook.mjs"); registerHooks({ resolve: resolveSync, load: loadSync }); } else { - nodeModule.register(new URL("./dist/cli/ts-hook.mjs", import.meta.url), import.meta.url); + mod.register(new URL("./dist/cli/ts-hook.mjs", import.meta.url), import.meta.url); } const configDir = dirname(configPath); process.chdir(configDir); From caf0818de710f339f24f95b976bfd08a443c50dc Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 24 Jun 2026 13:52:53 +0900 Subject: [PATCH 192/618] chore: add example migration 0004 for amaro/SWC whitespace differences --- example/migrations/0004/diff.json | 194 +++++++++ .../app/migrations/.gitkeep | 0 .../app/migrations/0000/schema.json | 382 ++++++++++++++++++ .../migration-fixtures/app/tailordb/.gitkeep | 0 .../app/tailordb/salesOrder.ts | 12 + .../app/tailordb/supplier.ts | 14 + .../migration-fixtures/app/tailordb/user.ts | 11 + packages/sdk/docs/migration/v2.md | 20 +- 8 files changed, 624 insertions(+), 9 deletions(-) create mode 100644 example/migrations/0004/diff.json delete mode 100644 example/tests/migration-fixtures/app/migrations/.gitkeep create mode 100644 example/tests/migration-fixtures/app/migrations/0000/schema.json delete mode 100644 example/tests/migration-fixtures/app/tailordb/.gitkeep create mode 100644 example/tests/migration-fixtures/app/tailordb/salesOrder.ts create mode 100644 example/tests/migration-fixtures/app/tailordb/supplier.ts create mode 100644 example/tests/migration-fixtures/app/tailordb/user.ts diff --git a/example/migrations/0004/diff.json b/example/migrations/0004/diff.json new file mode 100644 index 000000000..a38bf0c38 --- /dev/null +++ b/example/migrations/0004/diff.json @@ -0,0 +1,194 @@ +{ + "version": 1, + "namespace": "tailordb", + "createdAt": "2026-06-24T04:44:47.663Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "name", + "before": { + "type": "string", + "required": true, + "validate": [ + { + "script": { + "expr": "(({value})=>value.length>5)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "Name must be longer than 5 characters" + } + ] + }, + "after": { + "type": "string", + "required": true, + "validate": [ + { + "script": { + "expr": "(({ value })=>value.length > 5)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "Name must be longer than 5 characters" + } + ] + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "city", + "before": { + "type": "string", + "required": false, + "validate": [ + { + "script": { + "expr": "(({value})=>value?value.length>1:true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({value})=>value?value.length>1:true`" + }, + { + "script": { + "expr": "(({value})=>value?value.length<100:true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({value})=>value?value.length<100:true`" + } + ] + }, + "after": { + "type": "string", + "required": false, + "validate": [ + { + "script": { + "expr": "(({ value })=>value ? value.length > 1 : true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({ value })=>value ? value.length > 1 : true`" + }, + { + "script": { + "expr": "(({ value })=>value ? value.length < 100 : true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({ value })=>value ? value.length < 100 : true`" + } + ] + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "fullAddress", + "before": { + "type": "string", + "required": true, + "hooks": { + "create": { + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "string", + "required": true, + "hooks": { + "create": { + "expr": "(({ data })=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(({ data })=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "attachedFiles", + "before": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "size": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({value})=>value>0)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({value})=>value>0`" + } + ] + }, + "type": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "text" + }, + { + "value": "image" + } + ] + } + } + }, + "after": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "size": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({ value })=>value > 0)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({ value })=>value > 0`" + } + ] + }, + "type": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "text" + }, + { + "value": "image" + } + ] + } + } + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": false +} diff --git a/example/tests/migration-fixtures/app/migrations/.gitkeep b/example/tests/migration-fixtures/app/migrations/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/example/tests/migration-fixtures/app/migrations/0000/schema.json b/example/tests/migration-fixtures/app/migrations/0000/schema.json new file mode 100644 index 000000000..7dbfe28c2 --- /dev/null +++ b/example/tests/migration-fixtures/app/migrations/0000/schema.json @@ -0,0 +1,382 @@ +{ + "version": 1, + "namespace": "migrationdb", + "createdAt": "2026-06-24T04:43:34.470Z", + "types": { + "SalesOrder": { + "name": "SalesOrder", + "pluralForm": "SalesOrders", + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "customerID": { + "type": "uuid", + "required": true + }, + "status": { + "type": "string", + "required": false + }, + "totalPrice": { + "type": "integer", + "required": false + }, + "createdAt": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "updatedAt": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + "settings": {}, + "permissions": { + "record": { + "create": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "read": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "update": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "delete": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ] + }, + "gql": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "actions": ["create", "read", "update", "delete", "aggregate", "bulkUpsert"], + "permit": "allow" + } + ] + } + }, + "Supplier": { + "name": "Supplier", + "pluralForm": "Suppliers", + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": false + }, + "country": { + "type": "string", + "required": false + }, + "phone": { + "type": "string", + "required": true + }, + "state": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "Alabama" + }, + { + "value": "Alaska" + } + ] + }, + "city": { + "type": "string", + "required": true + }, + "createdAt": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "updatedAt": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + "settings": {}, + "permissions": { + "record": { + "create": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "read": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "update": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "delete": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ] + }, + "gql": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "actions": ["create", "read", "update", "delete", "aggregate", "bulkUpsert"], + "permit": "allow" + } + ] + } + }, + "User": { + "name": "User", + "pluralForm": "Users", + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "email": { + "type": "string", + "required": true + }, + "createdAt": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "updatedAt": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + "settings": {}, + "permissions": { + "record": { + "create": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "read": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "update": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "delete": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ] + }, + "gql": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "actions": ["create", "read", "update", "delete", "aggregate", "bulkUpsert"], + "permit": "allow" + } + ] + } + } + } +} diff --git a/example/tests/migration-fixtures/app/tailordb/.gitkeep b/example/tests/migration-fixtures/app/tailordb/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/example/tests/migration-fixtures/app/tailordb/salesOrder.ts b/example/tests/migration-fixtures/app/tailordb/salesOrder.ts new file mode 100644 index 000000000..3f6891755 --- /dev/null +++ b/example/tests/migration-fixtures/app/tailordb/salesOrder.ts @@ -0,0 +1,12 @@ +import { db } from "@tailor-platform/sdk"; +import { defaultGqlPermission, defaultPermission } from "../permissions"; + +export const salesOrder = db + .type("SalesOrder", { + customerID: db.uuid(), + status: db.string({ optional: true }), + totalPrice: db.int({ optional: true }), + ...db.fields.timestamps(), + }) + .permission(defaultPermission) + .gqlPermission(defaultGqlPermission); diff --git a/example/tests/migration-fixtures/app/tailordb/supplier.ts b/example/tests/migration-fixtures/app/tailordb/supplier.ts new file mode 100644 index 000000000..ba1019ca9 --- /dev/null +++ b/example/tests/migration-fixtures/app/tailordb/supplier.ts @@ -0,0 +1,14 @@ +import { db } from "@tailor-platform/sdk"; +import { defaultGqlPermission, defaultPermission } from "../permissions"; + +export const supplier = db + .type("Supplier", { + name: db.string({ optional: true }), + country: db.string({ optional: true }), + phone: db.string(), + state: db.enum(["Alabama", "Alaska"]), + city: db.string(), + ...db.fields.timestamps(), + }) + .permission(defaultPermission) + .gqlPermission(defaultGqlPermission); diff --git a/example/tests/migration-fixtures/app/tailordb/user.ts b/example/tests/migration-fixtures/app/tailordb/user.ts new file mode 100644 index 000000000..d70b9ade4 --- /dev/null +++ b/example/tests/migration-fixtures/app/tailordb/user.ts @@ -0,0 +1,11 @@ +import { db } from "@tailor-platform/sdk"; +import { defaultGqlPermission, defaultPermission } from "../permissions"; + +export const user = db + .type("User", { + name: db.string(), + email: db.string(), + ...db.fields.timestamps(), + }) + .permission(defaultPermission) + .gqlPermission(defaultGqlPermission); diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 1b982e6bd..15c50f6fa 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -217,11 +217,11 @@ happen to use `--machineuser` alone.
-## auth.invoker("name") → "name" +## auth.invoker("name") → invoker: "name" **Migration:** Partially automatic -Replace `auth.invoker("name")` calls with the bare `"name"` string and drop the `auth` import when no other reference remains. The `auth.invoker()` helper is deprecated in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle. +Rename statically identified SDK `authInvoker` options to `invoker`, replace `auth.invoker("name")` there with the bare `"name"` string, and drop the `auth` import when no other reference remains. Ambiguous workflow `.trigger()` calls are left for manual review. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle. Before: @@ -241,19 +241,21 @@ createResolver({ invoker: "manager" }); ```text In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the machine user name passed directly as a string. The codemod already rewrote the -string-literal form auth.invoker("name") to "name". These files still contain -auth.invoker(...) because the argument is not a plain string literal (a variable, -template literal, function call, or property access). +statically identified SDK option form authInvoker: auth.invoker("name") to invoker: "name" and renamed supported authInvoker option keys. These files still contain +auth.invoker(...) calls or authInvoker keys that need manual review. For each remaining auth.invoker() call: -1. Replace the whole call with as-is (e.g. auth.invoker(name) becomes name). -2. Make sure evaluates to the machine user name (a string); adjust it if it - resolves to an object or an auth config value instead. +1. Replace the whole call with only where the target option expects a + machine user name string; platform/runtime authInvoker payloads still expect + the object form. +2. Rename remaining authInvoker option keys to invoker only for SDK resolver, + executor, workflow.trigger(), or startWorkflow() options. Keep platform/runtime + payload keys such as tailor.workflow.triggerWorkflow(..., { authInvoker: ... }). 3. After removing every auth.invoker usage in a file, delete the now-unused auth import (keeping it pulls Node-only config modules into runtime bundles); leave the import if auth is still referenced elsewhere. -Do not change behavior beyond removing the auth.invoker() indirection. +Do not change behavior beyond the SDK option rename and auth.invoker() removal. ```
From 84b7e6e987d9c1ba8f8cee5e1a9bc36fb609bbeb Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 24 Jun 2026 22:16:44 +0900 Subject: [PATCH 193/618] fix(sdk): fix ts-hook.mjs URL handling; extract registerTsHook helper; fix changeset text --- .changeset/remove-tsx.md | 4 +-- packages/sdk/src/cli/index.ts | 16 ++-------- packages/sdk/src/cli/lib.ts | 16 ++-------- .../sdk/src/cli/shared/register-ts-hook.ts | 19 ++++++++++++ packages/sdk/src/cli/ts-hook.mjs | 30 +++++++++++-------- packages/sdk/tsdown.config.ts | 2 +- 6 files changed, 44 insertions(+), 43 deletions(-) create mode 100644 packages/sdk/src/cli/shared/register-ts-hook.ts diff --git a/.changeset/remove-tsx.md b/.changeset/remove-tsx.md index 32b522951..9b55d6611 100644 --- a/.changeset/remove-tsx.md +++ b/.changeset/remove-tsx.md @@ -8,6 +8,6 @@ Removes `tsx` (which pulled in esbuild's native binaries, ~10.5 MB) from `dependencies` and replaces it with `amaro` (~3.8 MB, zero transitive deps). A small `ts-hook.mjs` provides the Node.js module hook with both a resolver -(`.ts` extension fallback) and a load hook (`amaro/transform` for full -TypeScript support including enums). Dev-only scripts now use +(`.ts` extension fallback) and a load hook (`amaro` for full TypeScript +support including enums). Dev-only scripts now use `node --experimental-strip-types` instead. diff --git a/packages/sdk/src/cli/index.ts b/packages/sdk/src/cli/index.ts index 012d213a9..6992f45b0 100644 --- a/packages/sdk/src/cli/index.ts +++ b/packages/sdk/src/cli/index.ts @@ -35,21 +35,9 @@ import { commonArgs, isVerbose } from "./shared/args"; import { isCLIError } from "./shared/errors"; import { logger } from "./shared/logger"; import { readPackageJson } from "./shared/package-json"; -import { isNativeTypeScriptRuntime } from "./shared/runtime"; +import { registerTsHook } from "./shared/register-ts-hook"; -if (!isNativeTypeScriptRuntime()) { - const mod = await import("node:module"); - // registerHooks is available since Node 22.15.0; fall back to register() on older versions. - const registerHooks = (mod as unknown as Record).registerHooks as - | ((opts: { resolve?: unknown; load?: unknown }) => void) - | undefined; - if (registerHooks) { - const { resolveSync, loadSync } = await import("./ts-hook.mjs"); - registerHooks({ resolve: resolveSync, load: loadSync }); - } else { - mod.register(new URL("./ts-hook.mjs", import.meta.url), import.meta.url); - } -} +await registerTsHook(new URL("./ts-hook.mjs", import.meta.url)); // Runs before globalArgs effects load --env-file, so env file overrides for // TAILOR_CRASH_REPORTS_* are not available for early startup failures. diff --git a/packages/sdk/src/cli/lib.ts b/packages/sdk/src/cli/lib.ts index bf2a9e377..23f828005 100644 --- a/packages/sdk/src/cli/lib.ts +++ b/packages/sdk/src/cli/lib.ts @@ -1,19 +1,7 @@ // CLI API exports for programmatic usage -import { isNativeTypeScriptRuntime } from "./shared/runtime"; +import { registerTsHook } from "./shared/register-ts-hook"; -if (!isNativeTypeScriptRuntime()) { - const mod = await import("node:module"); - // registerHooks is available since Node 22.15.0; fall back to register() on older versions. - const registerHooks = (mod as unknown as Record).registerHooks as - | ((opts: { resolve?: unknown; load?: unknown }) => void) - | undefined; - if (registerHooks) { - const { resolveSync, loadSync } = await import("./ts-hook.mjs"); - registerHooks({ resolve: resolveSync, load: loadSync }); - } else { - mod.register(new URL("./ts-hook.mjs", import.meta.url), import.meta.url); - } -} +await registerTsHook(new URL("./ts-hook.mjs", import.meta.url)); export { deploy, deploy as apply } from "./commands/deploy/deploy"; export type { DeployOptions, DeployOptions as ApplyOptions } from "./commands/deploy/deploy"; diff --git a/packages/sdk/src/cli/shared/register-ts-hook.ts b/packages/sdk/src/cli/shared/register-ts-hook.ts new file mode 100644 index 000000000..1d755764c --- /dev/null +++ b/packages/sdk/src/cli/shared/register-ts-hook.ts @@ -0,0 +1,19 @@ +import { isNativeTypeScriptRuntime } from "./runtime"; + +// registerHooks is available since Node 22.15.0; fall back to register() on older versions. +export async function registerTsHook(tsHookUrl: URL): Promise { + if (isNativeTypeScriptRuntime()) return; + const mod = await import("node:module"); + const registerHooks = (mod as unknown as Record).registerHooks as + | ((opts: { resolve?: unknown; load?: unknown }) => void) + | undefined; + if (registerHooks) { + const { resolveSync, loadSync } = (await import(tsHookUrl.href)) as { + resolveSync: (...args: unknown[]) => unknown; + loadSync: (...args: unknown[]) => unknown; + }; + registerHooks({ resolve: resolveSync, load: loadSync }); + } else { + mod.register(tsHookUrl, tsHookUrl.href); + } +} diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index e22bfddb5..d358fa3a7 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -20,11 +20,14 @@ export async function resolve(specifier, context, nextResolve) { if (err.code !== "ERR_MODULE_NOT_FOUND") throw err; if (!specifier.startsWith(".") && !specifier.startsWith("/")) throw err; - for (const ext of TS_EXTENSIONS) { - try { - return await nextResolve(specifier + ext, context); - } catch (e) { - if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + const lastSegment = specifier.split("/").pop() ?? ""; + if (!lastSegment.includes(".")) { + for (const ext of TS_EXTENSIONS) { + try { + return await nextResolve(specifier + ext, context); + } catch (e) { + if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + } } } @@ -43,7 +46,7 @@ export async function resolve(specifier, context, nextResolve) { } export async function load(url, context, nextLoad) { - if (TS_EXTENSIONS.some((ext) => url.endsWith(ext))) { + if (url.startsWith("file:") && TS_EXTENSIONS.some((ext) => new URL(url).pathname.endsWith(ext))) { const filePath = fileURLToPath(url); const source = await readFile(filePath, "utf-8"); const { code } = transformSync(source, { mode: "transform", filename: filePath }); @@ -60,11 +63,14 @@ export function resolveSync(specifier, context, nextResolve) { if (err.code !== "ERR_MODULE_NOT_FOUND") throw err; if (!specifier.startsWith(".") && !specifier.startsWith("/")) throw err; - for (const ext of TS_EXTENSIONS) { - try { - return nextResolve(specifier + ext, context); - } catch (e) { - if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + const lastSegment = specifier.split("/").pop() ?? ""; + if (!lastSegment.includes(".")) { + for (const ext of TS_EXTENSIONS) { + try { + return nextResolve(specifier + ext, context); + } catch (e) { + if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + } } } @@ -83,7 +89,7 @@ export function resolveSync(specifier, context, nextResolve) { } export function loadSync(url, context, nextLoad) { - if (TS_EXTENSIONS.some((ext) => url.endsWith(ext))) { + if (url.startsWith("file:") && TS_EXTENSIONS.some((ext) => new URL(url).pathname.endsWith(ext))) { const filePath = fileURLToPath(url); const source = readFileSync(filePath, "utf-8"); const { code } = transformSync(source, { mode: "transform", filename: filePath }); diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index 4e20a51df..f9b2b3b32 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -83,6 +83,6 @@ export default defineConfig({ plugins, onSuccess: (config) => { copyErdViewerAssets(config.outDir); - cpSync("src/cli/ts-hook.mjs", path.join(config.outDir, "cli/ts-hook.mjs")); + cpSync(path.resolve("src/cli/ts-hook.mjs"), path.join(config.outDir, "cli/ts-hook.mjs")); }, }); From fdf9b5084def00e0a8e9c0f75cf7e64d00067647 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 08:39:42 +0900 Subject: [PATCH 194/618] fix(sdk): strip search/hash from file URL before fileURLToPath in ts-hook --- packages/sdk/src/cli/ts-hook.mjs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index d358fa3a7..45fbf0791 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -46,11 +46,16 @@ export async function resolve(specifier, context, nextResolve) { } export async function load(url, context, nextLoad) { - if (url.startsWith("file:") && TS_EXTENSIONS.some((ext) => new URL(url).pathname.endsWith(ext))) { - const filePath = fileURLToPath(url); - const source = await readFile(filePath, "utf-8"); - const { code } = transformSync(source, { mode: "transform", filename: filePath }); - return { format: "module", shortCircuit: true, source: `${code}\n//# sourceURL=${url}` }; + if (url.startsWith("file:")) { + const parsedUrl = new URL(url); + if (TS_EXTENSIONS.some((ext) => parsedUrl.pathname.endsWith(ext))) { + parsedUrl.search = ""; + parsedUrl.hash = ""; + const filePath = fileURLToPath(parsedUrl); + const source = await readFile(filePath, "utf-8"); + const { code } = transformSync(source, { mode: "transform", filename: filePath }); + return { format: "module", shortCircuit: true, source: `${code}\n//# sourceURL=${url}` }; + } } return nextLoad(url, context); } @@ -89,11 +94,16 @@ export function resolveSync(specifier, context, nextResolve) { } export function loadSync(url, context, nextLoad) { - if (url.startsWith("file:") && TS_EXTENSIONS.some((ext) => new URL(url).pathname.endsWith(ext))) { - const filePath = fileURLToPath(url); - const source = readFileSync(filePath, "utf-8"); - const { code } = transformSync(source, { mode: "transform", filename: filePath }); - return { format: "module", shortCircuit: true, source: `${code}\n//# sourceURL=${url}` }; + if (url.startsWith("file:")) { + const parsedUrl = new URL(url); + if (TS_EXTENSIONS.some((ext) => parsedUrl.pathname.endsWith(ext))) { + parsedUrl.search = ""; + parsedUrl.hash = ""; + const filePath = fileURLToPath(parsedUrl); + const source = readFileSync(filePath, "utf-8"); + const { code } = transformSync(source, { mode: "transform", filename: filePath }); + return { format: "module", shortCircuit: true, source: `${code}\n//# sourceURL=${url}` }; + } } return nextLoad(url, context); } From 936b658bf89fbfdaab60217f194cf55d1aba77d9 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 09:05:31 +0900 Subject: [PATCH 195/618] chore(sdk): remove unused shebang from non-executable check scripts --- packages/sdk/scripts/check-import-cycles.ts | 1 - packages/sdk/scripts/check-public-api-jsdoc.ts | 1 - packages/sdk/scripts/check-zod-isolation.ts | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/sdk/scripts/check-import-cycles.ts b/packages/sdk/scripts/check-import-cycles.ts index 8526efb17..917d4b630 100644 --- a/packages/sdk/scripts/check-import-cycles.ts +++ b/packages/sdk/scripts/check-import-cycles.ts @@ -1,4 +1,3 @@ -#!/usr/bin/env -S node --experimental-strip-types // Verify the src/ module graph is acyclic — including type-only edges. // // oxlint's import/no-cycle (ignoreTypes: false) already rejects cycles formed diff --git a/packages/sdk/scripts/check-public-api-jsdoc.ts b/packages/sdk/scripts/check-public-api-jsdoc.ts index 4c35999d8..38e2d53d8 100644 --- a/packages/sdk/scripts/check-public-api-jsdoc.ts +++ b/packages/sdk/scripts/check-public-api-jsdoc.ts @@ -1,4 +1,3 @@ -#!/usr/bin/env -S node --experimental-strip-types // Verify every public API export has JSDoc. // // "Public API" is derived from package.json#exports — each `types` entry diff --git a/packages/sdk/scripts/check-zod-isolation.ts b/packages/sdk/scripts/check-zod-isolation.ts index 78c026e0a..85599bc3e 100644 --- a/packages/sdk/scripts/check-zod-isolation.ts +++ b/packages/sdk/scripts/check-zod-isolation.ts @@ -1,4 +1,3 @@ -#!/usr/bin/env -S node --experimental-strip-types // Verify zod stays isolated to the CLI entry point. // // zinfer exists so that user-facing entry points never depend on zod: From e22dc3a6b472d765322eb52a7308eb53a82d287a Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 09:08:37 +0900 Subject: [PATCH 196/618] docs(codemod): regenerate v2 migration guide from registry --- packages/sdk/docs/migration/v2.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 1b982e6bd..15c50f6fa 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -217,11 +217,11 @@ happen to use `--machineuser` alone. -## auth.invoker("name") → "name" +## auth.invoker("name") → invoker: "name" **Migration:** Partially automatic -Replace `auth.invoker("name")` calls with the bare `"name"` string and drop the `auth` import when no other reference remains. The `auth.invoker()` helper is deprecated in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle. +Rename statically identified SDK `authInvoker` options to `invoker`, replace `auth.invoker("name")` there with the bare `"name"` string, and drop the `auth` import when no other reference remains. Ambiguous workflow `.trigger()` calls are left for manual review. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle. Before: @@ -241,19 +241,21 @@ createResolver({ invoker: "manager" }); ```text In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the machine user name passed directly as a string. The codemod already rewrote the -string-literal form auth.invoker("name") to "name". These files still contain -auth.invoker(...) because the argument is not a plain string literal (a variable, -template literal, function call, or property access). +statically identified SDK option form authInvoker: auth.invoker("name") to invoker: "name" and renamed supported authInvoker option keys. These files still contain +auth.invoker(...) calls or authInvoker keys that need manual review. For each remaining auth.invoker() call: -1. Replace the whole call with as-is (e.g. auth.invoker(name) becomes name). -2. Make sure evaluates to the machine user name (a string); adjust it if it - resolves to an object or an auth config value instead. +1. Replace the whole call with only where the target option expects a + machine user name string; platform/runtime authInvoker payloads still expect + the object form. +2. Rename remaining authInvoker option keys to invoker only for SDK resolver, + executor, workflow.trigger(), or startWorkflow() options. Keep platform/runtime + payload keys such as tailor.workflow.triggerWorkflow(..., { authInvoker: ... }). 3. After removing every auth.invoker usage in a file, delete the now-unused auth import (keeping it pulls Node-only config modules into runtime bundles); leave the import if auth is still referenced elsewhere. -Do not change behavior beyond removing the auth.invoker() indirection. +Do not change behavior beyond the SDK option rename and auth.invoker() removal. ``` From 87993741690f69c4429a8efb6541d41ff8f6a3b9 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 09:18:33 +0900 Subject: [PATCH 197/618] fix(sdk): tighten engines.node to >=22.6.0 and copy ts-hook types to dist --- packages/sdk/package.json | 2 +- packages/sdk/tsdown.config.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 147f865a5..2b1e6b5f8 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -245,6 +245,6 @@ }, "engines": { "bun": ">=1.2.0", - "node": ">=22" + "node": ">=22.6.0" } } diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index f9b2b3b32..14459dd56 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -84,5 +84,6 @@ export default defineConfig({ onSuccess: (config) => { copyErdViewerAssets(config.outDir); cpSync(path.resolve("src/cli/ts-hook.mjs"), path.join(config.outDir, "cli/ts-hook.mjs")); + cpSync(path.resolve("src/cli/ts-hook.d.mts"), path.join(config.outDir, "cli/ts-hook.d.mts")); }, }); From d505fa6cf03972a25c181795aa767fc6e9e275aa Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 09:26:02 +0900 Subject: [PATCH 198/618] fix(sdk): use import.meta.url as parentURL in module.register() call --- packages/sdk/src/cli/shared/register-ts-hook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/cli/shared/register-ts-hook.ts b/packages/sdk/src/cli/shared/register-ts-hook.ts index 1d755764c..c486bf4f6 100644 --- a/packages/sdk/src/cli/shared/register-ts-hook.ts +++ b/packages/sdk/src/cli/shared/register-ts-hook.ts @@ -14,6 +14,6 @@ export async function registerTsHook(tsHookUrl: URL): Promise { }; registerHooks({ resolve: resolveSync, load: loadSync }); } else { - mod.register(tsHookUrl, tsHookUrl.href); + mod.register(tsHookUrl, import.meta.url); } } From fb5754302a93ba7993fcdf0d61a66460a3fcd2ee Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 09:40:42 +0900 Subject: [PATCH 199/618] test(sdk): add unit tests for ts-hook load/loadSync URL handling and registerTsHook runtime detection --- .../src/cli/shared/register-ts-hook.test.ts | 26 ++++++++ packages/sdk/src/cli/ts-hook.test.ts | 63 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 packages/sdk/src/cli/shared/register-ts-hook.test.ts create mode 100644 packages/sdk/src/cli/ts-hook.test.ts diff --git a/packages/sdk/src/cli/shared/register-ts-hook.test.ts b/packages/sdk/src/cli/shared/register-ts-hook.test.ts new file mode 100644 index 000000000..8cd085dcd --- /dev/null +++ b/packages/sdk/src/cli/shared/register-ts-hook.test.ts @@ -0,0 +1,26 @@ +import { afterEach, describe, expect, test, vi } from "vitest"; + +vi.mock("node:module", () => ({ register: vi.fn() })); + +import { registerTsHook } from "./register-ts-hook"; + +describe("registerTsHook", () => { + afterEach(() => { + vi.unstubAllGlobals(); + vi.clearAllMocks(); + }); + + test("skips hook registration on Bun (native TypeScript runtime)", async () => { + vi.stubGlobal("Bun", {}); + const nodeModule = await import("node:module"); + await registerTsHook(new URL("file:///ts-hook.mjs")); + expect(nodeModule.register).not.toHaveBeenCalled(); + }); + + test("skips hook registration on Deno (native TypeScript runtime)", async () => { + vi.stubGlobal("Deno", {}); + const nodeModule = await import("node:module"); + await registerTsHook(new URL("file:///ts-hook.mjs")); + expect(nodeModule.register).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts new file mode 100644 index 000000000..c1eb75526 --- /dev/null +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, test, vi } from "vitest"; +import { load, loadSync } from "./ts-hook.mjs"; + +vi.mock("node:fs/promises", () => ({ + readFile: vi.fn().mockResolvedValue("const x: number = 1;"), +})); + +vi.mock("node:fs", () => ({ + readFileSync: vi.fn().mockReturnValue("const x: number = 1;"), +})); + +vi.mock("amaro", () => ({ + transformSync: vi.fn().mockReturnValue({ code: "const x = 1;" }), +})); + +describe("load", () => { + test("strips query string before fileURLToPath to avoid ERR_INVALID_FILE_URL_PATH", async () => { + const nextLoad = vi.fn(); + const result = await load("file:///path/to/foo.ts?tailorImportNonce=1", {}, nextLoad); + expect(result).toMatchObject({ format: "module", shortCircuit: true }); + expect(nextLoad).not.toHaveBeenCalled(); + }); + + test("strips hash before fileURLToPath", async () => { + const nextLoad = vi.fn(); + const result = await load("file:///path/to/foo.mts#anchor", {}, nextLoad); + expect(result).toMatchObject({ format: "module", shortCircuit: true }); + expect(nextLoad).not.toHaveBeenCalled(); + }); + + test("delegates non-file: URLs to nextLoad", async () => { + const nextLoad = vi.fn(); + await load("node:path", {}, nextLoad); + expect(nextLoad).toHaveBeenCalledWith("node:path", {}); + }); + + test("delegates non-TS file URLs to nextLoad", async () => { + const nextLoad = vi.fn(); + await load("file:///path/to/foo.js", {}, nextLoad); + expect(nextLoad).toHaveBeenCalledWith("file:///path/to/foo.js", {}); + }); +}); + +describe("loadSync", () => { + test("strips query string before fileURLToPath to avoid ERR_INVALID_FILE_URL_PATH", () => { + const nextLoad = vi.fn(); + const result = loadSync("file:///path/to/foo.ts?tailorImportNonce=1", {}, nextLoad); + expect(result).toMatchObject({ format: "module", shortCircuit: true }); + expect(nextLoad).not.toHaveBeenCalled(); + }); + + test("delegates non-file: URLs to nextLoad", () => { + const nextLoad = vi.fn(); + loadSync("node:path", {}, nextLoad); + expect(nextLoad).toHaveBeenCalledWith("node:path", {}); + }); + + test("delegates non-TS file URLs to nextLoad", () => { + const nextLoad = vi.fn(); + loadSync("file:///path/to/foo.js", {}, nextLoad); + expect(nextLoad).toHaveBeenCalledWith("file:///path/to/foo.js", {}); + }); +}); From 2f9562fa67a760a8c2dbd20248fff347ff8abd92 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 10:10:28 +0900 Subject: [PATCH 200/618] test(sdk): add resolve/resolveSync tests and registerTsHook Node path coverage --- .../src/cli/shared/register-ts-hook.test.ts | 32 ++++++-- packages/sdk/src/cli/ts-hook.test.ts | 81 ++++++++++++++++++- 2 files changed, 107 insertions(+), 6 deletions(-) diff --git a/packages/sdk/src/cli/shared/register-ts-hook.test.ts b/packages/sdk/src/cli/shared/register-ts-hook.test.ts index 8cd085dcd..1247053d0 100644 --- a/packages/sdk/src/cli/shared/register-ts-hook.test.ts +++ b/packages/sdk/src/cli/shared/register-ts-hook.test.ts @@ -1,6 +1,12 @@ import { afterEach, describe, expect, test, vi } from "vitest"; -vi.mock("node:module", () => ({ register: vi.fn() })); +const nodeModuleMock = vi.hoisted(() => ({ + register: vi.fn(), + registerHooks: undefined as ((opts: { resolve?: unknown; load?: unknown }) => void) | undefined, +})); + +vi.mock("node:module", () => nodeModuleMock); +vi.mock("../ts-hook.mjs", () => ({ resolveSync: vi.fn(), loadSync: vi.fn() })); import { registerTsHook } from "./register-ts-hook"; @@ -8,19 +14,35 @@ describe("registerTsHook", () => { afterEach(() => { vi.unstubAllGlobals(); vi.clearAllMocks(); + nodeModuleMock.registerHooks = undefined; }); test("skips hook registration on Bun (native TypeScript runtime)", async () => { vi.stubGlobal("Bun", {}); - const nodeModule = await import("node:module"); await registerTsHook(new URL("file:///ts-hook.mjs")); - expect(nodeModule.register).not.toHaveBeenCalled(); + expect(nodeModuleMock.register).not.toHaveBeenCalled(); }); test("skips hook registration on Deno (native TypeScript runtime)", async () => { vi.stubGlobal("Deno", {}); - const nodeModule = await import("node:module"); await registerTsHook(new URL("file:///ts-hook.mjs")); - expect(nodeModule.register).not.toHaveBeenCalled(); + expect(nodeModuleMock.register).not.toHaveBeenCalled(); + }); + + test("calls module.register() on Node.js when registerHooks is unavailable", async () => { + const tsHookUrl = new URL("../ts-hook.mjs", import.meta.url); + await registerTsHook(tsHookUrl); + expect(nodeModuleMock.register).toHaveBeenCalledWith(tsHookUrl, expect.any(String)); + }); + + test("calls module.registerHooks() with resolve/load when registerHooks is present", async () => { + const registerHooks = vi.fn(); + nodeModuleMock.registerHooks = registerHooks; + const tsHookUrl = new URL("../ts-hook.mjs", import.meta.url); + await registerTsHook(tsHookUrl); + expect(registerHooks).toHaveBeenCalledWith({ + resolve: expect.any(Function), + load: expect.any(Function), + }); }); }); diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index c1eb75526..4272982eb 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, vi } from "vitest"; -import { load, loadSync } from "./ts-hook.mjs"; +import { load, loadSync, resolve, resolveSync } from "./ts-hook.mjs"; vi.mock("node:fs/promises", () => ({ readFile: vi.fn().mockResolvedValue("const x: number = 1;"), @@ -61,3 +61,82 @@ describe("loadSync", () => { expect(nextLoad).toHaveBeenCalledWith("file:///path/to/foo.js", {}); }); }); + +const notFound = (specifier: string) => + Object.assign(new Error(`Cannot find '${specifier}'`), { code: "ERR_MODULE_NOT_FOUND" }); + +describe("resolve", () => { + test("retries with .ts extension for extensionless relative specifier on ERR_MODULE_NOT_FOUND", async () => { + const resolved = { url: "file:///path/to/foo.ts" }; + const nextResolve = vi + .fn() + .mockRejectedValueOnce(notFound("./foo")) + .mockResolvedValueOnce(resolved); + const result = await resolve("./foo", {}, nextResolve); + expect(result).toEqual(resolved); + expect(nextResolve).toHaveBeenCalledWith("./foo.ts", {}); + }); + + test("retries with .ts extension for .js specifier on ERR_MODULE_NOT_FOUND", async () => { + const resolved = { url: "file:///path/to/foo.ts" }; + const nextResolve = vi + .fn() + .mockRejectedValueOnce(notFound("./foo.js")) + .mockResolvedValueOnce(resolved); + const result = await resolve("./foo.js", {}, nextResolve); + expect(result).toEqual(resolved); + expect(nextResolve).toHaveBeenCalledWith("./foo.ts", {}); + }); + + test("does not append extensions when specifier already has a TS extension", async () => { + const nextResolve = vi.fn().mockRejectedValue(notFound("./foo.ts")); + await expect(resolve("./foo.ts", {}, nextResolve)).rejects.toMatchObject({ + code: "ERR_MODULE_NOT_FOUND", + }); + expect(nextResolve).toHaveBeenCalledTimes(1); + }); + + test("rethrows ERR_MODULE_NOT_FOUND for non-relative specifiers without retrying", async () => { + const nextResolve = vi.fn().mockRejectedValue(notFound("some-package")); + await expect(resolve("some-package", {}, nextResolve)).rejects.toMatchObject({ + code: "ERR_MODULE_NOT_FOUND", + }); + expect(nextResolve).toHaveBeenCalledTimes(1); + }); +}); + +describe("resolveSync", () => { + test("retries with .ts extension for extensionless relative specifier on ERR_MODULE_NOT_FOUND", () => { + const resolved = { url: "file:///path/to/foo.ts" }; + const nextResolve = vi + .fn() + .mockImplementationOnce(() => { + throw notFound("./foo"); + }) + .mockReturnValueOnce(resolved); + const result = resolveSync("./foo", {}, nextResolve); + expect(result).toEqual(resolved); + expect(nextResolve).toHaveBeenCalledWith("./foo.ts", {}); + }); + + test("retries with .ts extension for .js specifier on ERR_MODULE_NOT_FOUND", () => { + const resolved = { url: "file:///path/to/foo.ts" }; + const nextResolve = vi + .fn() + .mockImplementationOnce(() => { + throw notFound("./foo.js"); + }) + .mockReturnValueOnce(resolved); + const result = resolveSync("./foo.js", {}, nextResolve); + expect(result).toEqual(resolved); + expect(nextResolve).toHaveBeenCalledWith("./foo.ts", {}); + }); + + test("does not append extensions when specifier already has a TS extension", () => { + const nextResolve = vi.fn().mockImplementation(() => { + throw notFound("./foo.ts"); + }); + expect(() => resolveSync("./foo.ts", {}, nextResolve)).toThrow("Cannot find './foo.ts'"); + expect(nextResolve).toHaveBeenCalledTimes(1); + }); +}); From eaa6f8d1ab88b4c8fec732cb3c70f0c287e44217 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 10:31:39 +0900 Subject: [PATCH 201/618] refactor(workflow): rename defineWaitPoint/defineWaitPoints to createWaitPoint/createWaitPoints These functions create runtime instances with .wait()/.resolve() methods that call the platform API, so they follow the create* naming convention. Also document the define* vs create* rule in sdk-internals agent rules. --- .agents/rules/sdk-internals.md | 15 ++++++ .claude/rules/sdk-internals.md | 15 ++++++ AGENTS.md | 4 +- example/workflows/approval.ts | 4 +- .../workflow/src/workflow/approval.ts | 4 +- packages/sdk/docs/services/workflow.md | 16 +++--- .../services/workflow/wait-point.test.ts | 52 +++++++++---------- .../configure/services/workflow/wait-point.ts | 16 +++--- 8 files changed, 78 insertions(+), 48 deletions(-) diff --git a/.agents/rules/sdk-internals.md b/.agents/rules/sdk-internals.md index 45f9a46a3..2b2902855 100644 --- a/.agents/rules/sdk-internals.md +++ b/.agents/rules/sdk-internals.md @@ -1,5 +1,20 @@ # SDK Internals +## API Naming: `define*` vs `create*` + +Public functions in `src/configure/` follow a strict naming convention. + +| Prefix | Meaning | Output | Where used | +| --------- | --------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| `define*` | Declarative resource specification | Pure data/config struct (no SDK-managed runtime methods) | `tailor.config.ts` as resource entries | +| `create*` | Instantiation of a runtime-capable unit | Object with runtime-capable methods (`trigger`, `wait`, `resolve`) or user-written `body` function | Standalone files as default/named exports, or inside workflow job bodies | + +**`define*` examples:** `defineConfig`, `defineAuth`, `defineIdp`, `defineStaticWebSite`, `defineAIGateway`, `defineSecretManager`, `definePlugins` + +**`create*` examples:** `createResolver`, `createExecutor`, `createWorkflow`, `createWorkflowJob`, `createWaitPoint`, `createWaitPoints`, `createHttpAdapter` + +Decision rule: if the result has a method that calls the platform at runtime (`.trigger()`, `.wait()`, `.resolve()`, etc.) or carries a user-written `body` function, use `create*`. If the result is purely a typed config object that tooling/deployer reads, use `define*`. + ## Module Architecture and Import Rules The SDK enforces strict module boundaries to maintain a clean architecture: diff --git a/.claude/rules/sdk-internals.md b/.claude/rules/sdk-internals.md index 9b12c69cf..9f881a31c 100644 --- a/.claude/rules/sdk-internals.md +++ b/.claude/rules/sdk-internals.md @@ -9,6 +9,21 @@ paths: # SDK Internals +## API Naming: `define*` vs `create*` + +Public functions in `src/configure/` follow a strict naming convention. + +| Prefix | Meaning | Output | Where used | +| --------- | --------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| `define*` | Declarative resource specification | Pure data/config struct (no SDK-managed runtime methods) | `tailor.config.ts` as resource entries | +| `create*` | Instantiation of a runtime-capable unit | Object with runtime-capable methods (`trigger`, `wait`, `resolve`) or user-written `body` function | Standalone files as default/named exports, or inside workflow job bodies | + +**`define*` examples:** `defineConfig`, `defineAuth`, `defineIdp`, `defineStaticWebSite`, `defineAIGateway`, `defineSecretManager`, `definePlugins` + +**`create*` examples:** `createResolver`, `createExecutor`, `createWorkflow`, `createWorkflowJob`, `createWaitPoint`, `createWaitPoints`, `createHttpAdapter` + +Decision rule: if the result has a method that calls the platform at runtime (`.trigger()`, `.wait()`, `.resolve()`, etc.) or carries a user-written `body` function, use `create*`. If the result is purely a typed config object that tooling/deployer reads, use `define*`. + ## Module Architecture and Import Rules The SDK enforces strict module boundaries to maintain a clean architecture: diff --git a/AGENTS.md b/AGENTS.md index 11440a6d8..67c36fc0e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,9 +86,9 @@ Key files: - All jobs **must** be named exports (including mainJob and triggered jobs) - Job names must be unique across the entire project - Job `.trigger()` returns `Awaited` directly; read the value synchronously unless the job output itself is promise-like -- `defineWaitPoints(define => ({ key: define() }))` creates typed wait/resolve points +- `createWaitPoints(define => ({ key: define() }))` creates typed wait/resolve points - Wait/resolve methods runtime-delegate to `tailor.workflow.wait/resolve` on the platform; acquire the mock with `using wf = mockWorkflow()` from `@tailor-platform/sdk/vitest` (with the `tailor-runtime` environment) and use `wf.setWaitHandler` / `wf.setResolveHandler` to mock in tests — see [testing.md](packages/sdk/docs/testing.md#jobs-that-wait-on-approval) -- Use `wps.key.wait()` for namespaced access, or `export const { key } = defineWaitPoints(...)` for destructured 2-level access +- Use `wps.key.wait()` for namespaced access, or `export const { key } = createWaitPoints(...)` for destructured 2-level access ### Executors diff --git a/example/workflows/approval.ts b/example/workflows/approval.ts index 6c113e77c..5266b45fd 100644 --- a/example/workflows/approval.ts +++ b/example/workflows/approval.ts @@ -1,6 +1,6 @@ -import { createWorkflow, createWorkflowJob, defineWaitPoints } from "@tailor-platform/sdk"; +import { createWaitPoints, createWorkflow, createWorkflowJob } from "@tailor-platform/sdk"; -export const { approval } = defineWaitPoints((define) => ({ +export const { approval } = createWaitPoints((define) => ({ /** Approval for order processing */ approval: define<{ message: string; requestId: string }, { approved: boolean }>(), })); diff --git a/packages/create-sdk/templates/workflow/src/workflow/approval.ts b/packages/create-sdk/templates/workflow/src/workflow/approval.ts index 950450ed2..b8038baae 100644 --- a/packages/create-sdk/templates/workflow/src/workflow/approval.ts +++ b/packages/create-sdk/templates/workflow/src/workflow/approval.ts @@ -1,6 +1,6 @@ -import { createWorkflow, createWorkflowJob, defineWaitPoints } from "@tailor-platform/sdk"; +import { createWaitPoints, createWorkflow, createWorkflowJob } from "@tailor-platform/sdk"; -export const { approval } = defineWaitPoints((define) => ({ +export const { approval } = createWaitPoints((define) => ({ /** Approval for order processing */ approval: define<{ message: string; orderId: string }, { approved: boolean }>(), })); diff --git a/packages/sdk/docs/services/workflow.md b/packages/sdk/docs/services/workflow.md index fc1797eaf..0b91579aa 100644 --- a/packages/sdk/docs/services/workflow.md +++ b/packages/sdk/docs/services/workflow.md @@ -207,23 +207,23 @@ Wait points allow a workflow job to suspend execution and wait for an external s ### Defining Wait Points -Use `defineWaitPoint` to declare a single typed wait point: +Use `createWaitPoint` to create a single typed wait point: ```typescript -import { defineWaitPoint } from "@tailor-platform/sdk"; +import { createWaitPoint } from "@tailor-platform/sdk"; -export const approval = defineWaitPoint< +export const approval = createWaitPoint< { message: string; requestId: string }, { approved: boolean } >("approval"); ``` -For multiple wait points, use `defineWaitPoints` with a builder callback. Property names become wait point keys, and JSDoc on each property is preserved in IDE autocompletion: +For multiple wait points, use `createWaitPoints` with a builder callback. Property names become wait point keys, and JSDoc on each property is preserved in IDE autocompletion: ```typescript -import { defineWaitPoints } from "@tailor-platform/sdk"; +import { createWaitPoints } from "@tailor-platform/sdk"; -export const waitPoints = defineWaitPoints((define) => ({ +export const waitPoints = createWaitPoints((define) => ({ /** Manager approval step */ managerApproval: define<{ amount: number }, { approved: boolean }>(), /** Finance review step */ @@ -245,9 +245,9 @@ Both must be JsonValue-compatible (plain objects/arrays; no class instances or f Call `.wait()` inside a workflow job body to suspend execution: ```typescript -import { createWorkflow, createWorkflowJob, defineWaitPoint } from "@tailor-platform/sdk"; +import { createWaitPoint, createWorkflow, createWorkflowJob } from "@tailor-platform/sdk"; -export const approval = defineWaitPoint< +export const approval = createWaitPoint< { message: string; requestId: string }, { approved: boolean } >("approval"); 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 82e431c0b..c2573bb50 100644 --- a/packages/sdk/src/configure/services/workflow/wait-point.test.ts +++ b/packages/sdk/src/configure/services/workflow/wait-point.test.ts @@ -1,18 +1,18 @@ // oxlint-disable vitest/expect-expect -- Type-only assertions are checked by TypeScript. import { afterEach, describe, expect, test, expectTypeOf } from "vitest"; import { setupWaitPointMock, setupWorkflowMock } from "#/utils/test/mock"; -import { defineWaitPoint, defineWaitPoints } from "./wait-point"; +import { createWaitPoint, createWaitPoints } from "./wait-point"; import type { TailorRuntime } from "#/runtime/index"; const TailorGlobal = globalThis as { tailor?: TailorRuntime }; -describe("defineWaitPoints", () => { +describe("createWaitPoints", () => { afterEach(() => { delete TailorGlobal.tailor; }); test("creates instances with typed wait/resolve", () => { - const wps = defineWaitPoints((define) => ({ + const wps = createWaitPoints((define) => ({ approval: define<{ message: string }, { approved: boolean }>(), })); expectTypeOf(wps.approval.wait).toBeFunction(); @@ -20,53 +20,53 @@ describe("defineWaitPoints", () => { }); test("rejects Date in Payload / Result (pure JSON only)", () => { - defineWaitPoints((define) => ({ + createWaitPoints((define) => ({ // @ts-expect-error - Date is not JsonValue-compatible (Result) check: define(), })); - defineWaitPoints((define) => ({ + createWaitPoints((define) => ({ // @ts-expect-error - Date is not JsonValue-compatible (Payload) check: define<{ when: Date }, { ok: boolean }>(), })); }); test("rejects top-level null in Payload", () => { - defineWaitPoints((define) => ({ + createWaitPoints((define) => ({ // @ts-expect-error - null is not allowed at top level (even in union) check: define<{ id: string } | null, { ok: boolean }>(), })); }); test("rejects top-level undefined in Payload union", () => { - defineWaitPoints((define) => ({ + createWaitPoints((define) => ({ // @ts-expect-error - undefined is not allowed at top level (except when Payload = undefined alone) check: define<{ id: string } | undefined, { ok: boolean }>(), })); }); test("allows Payload = undefined (no-payload convention)", () => { - const wps = defineWaitPoints((define) => ({ + const wps = createWaitPoints((define) => ({ check: define(), })); expectTypeOf(wps.check.wait).toBeFunction(); }); test("allows nested null in Payload", () => { - const wps = defineWaitPoints((define) => ({ + const wps = createWaitPoints((define) => ({ check: define<{ data: string | null }, { ok: boolean }>(), })); expectTypeOf(wps.check.wait).toBeFunction(); }); test("allows top-level null in Result", () => { - const wps = defineWaitPoints((define) => ({ + const wps = createWaitPoints((define) => ({ check: define<{ id: string }, { data: string } | null>(), })); expectTypeOf(wps.check.wait).toBeFunction(); }); test("wait return type is Result as-is (no Jsonify transformation)", () => { - const wps = defineWaitPoints((define) => ({ + const wps = createWaitPoints((define) => ({ check: define(), })); type WaitReturn = Awaited>; @@ -74,7 +74,7 @@ describe("defineWaitPoints", () => { }); test("wait omits payload when Payload is undefined", () => { - const wps = defineWaitPoints((define) => ({ + const wps = createWaitPoints((define) => ({ signal: define(), })); type Params = Parameters; @@ -82,7 +82,7 @@ describe("defineWaitPoints", () => { }); test("throws without platform API or mock", () => { - const wps = defineWaitPoints((define) => ({ + const wps = createWaitPoints((define) => ({ approval: define(), })); expect(() => wps.approval.wait()).toThrow("mockWorkflow"); @@ -90,28 +90,28 @@ describe("defineWaitPoints", () => { test("throws a helpful error when only setupWorkflowMock is active (wait/resolve auto-stubbed)", () => { setupWorkflowMock(() => undefined); - const wps = defineWaitPoints((define) => ({ + const wps = createWaitPoints((define) => ({ approval: define(), })); expect(() => wps.approval.wait()).toThrow("mockWorkflow"); }); test("rejects Result = undefined (callback must return a value)", () => { - defineWaitPoints((define) => ({ + createWaitPoints((define) => ({ // @ts-expect-error - Result cannot be undefined; platform throws if callback returns undefined check: define(), })); }); test("rejects top-level undefined in Result union", () => { - defineWaitPoints((define) => ({ + createWaitPoints((define) => ({ // @ts-expect-error - Result cannot include top-level undefined check: define(), })); }); test("allows nested undefined (optional fields) in Result", () => { - const wps = defineWaitPoints((define) => ({ + const wps = createWaitPoints((define) => ({ check: define(), })); expectTypeOf(wps.check.wait).toBeFunction(); @@ -122,7 +122,7 @@ describe("defineWaitPoints", () => { onWait: (_key, _payload) => ({ approved: true }), }); - const wps = defineWaitPoints((define) => ({ + const wps = createWaitPoints((define) => ({ approval: define<{ msg: string }, { approved: boolean }>(), })); @@ -139,7 +139,7 @@ describe("defineWaitPoints", () => { }, }); - const wps = defineWaitPoints((define) => ({ + const wps = createWaitPoints((define) => ({ approval: define<{ msg: string }, { ok: boolean }>(), })); @@ -156,7 +156,7 @@ describe("defineWaitPoints", () => { onWait: () => "ok", }); - const wps = defineWaitPoints((define) => ({ + const wps = createWaitPoints((define) => ({ step: define(), })); @@ -165,24 +165,24 @@ describe("defineWaitPoints", () => { }); }); -describe("defineWaitPoint", () => { +describe("createWaitPoint", () => { afterEach(() => { delete TailorGlobal.tailor; }); test("creates a typed instance with the given key", () => { - const wp = defineWaitPoint<{ msg: string }, { ok: boolean }>("my-key"); + const wp = createWaitPoint<{ msg: string }, { ok: boolean }>("my-key"); expectTypeOf(wp.wait).toBeFunction(); expectTypeOf(wp.resolve).toBeFunction(); }); test("throws without platform API or mock", () => { - const wp = defineWaitPoint("my-step"); + const wp = createWaitPoint("my-step"); expect(() => wp.wait()).toThrow("mockWorkflow"); }); test("rejects Result = undefined (callback must return a value)", () => { - const wp = defineWaitPoint("my-step"); + const wp = createWaitPoint("my-step"); // @ts-expect-error - wp resolves to an error string, not WaitPointInstance expectTypeOf(wp.wait).toBeFunction(); }); @@ -192,7 +192,7 @@ describe("defineWaitPoint", () => { onWait: (_key, _payload) => ({ ok: true }), }); - const wp = defineWaitPoint<{ msg: string }, { ok: boolean }>("approval"); + const wp = createWaitPoint<{ msg: string }, { ok: boolean }>("approval"); await wp.wait({ msg: "please" }); expect(waitCalls[0]).toEqual({ key: "approval", payload: { msg: "please" } }); }); @@ -204,7 +204,7 @@ describe("defineWaitPoint", () => { }, }); - const wp = defineWaitPoint("my-step"); + const wp = createWaitPoint("my-step"); await wp.resolve("exec-1", () => ({ ok: true })); expect(resolveCalls[0]).toEqual({ executionId: "exec-1", key: "my-step" }); }); diff --git a/packages/sdk/src/configure/services/workflow/wait-point.ts b/packages/sdk/src/configure/services/workflow/wait-point.ts index 735676d3d..bb2c0848c 100644 --- a/packages/sdk/src/configure/services/workflow/wait-point.ts +++ b/packages/sdk/src/configure/services/workflow/wait-point.ts @@ -79,7 +79,7 @@ function createWaitPointInstance(initialKey: string): WaitPointWithSetter { } /** - * The type produced by `define()` / `defineWaitPoint(key)`. + * The type produced by `define()` / `createWaitPoint(key)`. * Resolves to `WaitPointInstance` when both types are JsonValue-compatible, * or to a template-literal error string that surfaces at the call site. */ @@ -100,7 +100,7 @@ type WaitPointDef = [null] extends [Payload] : "ERROR: Payload must be JsonValue-compatible (plain objects/arrays; no class instances or functions)"; /** - * The `define` function passed to the `defineWaitPoints` builder callback. + * The `define` function passed to the `createWaitPoints` builder callback. * Returns an actual WaitPointInstance (not a phantom marker) so that the * builder's return type can flow through as-is, preserving JSDoc comments * on each property for IDE autocompletion. @@ -112,7 +112,7 @@ type WaitPointDef = [null] extends [Payload] type DefineFn = () => WaitPointDef; /** - * Define a single typed wait point with an explicit key. + * Create a single typed wait point with an explicit key. * * `Payload` and `Result` must be JsonValue-compatible. * Functions and objects with a `toJSON` method are rejected at the type level; @@ -120,19 +120,19 @@ type DefineFn = () => WaitPointDef("approval"); + * export const approval = createWaitPoint<{ message: string }, { approved: boolean }>("approval"); * * await approval.wait({ message: "Please approve" }); */ /* @__NO_SIDE_EFFECTS__ */ -export function defineWaitPoint( +export function createWaitPoint( key: string, ): WaitPointDef { return createWaitPointInstance(key).instance as unknown as WaitPointDef; } /** - * Define a group of typed wait points for human-in-the-loop workflows. + * Create a group of typed wait points for human-in-the-loop workflows. * Property names become the wait point keys. * * The return type is the same as the builder's return type, so JSDoc on each @@ -144,7 +144,7 @@ export function defineWaitPoint( * @param builder - Callback that receives a `define` factory and returns an object of wait points * @returns The same object returned by the builder (with correct keys set on each instance) * @example - * export const waitPoints = defineWaitPoints(define => ({ + * export const waitPoints = createWaitPoints(define => ({ * // Preceding JSDoc on this property is shown in IDE autocompletion * approval: define<{ message: string }, { approved: boolean }>(), * })); @@ -156,7 +156,7 @@ export function defineWaitPoint( */ /* @__NO_SIDE_EFFECTS__ */ // oxlint-disable-next-line no-explicit-any -export function defineWaitPoints>>( +export function createWaitPoints>>( builder: (define: DefineFn) => T, ): T { const setters = new Map void>(); From 79c95c10defa99b2c15766a9298342a92a040042 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 11:33:49 +0900 Subject: [PATCH 202/618] chore(sdk): bump engines.node to >=22.15.0 for module.registerHooks --- package.json | 2 +- packages/sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6e86822ec..24e2b09f2 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "typescript": "6.0.3" }, "engines": { - "node": ">= 22.14.0" + "node": ">= 22.15.0" }, "packageManager": "pnpm@11.8.0" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index d3e19f699..6cc11ad51 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -246,6 +246,6 @@ }, "engines": { "bun": ">=1.2.0", - "node": ">=22.6.0" + "node": ">=22.15.0" } } From a90b8e27914da5e05f40010cdcb8c3ccf0355a57 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 11:36:48 +0900 Subject: [PATCH 203/618] refactor(sdk): remove register() fallback, require registerHooks directly --- packages/sdk/docs/migration/v2.md | 4 ++++ packages/sdk/postinstall.mjs | 10 ++-------- .../src/cli/shared/register-ts-hook.test.ts | 20 +++++-------------- .../sdk/src/cli/shared/register-ts-hook.ts | 20 ++++++------------- 4 files changed, 17 insertions(+), 37 deletions(-) diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 15c50f6fa..e5593b642 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -464,6 +464,10 @@ trigger result as the job output directly (no Promise wrapper to unwrap). These v2 changes alter runtime or CLI behavior; no source change is needed. +### Node.js minimum version raised to 22.15.0 + +v2 requires Node.js **22.15.0** or later (up from 22.6.0 in v1). This is the first version that includes `module.registerHooks()`, which the SDK uses to register its TypeScript loader hook synchronously in the main thread. No source change is required; ensure your environment runs Node.js 22.15.0+. + ### CLI tokens stored in the OS keyring CLI login tokens are stored in the OS keyring by default when available, falling back to the platform config file when it is not. No source change is required; re-login if you need tokens moved into the keyring. diff --git a/packages/sdk/postinstall.mjs b/packages/sdk/postinstall.mjs index 9fdff77fa..10b19bafa 100644 --- a/packages/sdk/postinstall.mjs +++ b/packages/sdk/postinstall.mjs @@ -32,14 +32,8 @@ async function install() { } try { - const mod = /** @type {{ register: Function, registerHooks?: Function }} */ (nodeModule); - const registerHooks = mod.registerHooks; - if (registerHooks) { - const { resolveSync, loadSync } = await import("./dist/cli/ts-hook.mjs"); - registerHooks({ resolve: resolveSync, load: loadSync }); - } else { - mod.register(new URL("./dist/cli/ts-hook.mjs", import.meta.url), import.meta.url); - } + const { resolveSync, loadSync } = await import("./dist/cli/ts-hook.mjs"); + nodeModule.registerHooks({ resolve: resolveSync, load: loadSync }); const configDir = dirname(configPath); process.chdir(configDir); diff --git a/packages/sdk/src/cli/shared/register-ts-hook.test.ts b/packages/sdk/src/cli/shared/register-ts-hook.test.ts index 1247053d0..7e02102d3 100644 --- a/packages/sdk/src/cli/shared/register-ts-hook.test.ts +++ b/packages/sdk/src/cli/shared/register-ts-hook.test.ts @@ -1,8 +1,7 @@ import { afterEach, describe, expect, test, vi } from "vitest"; const nodeModuleMock = vi.hoisted(() => ({ - register: vi.fn(), - registerHooks: undefined as ((opts: { resolve?: unknown; load?: unknown }) => void) | undefined, + registerHooks: vi.fn(), })); vi.mock("node:module", () => nodeModuleMock); @@ -14,33 +13,24 @@ describe("registerTsHook", () => { afterEach(() => { vi.unstubAllGlobals(); vi.clearAllMocks(); - nodeModuleMock.registerHooks = undefined; }); test("skips hook registration on Bun (native TypeScript runtime)", async () => { vi.stubGlobal("Bun", {}); await registerTsHook(new URL("file:///ts-hook.mjs")); - expect(nodeModuleMock.register).not.toHaveBeenCalled(); + expect(nodeModuleMock.registerHooks).not.toHaveBeenCalled(); }); test("skips hook registration on Deno (native TypeScript runtime)", async () => { vi.stubGlobal("Deno", {}); await registerTsHook(new URL("file:///ts-hook.mjs")); - expect(nodeModuleMock.register).not.toHaveBeenCalled(); + expect(nodeModuleMock.registerHooks).not.toHaveBeenCalled(); }); - test("calls module.register() on Node.js when registerHooks is unavailable", async () => { + test("calls module.registerHooks() with resolve/load on Node.js", async () => { const tsHookUrl = new URL("../ts-hook.mjs", import.meta.url); await registerTsHook(tsHookUrl); - expect(nodeModuleMock.register).toHaveBeenCalledWith(tsHookUrl, expect.any(String)); - }); - - test("calls module.registerHooks() with resolve/load when registerHooks is present", async () => { - const registerHooks = vi.fn(); - nodeModuleMock.registerHooks = registerHooks; - const tsHookUrl = new URL("../ts-hook.mjs", import.meta.url); - await registerTsHook(tsHookUrl); - expect(registerHooks).toHaveBeenCalledWith({ + expect(nodeModuleMock.registerHooks).toHaveBeenCalledWith({ resolve: expect.any(Function), load: expect.any(Function), }); diff --git a/packages/sdk/src/cli/shared/register-ts-hook.ts b/packages/sdk/src/cli/shared/register-ts-hook.ts index c486bf4f6..0c7bd636f 100644 --- a/packages/sdk/src/cli/shared/register-ts-hook.ts +++ b/packages/sdk/src/cli/shared/register-ts-hook.ts @@ -1,19 +1,11 @@ +import * as mod from "node:module"; import { isNativeTypeScriptRuntime } from "./runtime"; -// registerHooks is available since Node 22.15.0; fall back to register() on older versions. export async function registerTsHook(tsHookUrl: URL): Promise { if (isNativeTypeScriptRuntime()) return; - const mod = await import("node:module"); - const registerHooks = (mod as unknown as Record).registerHooks as - | ((opts: { resolve?: unknown; load?: unknown }) => void) - | undefined; - if (registerHooks) { - const { resolveSync, loadSync } = (await import(tsHookUrl.href)) as { - resolveSync: (...args: unknown[]) => unknown; - loadSync: (...args: unknown[]) => unknown; - }; - registerHooks({ resolve: resolveSync, load: loadSync }); - } else { - mod.register(tsHookUrl, import.meta.url); - } + const { resolveSync, loadSync } = (await import(tsHookUrl.href)) as { + resolveSync: Parameters[0]["resolve"]; + loadSync: Parameters[0]["load"]; + }; + mod.registerHooks({ resolve: resolveSync, load: loadSync }); } From ca7432d16cfeff7280fe0b19745fc9d5b909258d Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 11:41:52 +0900 Subject: [PATCH 204/618] chore: update changeset description --- .changeset/remove-tsx.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.changeset/remove-tsx.md b/.changeset/remove-tsx.md index 9b55d6611..7dfdaad96 100644 --- a/.changeset/remove-tsx.md +++ b/.changeset/remove-tsx.md @@ -11,3 +11,7 @@ A small `ts-hook.mjs` provides the Node.js module hook with both a resolver (`.ts` extension fallback) and a load hook (`amaro` for full TypeScript support including enums). Dev-only scripts now use `node --experimental-strip-types` instead. + +Raises the minimum Node.js version to 22.15.0 (from 22.6.0) to use +`module.registerHooks()`, which allows synchronous hook registration directly +in the main thread without a worker thread. From 9fb4670b2e2fe0e8ffa3352ec4fc8b3c169eb931 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 11:49:40 +0900 Subject: [PATCH 205/618] chore: bump engines.node to >=22.15.0 in example and llm-challenge --- example/package.json | 2 +- llm-challenge/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/package.json b/example/package.json index 2b4ba14eb..28b5ad25d 100644 --- a/example/package.json +++ b/example/package.json @@ -45,6 +45,6 @@ "vitest": "4.1.9" }, "engines": { - "node": ">= 22.14.0" + "node": ">= 22.15.0" } } diff --git a/llm-challenge/package.json b/llm-challenge/package.json index e6ef35ecc..40c02c117 100644 --- a/llm-challenge/package.json +++ b/llm-challenge/package.json @@ -18,6 +18,6 @@ "vitest": "4.1.9" }, "engines": { - "node": ">= 22.14.0" + "node": ">= 22.15.0" } } From 3b20a142431049f34d80e531c6a54e16b4f649e5 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 12:05:13 +0900 Subject: [PATCH 206/618] fix: address Copilot review comments on postinstall, v2.md, and changeset - Remove redundant registerHooks() call from postinstall.mjs; lib.mjs already registers the hook via registerTsHook() when imported - Remove unused node:module import from postinstall.mjs - Move Node.js version notice to sdk-codemod registry as a notice entry and regenerate v2.md via pnpm codemod:docs:update - Correct changeset wording: 'from 22.6.0' -> 'from >=22' to reflect the actual prior engines.node constraint on the v2 branch --- .changeset/remove-tsx.md | 2 +- packages/sdk-codemod/src/registry.ts | 9 +++++++++ packages/sdk/docs/migration/v2.md | 8 ++++---- packages/sdk/postinstall.mjs | 3 --- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.changeset/remove-tsx.md b/.changeset/remove-tsx.md index 7dfdaad96..c42c9e97d 100644 --- a/.changeset/remove-tsx.md +++ b/.changeset/remove-tsx.md @@ -12,6 +12,6 @@ A small `ts-hook.mjs` provides the Node.js module hook with both a resolver support including enums). Dev-only scripts now use `node --experimental-strip-types` instead. -Raises the minimum Node.js version to 22.15.0 (from 22.6.0) to use +Raises the minimum Node.js version to 22.15.0 (from >=22) to use `module.registerHooks()`, which allows synchronous hook registration directly in the main thread without a worker thread. diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 87bd986b4..40247b274 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -437,6 +437,15 @@ export const allCodemods: CodemodPackage[] = [ until: "2.0.0", notice: true, }, + { + id: "v2/node-minimum-22-15-0", + name: "Node.js minimum version raised to 22.15.0", + description: + "v2 requires Node.js **22.15.0** or later. This is the first version that includes `module.registerHooks()`, which the SDK uses to register its TypeScript loader hook synchronously in the main thread. No source change is required; ensure your environment runs Node.js 22.15.0+.", + since: "1.0.0", + until: "2.0.0", + notice: true, + }, ]; /** diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index e5593b642..b28b27dda 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -464,10 +464,6 @@ trigger result as the job output directly (no Promise wrapper to unwrap). These v2 changes alter runtime or CLI behavior; no source change is needed. -### Node.js minimum version raised to 22.15.0 - -v2 requires Node.js **22.15.0** or later (up from 22.6.0 in v1). This is the first version that includes `module.registerHooks()`, which the SDK uses to register its TypeScript loader hook synchronously in the main thread. No source change is required; ensure your environment runs Node.js 22.15.0+. - ### CLI tokens stored in the OS keyring CLI login tokens are stored in the OS keyring by default when available, falling back to the platform config file when it is not. No source change is required; re-login if you need tokens moved into the keyring. @@ -479,3 +475,7 @@ The CLI stores human users by their stable subject ID instead of email (email is ### function logs require a content hash for source mapping `tailor-sdk function logs` maps stack traces against the function bundle only when the execution recorded a `contentHash`. Executions without one now show raw stack traces instead of mapped frames. No source change is required. + +### Node.js minimum version raised to 22.15.0 + +v2 requires Node.js **22.15.0** or later. This is the first version that includes `module.registerHooks()`, which the SDK uses to register its TypeScript loader hook synchronously in the main thread. No source change is required; ensure your environment runs Node.js 22.15.0+. diff --git a/packages/sdk/postinstall.mjs b/packages/sdk/postinstall.mjs index 10b19bafa..d22c67e1a 100644 --- a/packages/sdk/postinstall.mjs +++ b/packages/sdk/postinstall.mjs @@ -1,7 +1,6 @@ #!/usr/bin/env node import { existsSync } from "node:fs"; -import * as nodeModule from "node:module"; import { dirname, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { findUpSync } from "find-up-simple"; @@ -32,8 +31,6 @@ async function install() { } try { - const { resolveSync, loadSync } = await import("./dist/cli/ts-hook.mjs"); - nodeModule.registerHooks({ resolve: resolveSync, load: loadSync }); const configDir = dirname(configPath); process.chdir(configDir); From d45e8362bb8ae5ed8e5c1a51ef4f4f48c1448448 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 12:20:20 +0900 Subject: [PATCH 207/618] chore: bump changeset level from patch to major for Node.js minimum version raise Raising the minimum Node.js version is listed as a major-level change in docs/changeset.md. --- .changeset/remove-tsx.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/remove-tsx.md b/.changeset/remove-tsx.md index c42c9e97d..6592438bd 100644 --- a/.changeset/remove-tsx.md +++ b/.changeset/remove-tsx.md @@ -1,5 +1,5 @@ --- -"@tailor-platform/sdk": patch +"@tailor-platform/sdk": major --- chore: replace tsx with amaro for TypeScript loading From a9d0c4ee8baf2e297eec71470ded464b8267e68a Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 12:27:53 +0900 Subject: [PATCH 208/618] test(sdk): remove misleading ts-hook.mjs mock from register-ts-hook test vi.mock("../ts-hook.mjs") does not intercept the dynamic import(tsHookUrl.href) that uses an absolute file: URL, making the mock ineffective. Remove it to clarify that the test relies on the actual ts-hook.mjs exports. --- packages/sdk/src/cli/shared/register-ts-hook.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/sdk/src/cli/shared/register-ts-hook.test.ts b/packages/sdk/src/cli/shared/register-ts-hook.test.ts index 7e02102d3..d40e0fba3 100644 --- a/packages/sdk/src/cli/shared/register-ts-hook.test.ts +++ b/packages/sdk/src/cli/shared/register-ts-hook.test.ts @@ -5,7 +5,6 @@ const nodeModuleMock = vi.hoisted(() => ({ })); vi.mock("node:module", () => nodeModuleMock); -vi.mock("../ts-hook.mjs", () => ({ resolveSync: vi.fn(), loadSync: vi.fn() })); import { registerTsHook } from "./register-ts-hook"; From ebf7ed4b4134361e551a02328d0d01a15c9da2fc Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 12:39:52 +0900 Subject: [PATCH 209/618] docs: update Node.js minimum version to 22.15.0 in getting-started.md --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index a89cb45da..c71f8349f 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -4,7 +4,7 @@ Guide for new SDK contributors. ## Prerequisites -- **Node.js** >= 22.14.0 +- **Node.js** >= 22.15.0 - **pnpm** 10.28.0 (see `packageManager` in root `package.json`) - **GPG key** for commit signing (enforced by Lefthook post-commit hook) From 06d15a64b74edadf54bd5095d238f96785b15b3e Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 13:06:35 +0900 Subject: [PATCH 210/618] chore: update changeset description to user-facing language --- .changeset/remove-tsx.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/remove-tsx.md b/.changeset/remove-tsx.md index 6592438bd..9f19d8dd8 100644 --- a/.changeset/remove-tsx.md +++ b/.changeset/remove-tsx.md @@ -2,7 +2,7 @@ "@tailor-platform/sdk": major --- -chore: replace tsx with amaro for TypeScript loading +Minimum Node.js version raised to 22.15.0; TypeScript loading switched from tsx to amaro Removes `tsx` (which pulled in esbuild's native binaries, ~10.5 MB) from `dependencies` and replaces it with `amaro` (~3.8 MB, zero transitive deps). From 7ff575fdfa15c00b5fc6282b28c0cb50bfdf927b Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 14:26:42 +0900 Subject: [PATCH 211/618] chore(sdk): rename CLI binary from tailor-sdk to tailor Rename the binary key in package.json from tailor-sdk to tailor, update the default output directory from .tailor-sdk to .tailor, and rename the GitHub Actions lock file path from .github/tailor-sdk.lock to .github/tailor.lock. All CLI message strings, docs, tests, GitHub Actions workflows, create-sdk templates, and config files are updated accordingly. Adds the v2/rename-bin codemod to automate migration of user projects. --- .agents/skills/e2e-setup/SKILL.md | 22 +++--- .../skills/llm-challenge/CREATING_PROBLEMS.md | 2 +- .changeset/rename-bin-command.md | 14 ++++ .github/workflows/cleanup-e2e-workspaces.yml | 6 +- .github/workflows/deploy.yml | 8 +- .github/workflows/docs-consistency-check.yml | 2 +- .github/workflows/erd-viewer-preview.yml | 4 +- .github/workflows/migration.yml | 6 +- .github/workflows/sdk-e2e.yml | 4 +- .github/workflows/sdk-metrics.yml | 4 +- .github/workflows/test.yml | 2 +- .gitignore | 4 +- .oxfmtrc.json | 2 +- docs/telemetry.md | 10 +-- example/.gitignore | 2 +- example/.oxlintrc.json | 2 +- example/e2e/utils.ts | 2 +- example/migrations/0003/migrate.ts | 2 +- .../migrations/analyticsdb/0001/migrate.ts | 2 +- example/package.json | 8 +- example/tests/invoker.types.ts | 2 +- example/tests/scripts/migration_e2e.ts | 2 +- lefthook.yml | 2 +- llm-challenge/problems/cli/generate/prompt.md | 2 +- .../cli/help-error-recovery/prompt.md | 2 +- .../cli/help-error-recovery/verify.json | 2 +- .../cli/tailordb-migrate-generate/prompt.md | 2 +- .../cli/tailordb-migrate-script/prompt.md | 2 +- llm-challenge/src/challenge.test.ts | 14 ++-- llm-challenge/src/workspace-files.ts | 2 +- llm-challenge/src/workspace.ts | 6 +- oxlint.vitest.json | 2 +- .../create-sdk/templates/executor/.gitignore | 2 +- .../templates/executor/.oxlintrc.json | 2 +- .../templates/executor/package.json | 4 +- .../create-sdk/templates/executor/tailor.d.ts | 2 +- .../templates/generators/.gitignore | 2 +- .../templates/generators/.oxlintrc.json | 2 +- .../templates/generators/package.json | 4 +- .../templates/generators/tailor.d.ts | 2 +- .../templates/hello-world/.gitignore | 2 +- .../templates/hello-world/.oxlintrc.json | 2 +- .../templates/hello-world/README.md | 12 +-- .../templates/hello-world/package.json | 4 +- .../templates/hello-world/tailor.d.ts | 2 +- .../templates/inventory-management/.gitignore | 2 +- .../inventory-management/.oxlintrc.json | 2 +- .../templates/inventory-management/README.md | 16 ++-- .../inventory-management/package.json | 4 +- .../inventory-management/tailor.d.ts | 2 +- .../templates/multi-application/.gitignore | 2 +- .../multi-application/.oxlintrc.json | 2 +- .../templates/multi-application/README.md | 4 +- .../templates/multi-application/package.json | 4 +- .../create-sdk/templates/resolver/.gitignore | 2 +- .../templates/resolver/.oxlintrc.json | 2 +- .../templates/resolver/package.json | 4 +- .../create-sdk/templates/resolver/tailor.d.ts | 2 +- .../templates/static-web-site/.gitignore | 2 +- .../templates/static-web-site/.oxlintrc.json | 2 +- .../templates/static-web-site/package.json | 4 +- .../templates/static-web-site/tailor.d.ts | 2 +- .../create-sdk/templates/tailordb/.gitignore | 2 +- .../templates/tailordb/.oxlintrc.json | 2 +- .../templates/tailordb/package.json | 4 +- .../create-sdk/templates/tailordb/tailor.d.ts | 2 +- .../create-sdk/templates/workflow/.gitignore | 2 +- .../templates/workflow/.oxlintrc.json | 2 +- .../templates/workflow/package.json | 4 +- .../create-sdk/templates/workflow/tailor.d.ts | 2 +- .../codemods/v2/rename-bin/codemod.yaml | 7 ++ .../v2/rename-bin/scripts/transform.ts | 58 +++++++++++++++ .../tests/basic-package-json/expected.json | 10 +++ .../tests/basic-package-json/input.json | 10 +++ .../rename-bin/tests/basic-shell/expected.sh | 7 ++ .../v2/rename-bin/tests/basic-shell/input.sh | 7 ++ .../rename-bin/tests/basic-yaml/expected.yml | 5 ++ .../v2/rename-bin/tests/basic-yaml/input.yml | 5 ++ .../v2/rename-bin/tests/no-match/input.sh | 7 ++ .../tests/version-qualified/expected.sh | 2 + .../tests/version-qualified/input.sh | 2 + packages/sdk-codemod/src/registry.ts | 70 ++++++++++++------ packages/sdk-codemod/src/runner.test.ts | 10 +-- packages/sdk/.oxlintrc.json | 2 +- packages/sdk/README.md | 6 +- .../{tailor-sdk => tailor}/SKILL.md | 2 +- packages/sdk/docs/cli-reference.md | 16 ++-- packages/sdk/docs/cli-reference.template.md | 8 +- packages/sdk/docs/cli/application.md | 34 ++++----- packages/sdk/docs/cli/auth.md | 24 +++--- packages/sdk/docs/cli/completion.md | 2 +- packages/sdk/docs/cli/crashreport.md | 6 +- packages/sdk/docs/cli/executor.md | 42 +++++------ packages/sdk/docs/cli/executor.template.md | 4 +- packages/sdk/docs/cli/function.md | 24 +++--- packages/sdk/docs/cli/organization.md | 22 +++--- packages/sdk/docs/cli/query.md | 2 +- packages/sdk/docs/cli/secret.md | 18 ++--- packages/sdk/docs/cli/setup.md | 4 +- packages/sdk/docs/cli/skills.md | 12 +-- packages/sdk/docs/cli/staticwebsite.md | 26 +++---- .../sdk/docs/cli/staticwebsite.template.md | 12 +-- packages/sdk/docs/cli/tailordb.md | 44 +++++------ packages/sdk/docs/cli/tailordb.template.md | 18 ++--- packages/sdk/docs/cli/upgrade.md | 4 +- packages/sdk/docs/cli/upgrade.template.md | 2 +- packages/sdk/docs/cli/user.md | 22 +++--- packages/sdk/docs/cli/workflow.md | 44 +++++------ packages/sdk/docs/cli/workflow.template.md | 24 +++--- packages/sdk/docs/cli/workspace.md | 38 +++++----- packages/sdk/docs/github-actions.md | 36 ++++----- packages/sdk/docs/migration/v2.md | 73 +++++++++++++------ packages/sdk/docs/multi-environment.md | 10 +-- packages/sdk/docs/plugin/index.md | 12 +-- packages/sdk/docs/quickstart.md | 10 +-- packages/sdk/docs/services/auth.md | 44 +++++------ packages/sdk/docs/services/resolver.md | 2 +- packages/sdk/docs/services/secret.md | 22 +++--- packages/sdk/docs/services/staticwebsite.md | 2 +- .../sdk/docs/services/tailordb-migration.md | 44 +++++------ packages/sdk/docs/services/tailordb.md | 4 +- packages/sdk/docs/services/workflow.md | 12 +-- packages/sdk/e2e/deploy.test.ts | 2 +- packages/sdk/e2e/function-test-run.test.ts | 2 +- packages/sdk/e2e/globalSetup.ts | 2 +- packages/sdk/e2e/migration.test.ts | 2 +- packages/sdk/package.json | 4 +- packages/sdk/scripts/perf/runtime-perf.sh | 6 +- packages/sdk/src/cli/commands/api/index.ts | 2 +- packages/sdk/src/cli/commands/api/inspect.ts | 4 +- .../sdk/src/cli/commands/api/proto-reflect.ts | 2 +- .../cli/commands/authconnection/authorize.ts | 4 +- .../src/cli/commands/crashreport/send.test.ts | 2 +- .../deploy/config-id-ci-guard.test.ts | 2 +- .../cli/commands/deploy/config-id-injector.ts | 2 +- .../sdk/src/cli/commands/deploy/confirm.ts | 8 +- .../sdk/src/cli/commands/deploy/deploy.ts | 2 +- .../src/cli/commands/deploy/executor.test.ts | 2 +- .../cli/commands/deploy/secrets-state.test.ts | 2 +- .../src/cli/commands/deploy/tailordb/index.ts | 8 +- .../sdk/src/cli/commands/executor/list.ts | 2 +- .../sdk/src/cli/commands/executor/webhook.ts | 4 +- .../sdk/src/cli/commands/function/test-run.ts | 4 +- .../generate/plugin-executor-generator.ts | 4 +- .../generate/plugin-type-generator.ts | 2 +- .../src/cli/commands/generate/seed/bundler.ts | 2 +- .../sdk/src/cli/commands/machineuser/token.ts | 2 +- packages/sdk/src/cli/commands/profile/list.ts | 2 +- packages/sdk/src/cli/commands/setup/check.ts | 6 +- .../src/cli/commands/setup/generate.test.ts | 2 +- .../sdk/src/cli/commands/setup/generate.ts | 4 +- .../sdk/src/cli/commands/setup/lock.test.ts | 8 +- packages/sdk/src/cli/commands/setup/lock.ts | 8 +- .../sdk/src/cli/commands/setup/templates.ts | 6 +- .../src/cli/commands/skills/install.test.ts | 2 +- .../sdk/src/cli/commands/skills/install.ts | 2 +- .../src/cli/commands/tailordb/erd/export.ts | 2 +- .../src/cli/commands/tailordb/erd/schema.ts | 2 +- .../src/cli/commands/tailordb/erd/serve.ts | 4 +- .../src/cli/commands/tailordb/erd/types.ts | 2 +- .../cli/commands/tailordb/erd/viewer.test.ts | 2 +- .../cli/commands/tailordb/migrate/bundler.ts | 2 +- .../cli/commands/tailordb/migrate/generate.ts | 4 +- .../cli/commands/tailordb/migrate/script.ts | 2 +- .../src/cli/commands/tailordb/migrate/sync.ts | 6 +- .../tailordb/migrate/template-generator.ts | 2 +- packages/sdk/src/cli/commands/user/current.ts | 4 +- packages/sdk/src/cli/commands/user/list.ts | 2 +- .../sdk/src/cli/commands/user/pat/create.ts | 2 +- .../sdk/src/cli/commands/user/pat/delete.ts | 2 +- .../sdk/src/cli/commands/user/pat/list.ts | 4 +- .../sdk/src/cli/commands/user/pat/update.ts | 2 +- packages/sdk/src/cli/commands/user/switch.ts | 2 +- .../sdk/src/cli/commands/workflow/start.ts | 2 +- .../sdk/src/cli/commands/workspace/create.ts | 2 +- packages/sdk/src/cli/completion.test.ts | 4 +- packages/sdk/src/cli/crashreport/index.ts | 2 +- .../sdk/src/cli/crashreport/sanitize.test.ts | 32 ++++---- .../sdk/src/cli/crashreport/sender.test.ts | 24 +++--- .../sdk/src/cli/crashreport/writer.test.ts | 8 +- packages/sdk/src/cli/index.ts | 2 +- packages/sdk/src/cli/query/errors.ts | 2 +- packages/sdk/src/cli/query/index.ts | 2 +- .../sdk/src/cli/services/executor/service.ts | 2 +- packages/sdk/src/cli/shared/context.ts | 14 ++-- packages/sdk/src/cli/shared/dist-dir.ts | 2 +- packages/sdk/src/cli/shared/errors.ts | 2 +- packages/sdk/src/cli/shared/readonly-guard.ts | 2 +- .../sdk/src/cli/shared/skills-installer.ts | 6 +- .../sdk/src/cli/shared/stack-trace.test.ts | 6 +- packages/sdk/src/cli/shared/stack-trace.ts | 2 +- packages/sdk/src/cli/shared/type-generator.ts | 2 +- packages/sdk/src/cli/shared/user-agent.ts | 2 +- packages/sdk/src/cli/telemetry/index.ts | 4 +- packages/sdk/src/cli/telemetry/interceptor.ts | 2 +- .../sdk/src/configure/services/auth/types.ts | 2 +- .../configure/services/tailordb/permission.ts | 2 +- packages/sdk/src/configure/types/idp-name.ts | 2 +- packages/sdk/src/plugin/manager.ts | 2 +- skills/{tailor-sdk => tailor}/SKILL.md | 2 +- 200 files changed, 841 insertions(+), 660 deletions(-) create mode 100644 .changeset/rename-bin-command.md create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-package-json/expected.json create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-package-json/input.json create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-yaml/expected.yml create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-yaml/input.yml create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/no-match/input.sh create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh rename packages/sdk/agent-skills/{tailor-sdk => tailor}/SKILL.md (98%) rename skills/{tailor-sdk => tailor}/SKILL.md (98%) diff --git a/.agents/skills/e2e-setup/SKILL.md b/.agents/skills/e2e-setup/SKILL.md index 20fa0ce28..d9bbd50b4 100644 --- a/.agents/skills/e2e-setup/SKILL.md +++ b/.agents/skills/e2e-setup/SKILL.md @@ -2,7 +2,7 @@ name: e2e-setup description: > Set up the environment and run e2e tests in this repo (`example/e2e` and `packages/sdk/e2e`). - Covers tailor-sdk authentication, workspace selection, and re-deploying `example/` when + Covers tailor authentication, workspace selection, and re-deploying `example/` when the deployed app drifts from the current code. Use when running e2e tests locally, fixing e2e failures, or when a run errors with "Failed to refresh token", "Workspace ID not found", or mismatched counts/fields in resolver/workflow assertions. @@ -50,7 +50,7 @@ set -a; source .agents/skills/e2e-setup/ids.local.env; set +a **Sanity check on `TAILOR_PLATFORM_WORKSPACE_ID`.** Before running `example/e2e`, confirm the workspace still has `my-app` deployed: ``` -pnpm exec tailor-sdk workspace app list --workspace-id "$TAILOR_PLATFORM_WORKSPACE_ID" +pnpm exec tailor workspace app list --workspace-id "$TAILOR_PLATFORM_WORKSPACE_ID" ``` If it is gone, re-deploy per [Pre-deploy](#one-time-setup) step 4. @@ -63,18 +63,18 @@ The tests assert against the **deployed** state of `example/` (resolver count, w Only needed when `ids.local.env` has no `TAILOR_PLATFORM_WORKSPACE_ID` yet, or the saved workspace was deleted / no longer hosts `my-app`. Write the resolved workspace ID back to `ids.local.env` when finished. -1. Make sure you are logged in. `tailor-sdk login` opens a browser — **only the user can run it**; the agent must ask. Verify with `pnpm exec tailor-sdk workspace list` (errors with `Tailor Platform token not found.` if unauthenticated). +1. Make sure you are logged in. `tailor login` opens a browser — **only the user can run it**; the agent must ask. Verify with `pnpm exec tailor workspace list` (errors with `Tailor Platform token not found.` if unauthenticated). 2. Look up the organization and folder you want the workspace to live under. The `workspace create` flags below need both IDs: ``` - pnpm exec tailor-sdk organization list - pnpm exec tailor-sdk organization folder list --organization-id + pnpm exec tailor organization list + pnpm exec tailor organization folder list --organization-id # Optional: drill into a sub-folder - pnpm exec tailor-sdk organization folder list --organization-id --parent-folder-id + pnpm exec tailor organization folder list --organization-id --parent-folder-id ``` 3. Pick or create a personal workspace. Suggested name: `example-e2e` (descriptive of purpose; fall back to a personal prefix only if it collides under the same folder). The agent must not invent a name — use the suggestion or ask the user: ``` - pnpm exec tailor-sdk workspace list - pnpm exec tailor-sdk workspace create --name example-e2e --region asia-northeast --organization-id --folder-id + pnpm exec tailor workspace list + pnpm exec tailor workspace create --name example-e2e --region asia-northeast --organization-id --folder-id ``` 4. Deploy `example/` into it once: ``` @@ -83,7 +83,7 @@ Only needed when `ids.local.env` has no `TAILOR_PLATFORM_WORKSPACE_ID` yet, or t Use `run deploy`, not bare `deploy` — pnpm has a builtin `deploy` command that shadows the package script when invoked via `--filter`. 5. Confirm `my-app` is present: ``` - pnpm exec tailor-sdk workspace app list --workspace-id + pnpm exec tailor workspace app list --workspace-id ``` Optionally save the workspace as a profile (`~/.config/tailor-platform/config.yaml`) so `TAILOR_PLATFORM_PROFILE=` alone is enough. @@ -134,9 +134,9 @@ The script uses `loadAccessToken()` so it works with keyring/config credentials | Error | Cause | Fix | | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | -| `Failed to refresh token. Your session may have expired.` | The refresh token in `~/.config/tailor-platform/config.yaml` is expired. | `pnpm exec tailor-sdk login` (interactive; only the user can run this — the agent should ask). | +| `Failed to refresh token. Your session may have expired.` | The refresh token in `~/.config/tailor-platform/config.yaml` is expired. | `pnpm exec tailor login` (interactive; only the user can run this — the agent should ask). | | `Workspace ID not found.` | No `--workspace-id`, no `TAILOR_PLATFORM_WORKSPACE_ID`, no profile set. | Set `TAILOR_PLATFORM_WORKSPACE_ID` or `TAILOR_PLATFORM_PROFILE`. | -| `Application my-app does not have an auth configuration.` / `Machine user manager-machine-user not found.` | Wrong workspace selected, or `example/` was never deployed there. | Confirm with `tailor-sdk workspace app list`; deploy if missing. | +| `Application my-app does not have an auth configuration.` / `Machine user manager-machine-user not found.` | Wrong workspace selected, or `example/` was never deployed there. | Confirm with `tailor workspace app list`; deploy if missing. | | `TAILOR_PLATFORM_ORGANIZATION_ID` / `..._FOLDER_ID` unset (in `packages/sdk/e2e`) | The suite needs these to create workspaces. | `source .agents/skills/e2e-setup/ids.local.env` (or follow the fallback in [Stored IDs](#stored-ids-idslocalenv) to populate it). | ## When the user reports an e2e failure diff --git a/.agents/skills/llm-challenge/CREATING_PROBLEMS.md b/.agents/skills/llm-challenge/CREATING_PROBLEMS.md index 8fd74097d..dcdfaa96e 100644 --- a/.agents/skills/llm-challenge/CREATING_PROBLEMS.md +++ b/.agents/skills/llm-challenge/CREATING_PROBLEMS.md @@ -18,7 +18,7 @@ Rules: - `verify.json`, when present, contains visible minimum-correctness checks only. Checks should encode conditions where missing evidence is definitely wrong, similar to type checking; do not put ideal implementations, hidden answers, scores, or broad quality judgments there. - Write `prompt.md` in English. - For `sdk-api`, do not include SDK API names, imports, code examples, or direct solution hints. -- For `cli`, the prompt may name the `tailor-sdk` binary, but must not name the target subcommand or exact arguments. +- For `cli`, the prompt may name the `tailor` binary, but must not name the target subcommand or exact arguments. - Keep `scaffold/` minimal and runnable enough for the task. Do not add `solution/`, evaluator tests, scoring metadata, or hidden hints. Validate discovery and focused behavior with narrow tests or a targeted dry run when practical. diff --git a/.changeset/rename-bin-command.md b/.changeset/rename-bin-command.md new file mode 100644 index 000000000..d696f409c --- /dev/null +++ b/.changeset/rename-bin-command.md @@ -0,0 +1,14 @@ +--- +"@tailor-platform/sdk": major +"@tailor-platform/sdk-codemod": patch +--- + +Rename the CLI binary from `tailor-sdk` to `tailor`. + +The output directory default changes from `.tailor-sdk` to `.tailor`, and the GitHub Actions lock file path changes from `.github/tailor-sdk.lock` to `.github/tailor.lock`. + +Run the `v2/rename-bin` codemod to migrate `tailor-sdk` invocations in package.json scripts, shell scripts, CI workflows, and documentation: + +```sh +npx @tailor-platform/sdk-codemod --from 1.x --to 2.0.0 +``` diff --git a/.github/workflows/cleanup-e2e-workspaces.yml b/.github/workflows/cleanup-e2e-workspaces.yml index f2fc84c95..ae178e807 100644 --- a/.github/workflows/cleanup-e2e-workspaces.yml +++ b/.github/workflows/cleanup-e2e-workspaces.yml @@ -35,7 +35,7 @@ jobs: uses: ./.github/actions/install-deps - name: Login as machine user - run: pnpm tailor-sdk login --machine-user + run: pnpm tailor login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} @@ -43,7 +43,7 @@ jobs: - name: List e2e workspaces working-directory: packages/sdk run: | - pnpm --silent tailor-sdk workspace list --json > /tmp/workspaces.json + pnpm --silent tailor workspace list --json > /tmp/workspaces.json jq -r '.[] | select(.name | test("^(e2e-ws-|template-e2e-|sdk-ci-)")) | "\(.id)\t\(.name)"' \ /tmp/workspaces.json > /tmp/e2e-workspaces.tsv echo "e2e workspace candidates:" @@ -91,7 +91,7 @@ jobs: echo "dry $name (run $run_id status=$status)" else echo "delete $name (run $run_id status=$status)" - if pnpm --silent tailor-sdk workspace delete --workspace-id "$id" --yes; then + if pnpm --silent tailor workspace delete --workspace-id "$id" --yes; then deleted=$((deleted + 1)) else failed=$((failed + 1)) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7377434b1..5e8c58895 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -73,7 +73,7 @@ jobs: - name: Login as machine user if: runner.os != 'Windows' - run: pnpm tailor-sdk login --machine-user + run: pnpm tailor login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} @@ -81,7 +81,7 @@ jobs: - name: Create workspace if: runner.os != 'Windows' working-directory: example - run: pnpm --silent tailor-sdk workspace create --name "sdk-ci-${RUN_ID}-${SLUG}" --region asia-northeast --json > workspace.json + run: pnpm --silent tailor workspace create --name "sdk-ci-${RUN_ID}-${SLUG}" --region asia-northeast --json > workspace.json env: RUN_ID: ${{ github.run_id }} SLUG: ${{ matrix.slug }} @@ -111,7 +111,7 @@ jobs: working-directory: example shell: bash run: | - STATUS=$(pnpm --silent tailor-sdk tailordb migration status --namespace tailordb) + STATUS=$(pnpm --silent tailor tailordb migration status --namespace tailordb) echo "$STATUS" # Check for pending migrations @@ -143,7 +143,7 @@ jobs: - name: Destroy workspace if: always() && runner.os != 'Windows' working-directory: example - run: pnpm tailor-sdk workspace delete --workspace-id "$TAILOR_PLATFORM_WORKSPACE_ID" --yes + run: pnpm tailor workspace delete --workspace-id "$TAILOR_PLATFORM_WORKSPACE_ID" --yes deploy-result: name: deploy-result # required status check context (ruleset status_check_main) — keep equal to the job id diff --git a/.github/workflows/docs-consistency-check.yml b/.github/workflows/docs-consistency-check.yml index 72f8c4441..375482449 100644 --- a/.github/workflows/docs-consistency-check.yml +++ b/.github/workflows/docs-consistency-check.yml @@ -111,7 +111,7 @@ jobs: For changed user-facing docs, check that they describe only what an SDK *user* needs and do not leak SDK/Platform internals. Flag the following: - - **Internal implementation details**: internal API / RPC / method / class names (e.g. `TestExecScript`), transport or wire-format terms the user never types (`proto` / `protobuf` / `gRPC` / `unary RPC` / `streaming method`), internal layer structure (`parser` / `configure` / `types` modules), runtime-internal delegation mechanics, internal wrapping behavior, internal on-disk layout or sanitization algorithms (e.g. how `.tailor-sdk/` names are derived), and AST / code-injection internals. + - **Internal implementation details**: internal API / RPC / method / class names (e.g. `TestExecScript`), transport or wire-format terms the user never types (`proto` / `protobuf` / `gRPC` / `unary RPC` / `streaming method`), internal layer structure (`parser` / `configure` / `types` modules), runtime-internal delegation mechanics, internal wrapping behavior, internal on-disk layout or sanitization algorithms (e.g. how `.tailor/` names are derived), and AST / code-injection internals. - **JSDoc duplication (SSOT)**: detailed API reference — full interface/type definitions, exhaustive field tables — copied into prose. JSDoc/IDE autocompletion is the single source of truth; the docs should explain intent and link out, not mirror every field. - **Private references**: links to or mentions of private/internal repositories or internal issue trackers (e.g. a `/` link or an `/#NNN` reference). - **Wrong-audience notes**: content addressed to someone other than the reader (e.g. a "Note for Platform developers" block inside user docs). diff --git a/.github/workflows/erd-viewer-preview.yml b/.github/workflows/erd-viewer-preview.yml index b6f21a587..e7896ad74 100644 --- a/.github/workflows/erd-viewer-preview.yml +++ b/.github/workflows/erd-viewer-preview.yml @@ -34,7 +34,7 @@ jobs: with: persist-credentials: false - # install-deps builds the SDK once, so the `tailor-sdk` CLI bin resolves. + # install-deps builds the SDK once, so the `tailor` CLI bin resolves. - name: Install deps uses: ./.github/actions/install-deps @@ -47,7 +47,7 @@ jobs: env: NAMESPACE: ${{ matrix.namespace }} run: | - pnpm exec tailor-sdk tailordb erd export --namespace "$NAMESPACE" --output "$RUNNER_TEMP/erd-export" + pnpm exec tailor tailordb erd export --namespace "$NAMESPACE" --output "$RUNNER_TEMP/erd-export" mkdir -p "$RUNNER_TEMP/erd-preview" cp "$RUNNER_TEMP/erd-export/$NAMESPACE/dist/index.html" "$RUNNER_TEMP/erd-preview/$NAMESPACE.html" diff --git a/.github/workflows/migration.yml b/.github/workflows/migration.yml index f92c721d6..f8dffb0ba 100644 --- a/.github/workflows/migration.yml +++ b/.github/workflows/migration.yml @@ -56,14 +56,14 @@ jobs: uses: ./.github/actions/install-deps - name: Login as machine user - run: pnpm tailor-sdk login --machine-user + run: pnpm tailor login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} - name: Create workspace working-directory: example - run: pnpm --silent tailor-sdk workspace create --name "sdk-ci-migration-${RUN_ID}" --region asia-northeast --json > workspace.json + run: pnpm --silent tailor workspace create --name "sdk-ci-migration-${RUN_ID}" --region asia-northeast --json > workspace.json env: RUN_ID: ${{ github.run_id }} TAILOR_PLATFORM_ORGANIZATION_ID: ${{ secrets.TAILOR_PLATFORM_ORGANIZATION_ID }} @@ -83,7 +83,7 @@ jobs: - name: Destroy workspace if: always() working-directory: example - run: pnpm tailor-sdk workspace delete --workspace-id "$TAILOR_PLATFORM_WORKSPACE_ID" --yes + run: pnpm tailor workspace delete --workspace-id "$TAILOR_PLATFORM_WORKSPACE_ID" --yes migration-result: name: Migration result diff --git a/.github/workflows/sdk-e2e.yml b/.github/workflows/sdk-e2e.yml index c3cd3fd5c..732eada9d 100644 --- a/.github/workflows/sdk-e2e.yml +++ b/.github/workflows/sdk-e2e.yml @@ -55,7 +55,7 @@ jobs: uses: ./.github/actions/install-deps - name: Login as machine user - run: pnpm tailor-sdk login --machine-user + run: pnpm tailor login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} @@ -94,7 +94,7 @@ jobs: uses: ./.github/actions/install-deps - name: Login as machine user - run: pnpm tailor-sdk login --machine-user + run: pnpm tailor login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} diff --git a/.github/workflows/sdk-metrics.yml b/.github/workflows/sdk-metrics.yml index 6323f05be..babdb977b 100644 --- a/.github/workflows/sdk-metrics.yml +++ b/.github/workflows/sdk-metrics.yml @@ -40,7 +40,7 @@ jobs: uses: ./.github/actions/install-deps - name: Login as machine user - run: pnpm tailor-sdk login --machine-user + run: pnpm tailor login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} @@ -82,7 +82,7 @@ jobs: uses: ./.github/actions/install-deps - name: Login as machine user - run: pnpm tailor-sdk login --machine-user + run: pnpm tailor login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d5988b8ed..dd4511b0b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,7 +66,7 @@ jobs: node-version: ${{ matrix.node }} - name: Login as machine user - run: pnpm tailor-sdk login --machine-user + run: pnpm tailor login --machine-user env: TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} diff --git a/.gitignore b/.gitignore index 9ec659334..84e2f6a68 100644 --- a/.gitignore +++ b/.gitignore @@ -32,7 +32,7 @@ coverage/ !.vscode/extensions.json # Tailor Platform SDK -**/.tailor-sdk/** +**/.tailor/** tailor.d.ts !packages/create-sdk/templates/*/tailor.d.ts @@ -46,7 +46,7 @@ tailor.d.ts .claude/skills/* !.claude/skills/llm-challenge !.claude/skills/docs-check -!.claude/skills/tailor-sdk +!.claude/skills/tailor !.claude/skills/e2e-setup AGENTS.local.md CLAUDE.local.md diff --git a/.oxfmtrc.json b/.oxfmtrc.json index ad8e59aac..8c3014a43 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -22,7 +22,7 @@ "pnpm-debug.log*", "lerna-debug.log*", "coverage/", - ".tailor-sdk", + ".tailor", "example/tests/fixtures/", "example/seed/", "packages/tailor-proto/", diff --git a/docs/telemetry.md b/docs/telemetry.md index 499f2c845..23c8e3e44 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -27,12 +27,12 @@ docker run -d --name jaeger-otlp \ ```bash cd example -OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 pnpm tailor-sdk deploy --dry-run +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 pnpm tailor deploy --dry-run ``` ### 3. View traces -Open http://localhost:16686, select service **tailor-sdk**, and click **Find Traces**. +Open http://localhost:16686, select service **tailor**, and click **Find Traces**. ## Span Hierarchy @@ -78,7 +78,7 @@ Individual RPC calls are also traced as `rpc.*` child spans (e.g., `rpc.CreateAp ### List spans sorted by duration ```bash -curl -s "http://localhost:16686/api/traces?service=tailor-sdk&limit=1" | jq ' +curl -s "http://localhost:16686/api/traces?service=tailor&limit=1" | jq ' .data[0].spans[] | {operationName, duration_ms: (.duration / 1000 | . * 100 | round / 100)} ' | jq -s 'sort_by(-.duration_ms)' @@ -87,7 +87,7 @@ curl -s "http://localhost:16686/api/traces?service=tailor-sdk&limit=1" | jq ' ### Show span hierarchy with parent info ```bash -curl -s "http://localhost:16686/api/traces?service=tailor-sdk&limit=1" | jq ' +curl -s "http://localhost:16686/api/traces?service=tailor&limit=1" | jq ' .data[0] as $trace | $trace.spans | map({ operationName, @@ -100,7 +100,7 @@ curl -s "http://localhost:16686/api/traces?service=tailor-sdk&limit=1" | jq ' ### Compare two traces (before/after) ```bash -curl -s "http://localhost:16686/api/traces?service=tailor-sdk&limit=2" | jq ' +curl -s "http://localhost:16686/api/traces?service=tailor&limit=2" | jq ' [.data[] | { traceID: .traceID, spans: [.spans[] diff --git a/example/.gitignore b/example/.gitignore index 045303397..45cfacc3b 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -7,7 +7,7 @@ build/ *.tsbuildinfo # Tailor Platform SDK generated files -.tailor-sdk/ +.tailor/ # Test fixtures (keep expected/ only) tests/fixtures/* diff --git a/example/.oxlintrc.json b/example/.oxlintrc.json index 9bf159a66..b956ae6f6 100644 --- a/example/.oxlintrc.json +++ b/example/.oxlintrc.json @@ -8,7 +8,7 @@ "builtin": true }, "ignorePatterns": [ - ".tailor-sdk/", + ".tailor/", "generated-perf", "generated/", "migrations", diff --git a/example/e2e/utils.ts b/example/e2e/utils.ts index 7f70ec2ee..7e85a97a9 100644 --- a/example/e2e/utils.ts +++ b/example/e2e/utils.ts @@ -23,7 +23,7 @@ function userAgentInterceptor(): Interceptor { return await next(req); } - req.header.set("User-Agent", "tailor-sdk-ci"); + req.header.set("User-Agent", "tailor-ci"); return await next(req); }; } diff --git a/example/migrations/0003/migrate.ts b/example/migrations/0003/migrate.ts index ba45b71dd..8b6518888 100644 --- a/example/migrations/0003/migrate.ts +++ b/example/migrations/0003/migrate.ts @@ -2,7 +2,7 @@ * Migration script for tailordb * * This script runs between the Pre-migration and Post-migration phases of - * 'tailor-sdk deploy'. Use it to transform existing data so that the schema + * 'tailor deploy'. Use it to transform existing data so that the schema * change can complete safely (for breaking changes, this is hard-required; * for warning-tier changes it is optional). Edit this file to implement * your data migration logic. diff --git a/example/migrations/analyticsdb/0001/migrate.ts b/example/migrations/analyticsdb/0001/migrate.ts index 072eda32b..43f459e54 100644 --- a/example/migrations/analyticsdb/0001/migrate.ts +++ b/example/migrations/analyticsdb/0001/migrate.ts @@ -2,7 +2,7 @@ * Migration script for analyticsdb * * This script runs between the Pre-migration and Post-migration phases of - * 'tailor-sdk deploy'. Use it to transform existing data so that the schema + * 'tailor deploy'. Use it to transform existing data so that the schema * change can complete safely (for breaking changes, this is hard-required; * for warning-tier changes it is optional). Edit this file to implement * your data migration logic. diff --git a/example/package.json b/example/package.json index 2b4ba14eb..19cbd5dd1 100644 --- a/example/package.json +++ b/example/package.json @@ -5,9 +5,9 @@ "license": "MIT", "type": "module", "scripts": { - "generate": "tailor-sdk generate -c tailor.config.ts", - "generate:watch": "tailor-sdk generate -c tailor.config.ts --watch", - "deploy": "tailor-sdk deploy -c tailor.config.ts", + "generate": "tailor generate -c tailor.config.ts", + "generate:watch": "tailor generate -c tailor.config.ts --watch", + "deploy": "tailor deploy -c tailor.config.ts", "test": "pnpm test:generator", "test:all": "pnpm test:generator:prepare && vitest", "test:generator": "pnpm test:generator:prepare && vitest --project generator", @@ -16,7 +16,7 @@ "test:e2e": "vitest --project e2e", "migration:e2e": "tsx tests/scripts/migration_e2e.ts", "seed:validate": "node ./seed/exec.mjs validate", - "seed:truncate": "tailor-sdk tailordb truncate -a", + "seed:truncate": "tailor tailordb truncate -a", "seed": "node ./seed/exec.mjs", "analyze:bundle": "tsx tests/scripts/analyze_minified_size.ts", "lint": "oxlint --type-aware .", diff --git a/example/tests/invoker.types.ts b/example/tests/invoker.types.ts index 6869f92c5..07b31daad 100644 --- a/example/tests/invoker.types.ts +++ b/example/tests/invoker.types.ts @@ -1,4 +1,4 @@ -// Type-level checks against the generated `tailor.d.ts`. Once `tailor-sdk +// Type-level checks against the generated `tailor.d.ts`. Once `tailor // generate` augments `MachineUserNameRegistry`, `MachineUserName` narrows to the // registered machine user union for both SDK entries — `@tailor-platform/sdk` // (resolver `invoker`) and `@tailor-platform/sdk/cli` (workflow-start diff --git a/example/tests/scripts/migration_e2e.ts b/example/tests/scripts/migration_e2e.ts index 8e99aa387..2d0586324 100644 --- a/example/tests/scripts/migration_e2e.ts +++ b/example/tests/scripts/migration_e2e.ts @@ -33,7 +33,7 @@ const tailorSdkBin = path.resolve( exampleDir, "node_modules", ".bin", - process.platform === "win32" ? "tailor-sdk.cmd" : "tailor-sdk", + process.platform === "win32" ? "tailor.cmd" : "tailor", ); const runTailorSdk = (args: string[]) => { diff --git a/lefthook.yml b/lefthook.yml index 004b68c5b..454d4752a 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -19,7 +19,7 @@ pre-commit: - "pnpm-debug.log*" - "lerna-debug.log*" - "coverage/**" - - ".tailor-sdk/**" + - ".tailor/**" - "example/tests/fixtures/**" - "example/generated/**" - "example/seed/**" diff --git a/llm-challenge/problems/cli/generate/prompt.md b/llm-challenge/problems/cli/generate/prompt.md index 4a2b3852c..66b266b5e 100644 --- a/llm-challenge/problems/cli/generate/prompt.md +++ b/llm-challenge/problems/cli/generate/prompt.md @@ -1,4 +1,4 @@ -Use the local `tailor-sdk` binary available after installing dependencies. +Use the local `tailor` binary available after installing dependencies. Set up a minimal Tailor SDK project for a task list application, then use the CLI to produce the generated project artifacts. Do not rely on a globally installed SDK. diff --git a/llm-challenge/problems/cli/help-error-recovery/prompt.md b/llm-challenge/problems/cli/help-error-recovery/prompt.md index 51b2ed798..54692179a 100644 --- a/llm-challenge/problems/cli/help-error-recovery/prompt.md +++ b/llm-challenge/problems/cli/help-error-recovery/prompt.md @@ -1,4 +1,4 @@ -Use the local `tailor-sdk` binary available after installing dependencies. +Use the local `tailor` binary available after installing dependencies. Start by using the CLI help and error output to discover how to perform a simple project maintenance action. Then run the corrected command and leave a short note in the workspace describing what you ran and why. diff --git a/llm-challenge/problems/cli/help-error-recovery/verify.json b/llm-challenge/problems/cli/help-error-recovery/verify.json index e2360bb18..a6538fb54 100644 --- a/llm-challenge/problems/cli/help-error-recovery/verify.json +++ b/llm-challenge/problems/cli/help-error-recovery/verify.json @@ -12,7 +12,7 @@ "id": "note-names-local-cli", "kind": "content-match", "glob": "**/*.md", - "pattern": "tailor-sdk|pnpm", + "pattern": "tailor|pnpm", "flags": "i", "description": "maintenance note names the local CLI command path" } diff --git a/llm-challenge/problems/cli/tailordb-migrate-generate/prompt.md b/llm-challenge/problems/cli/tailordb-migrate-generate/prompt.md index 98b7366b6..96d083d68 100644 --- a/llm-challenge/problems/cli/tailordb-migrate-generate/prompt.md +++ b/llm-challenge/problems/cli/tailordb-migrate-generate/prompt.md @@ -1,4 +1,4 @@ -Use the local `tailor-sdk` binary available after installing dependencies. +Use the local `tailor` binary available after installing dependencies. Create a minimal TailorDB model change for a customer directory, then use the CLI to create the migration artifacts needed for that change. Discover the necessary command shape from the CLI itself rather than assuming a global tool. diff --git a/llm-challenge/problems/cli/tailordb-migrate-script/prompt.md b/llm-challenge/problems/cli/tailordb-migrate-script/prompt.md index 655b902d4..ba9003835 100644 --- a/llm-challenge/problems/cli/tailordb-migrate-script/prompt.md +++ b/llm-challenge/problems/cli/tailordb-migrate-script/prompt.md @@ -1,4 +1,4 @@ -Use the local `tailor-sdk` binary available after installing dependencies. +Use the local `tailor` binary available after installing dependencies. Create a minimal TailorDB schema evolution for an inventory item and use the CLI to produce the executable migration script for it. Discover the needed command shape from local help and project files. diff --git a/llm-challenge/src/challenge.test.ts b/llm-challenge/src/challenge.test.ts index 01fb9b8b2..dcf1f33cc 100644 --- a/llm-challenge/src/challenge.test.ts +++ b/llm-challenge/src/challenge.test.ts @@ -296,12 +296,12 @@ describe("artifact summary", () => { await fs.mkdir(path.join(worktreePath, "src"), { recursive: true }); await fs.mkdir(path.join(worktreePath, "node_modules/pkg"), { recursive: true }); await fs.mkdir(path.join(worktreePath, ".pnpm-home/store"), { recursive: true }); - await fs.mkdir(path.join(worktreePath, ".tailor-sdk/cache"), { recursive: true }); + await fs.mkdir(path.join(worktreePath, ".tailor/cache"), { recursive: true }); await fs.mkdir(path.join(worktreePath, ".turbo/cache"), { recursive: true }); await fs.writeFile(path.join(worktreePath, "src/app.ts"), "export {};\n"); await fs.writeFile(path.join(worktreePath, "node_modules/pkg/index.js"), ""); await fs.writeFile(path.join(worktreePath, ".pnpm-home/store/index.db"), ""); - await fs.writeFile(path.join(worktreePath, ".tailor-sdk/cache/generated.json"), "{}"); + await fs.writeFile(path.join(worktreePath, ".tailor/cache/generated.json"), "{}"); await fs.writeFile(path.join(worktreePath, ".turbo/cache/state.json"), "{}"); await runCommand("git", ["init"], { cwd: worktreePath }); @@ -371,7 +371,7 @@ describe("artifact summary", () => { expect(summary.files).toContain("src/app.ts"); expect(summary.files).not.toContain("node_modules/pkg/index.js"); expect(summary.files).not.toContain(".pnpm-home/store/index.db"); - expect(summary.files).not.toContain(".tailor-sdk/cache/generated.json"); + expect(summary.files).not.toContain(".tailor/cache/generated.json"); expect(summary.files).not.toContain(".turbo/cache/state.json"); expect(summary.gitStatus).toContain("?? src/app.ts"); expect(summary.commands.map((command) => command.command)).toEqual([ @@ -563,9 +563,9 @@ describe("verification summary", () => { const problemRoot = path.join(dir, "problem"); const worktreePath = path.join(dir, "work"); await fs.mkdir(path.join(problemRoot, "scaffold"), { recursive: true }); - await fs.mkdir(path.join(worktreePath, ".tailor-sdk/cache"), { recursive: true }); + await fs.mkdir(path.join(worktreePath, ".tailor/cache"), { recursive: true }); await fs.writeFile(path.join(worktreePath, "package.json"), "{}\n"); - await fs.writeFile(path.join(worktreePath, ".tailor-sdk/cache/generated.ts"), "cacheOnly\n"); + await fs.writeFile(path.join(worktreePath, ".tailor/cache/generated.ts"), "cacheOnly\n"); const verifyPath = path.join(problemRoot, "verify.json"); await fs.writeFile( verifyPath, @@ -697,7 +697,7 @@ describe("workspace preparation", () => { const dir = await makeTempDir(); const worktreePath = path.join(dir, "work"); await Promise.all( - ["node_modules", ".pnpm-store", ".pnpm-home", ".cache", ".turbo", ".tailor-sdk/cache"].map( + ["node_modules", ".pnpm-store", ".pnpm-home", ".cache", ".turbo", ".tailor/cache"].map( (name) => fs.mkdir(path.join(worktreePath, name), { recursive: true }), ), ); @@ -710,7 +710,7 @@ describe("workspace preparation", () => { ".pnpm-home", ".cache", ".turbo", - ".tailor-sdk/cache", + ".tailor/cache", ]) { await expect(fs.access(path.join(worktreePath, name))).rejects.toThrow("ENOENT"); } diff --git a/llm-challenge/src/workspace-files.ts b/llm-challenge/src/workspace-files.ts index 1d5725f03..b70631e59 100644 --- a/llm-challenge/src/workspace-files.ts +++ b/llm-challenge/src/workspace-files.ts @@ -11,7 +11,7 @@ const EXCLUDED_DIRS = new Set([ ".turbo", "node_modules", ]); -const EXCLUDED_PATHS = new Set([".tailor-sdk/cache"]); +const EXCLUDED_PATHS = new Set([".tailor/cache"]); /** * Recursively list workspace files as posix-style paths relative to diff --git a/llm-challenge/src/workspace.ts b/llm-challenge/src/workspace.ts index c050be820..f575e7ad1 100644 --- a/llm-challenge/src/workspace.ts +++ b/llm-challenge/src/workspace.ts @@ -43,7 +43,7 @@ const GITIGNORE_PATTERNS = [ ".pnpm-store/", ".pnpm-home/", ".cache/", - ".tailor-sdk/cache/", + ".tailor/cache/", ]; export async function prepareWorkspace(options: { @@ -82,8 +82,8 @@ export function profileForProblem( export async function pruneWorkspaceDeps(worktreePath: string): Promise { await Promise.all( - ["node_modules", ".pnpm-store", ".pnpm-home", ".cache", ".turbo", ".tailor-sdk/cache"].map( - (name) => fs.rm(path.join(worktreePath, name), { recursive: true, force: true }), + ["node_modules", ".pnpm-store", ".pnpm-home", ".cache", ".turbo", ".tailor/cache"].map((name) => + fs.rm(path.join(worktreePath, name), { recursive: true, force: true }), ), ); } diff --git a/oxlint.vitest.json b/oxlint.vitest.json index 8fcd798ab..1edb5b812 100644 --- a/oxlint.vitest.json +++ b/oxlint.vitest.json @@ -9,7 +9,7 @@ "style": "off", "restriction": "off" }, - "ignorePatterns": ["**/node_modules/**", "**/dist/**", "**/.agent/**", "**/.tailor-sdk/**"], + "ignorePatterns": ["**/node_modules/**", "**/dist/**", "**/.agent/**", "**/.tailor/**"], "rules": { "vitest/consistent-each-for": "error", "vitest/consistent-test-it": [ diff --git a/packages/create-sdk/templates/executor/.gitignore b/packages/create-sdk/templates/executor/.gitignore index d04123787..f3b2377c8 100644 --- a/packages/create-sdk/templates/executor/.gitignore +++ b/packages/create-sdk/templates/executor/.gitignore @@ -1,2 +1,2 @@ node_modules/ -.tailor-sdk/ +.tailor/ diff --git a/packages/create-sdk/templates/executor/.oxlintrc.json b/packages/create-sdk/templates/executor/.oxlintrc.json index 537e488b8..46b42a60a 100644 --- a/packages/create-sdk/templates/executor/.oxlintrc.json +++ b/packages/create-sdk/templates/executor/.oxlintrc.json @@ -7,7 +7,7 @@ "env": { "builtin": true }, - "ignorePatterns": [".tailor-sdk/", "src/generated/", "tailor.d.ts"], + "ignorePatterns": [".tailor/", "src/generated/", "tailor.d.ts"], "rules": { "no-case-declarations": "error", "no-empty": "error", diff --git a/packages/create-sdk/templates/executor/package.json b/packages/create-sdk/templates/executor/package.json index d69f0dbb8..15bc1c018 100644 --- a/packages/create-sdk/templates/executor/package.json +++ b/packages/create-sdk/templates/executor/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "scripts": { - "generate": "tailor-sdk generate", - "deploy": "tailor-sdk deploy", + "generate": "tailor generate", + "deploy": "tailor deploy", "test": "vitest --project unit", "test:unit": "vitest --project unit", "format": "oxfmt --write .", diff --git a/packages/create-sdk/templates/executor/tailor.d.ts b/packages/create-sdk/templates/executor/tailor.d.ts index 9a473cac0..7295e7ff6 100644 --- a/packages/create-sdk/templates/executor/tailor.d.ts +++ b/packages/create-sdk/templates/executor/tailor.d.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @tailor-platform/sdk // Do not edit this file manually -// Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' +// Regenerated automatically when running 'tailor deploy' or 'tailor generate' declare module "@tailor-platform/sdk" { interface AttributeMap { diff --git a/packages/create-sdk/templates/generators/.gitignore b/packages/create-sdk/templates/generators/.gitignore index d04123787..f3b2377c8 100644 --- a/packages/create-sdk/templates/generators/.gitignore +++ b/packages/create-sdk/templates/generators/.gitignore @@ -1,2 +1,2 @@ node_modules/ -.tailor-sdk/ +.tailor/ diff --git a/packages/create-sdk/templates/generators/.oxlintrc.json b/packages/create-sdk/templates/generators/.oxlintrc.json index d195e8302..83f6e2b99 100644 --- a/packages/create-sdk/templates/generators/.oxlintrc.json +++ b/packages/create-sdk/templates/generators/.oxlintrc.json @@ -7,7 +7,7 @@ "env": { "builtin": true }, - "ignorePatterns": [".tailor-sdk/", "src/generated/", "src/seed/", "tailor.d.ts"], + "ignorePatterns": [".tailor/", "src/generated/", "src/seed/", "tailor.d.ts"], "rules": { "no-case-declarations": "error", "no-empty": "error", diff --git a/packages/create-sdk/templates/generators/package.json b/packages/create-sdk/templates/generators/package.json index 94e7c7bb5..2420d0405 100644 --- a/packages/create-sdk/templates/generators/package.json +++ b/packages/create-sdk/templates/generators/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "scripts": { - "generate": "tailor-sdk generate", - "deploy": "tailor-sdk deploy", + "generate": "tailor generate", + "deploy": "tailor deploy", "test": "vitest --project unit", "test:unit": "vitest --project unit", "format": "oxfmt --write .", diff --git a/packages/create-sdk/templates/generators/tailor.d.ts b/packages/create-sdk/templates/generators/tailor.d.ts index 9a473cac0..7295e7ff6 100644 --- a/packages/create-sdk/templates/generators/tailor.d.ts +++ b/packages/create-sdk/templates/generators/tailor.d.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @tailor-platform/sdk // Do not edit this file manually -// Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' +// Regenerated automatically when running 'tailor deploy' or 'tailor generate' declare module "@tailor-platform/sdk" { interface AttributeMap { diff --git a/packages/create-sdk/templates/hello-world/.gitignore b/packages/create-sdk/templates/hello-world/.gitignore index d04123787..f3b2377c8 100644 --- a/packages/create-sdk/templates/hello-world/.gitignore +++ b/packages/create-sdk/templates/hello-world/.gitignore @@ -1,2 +1,2 @@ node_modules/ -.tailor-sdk/ +.tailor/ diff --git a/packages/create-sdk/templates/hello-world/.oxlintrc.json b/packages/create-sdk/templates/hello-world/.oxlintrc.json index 627555ccc..76577f7bc 100644 --- a/packages/create-sdk/templates/hello-world/.oxlintrc.json +++ b/packages/create-sdk/templates/hello-world/.oxlintrc.json @@ -7,7 +7,7 @@ "env": { "builtin": true }, - "ignorePatterns": [".tailor-sdk/", "tailor.d.ts"], + "ignorePatterns": [".tailor/", "tailor.d.ts"], "rules": { "no-case-declarations": "error", "no-empty": "error", diff --git a/packages/create-sdk/templates/hello-world/README.md b/packages/create-sdk/templates/hello-world/README.md index df894c082..7b1ffca1a 100644 --- a/packages/create-sdk/templates/hello-world/README.md +++ b/packages/create-sdk/templates/hello-world/README.md @@ -9,12 +9,12 @@ This project was bootstrapped with [Create Tailor Platform SDK](https://www.npmj 1. Create a new workspace: ```bash -npx tailor-sdk login -npx tailor-sdk workspace create --name --region -npx tailor-sdk workspace list -# For yarn: yarn tailor-sdk -# For pnpm: pnpm tailor-sdk -# For bun: bun tailor-sdk +npx tailor login +npx tailor workspace create --name --region +npx tailor workspace list +# For yarn: yarn tailor +# For pnpm: pnpm tailor +# For bun: bun tailor # OR # Create a new workspace using Tailor Platform Console diff --git a/packages/create-sdk/templates/hello-world/package.json b/packages/create-sdk/templates/hello-world/package.json index 3ade80afa..0611942af 100644 --- a/packages/create-sdk/templates/hello-world/package.json +++ b/packages/create-sdk/templates/hello-world/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "scripts": { - "deploy": "tailor-sdk deploy", - "generate": "tailor-sdk generate", + "deploy": "tailor deploy", + "generate": "tailor generate", "format": "oxfmt --write .", "format:check": "oxfmt --check .", "lint": "oxlint --type-aware .", diff --git a/packages/create-sdk/templates/hello-world/tailor.d.ts b/packages/create-sdk/templates/hello-world/tailor.d.ts index f618bd50c..3043b411f 100644 --- a/packages/create-sdk/templates/hello-world/tailor.d.ts +++ b/packages/create-sdk/templates/hello-world/tailor.d.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @tailor-platform/sdk // Do not edit this file manually -// Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' +// Regenerated automatically when running 'tailor deploy' or 'tailor generate' declare module "@tailor-platform/sdk" { interface AttributeMap {} diff --git a/packages/create-sdk/templates/inventory-management/.gitignore b/packages/create-sdk/templates/inventory-management/.gitignore index d04123787..f3b2377c8 100644 --- a/packages/create-sdk/templates/inventory-management/.gitignore +++ b/packages/create-sdk/templates/inventory-management/.gitignore @@ -1,2 +1,2 @@ node_modules/ -.tailor-sdk/ +.tailor/ diff --git a/packages/create-sdk/templates/inventory-management/.oxlintrc.json b/packages/create-sdk/templates/inventory-management/.oxlintrc.json index 221090c46..99137dc77 100644 --- a/packages/create-sdk/templates/inventory-management/.oxlintrc.json +++ b/packages/create-sdk/templates/inventory-management/.oxlintrc.json @@ -7,7 +7,7 @@ "env": { "builtin": true }, - "ignorePatterns": [".tailor-sdk/", "src/generated/", "tailor.d.ts"], + "ignorePatterns": [".tailor/", "src/generated/", "tailor.d.ts"], "rules": { "no-case-declarations": "error", "no-empty": "error", diff --git a/packages/create-sdk/templates/inventory-management/README.md b/packages/create-sdk/templates/inventory-management/README.md index c129f1ab5..8f86cd1d5 100644 --- a/packages/create-sdk/templates/inventory-management/README.md +++ b/packages/create-sdk/templates/inventory-management/README.md @@ -9,12 +9,12 @@ This project was bootstrapped with [Create Tailor Platform SDK](https://www.npmj 1. Create a new workspace: ```bash -npx tailor-sdk login -npx tailor-sdk workspace create --name --region -npx tailor-sdk workspace list -# For yarn: yarn tailor-sdk -# For pnpm: pnpm tailor-sdk -# For bun: bun tailor-sdk +npx tailor login +npx tailor workspace create --name --region +npx tailor workspace list +# For yarn: yarn tailor +# For pnpm: pnpm tailor +# For bun: bun tailor # OR # Create a new workspace using Tailor Platform Console @@ -36,9 +36,9 @@ npm run deploy -- --workspace-id ```bash # Get Manager's token -npx tailor-sdk machineuser token manager --workspace-id +npx tailor machineuser token manager --workspace-id # Get Staff's token -npx tailor-sdk machineuser token staff --workspace-id +npx tailor machineuser token staff --workspace-id ``` ## Features diff --git a/packages/create-sdk/templates/inventory-management/package.json b/packages/create-sdk/templates/inventory-management/package.json index ee47a6341..4b93841e8 100644 --- a/packages/create-sdk/templates/inventory-management/package.json +++ b/packages/create-sdk/templates/inventory-management/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "scripts": { - "deploy": "tailor-sdk deploy", - "generate": "tailor-sdk generate", + "deploy": "tailor deploy", + "generate": "tailor generate", "format": "oxfmt --write .", "format:check": "oxfmt --check .", "lint": "oxlint --type-aware .", diff --git a/packages/create-sdk/templates/inventory-management/tailor.d.ts b/packages/create-sdk/templates/inventory-management/tailor.d.ts index ee802f4a5..2b8d6568f 100644 --- a/packages/create-sdk/templates/inventory-management/tailor.d.ts +++ b/packages/create-sdk/templates/inventory-management/tailor.d.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @tailor-platform/sdk // Do not edit this file manually -// Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' +// Regenerated automatically when running 'tailor deploy' or 'tailor generate' declare module "@tailor-platform/sdk" { interface AttributeMap { diff --git a/packages/create-sdk/templates/multi-application/.gitignore b/packages/create-sdk/templates/multi-application/.gitignore index d04123787..f3b2377c8 100644 --- a/packages/create-sdk/templates/multi-application/.gitignore +++ b/packages/create-sdk/templates/multi-application/.gitignore @@ -1,2 +1,2 @@ node_modules/ -.tailor-sdk/ +.tailor/ diff --git a/packages/create-sdk/templates/multi-application/.oxlintrc.json b/packages/create-sdk/templates/multi-application/.oxlintrc.json index 627555ccc..76577f7bc 100644 --- a/packages/create-sdk/templates/multi-application/.oxlintrc.json +++ b/packages/create-sdk/templates/multi-application/.oxlintrc.json @@ -7,7 +7,7 @@ "env": { "builtin": true }, - "ignorePatterns": [".tailor-sdk/", "tailor.d.ts"], + "ignorePatterns": [".tailor/", "tailor.d.ts"], "rules": { "no-case-declarations": "error", "no-empty": "error", diff --git a/packages/create-sdk/templates/multi-application/README.md b/packages/create-sdk/templates/multi-application/README.md index 99b14121a..fa1a56462 100644 --- a/packages/create-sdk/templates/multi-application/README.md +++ b/packages/create-sdk/templates/multi-application/README.md @@ -16,8 +16,8 @@ This project contains two applications: `user` and `admin`. 1. Create a new workspace: ```bash -npx tailor-sdk login -npx tailor-sdk workspace create --name --region +npx tailor login +npx tailor workspace create --name --region ``` 2. Deploy the project: diff --git a/packages/create-sdk/templates/multi-application/package.json b/packages/create-sdk/templates/multi-application/package.json index 6fcf8fe56..546a964b7 100644 --- a/packages/create-sdk/templates/multi-application/package.json +++ b/packages/create-sdk/templates/multi-application/package.json @@ -4,8 +4,8 @@ "type": "module", "scripts": { "deploy": "pnpm run deploy:user && pnpm run deploy:admin", - "deploy:user": "tailor-sdk deploy -c apps/user/tailor.config.ts", - "deploy:admin": "tailor-sdk deploy -c apps/admin/tailor.config.ts", + "deploy:user": "tailor deploy -c apps/user/tailor.config.ts", + "deploy:admin": "tailor deploy -c apps/admin/tailor.config.ts", "format": "oxfmt --write .", "format:check": "oxfmt --check .", "lint": "oxlint --type-aware .", diff --git a/packages/create-sdk/templates/resolver/.gitignore b/packages/create-sdk/templates/resolver/.gitignore index d04123787..f3b2377c8 100644 --- a/packages/create-sdk/templates/resolver/.gitignore +++ b/packages/create-sdk/templates/resolver/.gitignore @@ -1,2 +1,2 @@ node_modules/ -.tailor-sdk/ +.tailor/ diff --git a/packages/create-sdk/templates/resolver/.oxlintrc.json b/packages/create-sdk/templates/resolver/.oxlintrc.json index 537e488b8..46b42a60a 100644 --- a/packages/create-sdk/templates/resolver/.oxlintrc.json +++ b/packages/create-sdk/templates/resolver/.oxlintrc.json @@ -7,7 +7,7 @@ "env": { "builtin": true }, - "ignorePatterns": [".tailor-sdk/", "src/generated/", "tailor.d.ts"], + "ignorePatterns": [".tailor/", "src/generated/", "tailor.d.ts"], "rules": { "no-case-declarations": "error", "no-empty": "error", diff --git a/packages/create-sdk/templates/resolver/package.json b/packages/create-sdk/templates/resolver/package.json index 162d120ae..94d60507e 100644 --- a/packages/create-sdk/templates/resolver/package.json +++ b/packages/create-sdk/templates/resolver/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "scripts": { - "generate": "tailor-sdk generate", - "deploy": "tailor-sdk deploy", + "generate": "tailor generate", + "deploy": "tailor deploy", "test": "vitest --project unit", "test:unit": "vitest --project unit", "format": "oxfmt --write .", diff --git a/packages/create-sdk/templates/resolver/tailor.d.ts b/packages/create-sdk/templates/resolver/tailor.d.ts index 67384d1c5..709f0395a 100644 --- a/packages/create-sdk/templates/resolver/tailor.d.ts +++ b/packages/create-sdk/templates/resolver/tailor.d.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @tailor-platform/sdk // Do not edit this file manually -// Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' +// Regenerated automatically when running 'tailor deploy' or 'tailor generate' declare module "@tailor-platform/sdk" { interface AttributeMap { diff --git a/packages/create-sdk/templates/static-web-site/.gitignore b/packages/create-sdk/templates/static-web-site/.gitignore index d04123787..f3b2377c8 100644 --- a/packages/create-sdk/templates/static-web-site/.gitignore +++ b/packages/create-sdk/templates/static-web-site/.gitignore @@ -1,2 +1,2 @@ node_modules/ -.tailor-sdk/ +.tailor/ diff --git a/packages/create-sdk/templates/static-web-site/.oxlintrc.json b/packages/create-sdk/templates/static-web-site/.oxlintrc.json index 537e488b8..46b42a60a 100644 --- a/packages/create-sdk/templates/static-web-site/.oxlintrc.json +++ b/packages/create-sdk/templates/static-web-site/.oxlintrc.json @@ -7,7 +7,7 @@ "env": { "builtin": true }, - "ignorePatterns": [".tailor-sdk/", "src/generated/", "tailor.d.ts"], + "ignorePatterns": [".tailor/", "src/generated/", "tailor.d.ts"], "rules": { "no-case-declarations": "error", "no-empty": "error", diff --git a/packages/create-sdk/templates/static-web-site/package.json b/packages/create-sdk/templates/static-web-site/package.json index f70abe120..5bde1d85c 100644 --- a/packages/create-sdk/templates/static-web-site/package.json +++ b/packages/create-sdk/templates/static-web-site/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "scripts": { - "deploy": "tailor-sdk deploy", - "generate": "tailor-sdk generate", + "deploy": "tailor deploy", + "generate": "tailor generate", "format": "oxfmt --write .", "format:check": "oxfmt --check .", "lint": "oxlint --type-aware .", diff --git a/packages/create-sdk/templates/static-web-site/tailor.d.ts b/packages/create-sdk/templates/static-web-site/tailor.d.ts index 080ae97b4..a3d01a7ab 100644 --- a/packages/create-sdk/templates/static-web-site/tailor.d.ts +++ b/packages/create-sdk/templates/static-web-site/tailor.d.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @tailor-platform/sdk // Do not edit this file manually -// Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' +// Regenerated automatically when running 'tailor deploy' or 'tailor generate' declare module "@tailor-platform/sdk" { interface AttributeMap { diff --git a/packages/create-sdk/templates/tailordb/.gitignore b/packages/create-sdk/templates/tailordb/.gitignore index d04123787..f3b2377c8 100644 --- a/packages/create-sdk/templates/tailordb/.gitignore +++ b/packages/create-sdk/templates/tailordb/.gitignore @@ -1,2 +1,2 @@ node_modules/ -.tailor-sdk/ +.tailor/ diff --git a/packages/create-sdk/templates/tailordb/.oxlintrc.json b/packages/create-sdk/templates/tailordb/.oxlintrc.json index 537e488b8..46b42a60a 100644 --- a/packages/create-sdk/templates/tailordb/.oxlintrc.json +++ b/packages/create-sdk/templates/tailordb/.oxlintrc.json @@ -7,7 +7,7 @@ "env": { "builtin": true }, - "ignorePatterns": [".tailor-sdk/", "src/generated/", "tailor.d.ts"], + "ignorePatterns": [".tailor/", "src/generated/", "tailor.d.ts"], "rules": { "no-case-declarations": "error", "no-empty": "error", diff --git a/packages/create-sdk/templates/tailordb/package.json b/packages/create-sdk/templates/tailordb/package.json index e5b7acc2a..889140d43 100644 --- a/packages/create-sdk/templates/tailordb/package.json +++ b/packages/create-sdk/templates/tailordb/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "scripts": { - "generate": "tailor-sdk generate", - "deploy": "tailor-sdk deploy", + "generate": "tailor generate", + "deploy": "tailor deploy", "test": "vitest --project unit", "test:unit": "vitest --project unit", "format": "oxfmt --write .", diff --git a/packages/create-sdk/templates/tailordb/tailor.d.ts b/packages/create-sdk/templates/tailordb/tailor.d.ts index fb3e4de56..89b9a127d 100644 --- a/packages/create-sdk/templates/tailordb/tailor.d.ts +++ b/packages/create-sdk/templates/tailordb/tailor.d.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @tailor-platform/sdk // Do not edit this file manually -// Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' +// Regenerated automatically when running 'tailor deploy' or 'tailor generate' declare module "@tailor-platform/sdk" { interface AttributeMap { diff --git a/packages/create-sdk/templates/workflow/.gitignore b/packages/create-sdk/templates/workflow/.gitignore index d04123787..f3b2377c8 100644 --- a/packages/create-sdk/templates/workflow/.gitignore +++ b/packages/create-sdk/templates/workflow/.gitignore @@ -1,2 +1,2 @@ node_modules/ -.tailor-sdk/ +.tailor/ diff --git a/packages/create-sdk/templates/workflow/.oxlintrc.json b/packages/create-sdk/templates/workflow/.oxlintrc.json index 537e488b8..46b42a60a 100644 --- a/packages/create-sdk/templates/workflow/.oxlintrc.json +++ b/packages/create-sdk/templates/workflow/.oxlintrc.json @@ -7,7 +7,7 @@ "env": { "builtin": true }, - "ignorePatterns": [".tailor-sdk/", "src/generated/", "tailor.d.ts"], + "ignorePatterns": [".tailor/", "src/generated/", "tailor.d.ts"], "rules": { "no-case-declarations": "error", "no-empty": "error", diff --git a/packages/create-sdk/templates/workflow/package.json b/packages/create-sdk/templates/workflow/package.json index 9d66e9b1f..dfbb0e62f 100644 --- a/packages/create-sdk/templates/workflow/package.json +++ b/packages/create-sdk/templates/workflow/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "scripts": { - "generate": "tailor-sdk generate", - "deploy": "tailor-sdk deploy", + "generate": "tailor generate", + "deploy": "tailor deploy", "test": "vitest --project unit", "test:unit": "vitest --project unit", "test:e2e": "vitest --project e2e", diff --git a/packages/create-sdk/templates/workflow/tailor.d.ts b/packages/create-sdk/templates/workflow/tailor.d.ts index 9016c307d..f5888b7a8 100644 --- a/packages/create-sdk/templates/workflow/tailor.d.ts +++ b/packages/create-sdk/templates/workflow/tailor.d.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @tailor-platform/sdk // Do not edit this file manually -// Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' +// Regenerated automatically when running 'tailor deploy' or 'tailor generate' declare module "@tailor-platform/sdk" { interface AttributeMap { diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/codemod.yaml b/packages/sdk-codemod/codemods/v2/rename-bin/codemod.yaml new file mode 100644 index 000000000..03de46a1e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/rename-bin" +version: "1.0.0" +description: "Rename the CLI binary from tailor-sdk to tailor in scripts, CI workflows, and documentation" +engine: jssg +language: text +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts new file mode 100644 index 000000000..3dd999a02 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -0,0 +1,58 @@ +import * as path from "pathe"; + +// Match the `tailor-sdk` binary, optionally with a version pin (`@latest`, +// `@2.0.0`, etc.). Lookbehind excludes `.tailor-sdk` (directory path preceded +// by `.`) and `create-tailor-sdk` (package name preceded by `-`). +const TAILOR_SDK_RE = /(? + version ? `tailor${version}` : "tailor", + ); +} + +function transformPackageJson(source: string): string | null { + let parsed: Record; + try { + parsed = JSON.parse(source) as Record; + } catch { + return null; + } + + let modified = false; + const scripts = parsed.scripts; + if (typeof scripts === "object" && scripts != null && !Array.isArray(scripts)) { + for (const [name, value] of Object.entries(scripts as Record)) { + if (typeof value !== "string") continue; + if (!value.includes("tailor-sdk")) continue; + const updated = renameBinary(value); + if (updated !== value) { + (scripts as Record)[name] = updated; + modified = true; + } + } + } + + if (!modified) return null; + const trailing = source.endsWith("\n") ? "\n" : ""; + return JSON.stringify(parsed, null, 2) + trailing; +} + +/** + * Rename `tailor-sdk` binary references to `tailor`. + * + * Handles optional `@version` pins (`tailor-sdk@latest` → `tailor@latest`). + * Does not rewrite `.tailor-sdk` directory paths or `create-tailor-sdk`. + * @param source - File contents + * @param filePath - Absolute path to the file (used to dispatch package.json vs text) + * @returns Transformed source or null when nothing matched. + */ +export default function transform(source: string, filePath: string): string | null { + if (!source.includes("tailor-sdk")) return null; + + const ext = path.extname(filePath).toLowerCase(); + if (ext === ".json") return transformPackageJson(source); + + const updated = renameBinary(source); + return updated === source ? null : updated; +} diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-package-json/expected.json b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-package-json/expected.json new file mode 100644 index 000000000..1bedc183b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-package-json/expected.json @@ -0,0 +1,10 @@ +{ + "name": "my-app", + "version": "1.0.0", + "scripts": { + "deploy": "tailor deploy", + "login": "pnpm exec tailor login", + "generate": "tailor@latest generate", + "build": "tsc" + } +} diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-package-json/input.json b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-package-json/input.json new file mode 100644 index 000000000..0300c9746 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-package-json/input.json @@ -0,0 +1,10 @@ +{ + "name": "my-app", + "version": "1.0.0", + "scripts": { + "deploy": "tailor-sdk deploy", + "login": "pnpm exec tailor-sdk login", + "generate": "tailor-sdk@latest generate", + "build": "tsc" + } +} diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh new file mode 100644 index 000000000..027c87b73 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +pnpm exec tailor deploy +tailor login +tailor@latest deploy +npx tailor@2.0.0 workspace list diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh new file mode 100644 index 000000000..70d126faf --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +pnpm exec tailor-sdk deploy +tailor-sdk login +tailor-sdk@latest deploy +npx tailor-sdk@2.0.0 workspace list diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-yaml/expected.yml b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-yaml/expected.yml new file mode 100644 index 000000000..eda750943 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-yaml/expected.yml @@ -0,0 +1,5 @@ +steps: + - name: Deploy + run: tailor deploy -w ${{ secrets.WORKSPACE_ID }} + - name: Login check + run: npx tailor@latest login --help diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-yaml/input.yml b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-yaml/input.yml new file mode 100644 index 000000000..8c7f4a7e3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-yaml/input.yml @@ -0,0 +1,5 @@ +steps: + - name: Deploy + run: tailor-sdk deploy -w ${{ secrets.WORKSPACE_ID }} + - name: Login check + run: npx tailor-sdk@latest login --help diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/no-match/input.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/no-match/input.sh new file mode 100644 index 000000000..4a32df489 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/no-match/input.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# create-tailor-sdk is a scaffolding package, not the CLI binary +npx create-tailor-sdk@latest my-app + +# .tailor-sdk directory paths are not the binary +ls .tailor-sdk/cache +cat .tailor-sdk/config.json diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh new file mode 100644 index 000000000..f00720db0 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh @@ -0,0 +1,2 @@ +tailor@latest deploy -w my-workspace +pnpm dlx tailor@2.0.0 login diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh new file mode 100644 index 000000000..559cd868f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh @@ -0,0 +1,2 @@ +tailor-sdk@latest deploy -w my-workspace +pnpm dlx tailor-sdk@2.0.0 login diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 87bd986b4..51b7793a9 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -60,7 +60,7 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/test-run-arg-input", name: "function test-run --arg input unwrap", description: - "Strip the deprecated {input: ...} wrapper from `tailor-sdk function test-run --arg` JSON in scripts and docs", + "Strip the deprecated {input: ...} wrapper from `tailor function test-run --arg` JSON in scripts and docs", since: "1.0.0", until: "2.0.0", scriptPath: "v2/test-run-arg-input/scripts/transform.js", @@ -68,33 +68,32 @@ export const allCodemods: CodemodPackage[] = [ examples: [ { lang: "sh", - before: 'tailor-sdk function test-run resolvers/add.ts --arg \'{"input":{"a":1}}\'', - after: "tailor-sdk function test-run resolvers/add.ts --arg '{\"a\":1}'", + before: 'tailor function test-run resolvers/add.ts --arg \'{"input":{"a":1}}\'', + after: "tailor function test-run resolvers/add.ts --arg '{\"a\":1}'", }, ], }, { id: "v2/sdk-skills-shim", - name: "tailor-sdk-skills → tailor-sdk skills install", - description: - "Replace deprecated `tailor-sdk-skills` invocations with `tailor-sdk skills install`", + name: "tailor-skills → tailor skills install", + description: "Replace deprecated `tailor-skills` invocations with `tailor skills install`", since: "1.0.0", until: "2.0.0", scriptPath: "v2/sdk-skills-shim/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], - legacyPatterns: ["tailor-sdk-skills"], + legacyPatterns: ["tailor-skills"], examples: [ { lang: "sh", - before: "npx tailor-sdk-skills", - after: "tailor-sdk skills install", + before: "npx tailor-skills", + after: "tailor skills install", }, ], prompt: [ - "The standalone tailor-sdk-skills binary is removed in v2; call the skills install", - "subcommand on the main tailor-sdk CLI instead. Replace any remaining", - "tailor-sdk-skills invocations the codemod did not rewrite with", - "`tailor-sdk skills install`.", + "The standalone tailor-skills binary is removed in v2; call the skills install", + "subcommand on the main tailor CLI instead. Replace any remaining", + "tailor-skills invocations the codemod did not rewrite with", + "`tailor skills install`.", ].join("\n"), }, { @@ -148,9 +147,9 @@ export const allCodemods: CodemodPackage[] = [ }, { id: "v2/apply-to-deploy", - name: "tailor-sdk apply → tailor-sdk deploy", + name: "tailor apply → tailor deploy", description: - "Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor-sdk deploy` command", + "Rewrite `tailor apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor deploy` command", since: "1.0.0", until: "2.0.0", scriptPath: "v2/apply-to-deploy/scripts/transform.js", @@ -163,8 +162,8 @@ export const allCodemods: CodemodPackage[] = [ examples: [ { lang: "sh", - before: "tailor-sdk apply --profile prod", - after: "tailor-sdk deploy --profile prod", + before: "tailor apply --profile prod", + after: "tailor deploy --profile prod", }, ], }, @@ -172,22 +171,22 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/cli-rename", name: "v2 CLI rename", description: - "Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs", + "Rewrite `tailor crash-report` to `tailor crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs", since: "1.0.0", until: "2.0.0", scriptPath: "v2/cli-rename/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], - legacyPatterns: ["tailor-sdk crash-report", "--machineuser"], + legacyPatterns: ["tailor crash-report", "--machineuser"], examples: [ { lang: "sh", - before: "tailor-sdk crash-report list\ntailor-sdk login --machineuser", - after: "tailor-sdk crashreport list\ntailor-sdk login --machine-user", + before: "tailor crash-report list\ntailor-sdk login --machineuser", + after: "tailor crashreport list\ntailor-sdk login --machine-user", }, ], prompt: [ - "Apply the v2 CLI renames the codemod did not reach (only `tailor-sdk`-prefixed", - "invocations are rewritten): `tailor-sdk crash-report` -> `tailor-sdk crashreport`", + "Apply the v2 CLI renames the codemod did not reach (only `tailor`-prefixed", + "invocations are rewritten): `tailor crash-report` -> `tailor crashreport`", "and the `--machineuser` option -> `--machine-user`. Leave unrelated commands that", "happen to use `--machineuser` alone.", ].join("\n"), @@ -432,11 +431,34 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/function-logs-content-hash", name: "function logs require a content hash for source mapping", description: - "`tailor-sdk function logs` maps stack traces against the function bundle only when the execution recorded a `contentHash`. Executions without one now show raw stack traces instead of mapped frames. No source change is required.", + "`tailor function logs` maps stack traces against the function bundle only when the execution recorded a `contentHash`. Executions without one now show raw stack traces instead of mapped frames. No source change is required.", since: "1.0.0", until: "2.0.0", notice: true, }, + { + id: "v2/rename-bin", + name: "tailor-sdk binary → tailor", + description: + "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package.", + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/rename-bin/scripts/transform.js", + filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], + legacyPatterns: ["tailor-sdk"], + examples: [ + { + lang: "sh", + before: "tailor-sdk deploy\nnpx tailor-sdk@latest login", + after: "tailor deploy\nnpx tailor@latest login", + }, + ], + prompt: [ + "Rename any remaining `tailor-sdk` binary invocations to `tailor`. Only rewrite", + "the binary name — leave `.tailor-sdk` directory paths and `create-tailor-sdk`", + "package references unchanged.", + ].join("\n"), + }, ]; /** diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 37c8b1893..4f59b9e72 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -288,7 +288,7 @@ describe("runCodemods", () => { await fs.promises.writeFile(workflowPath, "hello", "utf-8"); await fs.promises.writeFile( agentPackagePath, - '{"scripts":{"deploy":"tailor-sdk apply"}}', + '{"scripts":{"deploy":"tailor apply"}}', "utf-8", ); await fs.promises.writeFile(nextYamlPath, "hello", "utf-8"); @@ -307,7 +307,7 @@ describe("runCodemods", () => { expect(result.filesModified).toEqual([workflowPath]); await expect(fs.promises.readFile(workflowPath, "utf-8")).resolves.toBe("HELLO"); await expect(fs.promises.readFile(agentPackagePath, "utf-8")).resolves.toBe( - '{"scripts":{"deploy":"tailor-sdk apply"}}', + '{"scripts":{"deploy":"tailor apply"}}', ); await expect(fs.promises.readFile(nextYamlPath, "utf-8")).resolves.toBe("hello"); }); @@ -320,7 +320,7 @@ describe("runCodemods", () => { await fs.promises.writeFile( partialTransformPath, `export default function transform(source) { - return source.replaceAll("tailor-sdk crash-report", "tailor-sdk crashreport"); + return source.replaceAll("tailor crash-report", "tailor crashreport"); }`, "utf-8", ); @@ -335,7 +335,7 @@ describe("runCodemods", () => { tmpDir = dir; await fs.promises.writeFile( path.join(dir, "README.md"), - "Run `tailor-sdk crash-report list`.\nRun tailor-sdk login --machineuser.\n", + "Run `tailor crash-report list`.\nRun tailor login --machineuser.\n", "utf-8", ); @@ -348,7 +348,7 @@ describe("runCodemods", () => { "test/partial", partialTransformPath, ["**/*.md"], - ["tailor-sdk crash-report", "--machineuser"], + ["tailor crash-report", "--machineuser"], ), scriptPath: partialTransformPath, }, diff --git a/packages/sdk/.oxlintrc.json b/packages/sdk/.oxlintrc.json index 71a2dbfdb..4d4c18071 100644 --- a/packages/sdk/.oxlintrc.json +++ b/packages/sdk/.oxlintrc.json @@ -11,7 +11,7 @@ "dist/", "src/types/*.generated.ts", "e2e/fixtures/", - ".tailor-sdk/", + ".tailor/", "tailor.d.ts", "plugin-defined.d.ts", "**/__test_fixtures__/dist/", diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 71e4bc1a7..08e0324af 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -50,13 +50,13 @@ For more details, see the [Quickstart Guide](./docs/quickstart.md). ## Agent Skill -Install the `tailor-sdk` skill from the locally installed SDK package: +Install the `tailor` skill from the locally installed SDK package: ```bash -npx tailor-sdk skills install +npx tailor skills install # Example: install to Codex in non-interactive mode -npx tailor-sdk skills install -a codex -y +npx tailor skills install -a codex -y ``` This uses the `skills` CLI under the hood, sourcing the skill from diff --git a/packages/sdk/agent-skills/tailor-sdk/SKILL.md b/packages/sdk/agent-skills/tailor/SKILL.md similarity index 98% rename from packages/sdk/agent-skills/tailor-sdk/SKILL.md rename to packages/sdk/agent-skills/tailor/SKILL.md index 7d1dcdcf2..68c54ad5b 100644 --- a/packages/sdk/agent-skills/tailor-sdk/SKILL.md +++ b/packages/sdk/agent-skills/tailor/SKILL.md @@ -1,5 +1,5 @@ --- -name: tailor-sdk +name: tailor description: Use this skill when working with @tailor-platform/sdk projects, including service configuration, CLI usage, and docs navigation. --- diff --git a/packages/sdk/docs/cli-reference.md b/packages/sdk/docs/cli-reference.md index 2bf4113d8..9c4e90dae 100644 --- a/packages/sdk/docs/cli-reference.md +++ b/packages/sdk/docs/cli-reference.md @@ -1,11 +1,11 @@ -# tailor-sdk +# tailor Tailor Platform SDK - The SDK to work with Tailor Platform ## Usage ```bash -tailor-sdk [options] +tailor [options] ``` ## Global Options @@ -51,10 +51,10 @@ Both `--env-file` and `--env-file-if-exists` can be specified multiple times and ```bash # Load .env (required) and .env.local (optional, if exists) -tailor-sdk deploy --env-file .env --env-file-if-exists .env.local +tailor deploy --env-file .env --env-file-if-exists .env.local # Load multiple files -tailor-sdk deploy --env-file .env --env-file .env.production +tailor deploy --env-file .env --env-file .env.production ``` ## Environment Variables @@ -324,10 +324,10 @@ Commands for upgrading SDK versions with automated code migration. Commands for installing Tailor SDK agent skills. -| Command | Description | -| ------------------------------------------------ | ------------------------------------------------------------------ | -| [skills](./cli/skills.md#skills) | Manage Tailor SDK agent skills. | -| [skills install](./cli/skills.md#skills-install) | Install the tailor-sdk agent skill from the installed SDK package. | +| Command | Description | +| ------------------------------------------------ | -------------------------------------------------------------- | +| [skills](./cli/skills.md#skills) | Manage Tailor SDK agent skills. | +| [skills install](./cli/skills.md#skills-install) | Install the tailor agent skill from the installed SDK package. | ### [Completion](./cli/completion.md) diff --git a/packages/sdk/docs/cli-reference.template.md b/packages/sdk/docs/cli-reference.template.md index f8b2c4c95..b9422102e 100644 --- a/packages/sdk/docs/cli-reference.template.md +++ b/packages/sdk/docs/cli-reference.template.md @@ -1,11 +1,11 @@ -# tailor-sdk +# tailor Tailor Platform SDK - The SDK to work with Tailor Platform ## Usage ```bash -tailor-sdk [options] +tailor [options] ``` ## Global Options @@ -45,10 +45,10 @@ Both `--env-file` and `--env-file-if-exists` can be specified multiple times and ```bash # Load .env (required) and .env.local (optional, if exists) -tailor-sdk deploy --env-file .env --env-file-if-exists .env.local +tailor deploy --env-file .env --env-file-if-exists .env.local # Load multiple files -tailor-sdk deploy --env-file .env --env-file .env.production +tailor deploy --env-file .env --env-file .env.production ``` ## Environment Variables diff --git a/packages/sdk/docs/cli/application.md b/packages/sdk/docs/cli/application.md index 34e6e5af9..9095d4e7a 100644 --- a/packages/sdk/docs/cli/application.md +++ b/packages/sdk/docs/cli/application.md @@ -9,7 +9,7 @@ Initialize a new project using create-sdk. **Usage** ``` -tailor-sdk init [options] [name] +tailor init [options] [name] ``` **Arguments** @@ -33,7 +33,7 @@ Generate files using Tailor configuration. **Usage** ``` -tailor-sdk generate [options] +tailor generate [options] ``` **Options** @@ -52,7 +52,7 @@ Deploy your application by applying the Tailor configuration. **Usage** ``` -tailor-sdk deploy [options] +tailor deploy [options] ``` **Options** @@ -120,7 +120,7 @@ Remove all resources managed by the application from the workspace. **Usage** ``` -tailor-sdk remove [options] +tailor remove [options] ``` **Options** @@ -141,7 +141,7 @@ Show information about the deployed application. **Usage** ``` -tailor-sdk show [options] +tailor show [options] ``` **Options** @@ -161,7 +161,7 @@ Open Tailor Platform Console. **Usage** ``` -tailor-sdk open [options] +tailor open [options] ``` **Options** @@ -181,7 +181,7 @@ Call Tailor Platform API endpoints directly. **Usage** ``` -tailor-sdk api [options] [command] +tailor api [options] [command] ``` **Arguments** @@ -214,30 +214,30 @@ See [Global Options](../cli-reference.md#global-options) for options available t **Call an endpoint; workspaceId is auto-injected.** ```bash -$ tailor-sdk api GetApplication -b '{"applicationName":"app-1"}' +$ tailor api GetApplication -b '{"applicationName":"app-1"}' ``` **Same as above, using --field instead of --body.** ```bash -$ tailor-sdk api GetApplication -f applicationName=app-1 +$ tailor api GetApplication -f applicationName=app-1 ``` **List all invocable OperatorService methods.** ```bash -$ tailor-sdk api list +$ tailor api list ``` **Show the input message tree for an endpoint.** ```bash -$ tailor-sdk api inspect GetApplication +$ tailor api inspect GetApplication ``` **Notes** -Use `tailor-sdk api list` to enumerate invocable methods and `tailor-sdk api inspect ` to print an endpoint's input message tree (combine with `--json` for machine-readable output). +Use `tailor api list` to enumerate invocable methods and `tailor api inspect ` to print an endpoint's input message tree (combine with `--json` for machine-readable output). The request body is inferred from the target endpoint's request schema, and commonly required fields are auto-injected so they can be omitted from `--body`: @@ -257,7 +257,7 @@ Print the input message tree of an OperatorService endpoint. **Usage** ``` -tailor-sdk api inspect +tailor api inspect ``` **Arguments** @@ -273,18 +273,18 @@ See [Global Options](../cli-reference.md#global-options) for options available t **Show fields of GetApplicationRequest.** ```bash -$ tailor-sdk api inspect GetApplication +$ tailor api inspect GetApplication ``` **Inspect a deeply nested input with `(oneof config)` annotations.** ```bash -$ tailor-sdk api inspect CreateExecutorExecutor +$ tailor api inspect CreateExecutorExecutor ``` **Notes** -Combine with the global `--json` flag for a machine-readable descriptor. Recursive type references and `oneof` membership are annotated. Use `tailor-sdk api list` to discover endpoint names. +Combine with the global `--json` flag for a machine-readable descriptor. Recursive type references and `oneof` membership are annotated. Use `tailor api list` to discover endpoint names. ### api list @@ -293,7 +293,7 @@ List all invocable OperatorService methods. **Usage** ``` -tailor-sdk api list +tailor api list ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. diff --git a/packages/sdk/docs/cli/auth.md b/packages/sdk/docs/cli/auth.md index 30cab7c5b..75d162bb0 100644 --- a/packages/sdk/docs/cli/auth.md +++ b/packages/sdk/docs/cli/auth.md @@ -9,7 +9,7 @@ Manage auth connections. **Usage** ``` -tailor-sdk authconnection [command] +tailor authconnection [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -31,7 +31,7 @@ Authorize an auth connection via OAuth2 flow. **Usage** ``` -tailor-sdk authconnection authorize [options] +tailor authconnection authorize [options] ``` **Options** @@ -54,7 +54,7 @@ Delete an auth connection entirely. **Usage** ``` -tailor-sdk authconnection delete [options] +tailor authconnection delete [options] ``` **Options** @@ -75,7 +75,7 @@ List all auth connections. **Usage** ``` -tailor-sdk authconnection list [options] +tailor authconnection list [options] ``` **Options** @@ -96,7 +96,7 @@ Open the auth connections page in the Tailor Platform Console. **Usage** ``` -tailor-sdk authconnection open [options] +tailor authconnection open [options] ``` **Options** @@ -115,7 +115,7 @@ Revoke an auth connection's tokens (keeps the connection; use 'delete' to remove **Usage** ``` -tailor-sdk authconnection revoke [options] +tailor authconnection revoke [options] ``` **Options** @@ -140,7 +140,7 @@ Manage machine users in your Tailor Platform application. **Usage** ``` -tailor-sdk machineuser [command] +tailor machineuser [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -159,7 +159,7 @@ List all machine users in the application. **Usage** ``` -tailor-sdk machineuser list [options] +tailor machineuser list [options] ``` **Options** @@ -181,7 +181,7 @@ Get an access token for a machine user. **Usage** ``` -tailor-sdk machineuser token [options] [name] +tailor machineuser token [options] [name] ``` **Arguments** @@ -207,7 +207,7 @@ Manage OAuth2 clients in your Tailor Platform application. **Usage** ``` -tailor-sdk oauth2client [command] +tailor oauth2client [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -226,7 +226,7 @@ List all OAuth2 clients in the application. **Usage** ``` -tailor-sdk oauth2client list [options] +tailor oauth2client list [options] ``` **Options** @@ -259,7 +259,7 @@ Get OAuth2 client credentials (including client secret). **Usage** ``` -tailor-sdk oauth2client get [options] +tailor oauth2client get [options] ``` **Arguments** diff --git a/packages/sdk/docs/cli/completion.md b/packages/sdk/docs/cli/completion.md index d85dc1788..330341e05 100644 --- a/packages/sdk/docs/cli/completion.md +++ b/packages/sdk/docs/cli/completion.md @@ -7,7 +7,7 @@ Generate shell completion script **Usage** ``` -tailor-sdk completion [options] [shell] +tailor completion [options] [shell] ``` **Arguments** diff --git a/packages/sdk/docs/cli/crashreport.md b/packages/sdk/docs/cli/crashreport.md index 7f6152438..4a95b3430 100644 --- a/packages/sdk/docs/cli/crashreport.md +++ b/packages/sdk/docs/cli/crashreport.md @@ -9,7 +9,7 @@ Manage crash reports. **Usage** ``` -tailor-sdk crashreport [command] +tailor crashreport [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -28,7 +28,7 @@ List local crash report files. **Usage** ``` -tailor-sdk crashreport list [options] +tailor crashreport list [options] ``` **Options** @@ -47,7 +47,7 @@ Submit a crash report to help improve the SDK. **Usage** ``` -tailor-sdk crashreport send [options] +tailor crashreport send [options] ``` **Options** diff --git a/packages/sdk/docs/cli/executor.md b/packages/sdk/docs/cli/executor.md index 55a6dea49..c78d37749 100644 --- a/packages/sdk/docs/cli/executor.md +++ b/packages/sdk/docs/cli/executor.md @@ -9,7 +9,7 @@ Manage executors **Usage** ``` -tailor-sdk executor [command] +tailor executor [command] ``` **Commands** @@ -31,7 +31,7 @@ List all executors **Usage** ``` -tailor-sdk executor list [options] +tailor executor list [options] ``` **Options** @@ -52,7 +52,7 @@ Get executor details **Usage** ``` -tailor-sdk executor get [options] +tailor executor get [options] ``` **Arguments** @@ -77,7 +77,7 @@ List or get executor jobs. **Usage** ``` -tailor-sdk executor jobs [options] [job-id] +tailor executor jobs [options] [job-id] ``` **Arguments** @@ -109,43 +109,43 @@ See [Global Options](../cli-reference.md#global-options) for options available t **List jobs for an executor (default: 50 jobs)** ```bash -$ tailor-sdk executor jobs my-executor +$ tailor executor jobs my-executor ``` **Limit the number of jobs** ```bash -$ tailor-sdk executor jobs my-executor --limit 10 +$ tailor executor jobs my-executor --limit 10 ``` **Filter by status** ```bash -$ tailor-sdk executor jobs my-executor -s RUNNING +$ tailor executor jobs my-executor -s RUNNING ``` **Get job details** ```bash -$ tailor-sdk executor jobs my-executor +$ tailor executor jobs my-executor ``` **Get job details with attempts** ```bash -$ tailor-sdk executor jobs my-executor --attempts +$ tailor executor jobs my-executor --attempts ``` **Wait for job to complete** ```bash -$ tailor-sdk executor jobs my-executor -W +$ tailor executor jobs my-executor -W ``` **Wait for job with logs** ```bash -$ tailor-sdk executor jobs my-executor -W -l +$ tailor executor jobs my-executor -W -l ``` ### executor trigger @@ -155,7 +155,7 @@ Trigger an executor manually. **Usage** ``` -tailor-sdk executor trigger [options] +tailor executor trigger [options] ``` **Arguments** @@ -182,31 +182,31 @@ tailor-sdk executor trigger [options] **Trigger an executor** ```bash -$ tailor-sdk executor trigger my-executor +$ tailor executor trigger my-executor ``` **Trigger with data** ```bash -$ tailor-sdk executor trigger my-executor -d '{"message": "hello"}' +$ tailor executor trigger my-executor -d '{"message": "hello"}' ``` **Trigger with data and headers** ```bash -$ tailor-sdk executor trigger my-executor -d '{"message": "hello"}' -H "X-Custom: value" -H "X-Another: value2" +$ tailor executor trigger my-executor -d '{"message": "hello"}' -H "X-Custom: value" -H "X-Another: value2" ``` **Trigger and wait for completion** ```bash -$ tailor-sdk executor trigger my-executor -W +$ tailor executor trigger my-executor -W ``` **Trigger, wait, and show logs** ```bash -$ tailor-sdk executor trigger my-executor -W -l +$ tailor executor trigger my-executor -W -l ``` **Shell automation** @@ -215,7 +215,7 @@ Trigger an executor and wait for the executor job plus any downstream workflow o function execution: ```bash -tailor-sdk executor trigger daily-workflow \ +tailor executor trigger daily-workflow \ --wait \ --timeout 5m \ --interval 5s \ @@ -225,7 +225,7 @@ tailor-sdk executor trigger daily-workflow \ Wait for an existing job when another process already captured the job ID: ```bash -tailor-sdk executor jobs daily-workflow "$job_id" \ +tailor executor jobs daily-workflow "$job_id" \ --wait \ --timeout 5m \ --logs \ @@ -286,7 +286,7 @@ Manage executor webhooks **Usage** ``` -tailor-sdk executor webhook [command] +tailor executor webhook [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -304,7 +304,7 @@ List executors with incoming webhook triggers **Usage** ``` -tailor-sdk executor webhook list [options] +tailor executor webhook list [options] ``` **Options** diff --git a/packages/sdk/docs/cli/executor.template.md b/packages/sdk/docs/cli/executor.template.md index c1e30b1fe..46752b5a3 100644 --- a/packages/sdk/docs/cli/executor.template.md +++ b/packages/sdk/docs/cli/executor.template.md @@ -40,7 +40,7 @@ Trigger an executor and wait for the executor job plus any downstream workflow o function execution: ```bash -tailor-sdk executor trigger daily-workflow \ +tailor executor trigger daily-workflow \ --wait \ --timeout 5m \ --interval 5s \ @@ -50,7 +50,7 @@ tailor-sdk executor trigger daily-workflow \ Wait for an existing job when another process already captured the job ID: ```bash -tailor-sdk executor jobs daily-workflow "$job_id" \ +tailor executor jobs daily-workflow "$job_id" \ --wait \ --timeout 5m \ --logs \ diff --git a/packages/sdk/docs/cli/function.md b/packages/sdk/docs/cli/function.md index e06abea24..bcc74b55e 100644 --- a/packages/sdk/docs/cli/function.md +++ b/packages/sdk/docs/cli/function.md @@ -9,7 +9,7 @@ Manage functions **Usage** ``` -tailor-sdk function [command] +tailor function [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -30,7 +30,7 @@ Get a function registry by name **Usage** ``` -tailor-sdk function get [options] +tailor function get [options] ``` **Options** @@ -50,7 +50,7 @@ List function registries in a workspace **Usage** ``` -tailor-sdk function list [options] +tailor function list [options] ``` **Options** @@ -71,7 +71,7 @@ List or get function execution logs. **Usage** ``` -tailor-sdk function logs [options] [execution-id] +tailor function logs [options] [execution-id] ``` **Arguments** @@ -96,25 +96,25 @@ See [Global Options](../cli-reference.md#global-options) for options available t **List all function execution logs** ```bash -$ tailor-sdk function logs +$ tailor function logs ``` **Get execution details with logs** ```bash -$ tailor-sdk function logs +$ tailor function logs ``` **Output as JSON** ```bash -$ tailor-sdk function logs --json +$ tailor function logs --json ``` **Get execution details as JSON** ```bash -$ tailor-sdk function logs --json +$ tailor function logs --json ``` **Notes** @@ -130,7 +130,7 @@ Run a function on the Tailor Platform server without deploying. **Usage** ``` -tailor-sdk function test-run [options] +tailor function test-run [options] ``` **Arguments** @@ -157,19 +157,19 @@ See [Global Options](../cli-reference.md#global-options) for options available t **Run a resolver with input arguments** ```bash -$ tailor-sdk function test-run resolvers/add.ts --arg '{"a":1,"b":2}' +$ tailor function test-run resolvers/add.ts --arg '{"a":1,"b":2}' ``` **Run a specific workflow job by name** ```bash -$ tailor-sdk function test-run workflows/sample.ts --name validate-order +$ tailor function test-run workflows/sample.ts --name validate-order ``` **Run a pre-bundled .js file directly** ```bash -$ tailor-sdk function test-run build/resolvers/add.js --arg '{"a":1,"b":2}' +$ tailor function test-run build/resolvers/add.js --arg '{"a":1,"b":2}' ``` **Notes** diff --git a/packages/sdk/docs/cli/organization.md b/packages/sdk/docs/cli/organization.md index f1e29ec6d..23020746e 100644 --- a/packages/sdk/docs/cli/organization.md +++ b/packages/sdk/docs/cli/organization.md @@ -5,7 +5,7 @@ Manage Tailor Platform organizations. **Usage** ``` -tailor-sdk organization [command] +tailor organization [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -27,7 +27,7 @@ Manage organization folders. **Usage** ``` -tailor-sdk organization folder +tailor organization folder ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -49,7 +49,7 @@ Create a new folder in an organization. **Usage** ``` -tailor-sdk organization folder create [options] +tailor organization folder create [options] ``` **Options** @@ -69,7 +69,7 @@ Delete a folder from an organization. **Usage** ``` -tailor-sdk organization folder delete [options] +tailor organization folder delete [options] ``` **Options** @@ -89,7 +89,7 @@ Show detailed information about a folder. **Usage** ``` -tailor-sdk organization folder get [options] +tailor organization folder get [options] ``` **Options** @@ -108,7 +108,7 @@ List folders in an organization. **Usage** ``` -tailor-sdk organization folder list [options] +tailor organization folder list [options] ``` **Options** @@ -129,7 +129,7 @@ Update a folder's name. **Usage** ``` -tailor-sdk organization folder update [options] +tailor organization folder update [options] ``` **Options** @@ -149,7 +149,7 @@ Show detailed information about an organization. **Usage** ``` -tailor-sdk organization get [options] +tailor organization get [options] ``` **Options** @@ -167,7 +167,7 @@ List organizations you belong to. **Usage** ``` -tailor-sdk organization list [options] +tailor organization list [options] ``` **Options** @@ -185,7 +185,7 @@ Display organization folder hierarchy as a tree. **Usage** ``` -tailor-sdk organization tree [options] +tailor organization tree [options] ``` **Options** @@ -204,7 +204,7 @@ Update an organization's name. **Usage** ``` -tailor-sdk organization update [options] +tailor organization update [options] ``` **Options** diff --git a/packages/sdk/docs/cli/query.md b/packages/sdk/docs/cli/query.md index 567f99425..86d4570fe 100644 --- a/packages/sdk/docs/cli/query.md +++ b/packages/sdk/docs/cli/query.md @@ -5,7 +5,7 @@ Run SQL/GraphQL query. **Usage** ``` -tailor-sdk query [options] +tailor query [options] ``` **Options** diff --git a/packages/sdk/docs/cli/secret.md b/packages/sdk/docs/cli/secret.md index 5f93a04b9..0d08bb31a 100644 --- a/packages/sdk/docs/cli/secret.md +++ b/packages/sdk/docs/cli/secret.md @@ -9,7 +9,7 @@ Manage Secret Manager vaults and secrets. **Usage** ``` -tailor-sdk secret [command] +tailor secret [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -31,7 +31,7 @@ Create a secret in a vault. **Usage** ``` -tailor-sdk secret create [options] +tailor secret create [options] ``` **Options** @@ -54,7 +54,7 @@ Delete a secret in a vault. **Usage** ``` -tailor-sdk secret delete [options] +tailor secret delete [options] ``` **Options** @@ -76,7 +76,7 @@ List all secrets in a vault. **Usage** ``` -tailor-sdk secret list [options] +tailor secret list [options] ``` **Options** @@ -98,7 +98,7 @@ Update a secret in a vault. **Usage** ``` -tailor-sdk secret update [options] +tailor secret update [options] ``` **Options** @@ -121,7 +121,7 @@ Manage Secret Manager vaults. **Usage** ``` -tailor-sdk secret vault [command] +tailor secret vault [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -141,7 +141,7 @@ Create a new Secret Manager vault. **Usage** ``` -tailor-sdk secret vault create [options] +tailor secret vault create [options] ``` **Arguments** @@ -166,7 +166,7 @@ Delete a Secret Manager vault. **Usage** ``` -tailor-sdk secret vault delete [options] +tailor secret vault delete [options] ``` **Arguments** @@ -192,7 +192,7 @@ List all Secret Manager vaults in the workspace. **Usage** ``` -tailor-sdk secret vault list [options] +tailor secret vault list [options] ``` **Options** diff --git a/packages/sdk/docs/cli/setup.md b/packages/sdk/docs/cli/setup.md index c631bffc0..4f07cecc0 100644 --- a/packages/sdk/docs/cli/setup.md +++ b/packages/sdk/docs/cli/setup.md @@ -9,7 +9,7 @@ Generate a CI deploy workflow for your project. (beta) **Usage** ``` -tailor-sdk setup [options] [command] +tailor setup [options] [command] ``` **Options** @@ -41,7 +41,7 @@ Audit generated workflows for drift against the current config/repo (read-only). **Usage** ``` -tailor-sdk setup check +tailor setup check ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. diff --git a/packages/sdk/docs/cli/skills.md b/packages/sdk/docs/cli/skills.md index 17fcb873a..84514c903 100644 --- a/packages/sdk/docs/cli/skills.md +++ b/packages/sdk/docs/cli/skills.md @@ -5,25 +5,25 @@ Manage Tailor SDK agent skills. **Usage** ``` -tailor-sdk skills [command] +tailor skills [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. **Commands** -| Command | Description | -| ----------------------------------- | ------------------------------------------------------------------ | -| [`skills install`](#skills-install) | Install the tailor-sdk agent skill from the installed SDK package. | +| Command | Description | +| ----------------------------------- | -------------------------------------------------------------- | +| [`skills install`](#skills-install) | Install the tailor agent skill from the installed SDK package. | ### skills install -Install the tailor-sdk agent skill from the installed SDK package. +Install the tailor agent skill from the installed SDK package. **Usage** ``` -tailor-sdk skills install [options] +tailor skills install [options] ``` **Options** diff --git a/packages/sdk/docs/cli/staticwebsite.md b/packages/sdk/docs/cli/staticwebsite.md index e899c158e..1a6ffa768 100644 --- a/packages/sdk/docs/cli/staticwebsite.md +++ b/packages/sdk/docs/cli/staticwebsite.md @@ -9,7 +9,7 @@ Manage static websites in your workspace. **Usage** ``` -tailor-sdk staticwebsite [command] +tailor staticwebsite [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -30,7 +30,7 @@ Deploy a static website from a local build directory. **Usage** ``` -tailor-sdk staticwebsite deploy [options] +tailor staticwebsite deploy [options] ``` **Options** @@ -48,10 +48,10 @@ See [Global Options](../cli-reference.md#global-options) for options available t ```bash # Deploy a static website from the dist directory -tailor-sdk staticwebsite deploy --name my-website --dir ./dist +tailor staticwebsite deploy --name my-website --dir ./dist # Deploy with workspace ID -tailor-sdk staticwebsite deploy -n my-website -d ./dist -w ws_abc123 +tailor staticwebsite deploy -n my-website -d ./dist -w ws_abc123 ``` **Notes:** @@ -68,7 +68,7 @@ List all static websites in a workspace. **Usage** ``` -tailor-sdk staticwebsite list [options] +tailor staticwebsite list [options] ``` **Options** @@ -86,10 +86,10 @@ See [Global Options](../cli-reference.md#global-options) for options available t ```bash # List all static websites -tailor-sdk staticwebsite list +tailor staticwebsite list # List with JSON output -tailor-sdk staticwebsite list --json +tailor staticwebsite list --json ``` ### staticwebsite domain @@ -99,7 +99,7 @@ Manage custom domains for static websites. **Usage** ``` -tailor-sdk staticwebsite domain +tailor staticwebsite domain ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -118,7 +118,7 @@ Get details of a custom domain. **Usage** ``` -tailor-sdk staticwebsite domain get [options] +tailor staticwebsite domain get [options] ``` **Arguments** @@ -143,7 +143,7 @@ List custom domains for a static website. **Usage** ``` -tailor-sdk staticwebsite domain list [options] +tailor staticwebsite domain list [options] ``` **Arguments** @@ -168,7 +168,7 @@ Get details of a specific static website. **Usage** ``` -tailor-sdk staticwebsite get [options] +tailor staticwebsite get [options] ``` **Arguments** @@ -190,8 +190,8 @@ See [Global Options](../cli-reference.md#global-options) for options available t ```bash # Get details of a static website -tailor-sdk staticwebsite get my-website +tailor staticwebsite get my-website # Get with JSON output -tailor-sdk staticwebsite get my-website --json +tailor staticwebsite get my-website --json ``` diff --git a/packages/sdk/docs/cli/staticwebsite.template.md b/packages/sdk/docs/cli/staticwebsite.template.md index 708af789d..9fe41c137 100644 --- a/packages/sdk/docs/cli/staticwebsite.template.md +++ b/packages/sdk/docs/cli/staticwebsite.template.md @@ -25,10 +25,10 @@ Commands for managing and deploying static websites. ```bash # Deploy a static website from the dist directory -tailor-sdk staticwebsite deploy --name my-website --dir ./dist +tailor staticwebsite deploy --name my-website --dir ./dist # Deploy with workspace ID -tailor-sdk staticwebsite deploy -n my-website -d ./dist -w ws_abc123 +tailor staticwebsite deploy -n my-website -d ./dist -w ws_abc123 ``` **Notes:** @@ -44,10 +44,10 @@ tailor-sdk staticwebsite deploy -n my-website -d ./dist -w ws_abc123 ```bash # List all static websites -tailor-sdk staticwebsite list +tailor staticwebsite list # List with JSON output -tailor-sdk staticwebsite list --json +tailor staticwebsite list --json ``` {{politty:command:staticwebsite domain}} @@ -57,8 +57,8 @@ tailor-sdk staticwebsite list --json ```bash # Get details of a static website -tailor-sdk staticwebsite get my-website +tailor staticwebsite get my-website # Get with JSON output -tailor-sdk staticwebsite get my-website --json +tailor staticwebsite get my-website --json ``` diff --git a/packages/sdk/docs/cli/tailordb.md b/packages/sdk/docs/cli/tailordb.md index 08688fad9..b00919676 100644 --- a/packages/sdk/docs/cli/tailordb.md +++ b/packages/sdk/docs/cli/tailordb.md @@ -9,7 +9,7 @@ Manage TailorDB tables and data. **Usage** ``` -tailor-sdk tailordb +tailor tailordb ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -29,7 +29,7 @@ Truncate (delete all records from) TailorDB tables. **Usage** ``` -tailor-sdk tailordb truncate [options] [types] +tailor tailordb truncate [options] [types] ``` **Arguments** @@ -55,19 +55,19 @@ See [Global Options](../cli-reference.md#global-options) for options available t ```bash # Truncate all tables in all namespaces (requires confirmation) -tailor-sdk tailordb truncate --all +tailor tailordb truncate --all # Truncate all tables in all namespaces (skip confirmation) -tailor-sdk tailordb truncate --all --yes +tailor tailordb truncate --all --yes # Truncate all tables in a specific namespace -tailor-sdk tailordb truncate --namespace myNamespace +tailor tailordb truncate --namespace myNamespace # Truncate specific types (namespace is auto-detected) -tailor-sdk tailordb truncate User Post Comment +tailor tailordb truncate User Post Comment # Truncate specific types with confirmation skipped -tailor-sdk tailordb truncate User Post --yes +tailor tailordb truncate User Post --yes ``` **Notes:** @@ -85,12 +85,12 @@ tailor-sdk tailordb truncate User Post --yes Manage TailorDB schema migrations. -Note: Migration scripts are automatically executed during `tailor-sdk deploy`. See [Automatic Migration Execution](../services/tailordb-migration.md#automatic-migration-execution) for details. +Note: Migration scripts are automatically executed during `tailor deploy`. See [Automatic Migration Execution](../services/tailordb-migration.md#automatic-migration-execution) for details. **Usage** ``` -tailor-sdk tailordb migration +tailor tailordb migration ``` **Commands** @@ -112,7 +112,7 @@ Generate migration files by detecting schema differences between current local t **Usage** ``` -tailor-sdk tailordb migration generate [options] +tailor tailordb migration generate [options] ``` **Options** @@ -133,7 +133,7 @@ Add a migration script (migrate.ts) template to an existing migration directory. **Usage** ``` -tailor-sdk tailordb migration script [options] +tailor tailordb migration script [options] ``` **Arguments** @@ -158,7 +158,7 @@ Set migration checkpoint to a specific number. **Usage** ``` -tailor-sdk tailordb migration set [options] +tailor tailordb migration set [options] ``` **Arguments** @@ -186,7 +186,7 @@ Show the current migration status for TailorDB namespaces, including applied and **Usage** ``` -tailor-sdk tailordb migration status [options] +tailor tailordb migration status [options] ``` **Options** @@ -207,7 +207,7 @@ Sync remote TailorDB schema to a specific migration snapshot (recovery from --no **Usage** ``` -tailor-sdk tailordb migration sync [options] +tailor tailordb migration sync [options] ``` **Arguments** @@ -237,7 +237,7 @@ Generate TailorDB ERD viewer artifacts from local TailorDB schema. (beta) **Usage** ``` -tailor-sdk tailordb erd +tailor tailordb erd ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -257,7 +257,7 @@ Export TailorDB ERD static viewer from local TailorDB schema. **Usage** ``` -tailor-sdk tailordb erd export [options] +tailor tailordb erd export [options] ``` **Options** @@ -266,7 +266,7 @@ tailor-sdk tailordb erd export [options] | ------------------------- | ----- | ---------------------------------------------------------------------------------------------- | -------- | -------------------- | --------------------------------- | | `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | | `--namespace ` | `-n` | TailorDB namespace name (optional if only one namespace is defined in config) | No | - | - | -| `--output ` | `-o` | Output directory path for TailorDB ERD viewer files (writes to `//dist`) | No | `".tailor-sdk/erd"` | - | +| `--output ` | `-o` | Output directory path for TailorDB ERD viewer files (writes to `//dist`) | No | `".tailor/erd"` | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -277,7 +277,7 @@ Generate and serve TailorDB ERD locally with watch reload. (beta) **Usage** ``` -tailor-sdk tailordb erd serve [options] +tailor tailordb erd serve [options] ``` **Options** @@ -298,7 +298,7 @@ Deploy ERD static website for TailorDB namespace(s). **Usage** ``` -tailor-sdk tailordb erd deploy [options] +tailor tailordb erd deploy [options] ``` **Options** @@ -323,13 +323,13 @@ See [Global Options](../cli-reference.md#global-options) for options available t ```bash # Deploy ERD for all namespaces with erdSite configured -tailor-sdk tailordb erd deploy +tailor tailordb erd deploy # Deploy ERD for a specific namespace -tailor-sdk tailordb erd deploy --namespace myNamespace +tailor tailordb erd deploy --namespace myNamespace # Deploy ERD with JSON output -tailor-sdk tailordb erd deploy --json +tailor tailordb erd deploy --json ``` **Notes:** diff --git a/packages/sdk/docs/cli/tailordb.template.md b/packages/sdk/docs/cli/tailordb.template.md index 140aeb72d..ae65f4992 100644 --- a/packages/sdk/docs/cli/tailordb.template.md +++ b/packages/sdk/docs/cli/tailordb.template.md @@ -25,19 +25,19 @@ Commands for managing TailorDB tables, data, and schema migrations. ```bash # Truncate all tables in all namespaces (requires confirmation) -tailor-sdk tailordb truncate --all +tailor tailordb truncate --all # Truncate all tables in all namespaces (skip confirmation) -tailor-sdk tailordb truncate --all --yes +tailor tailordb truncate --all --yes # Truncate all tables in a specific namespace -tailor-sdk tailordb truncate --namespace myNamespace +tailor tailordb truncate --namespace myNamespace # Truncate specific types (namespace is auto-detected) -tailor-sdk tailordb truncate User Post Comment +tailor tailordb truncate User Post Comment # Truncate specific types with confirmation skipped -tailor-sdk tailordb truncate User Post --yes +tailor tailordb truncate User Post --yes ``` **Notes:** @@ -55,7 +55,7 @@ tailor-sdk tailordb truncate User Post --yes {{politty:command:tailordb migration:description}} -Note: Migration scripts are automatically executed during `tailor-sdk deploy`. See [Automatic Migration Execution](../services/tailordb-migration.md#automatic-migration-execution) for details. +Note: Migration scripts are automatically executed during `tailor deploy`. See [Automatic Migration Execution](../services/tailordb-migration.md#automatic-migration-execution) for details. {{politty:command:tailordb migration:usage}} @@ -95,13 +95,13 @@ Note: Migration scripts are automatically executed during `tailor-sdk deploy`. S ```bash # Deploy ERD for all namespaces with erdSite configured -tailor-sdk tailordb erd deploy +tailor tailordb erd deploy # Deploy ERD for a specific namespace -tailor-sdk tailordb erd deploy --namespace myNamespace +tailor tailordb erd deploy --namespace myNamespace # Deploy ERD with JSON output -tailor-sdk tailordb erd deploy --json +tailor tailordb erd deploy --json ``` **Notes:** diff --git a/packages/sdk/docs/cli/upgrade.md b/packages/sdk/docs/cli/upgrade.md index 4485bfd2c..bb1889c93 100644 --- a/packages/sdk/docs/cli/upgrade.md +++ b/packages/sdk/docs/cli/upgrade.md @@ -5,7 +5,7 @@ Run codemods to upgrade your project to a newer SDK version. **Usage** ``` -tailor-sdk upgrade [options] +tailor upgrade [options] ``` **Options** @@ -25,7 +25,7 @@ The `upgrade` command runs codemods that automatically transform your project co **Typical workflow:** 1. Update your SDK packages to the new version (e.g., `pnpm update @tailor-platform/sdk`) -2. Run `tailor-sdk upgrade --from ` to apply codemods +2. Run `tailor upgrade --from ` to apply codemods 3. Review changes and commit Use `--dry-run` to preview what changes will be made before applying them. diff --git a/packages/sdk/docs/cli/upgrade.template.md b/packages/sdk/docs/cli/upgrade.template.md index 592192f2f..b36976a24 100644 --- a/packages/sdk/docs/cli/upgrade.template.md +++ b/packages/sdk/docs/cli/upgrade.template.md @@ -14,7 +14,7 @@ The `upgrade` command runs codemods that automatically transform your project co **Typical workflow:** 1. Update your SDK packages to the new version (e.g., `pnpm update @tailor-platform/sdk`) -2. Run `tailor-sdk upgrade --from ` to apply codemods +2. Run `tailor upgrade --from ` to apply codemods 3. Review changes and commit Use `--dry-run` to preview what changes will be made before applying them. diff --git a/packages/sdk/docs/cli/user.md b/packages/sdk/docs/cli/user.md index fdaab3f36..55cff8722 100644 --- a/packages/sdk/docs/cli/user.md +++ b/packages/sdk/docs/cli/user.md @@ -9,7 +9,7 @@ Login to Tailor Platform. **Usage** ``` -tailor-sdk login [options] +tailor login [options] ``` **Options** @@ -37,7 +37,7 @@ Logout from Tailor Platform. **Usage** ``` -tailor-sdk logout +tailor logout ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -49,7 +49,7 @@ Manage Tailor Platform users. **Usage** ``` -tailor-sdk user [command] +tailor user [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -70,7 +70,7 @@ Show current user. **Usage** ``` -tailor-sdk user current +tailor user current ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -82,7 +82,7 @@ List all users. **Usage** ``` -tailor-sdk user list +tailor user list ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -94,7 +94,7 @@ Manage personal access tokens. **Usage** ``` -tailor-sdk user pat [command] +tailor user pat [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -115,7 +115,7 @@ Create a new personal access token. **Usage** ``` -tailor-sdk user pat create [options] +tailor user pat create [options] ``` **Arguments** @@ -139,7 +139,7 @@ Delete a personal access token. **Usage** ``` -tailor-sdk user pat delete +tailor user pat delete ``` **Arguments** @@ -157,7 +157,7 @@ List all personal access tokens. **Usage** ``` -tailor-sdk user pat list [options] +tailor user pat list [options] ``` **Options** @@ -176,7 +176,7 @@ Update a personal access token (delete and recreate). **Usage** ``` -tailor-sdk user pat update [options] +tailor user pat update [options] ``` **Arguments** @@ -200,7 +200,7 @@ Set current user. **Usage** ``` -tailor-sdk user switch +tailor user switch ``` **Arguments** diff --git a/packages/sdk/docs/cli/workflow.md b/packages/sdk/docs/cli/workflow.md index ee6c590f5..5b9884556 100644 --- a/packages/sdk/docs/cli/workflow.md +++ b/packages/sdk/docs/cli/workflow.md @@ -9,7 +9,7 @@ Manage workflows and workflow executions. **Usage** ``` -tailor-sdk workflow [command] +tailor workflow [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -32,7 +32,7 @@ List all workflows in the workspace. **Usage** ``` -tailor-sdk workflow list [options] +tailor workflow list [options] ``` **Options** @@ -53,7 +53,7 @@ Get workflow details. **Usage** ``` -tailor-sdk workflow get [options] +tailor workflow get [options] ``` **Arguments** @@ -78,7 +78,7 @@ Start a workflow execution. **Usage** ``` -tailor-sdk workflow start [options] +tailor workflow start [options] ``` **Arguments** @@ -108,13 +108,13 @@ See [Global Options](../cli-reference.md#global-options) for options available t ```bash # Start a workflow -tailor-sdk workflow start my-workflow -m admin-machine-user +tailor workflow start my-workflow -m admin-machine-user # Start with argument -tailor-sdk workflow start my-workflow -m admin -a '{"userId": "123"}' +tailor workflow start my-workflow -m admin -a '{"userId": "123"}' # Start and wait for completion -tailor-sdk workflow start my-workflow -m admin -W +tailor workflow start my-workflow -m admin -W ``` ### workflow wait @@ -124,7 +124,7 @@ Wait for a workflow execution. **Usage** ``` -tailor-sdk workflow wait [options] +tailor workflow wait [options] ``` **Arguments** @@ -151,19 +151,19 @@ See [Global Options](../cli-reference.md#global-options) for options available t **Wait for workflow success** ```bash -$ tailor-sdk workflow wait execution-id --until success --timeout 10m --json +$ tailor workflow wait execution-id --until success --timeout 10m --json ``` **Wait for a workflow wait point** ```bash -$ tailor-sdk workflow wait execution-id --until suspended --timeout 6m --logs --json +$ tailor workflow wait execution-id --until suspended --timeout 6m --logs --json ``` **Wait for success, failure, or suspension** ```bash -$ tailor-sdk workflow wait execution-id --until terminal +$ tailor workflow wait execution-id --until terminal ``` **Shell automation** @@ -173,10 +173,10 @@ separate command: ```bash execution_id="$( - tailor-sdk workflow start order-workflow --json | jq -r '.executionId' + tailor workflow start order-workflow --json | jq -r '.executionId' )" -tailor-sdk workflow wait "$execution_id" \ +tailor workflow wait "$execution_id" \ --until success \ --timeout 10m \ --interval 5s \ @@ -186,7 +186,7 @@ tailor-sdk workflow wait "$execution_id" \ Wait until a workflow reaches a wait point, such as an approval step: ```bash -tailor-sdk workflow wait "$execution_id" \ +tailor workflow wait "$execution_id" \ --until suspended \ --timeout 6m \ --logs \ @@ -226,7 +226,7 @@ List or get workflow executions. **Usage** ``` -tailor-sdk workflow executions [options] [execution-id] +tailor workflow executions [options] [execution-id] ``` **Arguments** @@ -257,22 +257,22 @@ See [Global Options](../cli-reference.md#global-options) for options available t ```bash # List all executions -tailor-sdk workflow executions +tailor workflow executions # Filter by workflow name -tailor-sdk workflow executions -n my-workflow +tailor workflow executions -n my-workflow # Filter by status -tailor-sdk workflow executions -s RUNNING +tailor workflow executions -s RUNNING # Get execution details -tailor-sdk workflow executions +tailor workflow executions # Get execution details with logs -tailor-sdk workflow executions --logs +tailor workflow executions --logs # Wait for execution to complete -tailor-sdk workflow executions -W +tailor workflow executions -W ``` ### workflow resume @@ -282,7 +282,7 @@ Resume a failed or pending workflow execution. **Usage** ``` -tailor-sdk workflow resume [options] +tailor workflow resume [options] ``` **Arguments** diff --git a/packages/sdk/docs/cli/workflow.template.md b/packages/sdk/docs/cli/workflow.template.md index 01001d71f..e9981a7cc 100644 --- a/packages/sdk/docs/cli/workflow.template.md +++ b/packages/sdk/docs/cli/workflow.template.md @@ -27,13 +27,13 @@ Commands for managing workflows and workflow executions. ```bash # Start a workflow -tailor-sdk workflow start my-workflow -m admin-machine-user +tailor workflow start my-workflow -m admin-machine-user # Start with argument -tailor-sdk workflow start my-workflow -m admin -a '{"userId": "123"}' +tailor workflow start my-workflow -m admin -a '{"userId": "123"}' # Start and wait for completion -tailor-sdk workflow start my-workflow -m admin -W +tailor workflow start my-workflow -m admin -W ``` {{politty:command:workflow wait}} @@ -45,10 +45,10 @@ separate command: ```bash execution_id="$( - tailor-sdk workflow start order-workflow --json | jq -r '.executionId' + tailor workflow start order-workflow --json | jq -r '.executionId' )" -tailor-sdk workflow wait "$execution_id" \ +tailor workflow wait "$execution_id" \ --until success \ --timeout 10m \ --interval 5s \ @@ -58,7 +58,7 @@ tailor-sdk workflow wait "$execution_id" \ Wait until a workflow reaches a wait point, such as an approval step: ```bash -tailor-sdk workflow wait "$execution_id" \ +tailor workflow wait "$execution_id" \ --until suspended \ --timeout 6m \ --logs \ @@ -97,22 +97,22 @@ if (result.timedOut) { ```bash # List all executions -tailor-sdk workflow executions +tailor workflow executions # Filter by workflow name -tailor-sdk workflow executions -n my-workflow +tailor workflow executions -n my-workflow # Filter by status -tailor-sdk workflow executions -s RUNNING +tailor workflow executions -s RUNNING # Get execution details -tailor-sdk workflow executions +tailor workflow executions # Get execution details with logs -tailor-sdk workflow executions --logs +tailor workflow executions --logs # Wait for execution to complete -tailor-sdk workflow executions -W +tailor workflow executions -W ``` {{politty:command:workflow resume}} diff --git a/packages/sdk/docs/cli/workspace.md b/packages/sdk/docs/cli/workspace.md index 2a8000dcc..be5f77c66 100644 --- a/packages/sdk/docs/cli/workspace.md +++ b/packages/sdk/docs/cli/workspace.md @@ -9,7 +9,7 @@ Manage Tailor Platform workspaces. **Usage** ``` -tailor-sdk workspace [command] +tailor workspace [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -33,7 +33,7 @@ Manage workspace applications **Usage** ``` -tailor-sdk workspace app [command] +tailor workspace app [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -52,7 +52,7 @@ Check application schema health **Usage** ``` -tailor-sdk workspace app health [options] +tailor workspace app health [options] ``` **Options** @@ -72,7 +72,7 @@ List applications in a workspace **Usage** ``` -tailor-sdk workspace app list [options] +tailor workspace app list [options] ``` **Options** @@ -93,7 +93,7 @@ Create a new Tailor Platform workspace. **Usage** ``` -tailor-sdk workspace create [options] +tailor workspace create [options] ``` **Options** @@ -118,7 +118,7 @@ Delete a Tailor Platform workspace. **Usage** ``` -tailor-sdk workspace delete [options] +tailor workspace delete [options] ``` **Options** @@ -137,7 +137,7 @@ Show detailed information about a workspace **Usage** ``` -tailor-sdk workspace get [options] +tailor workspace get [options] ``` **Options** @@ -156,7 +156,7 @@ List all Tailor Platform workspaces. **Usage** ``` -tailor-sdk workspace list [options] +tailor workspace list [options] ``` **Options** @@ -175,7 +175,7 @@ Restore a deleted workspace **Usage** ``` -tailor-sdk workspace restore [options] +tailor workspace restore [options] ``` **Options** @@ -194,7 +194,7 @@ Manage workspace users **Usage** ``` -tailor-sdk workspace user [command] +tailor workspace user [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -215,7 +215,7 @@ Invite a user to a workspace **Usage** ``` -tailor-sdk workspace user invite [options] +tailor workspace user invite [options] ``` **Options** @@ -236,7 +236,7 @@ List users in a workspace **Usage** ``` -tailor-sdk workspace user list [options] +tailor workspace user list [options] ``` **Options** @@ -257,7 +257,7 @@ Remove a user from a workspace **Usage** ``` -tailor-sdk workspace user remove [options] +tailor workspace user remove [options] ``` **Options** @@ -278,7 +278,7 @@ Update a user's role in a workspace **Usage** ``` -tailor-sdk workspace user update [options] +tailor workspace user update [options] ``` **Options** @@ -299,7 +299,7 @@ Manage workspace profiles (user + workspace combinations). **Usage** ``` -tailor-sdk profile [command] +tailor profile [command] ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -320,7 +320,7 @@ Create a new profile. **Usage** ``` -tailor-sdk profile create [options] +tailor profile create [options] ``` **Arguments** @@ -348,7 +348,7 @@ Delete a profile. **Usage** ``` -tailor-sdk profile delete +tailor profile delete ``` **Arguments** @@ -366,7 +366,7 @@ List all profiles. **Usage** ``` -tailor-sdk profile list +tailor profile list ``` See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -378,7 +378,7 @@ Update profile properties. **Usage** ``` -tailor-sdk profile update [options] +tailor profile update [options] ``` **Arguments** diff --git a/packages/sdk/docs/github-actions.md b/packages/sdk/docs/github-actions.md index f8da796a4..fb97ba568 100644 --- a/packages/sdk/docs/github-actions.md +++ b/packages/sdk/docs/github-actions.md @@ -1,10 +1,10 @@ # GitHub Actions Integration -`tailor-sdk setup` generates a GitHub Actions workflow that deploys your +`tailor setup` generates a GitHub Actions workflow that deploys your Tailor Platform application automatically on push or tag. > **Beta:** This command is under active development. CLI flags, the generated -> workflow, and the `.github/tailor-sdk.lock` schema may change before general +> workflow, and the `.github/tailor.lock` schema may change before general > availability. ## Quick start @@ -14,10 +14,10 @@ lives): ```bash # Branch target: deploy to stg on every push to main -tailor-sdk setup -n my-app-stg +tailor setup -n my-app-stg # Tag target: deploy to production when a tag is pushed, with an approval gate -tailor-sdk setup -n my-app-prod \ +tailor setup -n my-app-prod \ --tag --branch main --environment production ``` @@ -44,9 +44,9 @@ The branch target fires on pull requests and pushes to the branch you specify (defaulting to the repository's default branch when `--branch` is omitted): ```bash -tailor-sdk setup -n my-app-stg +tailor setup -n my-app-stg # Equivalent to: -tailor-sdk setup -n my-app-stg --branch main +tailor setup -n my-app-stg --branch main ``` What it does: @@ -69,7 +69,7 @@ The tag target fires when a tag matching `--tag-pattern` (default `v*`) is pushed: ```bash -tailor-sdk setup -n my-app-prod \ +tailor setup -n my-app-prod \ --tag --tag-pattern "v*" --branch main --environment production ``` @@ -118,7 +118,7 @@ Because the variable is scoped to a GitHub Environment, both the `plan` and this is a manual step: ```bash - tailor-sdk workspace create # copy the printed workspace id + tailor workspace create # copy the printed workspace id ``` 2. Set the id as the Environment variable (the environment name is your @@ -161,7 +161,7 @@ step or added your own — the command stops and reports the conflict. Pass re-apply your own steps. (Preserving user-added steps across regeneration is planned.) -### `.github/tailor-sdk.lock` +### `.github/tailor.lock` A machine-owned JSON file that tracks which files the SDK manages, the inputs they were generated from, and their content hashes. **Commit this file. Never @@ -171,7 +171,7 @@ detect hand edits. ### `tailor.config.ts` (id injection) If your config does not already have an `id` field, `setup` injects one. -This `id` must be committed alongside the workflow file. In CI, `tailor-sdk +This `id` must be committed alongside the workflow file. In CI, `tailor deploy` refuses to inject a new id — if the id were assigned fresh on each CI run, every deploy would create a brand-new application and lose ownership of previously deployed resources. @@ -241,7 +241,7 @@ you can deploy any commit regardless of branch membership. For a monorepo where your SDK app lives in a subdirectory, pass `--dir`: ```bash -tailor-sdk setup -n my-app --dir apps/backend +tailor setup -n my-app --dir apps/backend ``` The generated workflow adds a `paths` filter on `apps/backend/**` so the @@ -250,7 +250,7 @@ SDK commands is set accordingly. ## Rollback -`tailor-sdk deploy` is declarative: redeploying a past configuration returns +`tailor deploy` is declarative: redeploying a past configuration returns the platform to that state. The recommended rollback approaches are: ### Option 1 — Revert the commit (branch target) @@ -306,10 +306,10 @@ A typical setup with staging and production: ```bash # Staging: main → stg (deploy on every push to main) -tailor-sdk setup -n my-app-stg +tailor setup -n my-app-stg # Production: tagged commits → prod, with approval gate and branch guard -tailor-sdk setup -n my-app-prod \ +tailor setup -n my-app-prod \ --tag --branch main --environment production ``` @@ -327,12 +327,12 @@ gh secret set TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID --env production gh secret set TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET --env production ``` -Commit both workflow files and `.github/tailor-sdk.lock`. +Commit both workflow files and `.github/tailor.lock`. ## Checking for drift -`tailor-sdk setup check` audits the workflows recorded in -`.github/tailor-sdk.lock` against your current config and repository, without +`tailor setup check` audits the workflows recorded in +`.github/tailor.lock` against your current config and repository, without writing anything. It reports when a workflow file is missing or hand-edited, a newer template is available, `tailor.config.ts` is no longer under the recorded `--dir`, or the repository default branch no longer matches a branch target's @@ -346,5 +346,5 @@ template improvements. If the SDK detects that you have hand-edited a managed section, it stops and asks you to use `--force` to overwrite your edits, or to move your customizations into your own steps before regenerating. -The `.github/tailor-sdk.lock` file records the flags used at generation time, +The `.github/tailor.lock` file records the flags used at generation time, so you can check what arguments were used previously. diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 15c50f6fa..d149d8a5a 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -68,46 +68,46 @@ import { kyselyTypePlugin } from "@tailor-platform/sdk/plugin/kysely-type"; **Migration:** Automatic -Strip the deprecated {input: ...} wrapper from `tailor-sdk function test-run --arg` JSON in scripts and docs +Strip the deprecated {input: ...} wrapper from `tailor function test-run --arg` JSON in scripts and docs Before: ```sh -tailor-sdk function test-run resolvers/add.ts --arg '{"input":{"a":1}}' +tailor function test-run resolvers/add.ts --arg '{"input":{"a":1}}' ``` After: ```sh -tailor-sdk function test-run resolvers/add.ts --arg '{"a":1}' +tailor function test-run resolvers/add.ts --arg '{"a":1}' ``` -## tailor-sdk-skills → tailor-sdk skills install +## tailor-skills → tailor skills install **Migration:** Partially automatic -Replace deprecated `tailor-sdk-skills` invocations with `tailor-sdk skills install` +Replace deprecated `tailor-skills` invocations with `tailor skills install` Before: ```sh -npx tailor-sdk-skills +npx tailor-skills ``` After: ```sh -tailor-sdk skills install +tailor skills install ```
Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text -The standalone tailor-sdk-skills binary is removed in v2; call the skills install -subcommand on the main tailor-sdk CLI instead. Replace any remaining -tailor-sdk-skills invocations the codemod did not rewrite with -`tailor-sdk skills install`. +The standalone tailor-skills binary is removed in v2; call the skills install +subcommand on the main tailor CLI instead. Replace any remaining +tailor-skills invocations the codemod did not rewrite with +`tailor skills install`. ```
@@ -167,41 +167,41 @@ Use TailorPrincipal for the unified user/actor/invoker type. -## tailor-sdk apply → tailor-sdk deploy +## tailor apply → tailor deploy **Migration:** Automatic -Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor-sdk deploy` command +Rewrite `tailor apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor deploy` command Before: ```sh -tailor-sdk apply --profile prod +tailor apply --profile prod ``` After: ```sh -tailor-sdk deploy --profile prod +tailor deploy --profile prod ``` ## v2 CLI rename **Migration:** Partially automatic -Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs +Rewrite `tailor crash-report` to `tailor crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs Before: ```sh -tailor-sdk crash-report list +tailor crash-report list tailor-sdk login --machineuser ``` After: ```sh -tailor-sdk crashreport list +tailor crashreport list tailor-sdk login --machine-user ``` @@ -209,8 +209,8 @@ tailor-sdk login --machine-user Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text -Apply the v2 CLI renames the codemod did not reach (only `tailor-sdk`-prefixed -invocations are rewritten): `tailor-sdk crash-report` -> `tailor-sdk crashreport` +Apply the v2 CLI renames the codemod did not reach (only `tailor`-prefixed +invocations are rewritten): `tailor crash-report` -> `tailor crashreport` and the `--machineuser` option -> `--machine-user`. Leave unrelated commands that happen to use `--machineuser` alone. ``` @@ -460,6 +460,37 @@ trigger result as the job output directly (no Promise wrapper to unwrap). +## tailor-sdk binary → tailor + +**Migration:** Partially automatic + +Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. + +Before: + +```sh +tailor-sdk deploy +npx tailor-sdk@latest login +``` + +After: + +```sh +tailor deploy +npx tailor@latest login +``` + +
+Prompt for an AI agent (to finish the cases the codemod could not migrate) + +```text +Rename any remaining `tailor-sdk` binary invocations to `tailor`. Only rewrite +the binary name — leave `.tailor-sdk` directory paths and `create-tailor-sdk` +package references unchanged. +``` + +
+ ## Behavioral changes (no migration required) These v2 changes alter runtime or CLI behavior; no source change is needed. @@ -474,4 +505,4 @@ The CLI stores human users by their stable subject ID instead of email (email is ### function logs require a content hash for source mapping -`tailor-sdk function logs` maps stack traces against the function bundle only when the execution recorded a `contentHash`. Executions without one now show raw stack traces instead of mapped frames. No source change is required. +`tailor function logs` maps stack traces against the function bundle only when the execution recorded a `contentHash`. Executions without one now show raw stack traces instead of mapped frames. No source change is required. diff --git a/packages/sdk/docs/multi-environment.md b/packages/sdk/docs/multi-environment.md index 2f4b54b36..dfcd99561 100644 --- a/packages/sdk/docs/multi-environment.md +++ b/packages/sdk/docs/multi-environment.md @@ -11,16 +11,16 @@ Deployment commands resolve the target workspace from, in priority order, the `- For one-off commands, pass the workspace explicitly: ```bash -tailor-sdk deploy -w +tailor deploy -w ``` For environments you switch between regularly, create a named profile per environment with the [profile commands](./cli/workspace.md#profile-create) and select it with `--profile` (`-p`) or `TAILOR_PLATFORM_PROFILE`: ```bash -tailor-sdk profile create staging -u you@example.com -w -tailor-sdk profile create production -u you@example.com -w --permission read +tailor profile create staging -u you@example.com -w +tailor profile create production -u you@example.com -w --permission read -tailor-sdk deploy -p staging +tailor deploy -p staging ``` Profiles are created with `write` permission by default. The production profile above opts into `--permission read`, which blocks write commands such as `deploy` while the profile is active — a guard against deploying to production by accident. To deploy to production deliberately, pass the workspace explicitly with `-w` without selecting the profile — the guard applies only while a profile is selected via `-p` or `TAILOR_PLATFORM_PROFILE` — or use a separate profile created with `write` permission. @@ -36,7 +36,7 @@ LOG_LEVEL=WARN ``` ```bash -tailor-sdk deploy -w --env-file .env.production +tailor deploy -w --env-file .env.production ``` ```typescript diff --git a/packages/sdk/docs/plugin/index.md b/packages/sdk/docs/plugin/index.md index c646700bf..dbb2012a5 100644 --- a/packages/sdk/docs/plugin/index.md +++ b/packages/sdk/docs/plugin/index.md @@ -6,7 +6,7 @@ Plugins extend TailorDB types by automatically generating additional types, exec ## Overview -When you run `tailor-sdk generate`, the SDK: +When you run `tailor generate`, the SDK: 1. Loads all TailorDB types with plugin attachments 2. Passes each type to the attached plugins @@ -102,18 +102,18 @@ Plugins can generate: - **Field Extensions**: Additional fields added to the source type - **Output Files**: TypeScript code and other files via generation-time hooks -Generated files are placed under `.tailor-sdk//` (the plugin ID is sanitized, +Generated files are placed under `.tailor//` (the plugin ID is sanitized, e.g. `@example/soft-delete` → `example-soft-delete`), such as: -- `.tailor-sdk/example-soft-delete/types` -- `.tailor-sdk/example-soft-delete/executors` +- `.tailor/example-soft-delete/types` +- `.tailor/example-soft-delete/executors` ## Plugin Lifecycle -Plugins have 5 hooks across two lifecycle phases. Each hook fires at a specific point in the `tailor-sdk generate` pipeline: +Plugins have 5 hooks across two lifecycle phases. Each hook fires at a specific point in the `tailor generate` pipeline: ``` -tailor-sdk generate +tailor generate │ ├─ Load TailorDB types │ ├─ onTypeLoaded ← per type with .plugin() attached diff --git a/packages/sdk/docs/quickstart.md b/packages/sdk/docs/quickstart.md index b3071e772..95d73b16a 100644 --- a/packages/sdk/docs/quickstart.md +++ b/packages/sdk/docs/quickstart.md @@ -31,13 +31,13 @@ npm create @tailor-platform/sdk -- --template hello-world example-app Before deploying your app, you need to create a workspace: ```bash -npx tailor-sdk login -npx tailor-sdk workspace create --name --region -npx tailor-sdk workspace list +npx tailor login +npx tailor workspace create --name --region +npx tailor workspace list # Or with Bun: -# bunx tailor-sdk login -# bunx tailor-sdk workspace create --name --region +# bunx tailor login +# bunx tailor workspace create --name --region # OR # Create a new workspace using Tailor Platform Console diff --git a/packages/sdk/docs/services/auth.md b/packages/sdk/docs/services/auth.md index 07a5d0418..b579879d8 100644 --- a/packages/sdk/docs/services/auth.md +++ b/packages/sdk/docs/services/auth.md @@ -182,7 +182,7 @@ export const auth = defineAuth("my-auth", { To update types in `tailor.d.ts`, run: ```bash -tailor-sdk generate +tailor generate ``` ## Machine Users @@ -264,7 +264,7 @@ Machine users are useful for: Get a machine user token using the CLI: ```bash -tailor-sdk machineuser token +tailor machineuser token ``` ### Specifying a machine user invoker @@ -308,7 +308,7 @@ export default createResolver({ }); ``` -Type narrowing is provided by the generated `tailor.d.ts` (the `MachineUserNameRegistry` interface). Run `tailor-sdk generate` (or `apply`) after defining new machine users to refresh it. +Type narrowing is provided by the generated `tailor.d.ts` (the `MachineUserNameRegistry` interface). Run `tailor generate` (or `apply`) after defining new machine users to refresh it. ## OAuth 2.0 Clients @@ -348,7 +348,7 @@ oauth2Clients: { Get OAuth2 client credentials using the CLI: ```bash -tailor-sdk oauth2client get +tailor oauth2client get ``` ## Identity Provider @@ -369,12 +369,12 @@ For the official Tailor Platform documentation, see [AuthConnection Guide](https > [!WARNING] > **Managing connections through `tailor.config.ts` is unreliable for shared and CI deploys.** -> A deploy revokes and recreates the connection — discarding the token obtained via `authconnection authorize` — whenever it cannot confirm the secret is unchanged. That check relies on a hash stored locally in `.tailor-sdk/secrets-state.json`, which is gitignored and therefore not shared across machines. So a deploy from CI, a clean checkout, another developer's machine, or after deleting `.tailor-sdk/` recreates the connection and drops its token. Only repeated deploys from the same machine that still holds that state keep the token. +> A deploy revokes and recreates the connection — discarding the token obtained via `authconnection authorize` — whenever it cannot confirm the secret is unchanged. That check relies on a hash stored locally in `.tailor/secrets-state.json`, which is gitignored and therefore not shared across machines. So a deploy from CI, a clean checkout, another developer's machine, or after deleting `.tailor/` recreates the connection and drops its token. Only repeated deploys from the same machine that still holds that state keep the token. > > Because of this, prefer to **create the connection and its token from the Console**. You can jump to the connections page with: > > ```bash -> tailor-sdk authconnection open +> tailor authconnection open > ``` > > The `connections` field in `defineAuth()` and the `authconnection authorize` flow are documented below for reference. @@ -409,10 +409,10 @@ export const auth = defineAuth("my-auth", { }); ``` -After `tailor-sdk deploy`, authorize the connection: +After `tailor deploy`, authorize the connection: ```bash -tailor-sdk authconnection authorize --name google-connection \ +tailor authconnection authorize --name google-connection \ --scopes "openid,profile,email" ``` @@ -432,10 +432,10 @@ The authorize command opens a browser for the OAuth2 flow. The authorization cod ### Change Detection -The SDK uses hash-based change detection for connection configs. Only connections whose configuration has changed since the last `apply` are updated (revoked and recreated). Deleting the `.tailor-sdk/` directory forces all connections to be re-sent. +The SDK uses hash-based change detection for connection configs. Only connections whose configuration has changed since the last `apply` are updated (revoked and recreated). Deleting the `.tailor/` directory forces all connections to be re-sent. > [!WARNING] -> The secret hash lives in `.tailor-sdk/secrets-state.json`, which is gitignored and not shared across machines or CI. When that state is missing — a clean checkout, CI, another machine, or after deleting `.tailor-sdk/` — a deploy cannot confirm the secret is unchanged, so it revokes and recreates the connection and discards the token stored by `authconnection authorize`. For shared and CI workflows, manage the connection and create its token from the Console (`tailor-sdk authconnection open`) instead. +> The secret hash lives in `.tailor/secrets-state.json`, which is gitignored and not shared across machines or CI. When that state is missing — a clean checkout, CI, another machine, or after deleting `.tailor/` — a deploy cannot confirm the secret is unchanged, so it revokes and recreates the connection and discards the token stored by `authconnection authorize`. For shared and CI workflows, manage the connection and create its token from the Console (`tailor authconnection open`) instead. ### `auth.getConnectionToken()` @@ -463,19 +463,19 @@ Auth connections can also be managed via the CLI: ```bash # Open the connections page in the Console (recommended for creating connections/tokens) -tailor-sdk authconnection open +tailor authconnection open # Authorize (opens browser for OAuth2 flow) -tailor-sdk authconnection authorize --name google-connection +tailor authconnection authorize --name google-connection # List all connections -tailor-sdk authconnection list +tailor authconnection list # Revoke a connection -tailor-sdk authconnection revoke --name google-connection +tailor authconnection revoke --name google-connection ``` -Connection creation is handled by `tailor-sdk deploy` via the config, but recreation on deploy can drop the authorized token (see the warning at the top of this section) — for shared and CI workflows, create connections and tokens from the Console (`tailor-sdk authconnection open`) instead. +Connection creation is handled by `tailor deploy` via the config, but recreation on deploy can drop the authorized token (see the warning at the top of this section) — for shared and CI workflows, create connections and tokens from the Console (`tailor authconnection open`) instead. See [Auth Resource Commands](../cli/auth.md) for full CLI documentation. @@ -538,21 +538,21 @@ Manage Auth resources using the CLI: ```bash # Auth connections -tailor-sdk authconnection authorize --name -tailor-sdk authconnection list -tailor-sdk authconnection revoke --name +tailor authconnection authorize --name +tailor authconnection list +tailor authconnection revoke --name # List machine users -tailor-sdk machineuser list +tailor machineuser list # Get machine user token -tailor-sdk machineuser token +tailor machineuser token # List OAuth2 clients -tailor-sdk oauth2client list +tailor oauth2client list # Get OAuth2 client credentials -tailor-sdk oauth2client get +tailor oauth2client get ``` See [Auth Resource Commands](../cli/auth.md) for full documentation. diff --git a/packages/sdk/docs/services/resolver.md b/packages/sdk/docs/services/resolver.md index 6c44f617d..51d2a6f11 100644 --- a/packages/sdk/docs/services/resolver.md +++ b/packages/sdk/docs/services/resolver.md @@ -306,7 +306,7 @@ createResolver({ - When `publishEvents: true`, resolver execution events are published - When not specified, it is **automatically set to `true`** if an executor uses this resolver with `resolverExecutedTrigger` -- When explicitly set to `false` while an executor uses this resolver, an error is thrown during `tailor-sdk deploy` +- When explicitly set to `false` while an executor uses this resolver, an error is thrown during `tailor deploy` **Use cases:** diff --git a/packages/sdk/docs/services/secret.md b/packages/sdk/docs/services/secret.md index 374cb7dd4..6cc7543fb 100644 --- a/packages/sdk/docs/services/secret.md +++ b/packages/sdk/docs/services/secret.md @@ -34,11 +34,11 @@ Secrets are key-value pairs stored within a vault. Secret values are encrypted a ## Managing Secrets -There are two ways to manage secrets: declaratively via `defineSecretManager()` in `tailor.config.ts`, or imperatively via the [CLI](#cli-management). Management is scoped per vault — **do not mix both approaches for the same vault**. When a vault is defined in config, the config becomes the source of truth: any secrets in that vault not present in the config will be deleted on `tailor-sdk deploy`. +There are two ways to manage secrets: declaratively via `defineSecretManager()` in `tailor.config.ts`, or imperatively via the [CLI](#cli-management). Management is scoped per vault — **do not mix both approaches for the same vault**. When a vault is defined in config, the config becomes the source of truth: any secrets in that vault not present in the config will be deleted on `tailor deploy`. ### Declarative Configuration -Define your secrets in `tailor.config.ts` using `defineSecretManager()`. Each key is a vault name, and its value is a record of secret names to their values. These values are deployed to each vault on `tailor-sdk deploy`. +Define your secrets in `tailor.config.ts` using `defineSecretManager()`. Each key is a vault name, and its value is a record of secret names to their values. These values are deployed to each vault on `tailor deploy`. Since secret values should not be committed to source control, use environment variables: @@ -87,7 +87,7 @@ When `ignoreNullishValues: true`: - Skipped secrets are shown in the deploy output for visibility - Secrets removed from the config entirely are still deleted (orphan cleanup) -This allows you to set secret values once (e.g., via local `tailor-sdk deploy` or the CLI) and then deploy from CI without needing the actual values in CI environment variables. +This allows you to set secret values once (e.g., via local `tailor deploy` or the CLI) and then deploy from CI without needing the actual values in CI environment variables. ## Using Secrets @@ -175,25 +175,25 @@ At runtime, these references are replaced with the actual secret values. Use the CLI to manage vaults that are **not** defined in `defineSecretManager()`. If you attempt to modify a vault that is managed by the config, the CLI will show a warning and ask for confirmation. Once confirmed, the CLI releases the vault's ownership label so it is no longer managed by config. -After ownership is released, the next `tailor-sdk deploy` will treat the vault as an unmanaged resource and prompt for confirmation before taking any action on it. +After ownership is released, the next `tailor deploy` will treat the vault as an unmanaged resource and prompt for confirmation before taking any action on it. ### Create a Vault ```bash -tailor-sdk secret vault create --name api-keys +tailor secret vault create --name api-keys ``` ### Add Secrets ```bash # Create a secret -tailor-sdk secret create \ +tailor secret create \ --vault-name api-keys \ --name stripe-secret-key \ --value sk_live_xxxxx # Update a secret -tailor-sdk secret update \ +tailor secret update \ --vault-name api-keys \ --name stripe-secret-key \ --value sk_live_yyyyy @@ -203,20 +203,20 @@ tailor-sdk secret update \ ```bash # List vaults -tailor-sdk secret vault list +tailor secret vault list # List secrets in a vault (values are hidden) -tailor-sdk secret list --vault-name api-keys +tailor secret list --vault-name api-keys ``` ### Delete Secrets ```bash # Delete a secret -tailor-sdk secret delete --vault-name api-keys --name old-key --yes +tailor secret delete --vault-name api-keys --name old-key --yes # Delete a vault (must be empty) -tailor-sdk secret vault delete --name old-vault --yes +tailor secret vault delete --name old-vault --yes ``` See [Secret CLI Commands](../cli/secret.md) for full documentation. diff --git a/packages/sdk/docs/services/staticwebsite.md b/packages/sdk/docs/services/staticwebsite.md index 70143c61a..6a7c6dcc0 100644 --- a/packages/sdk/docs/services/staticwebsite.md +++ b/packages/sdk/docs/services/staticwebsite.md @@ -72,7 +72,7 @@ defineStaticWebSite("my-website", { }); ``` -After deploying, use `tailor-sdk staticwebsite domain get ` to check domain status and retrieve the CNAME targets required for DNS configuration. +After deploying, use `tailor staticwebsite domain get ` to check domain status and retrieve the CNAME targets required for DNS configuration. A domain can be associated with only one workspace at a time. To set custom domains only in the workspace that owns the domain, see [Multi-Environment Configuration](../multi-environment.md#settings-that-belong-to-a-single-environment). diff --git a/packages/sdk/docs/services/tailordb-migration.md b/packages/sdk/docs/services/tailordb-migration.md index 276dba4a5..68f0af10a 100644 --- a/packages/sdk/docs/services/tailordb-migration.md +++ b/packages/sdk/docs/services/tailordb-migration.md @@ -12,7 +12,7 @@ For the CLI command reference, see [`tailordb migration`](../cli/tailordb.md#tai - **Local snapshot–based diff detection** — each migration is generated by diffing your current type definitions against the previous snapshot stored in `migrations//`. - **Transaction-wrapped data migrations** — each `migrate.ts` script runs inside a database transaction on the platform; if the script throws, all changes in that migration roll back. -- **Automatic execution during `apply`** — `tailor-sdk deploy` detects pending migrations, runs the two-stage type update (pre-migration → script → post-migration), and updates the migration checkpoint label. +- **Automatic execution during `apply`** — `tailor deploy` detects pending migrations, runs the two-stage type update (pre-migration → script → post-migration), and updates the migration checkpoint label. - **Type-safe scripts** — the generated `db.ts` provides Kysely types that reflect the schema state **before** the migration runs, so transformations are written against the actual data shape. **Files in `migrations/`** @@ -42,24 +42,24 @@ When you start with no `migrations/` directory: 2. Define your initial types in `tailordb/`. 3. Generate the initial migration: ```bash - tailor-sdk tailordb migration generate + tailor tailordb migration generate ``` This creates `migrations/0000/schema.json` from your current types. -4. Run `tailor-sdk deploy`. The migration label is set to `0000` on the deployed namespace. +4. Run `tailor deploy`. The migration label is set to `0000` on the deployed namespace. ### Adding migrations to an existing project If you already have a deployed workspace whose schema matches your local type definitions: 1. Add the `migration` block to `tailor.config.ts`. -2. Run `tailor-sdk tailordb migration generate` to create `0000/schema.json` from current local types. -3. Run `tailor-sdk deploy`. Because remote schema already matches, no script runs; only the migration label is set. +2. Run `tailor tailordb migration generate` to create `0000/schema.json` from current local types. +3. Run `tailor deploy`. Because remote schema already matches, no script runs; only the migration label is set. If your local types and remote schema have **diverged**, reconcile them before introducing migrations — either update local types to match remote, or accept that the first non-`0000` migration will reflect that gap. ### Resetting -`tailor-sdk tailordb migration generate --init` deletes the existing `migrations/` directory and starts over from `0000`. Use this only on projects that are not yet deployed, or when you have decided to re-baseline (the next `apply` will see all migrations as new and require coordination — see [Resetting a deployed project](#resetting-a-deployed-project)). +`tailor tailordb migration generate --init` deletes the existing `migrations/` directory and starts over from `0000`. Use this only on projects that are not yet deployed, or when you have decided to re-baseline (the next `apply` will see all migrations as new and require coordination — see [Resetting a deployed project](#resetting-a-deployed-project)). ## Migration Workflow @@ -79,7 +79,7 @@ A typical change cycle: 2. **Generate the migration.** ```bash - tailor-sdk tailordb migration generate --name "add email to user" + tailor tailordb migration generate --name "add email to user" ``` Output: @@ -109,7 +109,7 @@ A typical change cycle: 4. **Apply.** ```bash - tailor-sdk deploy + tailor deploy ``` The pre-migration phase relaxes the new field to optional, the script runs and populates values, then the post-migration phase enforces `required: true`. @@ -126,10 +126,10 @@ Warning: data loss possible: No `migrate.ts` is generated automatically because the schema change itself is non-breaking, but the existing data is dropped during the post-migration phase. If you need to preserve or transform that data first (for example, copy a column into another table before it disappears), add a script with: ```bash -tailor-sdk tailordb migration script 0002 +tailor tailordb migration script 0002 ``` -This writes `migrations/0002/migrate.ts` and `migrations/0002/db.ts` next to the existing `diff.json`. The removed field stays readable inside `migrate.ts` because the pre-migration phase keeps it on the type until the script finishes (see [Per-migration phases](#per-migration-phases)). The next `tailor-sdk deploy` runs the script automatically — `migrate.ts` is executed whenever the file exists on disk, regardless of whether the diff itself required it. +This writes `migrations/0002/migrate.ts` and `migrations/0002/db.ts` next to the existing `diff.json`. The removed field stays readable inside `migrate.ts` because the pre-migration phase keeps it on the type until the script finishes (see [Per-migration phases](#per-migration-phases)). The next `tailor deploy` runs the script automatically — `migrate.ts` is executed whenever the file exists on disk, regardless of whether the diff itself required it. ## Configuration @@ -245,7 +245,7 @@ The same pattern works for switching between scalar and array. ## Automatic Migration Execution -When you run `tailor-sdk deploy`, the SDK detects pending migrations (anything past the current `sdk-migration` label on the deployed namespace) and runs them in order before continuing with the rest of the apply. +When you run `tailor deploy`, the SDK detects pending migrations (anything past the current `sdk-migration` label on the deployed namespace) and runs them in order before continuing with the rest of the apply. ### Per-migration phases @@ -278,7 +278,7 @@ Namespace: tailordb To bypass both checks (not recommended outside of recovery scenarios): ```bash -tailor-sdk deploy --no-schema-check +tailor deploy --no-schema-check ``` ### Example output @@ -297,7 +297,7 @@ tailor-sdk deploy --no-schema-check ## `migration set` Semantics -`tailor-sdk tailordb migration set ` updates the `sdk-migration` label on the deployed namespace's metadata. **It does not modify any data or schema.** It only changes which migrations the next `apply` will consider pending. +`tailor tailordb migration set ` updates the `sdk-migration` label on the deployed namespace's metadata. **It does not modify any data or schema.** It only changes which migrations the next `apply` will consider pending. | Movement | Effect on next `apply` | Effect on data | | -------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------- | @@ -314,7 +314,7 @@ Use cases: ## `migration sync` Semantics -`tailor-sdk tailordb migration sync ` reconstructs the schema snapshot at migration `N` from the working tree's migration history and **overwrites the remote schema to match it**, then sets the `sdk-migration` label to `N`. Unlike `migration set`, it changes the remote schema as well as the bookkeeping. Like `set`, it never runs `migrate.ts` scripts itself — it only changes what the next `apply` considers pending: +`tailor tailordb migration sync ` reconstructs the schema snapshot at migration `N` from the working tree's migration history and **overwrites the remote schema to match it**, then sets the `sdk-migration` label to `N`. Unlike `migration set`, it changes the remote schema as well as the bookkeeping. Like `set`, it never runs `migrate.ts` scripts itself — it only changes what the next `apply` considers pending: | Movement | Effect on next `apply` | Effect on data | | -------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | @@ -325,7 +325,7 @@ Before anything is sent to the remote, `sync` verifies that replaying the full m Because syncing backward causes already-applied scripts to re-execute on the next deploy, **write `migrate.ts` scripts to be idempotent** (see [Performance and Large Tables](#performance-and-large-tables) for resumable `where` clauses). -The main use case is recovering from drift after a `deploy --no-schema-check` from an older revision: instead of checking out that revision, run `migration sync ` to restore the remote to a known snapshot, then `tailor-sdk deploy` to apply the remaining migrations from the working tree. +The main use case is recovering from drift after a `deploy --no-schema-check` from an older revision: instead of checking out that revision, run `migration sync ` to restore the remote to a known snapshot, then `tailor deploy` to apply the remaining migrations from the working tree. ## Team Workflow and CI/CD @@ -340,7 +340,7 @@ Migration numbers are assigned sequentially, so two developers branching off the ### CI / CD - For non-interactive environments, pass `--yes` to `migration generate` and `--yes` to `apply`. `apply` runs migrations automatically when the `migrations/` directory is configured. -- Run `tailor-sdk tailordb migration status` in CI to detect "developer forgot to commit a migration" situations early. The exit code is non-zero only on errors, so check the output. +- Run `tailor tailordb migration status` in CI to detect "developer forgot to commit a migration" situations early. The exit code is non-zero only on errors, so check the output. - Avoid running migrations in parallel against the same workspace — there is no locking. Serialize deploys per environment. ### Resetting a deployed project @@ -348,8 +348,8 @@ Migration numbers are assigned sequentially, so two developers branching off the `migration generate --init` is destructive locally but does not touch the deployed workspace. Re-baselining a deployed project requires: 1. Run `migration generate --init` to start over from `0000`. -2. Run `tailor-sdk tailordb migration set 0` against the deployed namespace. -3. Run `tailor-sdk deploy` — the new `0000` becomes the baseline. +2. Run `tailor tailordb migration set 0` against the deployed namespace. +3. Run `tailor deploy` — the new `0000` becomes the baseline. Coordinate this with your team because everyone else's local migrations will be invalidated. @@ -367,7 +367,7 @@ After a failure: 1. Read the `Logs:` block in the apply output to find the cause. 2. Fix `migrate.ts` (or the data it depends on). -3. Re-run `tailor-sdk deploy`. The same migration runs again because its label was never bumped, and the prior-checkpoint schema is a clean baseline to retry against. +3. Re-run `tailor deploy`. The same migration runs again because its label was never bumped, and the prior-checkpoint schema is a clean baseline to retry against. If a migration **succeeds in script** but the **post-migration phase** fails (rare; usually a constraint violation the script should have prevented), the pre-migration changes are **not** rolled back: the script's data changes are already committed and the post-migration phase may have dropped removed columns or types, which cannot be reverted without data loss. Investigate, fix, and re-run. @@ -396,7 +396,7 @@ The machine user needs read/write access to every type the migration script touc If you see `No machine user available for migration execution`, either: -- Add `machineUsers: { ... }` to your auth config and `tailor-sdk deploy` it, or +- Add `machineUsers: { ... }` to your auth config and `tailor deploy` it, or - Set `migration.machineUser` to an existing machine user name in the db config. ## Multi-Namespace Coordination @@ -439,7 +439,7 @@ For genuinely different schemas across environments, prefer separate workspaces **Resolution:** -1. `tailor-sdk tailordb migration status` to see local vs remote. +1. `tailor tailordb migration status` to see local vs remote. 2. Compare with teammates — has someone applied different migrations? 3. If remote was changed manually, decide whether to update local migrations to match or to use `migration set ` to align bookkeeping. 4. To force the remote schema back to a known snapshot, use `migration sync ` (see [`migration sync` Semantics](#migration-sync-semantics)). @@ -471,7 +471,7 @@ For genuinely different schemas across environments, prefer separate workspaces **Cause:** Runtime error in your `migrate.ts`, a permission error from the machine user, or a constraint violation when post-migration tightens types. -**Resolution:** Read the `Logs:` block. Fix the script or the data assumption it relies on, and re-run `tailor-sdk deploy`. The label is not bumped on failure, so the same migration retries. +**Resolution:** Read the `Logs:` block. Fix the script or the data assumption it relies on, and re-run `tailor deploy`. The label is not bumped on failure, so the same migration retries. ### `migrate.ts` not found for a migration that needs one diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index 50588631a..e48d3eca6 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -463,7 +463,7 @@ db.type("User", { - When `publishEvents: true`, record creation/update/deletion events are published - When not specified, it is **automatically set to `true`** if an executor uses this type with `recordCreatedTrigger`, `recordUpdatedTrigger`, or `recordDeletedTrigger` -- When explicitly set to `false` while an executor uses this type, an error is thrown during `tailor-sdk deploy` +- When explicitly set to `false` while an executor uses this type, an error is thrown during `tailor deploy` **Use cases:** @@ -622,6 +622,6 @@ db.type("User", { ## Migrations -When you change a TailorDB type definition, the SDK can generate a migration that captures the diff and, for breaking changes, runs a data transformation script during `tailor-sdk deploy`. See the [TailorDB Migrations guide](./tailordb-migration.md) for the full workflow, configuration, supported change types, team coordination, and troubleshooting. +When you change a TailorDB type definition, the SDK can generate a migration that captures the diff and, for breaking changes, runs a data transformation script during `tailor deploy`. See the [TailorDB Migrations guide](./tailordb-migration.md) for the full workflow, configuration, supported change types, team coordination, and troubleshooting. For the CLI command reference, see [`tailordb migration`](../cli/tailordb.md#tailordb-migration). diff --git a/packages/sdk/docs/services/workflow.md b/packages/sdk/docs/services/workflow.md index fc1797eaf..abdd9c6da 100644 --- a/packages/sdk/docs/services/workflow.md +++ b/packages/sdk/docs/services/workflow.md @@ -406,22 +406,22 @@ Manage workflows using the CLI: ```bash # List workflows -tailor-sdk workflow list +tailor workflow list # Get workflow details -tailor-sdk workflow get +tailor workflow get # Start a workflow -tailor-sdk workflow start -m -a '{"key": "value"}' +tailor workflow start -m -a '{"key": "value"}' # List executions -tailor-sdk workflow executions +tailor workflow executions # Get execution details with logs -tailor-sdk workflow executions --logs +tailor workflow executions --logs # Resume a failed execution -tailor-sdk workflow resume +tailor workflow resume ``` See [Workflow CLI Commands](../cli/workflow.md) for full documentation. diff --git a/packages/sdk/e2e/deploy.test.ts b/packages/sdk/e2e/deploy.test.ts index b2507a493..fad7a44ff 100644 --- a/packages/sdk/e2e/deploy.test.ts +++ b/packages/sdk/e2e/deploy.test.ts @@ -8,7 +8,7 @@ * The fix ensures services are deleted AFTER the Application is deleted. * * Prerequisites: - * - Authentication via TAILOR_PLATFORM_TOKEN env var or `tailor-sdk login` + * - Authentication via TAILOR_PLATFORM_TOKEN env var or `tailor login` * - TAILOR_PLATFORM_ORGANIZATION_ID environment variable must be set */ diff --git a/packages/sdk/e2e/function-test-run.test.ts b/packages/sdk/e2e/function-test-run.test.ts index a59e39e6f..42bdb743d 100644 --- a/packages/sdk/e2e/function-test-run.test.ts +++ b/packages/sdk/e2e/function-test-run.test.ts @@ -9,7 +9,7 @@ * since it orchestrates multiple services. * * Prerequisites: - * - Authentication via TAILOR_PLATFORM_TOKEN env var or `tailor-sdk login` + * - Authentication via TAILOR_PLATFORM_TOKEN env var or `tailor login` * - TAILOR_PLATFORM_ORGANIZATION_ID environment variable must be set * - packages/sdk must be built (dist/cli/index.mjs must exist) * diff --git a/packages/sdk/e2e/globalSetup.ts b/packages/sdk/e2e/globalSetup.ts index ec92f1d06..e2b74f02f 100644 --- a/packages/sdk/e2e/globalSetup.ts +++ b/packages/sdk/e2e/globalSetup.ts @@ -8,7 +8,7 @@ import * as path from "node:path"; import { initOperatorClient, type OperatorClient } from "../src/cli/shared/client"; import { loadAccessToken } from "../src/cli/shared/context"; -// e2e must authenticate as the machine user from `tailor-sdk login --machine-user`, +// e2e must authenticate as the machine user from `tailor login --machine-user`, // never as the developer's locally configured profile. delete process.env.TAILOR_PLATFORM_PROFILE; diff --git a/packages/sdk/e2e/migration.test.ts b/packages/sdk/e2e/migration.test.ts index c89ae4648..8779b08e4 100644 --- a/packages/sdk/e2e/migration.test.ts +++ b/packages/sdk/e2e/migration.test.ts @@ -8,7 +8,7 @@ * - Apply with migrations * * Prerequisites: - * - Authentication via TAILOR_PLATFORM_TOKEN env var or `tailor-sdk login` + * - Authentication via TAILOR_PLATFORM_TOKEN env var or `tailor login` * - TAILOR_PLATFORM_ORGANIZATION_ID environment variable must be set * * Running Tests: diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 26eefbefa..287be22a5 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -9,7 +9,7 @@ "directory": "packages/sdk" }, "bin": { - "tailor-sdk": "./dist/cli/index.mjs" + "tailor": "./dist/cli/index.mjs" }, "files": [ "CHANGELOG.md", @@ -144,7 +144,7 @@ "test:coverage": "vitest --coverage", "docs:check": "vitest run --project=unit* src/cli/docs.test.ts", "docs:update": "POLITTY_DOCS_UPDATE=true vitest run --project=unit* src/cli/docs.test.ts", - "build": "tsdown && politty generate-worker --bin dist/cli/index.mjs --program tailor-sdk --shell zsh --verify", + "build": "tsdown && politty generate-worker --bin dist/cli/index.mjs --program tailor --shell zsh --verify", "lint": "oxlint --type-aware .", "check:public-api-jsdoc": "tsx scripts/check-public-api-jsdoc.ts", "check:zod-isolation": "tsx scripts/check-zod-isolation.ts", diff --git a/packages/sdk/scripts/perf/runtime-perf.sh b/packages/sdk/scripts/perf/runtime-perf.sh index 14f65b3dc..6597d7714 100644 --- a/packages/sdk/scripts/perf/runtime-perf.sh +++ b/packages/sdk/scripts/perf/runtime-perf.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Runtime performance benchmark for tailor-sdk generate and apply -d commands +# Runtime performance benchmark for tailor generate and apply -d commands # These options ensure the script fails on errors set -euo pipefail @@ -46,7 +46,7 @@ if ! pnpm generate > "${LOG_DIR}/generate-warmup.log" 2>&1; then fi echo "Warmup: Running apply (build-only)..." -if ! TAILOR_PLATFORM_SDK_BUILD_ONLY=true pnpm exec tailor-sdk deploy -c tailor.config.ts > "${LOG_DIR}/apply-warmup.log" 2>&1; then +if ! TAILOR_PLATFORM_SDK_BUILD_ONLY=true pnpm exec tailor deploy -c tailor.config.ts > "${LOG_DIR}/apply-warmup.log" 2>&1; then echo "ERROR: apply warmup failed" cat "${LOG_DIR}/apply-warmup.log" exit 1 @@ -78,7 +78,7 @@ echo "Measuring apply (build-only) command..." for i in $(seq 1 $ITERATIONS); do echo " apply iteration $i/$ITERATIONS..." START=$(get_timestamp_ms) - if ! TAILOR_PLATFORM_SDK_BUILD_ONLY=true pnpm exec tailor-sdk deploy -c tailor.config.ts > "${LOG_DIR}/apply-iter-${i}.log" 2>&1; then + if ! TAILOR_PLATFORM_SDK_BUILD_ONLY=true pnpm exec tailor deploy -c tailor.config.ts > "${LOG_DIR}/apply-iter-${i}.log" 2>&1; then echo "ERROR: apply iteration $i failed" cat "${LOG_DIR}/apply-iter-${i}.log" exit 1 diff --git a/packages/sdk/src/cli/commands/api/index.ts b/packages/sdk/src/cli/commands/api/index.ts index b92d7537c..4ddf57565 100644 --- a/packages/sdk/src/cli/commands/api/index.ts +++ b/packages/sdk/src/cli/commands/api/index.ts @@ -182,7 +182,7 @@ const fieldArg = z.string().transform((val, ctx): ParsedField => { export const apiCommand = defineAppCommand({ name: "api", description: "Call Tailor Platform API endpoints directly.", - notes: `Use \`tailor-sdk api list\` to enumerate invocable methods and \`tailor-sdk api inspect \` to print an endpoint's input message tree (combine with \`--json\` for machine-readable output). + notes: `Use \`tailor api list\` to enumerate invocable methods and \`tailor api inspect \` to print an endpoint's input message tree (combine with \`--json\` for machine-readable output). The request body is inferred from the target endpoint's request schema, and commonly required fields are auto-injected so they can be omitted from \`--body\`: diff --git a/packages/sdk/src/cli/commands/api/inspect.ts b/packages/sdk/src/cli/commands/api/inspect.ts index bcbbe7fe2..921be6070 100644 --- a/packages/sdk/src/cli/commands/api/inspect.ts +++ b/packages/sdk/src/cli/commands/api/inspect.ts @@ -10,7 +10,7 @@ export const inspectCommand = defineAppCommand({ name: "inspect", description: "Print the input message tree of an OperatorService endpoint.", notes: - "Combine with the global `--json` flag for a machine-readable descriptor. Recursive type references and `oneof` membership are annotated. Use `tailor-sdk api list` to discover endpoint names.", + "Combine with the global `--json` flag for a machine-readable descriptor. Recursive type references and `oneof` membership are annotated. Use `tailor api list` to discover endpoint names.", examples: [ { cmd: "GetApplication", desc: "Show fields of GetApplicationRequest." }, { @@ -34,7 +34,7 @@ export const inspectCommand = defineAppCommand({ if (!method) { throw CLIError({ message: `unknown method: ${methodName}`, - suggestion: "Run `tailor-sdk api list` to see available methods.", + suggestion: "Run `tailor api list` to see available methods.", command: "api inspect", }); } diff --git a/packages/sdk/src/cli/commands/api/proto-reflect.ts b/packages/sdk/src/cli/commands/api/proto-reflect.ts index 1ab5bf021..906714d38 100644 --- a/packages/sdk/src/cli/commands/api/proto-reflect.ts +++ b/packages/sdk/src/cli/commands/api/proto-reflect.ts @@ -2,7 +2,7 @@ import { ScalarType } from "@bufbuild/protobuf"; import { OperatorService } from "@tailor-platform/tailor-proto/service_pb"; import type { DescField, DescMessage, DescMethodUnary } from "@bufbuild/protobuf"; -// `tailor-sdk api` issues a single JSON POST and reads one JSON response, so +// `tailor api` issues a single JSON POST and reads one JSON response, so // only unary RPCs can be invoked. Streaming methods are filtered out of all // discovery surfaces (`api list`, `api inspect`). `OperatorService.methods` // is invariant at runtime, so we filter once and reuse — completion-script diff --git a/packages/sdk/src/cli/commands/authconnection/authorize.ts b/packages/sdk/src/cli/commands/authconnection/authorize.ts index 80deb7634..a018b448c 100644 --- a/packages/sdk/src/cli/commands/authconnection/authorize.ts +++ b/packages/sdk/src/cli/commands/authconnection/authorize.ts @@ -185,7 +185,7 @@ export const authorizeAuthConnectionCommand = defineAppCommand({ logger.warn( `Could not start the local callback server on port ${args.port}${code ? ` (${code})` : ""}.\n` + `${portHint}\n` + - ` tailor-sdk authconnection open`, + ` tailor authconnection open`, ); reject(err); }); @@ -199,7 +199,7 @@ export const authorizeAuthConnectionCommand = defineAppCommand({ ); logger.info( `If this flow doesn't complete, you can authorize via the Console instead:\n` + - ` tailor-sdk authconnection open`, + ` tailor authconnection open`, ); if (!args["no-browser"]) { try { diff --git a/packages/sdk/src/cli/commands/crashreport/send.test.ts b/packages/sdk/src/cli/commands/crashreport/send.test.ts index 244cd5ca1..c48043ecb 100644 --- a/packages/sdk/src/cli/commands/crashreport/send.test.ts +++ b/packages/sdk/src/cli/commands/crashreport/send.test.ts @@ -16,7 +16,7 @@ function makeCrashReport(): CrashReport { osRelease: "25.3.0", arch: "arm64", command: "apply", - argv: ["node", "tailor-sdk", "apply"], + argv: ["node", "tailor", "apply"], errorName: "TypeError", errorMessage: "Cannot read properties of undefined", stackTrace: diff --git a/packages/sdk/src/cli/commands/deploy/config-id-ci-guard.test.ts b/packages/sdk/src/cli/commands/deploy/config-id-ci-guard.test.ts index a32b75d34..cd881f205 100644 --- a/packages/sdk/src/cli/commands/deploy/config-id-ci-guard.test.ts +++ b/packages/sdk/src/cli/commands/deploy/config-id-ci-guard.test.ts @@ -46,7 +46,7 @@ describe("ensureConfigIdForDeploy", () => { const { ensureConfigIdForDeploy } = await load(true); await expect( ensureConfigIdForDeploy({ configPath: filePath, dryRun: false, buildOnly: false }), - ).rejects.toThrow(/missing an 'id'|tailor-sdk setup|deploy/); + ).rejects.toThrow(/missing an 'id'|tailor setup|deploy/); // Must not have injected anything in CI. expect(await fs.promises.readFile(filePath, "utf-8")).toBe(configWithoutId); }); diff --git a/packages/sdk/src/cli/commands/deploy/config-id-injector.ts b/packages/sdk/src/cli/commands/deploy/config-id-injector.ts index 3a32d6db5..15356e975 100644 --- a/packages/sdk/src/cli/commands/deploy/config-id-injector.ts +++ b/packages/sdk/src/cli/commands/deploy/config-id-injector.ts @@ -169,7 +169,7 @@ async function assertConfigIdInCI(configPath: string): Promise { throw new Error( `tailor.config.ts is missing an 'id'. CI does not auto-generate one ` + `(each run would be treated as a separate app and break resource ownership). ` + - `Run 'tailor-sdk setup' or 'tailor-sdk deploy' locally and commit the injected id.`, + `Run 'tailor setup' or 'tailor deploy' locally and commit the injected id.`, ); } // Keep CI and local behavior aligned: ensureConfigId() enforces the same diff --git a/packages/sdk/src/cli/commands/deploy/confirm.ts b/packages/sdk/src/cli/commands/deploy/confirm.ts index a805f970f..2159c59a3 100644 --- a/packages/sdk/src/cli/commands/deploy/confirm.ts +++ b/packages/sdk/src/cli/commands/deploy/confirm.ts @@ -118,7 +118,7 @@ async function confirmNameMismatch( } /** - * Confirm allowing tailor-sdk to manage previously unmanaged resources. + * Confirm allowing tailor to manage previously unmanaged resources. * @param resources - Unmanaged resources * @param appName - Target application name * @param yes - Whether to auto-confirm without prompting @@ -131,7 +131,7 @@ export async function confirmUnmanagedResources( ): Promise { if (resources.length === 0) return; - logger.warn("Existing resources not tracked by tailor-sdk were found:"); + logger.warn("Existing resources not tracked by tailor were found:"); logger.log(` ${styles.info("Resources")}:`); for (const r of resources) { @@ -139,7 +139,7 @@ export async function confirmUnmanagedResources( } logger.newline(); logger.log(" These resources may have been created by older SDK versions, Terraform, or CUE."); - logger.log(" To continue, confirm that tailor-sdk should manage them."); + logger.log(" To continue, confirm that tailor should manage them."); logger.log( " If they are managed by another tool (e.g., Terraform), cancel and manage them there instead.", ); @@ -152,7 +152,7 @@ export async function confirmUnmanagedResources( } const confirmed = await prompt.confirm({ - message: `Allow tailor-sdk to manage these resources for "${appName}"?`, + message: `Allow tailor to manage these resources for "${appName}"?`, default: false, }); if (!confirmed) { diff --git a/packages/sdk/src/cli/commands/deploy/deploy.ts b/packages/sdk/src/cli/commands/deploy/deploy.ts index dd85d0f7d..14d070d00 100644 --- a/packages/sdk/src/cli/commands/deploy/deploy.ts +++ b/packages/sdk/src/cli/commands/deploy/deploy.ts @@ -482,7 +482,7 @@ export async function deploy(options?: DeployOptions) { } // Note: the normal apply path intentionally skips writing bundle files to - // .tailor-sdk/. Bundles are kept in memory and uploaded directly to the + // .tailor/. Bundles are kept in memory and uploaded directly to the // function registry. To test a function locally, use `function test-run` // with a .ts source file instead of a pre-bundled .js file. diff --git a/packages/sdk/src/cli/commands/deploy/executor.test.ts b/packages/sdk/src/cli/commands/deploy/executor.test.ts index 49d96fb92..a3d4ad404 100644 --- a/packages/sdk/src/cli/commands/deploy/executor.test.ts +++ b/packages/sdk/src/cli/commands/deploy/executor.test.ts @@ -16,7 +16,7 @@ vi.mock("node:fs", () => ({ // Mock dist-dir to avoid getDistDir issues vi.mock("#/cli/shared/dist-dir", () => ({ - getDistDir: vi.fn().mockReturnValue(".tailor-sdk"), + getDistDir: vi.fn().mockReturnValue(".tailor"), })); // Mock config values for tests diff --git a/packages/sdk/src/cli/commands/deploy/secrets-state.test.ts b/packages/sdk/src/cli/commands/deploy/secrets-state.test.ts index 46e531a6d..a1bcbb383 100644 --- a/packages/sdk/src/cli/commands/deploy/secrets-state.test.ts +++ b/packages/sdk/src/cli/commands/deploy/secrets-state.test.ts @@ -9,7 +9,7 @@ import { } from "./secrets-state"; vi.mock("#/cli/shared/dist-dir", () => ({ - getDistDir: () => "/tmp/tailor-sdk-test-secrets-state", + getDistDir: () => "/tmp/tailor-test-secrets-state", })); describe("secrets-state", () => { diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/index.ts b/packages/sdk/src/cli/commands/deploy/tailordb/index.ts index 128a0e3d7..cf6afabb3 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/index.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/index.ts @@ -272,7 +272,7 @@ async function validateAndDetectMigrations( logger.error("Schema changes detected that are not in migration files:"); logger.log(formatMigrationCheckResults(migrationResults)); logger.newline(); - logger.info("Run 'tailor-sdk tailordb migration generate' to create migration files."); + logger.info("Run 'tailor tailordb migration generate' to create migration files."); logger.info("Or use '--no-schema-check' to skip this check."); throw new Error("Schema migration check failed"); } @@ -549,7 +549,7 @@ export async function applyTailorDB( ]); } catch (error) { handleOptionalToRequiredError(error, [ - "Run 'tailor-sdk tailordb migration generate' to create migration files.", + "Run 'tailor tailordb migration generate' to create migration files.", "Migration scripts allow you to handle existing data before applying the schema change.", ]); } @@ -1933,9 +1933,7 @@ function formatMigrationCheckResults(results: MigrationCheckResult[]): string { lines.push(`Namespace: ${result.namespace}`); if (!result.diff) { - lines.push( - " No migration snapshot found. Run 'tailor-sdk tailordb migration generate' first.", - ); + lines.push(" No migration snapshot found. Run 'tailor tailordb migration generate' first."); } else { lines.push(` ${formatDiffSummary(result.diff)}`); lines.push(""); diff --git a/packages/sdk/src/cli/commands/executor/list.ts b/packages/sdk/src/cli/commands/executor/list.ts index 9cf743074..3a7ba1537 100644 --- a/packages/sdk/src/cli/commands/executor/list.ts +++ b/packages/sdk/src/cli/commands/executor/list.ts @@ -81,7 +81,7 @@ export const listCommand = defineAppCommand({ if (!jsonOutput) { const hasWebhook = executors.some((e) => e.triggerType === "webhook"); if (hasWebhook) { - logger.info("To see webhook URLs, run: tailor-sdk executor webhook list"); + logger.info("To see webhook URLs, run: tailor executor webhook list"); } } }, diff --git a/packages/sdk/src/cli/commands/executor/webhook.ts b/packages/sdk/src/cli/commands/executor/webhook.ts index e6fa4d6a2..c65fb42e8 100644 --- a/packages/sdk/src/cli/commands/executor/webhook.ts +++ b/packages/sdk/src/cli/commands/executor/webhook.ts @@ -90,9 +90,7 @@ const listWebhookCommand = defineAppCommand({ }); if (!jsonOutput) { - logger.info( - 'To test a webhook, run: tailor-sdk executor trigger -d \'{"key":"value"}\'', - ); + logger.info('To test a webhook, run: tailor executor trigger -d \'{"key":"value"}\''); } }, }); diff --git a/packages/sdk/src/cli/commands/function/test-run.ts b/packages/sdk/src/cli/commands/function/test-run.ts index 5224ef323..8c1201271 100644 --- a/packages/sdk/src/cli/commands/function/test-run.ts +++ b/packages/sdk/src/cli/commands/function/test-run.ts @@ -1,5 +1,5 @@ /** - * `tailor-sdk function test-run` command + * `tailor function test-run` command * * Bundles and executes a function on the Tailor Platform server * without deploying (applying) the application. @@ -281,7 +281,7 @@ async function resolveMachineUserName(options: ResolveMachineUserNameOptions): P } } throw new Error( - "Machine user is required. Provide --machine-user, set TAILOR_PLATFORM_MACHINE_USER_NAME, set a profile default with 'tailor-sdk profile update --machine-user ', or ensure tailor.config.ts has machine users configured.", + "Machine user is required. Provide --machine-user, set TAILOR_PLATFORM_MACHINE_USER_NAME, set a profile default with 'tailor profile update --machine-user ', or ensure tailor.config.ts has machine users configured.", ); } diff --git a/packages/sdk/src/cli/commands/generate/plugin-executor-generator.ts b/packages/sdk/src/cli/commands/generate/plugin-executor-generator.ts index 19311d8ed..a30e8a2e1 100644 --- a/packages/sdk/src/cli/commands/generate/plugin-executor-generator.ts +++ b/packages/sdk/src/cli/commands/generate/plugin-executor-generator.ts @@ -46,7 +46,7 @@ interface TypeImportInfo { * Generate TypeScript files for plugin-generated executors. * These files will be processed by the standard executor bundler. * @param executors - Array of plugin executor information - * @param outputDir - Base output directory (e.g., .tailor-sdk) + * @param outputDir - Base output directory (e.g., .tailor) * @param typeGenerationResult - Result from plugin type generation (for import resolution) * @param sourceTypeInfoMap - Map of source type names to their source info * @param configPath - Path to tailor.config.ts (used for resolving plugin import paths) @@ -88,7 +88,7 @@ export function generatePluginExecutorFiles( /** * Generate a single executor file. * @param info - Plugin executor metadata and definition - * @param outputDir - Base output directory (e.g., .tailor-sdk) + * @param outputDir - Base output directory (e.g., .tailor) * @param typeGenerationResult - Result from plugin type generation * @param sourceTypeInfoMap - Map of source type names to their source info * @param baseDirs - Base directories for resolving plugin import paths diff --git a/packages/sdk/src/cli/commands/generate/plugin-type-generator.ts b/packages/sdk/src/cli/commands/generate/plugin-type-generator.ts index ecc2da6b1..44d9e032a 100644 --- a/packages/sdk/src/cli/commands/generate/plugin-type-generator.ts +++ b/packages/sdk/src/cli/commands/generate/plugin-type-generator.ts @@ -34,7 +34,7 @@ function isFieldDefinition(value: unknown): value is FieldDefinition { * Generate TypeScript files for plugin-generated types. * These files export the type definition and can be imported by executor files. * @param types - Array of plugin type information - * @param outputDir - Base output directory (e.g., .tailor-sdk) + * @param outputDir - Base output directory (e.g., .tailor) * @returns Generation result with file paths */ export function generatePluginTypeFiles( diff --git a/packages/sdk/src/cli/commands/generate/seed/bundler.ts b/packages/sdk/src/cli/commands/generate/seed/bundler.ts index dd6e26e3c..26b528f62 100644 --- a/packages/sdk/src/cli/commands/generate/seed/bundler.ts +++ b/packages/sdk/src/cli/commands/generate/seed/bundler.ts @@ -112,7 +112,7 @@ export async function bundleSeedScript( namespace: string, typeNames: string[], ): Promise { - // Output directory in .tailor-sdk (relative to project root) + // Output directory in .tailor (relative to project root) const outputDir = path.resolve(getDistDir(), "seed"); fs.mkdirSync(outputDir, { recursive: true }); diff --git a/packages/sdk/src/cli/commands/machineuser/token.ts b/packages/sdk/src/cli/commands/machineuser/token.ts index 0fb4d3c8a..1b77bd50b 100644 --- a/packages/sdk/src/cli/commands/machineuser/token.ts +++ b/packages/sdk/src/cli/commands/machineuser/token.ts @@ -32,7 +32,7 @@ export async function getMachineUserToken( const name = await loadMachineUserName({ machineUser: options.name, profile: options.profile }); if (!name) { throw new Error( - "Machine user is required. Provide the NAME positional argument, set TAILOR_PLATFORM_MACHINE_USER_NAME, or set a profile default with 'tailor-sdk profile update --machine-user '.", + "Machine user is required. Provide the NAME positional argument, set TAILOR_PLATFORM_MACHINE_USER_NAME, or set a profile default with 'tailor profile update --machine-user '.", ); } diff --git a/packages/sdk/src/cli/commands/profile/list.ts b/packages/sdk/src/cli/commands/profile/list.ts index 919357974..8924a8709 100644 --- a/packages/sdk/src/cli/commands/profile/list.ts +++ b/packages/sdk/src/cli/commands/profile/list.ts @@ -18,7 +18,7 @@ export const listCommand = defineAppCommand({ if (profiles.length === 0) { logger.info(ml` No profiles found. - Please create a profile first using 'tailor-sdk profile create' command. + Please create a profile first using 'tailor profile create' command. `); if (jsonOutput) { logger.out([]); diff --git a/packages/sdk/src/cli/commands/setup/check.ts b/packages/sdk/src/cli/commands/setup/check.ts index 0452af750..6439451f8 100644 --- a/packages/sdk/src/cli/commands/setup/check.ts +++ b/packages/sdk/src/cli/commands/setup/check.ts @@ -170,8 +170,8 @@ export function checkGitHub(options: CheckGitHubOptions): void { const lock = readLock(outputDir); if (!lock || lock.targets.length === 0) { throw new Error( - "No managed workflows found (.github/tailor-sdk.lock is missing or empty). " + - "Run `tailor-sdk setup` first.", + "No managed workflows found (.github/tailor.lock is missing or empty). " + + "Run `tailor setup` first.", ); } @@ -208,6 +208,6 @@ export function checkGitHub(options: CheckGitHubOptions): void { } throw new Error( `Detected ${String(findings.length)} drift finding(s) across ${String(count)} target(s). ` + - "Re-run `tailor-sdk setup` to regenerate, or address each finding above.", + "Re-run `tailor setup` to regenerate, or address each finding above.", ); } diff --git a/packages/sdk/src/cli/commands/setup/generate.test.ts b/packages/sdk/src/cli/commands/setup/generate.test.ts index ad2218652..e183d05fa 100644 --- a/packages/sdk/src/cli/commands/setup/generate.test.ts +++ b/packages/sdk/src/cli/commands/setup/generate.test.ts @@ -174,7 +174,7 @@ describe("renderBranchWorkflow", () => { const { content } = renderBranchWorkflow(branchBase); expect(content).toContain("uses: tailor-platform/actions/generate-check@"); expect(content).not.toContain("git add -A"); - expect(content).not.toContain("tailor-sdk generate"); + expect(content).not.toContain("tailor generate"); }); test("--no-plan drops the plan job, pull_request trigger, and dry-run input", () => { diff --git a/packages/sdk/src/cli/commands/setup/generate.ts b/packages/sdk/src/cli/commands/setup/generate.ts index b9a708da8..4b316bfaa 100644 --- a/packages/sdk/src/cli/commands/setup/generate.ts +++ b/packages/sdk/src/cli/commands/setup/generate.ts @@ -370,13 +370,13 @@ function printNextSteps(obj: { environment: string; idInjected: boolean }): void `2. Provision the workspace and set its id as the TAILOR_PLATFORM_WORKSPACE_ID variable ` + `on the "${environment}" environment:`, ); - logger.log(" tailor-sdk workspace create # if it does not exist yet; copy the id"); + logger.log(" tailor workspace create # if it does not exist yet; copy the id"); logger.log(` gh variable set TAILOR_PLATFORM_WORKSPACE_ID --env ${environment}`); logger.newline(); logger.log("3. Commit the generated files:"); logger.log(" - .github/workflows/tailor-*.yml"); - logger.log(" - .github/tailor-sdk.lock"); + logger.log(" - .github/tailor.lock"); if (idInjected) { logger.log(" - tailor.config.ts (app id was added)"); } diff --git a/packages/sdk/src/cli/commands/setup/lock.test.ts b/packages/sdk/src/cli/commands/setup/lock.test.ts index 3958817a8..0249014f3 100644 --- a/packages/sdk/src/cli/commands/setup/lock.test.ts +++ b/packages/sdk/src/cli/commands/setup/lock.test.ts @@ -57,7 +57,7 @@ describe("readLock / writeLock", () => { test("round-trips through disk with 2-space indent and trailing newline", () => { const lock = makeLock(); writeLock(testDir, lock); - const raw = fs.readFileSync(path.join(testDir, ".github/tailor-sdk.lock"), "utf-8"); + const raw = fs.readFileSync(path.join(testDir, ".github/tailor.lock"), "utf-8"); expect(raw.endsWith("\n")).toBe(true); expect(raw).toContain(' "version": 1'); expect(readLock(testDir)).toEqual(lock); @@ -75,7 +75,7 @@ describe("readLock / writeLock", () => { delete lock.version; fs.mkdirSync(path.join(testDir, ".github"), { recursive: true }); fs.writeFileSync( - path.join(testDir, ".github/tailor-sdk.lock"), + path.join(testDir, ".github/tailor.lock"), `${JSON.stringify(lock, null, 2)}\n`, ); expect(() => readLock(testDir)).toThrow(/no valid 'version'/); @@ -84,7 +84,7 @@ describe("readLock / writeLock", () => { test("throws with restore guidance when targets is not an array", () => { fs.mkdirSync(path.join(testDir, ".github"), { recursive: true }); fs.writeFileSync( - path.join(testDir, ".github/tailor-sdk.lock"), + path.join(testDir, ".github/tailor.lock"), `${JSON.stringify({ version: LOCK_VERSION }, null, 2)}\n`, ); expect(() => readLock(testDir)).toThrow(/no valid 'targets'/); @@ -92,7 +92,7 @@ describe("readLock / writeLock", () => { test("throws on invalid JSON", () => { fs.mkdirSync(path.join(testDir, ".github"), { recursive: true }); - fs.writeFileSync(path.join(testDir, ".github/tailor-sdk.lock"), "{ not json"); + fs.writeFileSync(path.join(testDir, ".github/tailor.lock"), "{ not json"); expect(() => readLock(testDir)).toThrow(/not valid JSON/); }); }); diff --git a/packages/sdk/src/cli/commands/setup/lock.ts b/packages/sdk/src/cli/commands/setup/lock.ts index 9c8dd0995..95e5bd0a0 100644 --- a/packages/sdk/src/cli/commands/setup/lock.ts +++ b/packages/sdk/src/cli/commands/setup/lock.ts @@ -6,7 +6,7 @@ import * as path from "pathe"; export const LOCK_VERSION = 1; /** Lock file path, relative to the repository root. */ -const LOCK_FILENAME = ".github/tailor-sdk.lock"; +const LOCK_FILENAME = ".github/tailor.lock"; export type TargetKind = "branch" | "tag"; @@ -78,14 +78,14 @@ export function readLock(outputDir: string): LockFile | null { } catch (cause) { throw new Error( `${LOCK_FILENAME} is not valid JSON. The lock file is machine-owned; ` + - "restore it from git (git checkout -- .github/tailor-sdk.lock) and re-run setup.", + "restore it from git (git checkout -- .github/tailor.lock) and re-run setup.", { cause }, ); } if (typeof parsed.version !== "number") { throw new Error( `${LOCK_FILENAME} has no valid 'version' field. The lock file is machine-owned; ` + - "restore it from git (git checkout -- .github/tailor-sdk.lock) and re-run setup.", + "restore it from git (git checkout -- .github/tailor.lock) and re-run setup.", ); } if (parsed.version > LOCK_VERSION) { @@ -97,7 +97,7 @@ export function readLock(outputDir: string): LockFile | null { if (!Array.isArray(parsed.targets)) { throw new Error( `${LOCK_FILENAME} has no valid 'targets' array. The lock file is machine-owned; ` + - "restore it from git (git checkout -- .github/tailor-sdk.lock) and re-run setup.", + "restore it from git (git checkout -- .github/tailor.lock) and re-run setup.", ); } return parsed; diff --git a/packages/sdk/src/cli/commands/setup/templates.ts b/packages/sdk/src/cli/commands/setup/templates.ts index ea81f144f..88143ff12 100644 --- a/packages/sdk/src/cli/commands/setup/templates.ts +++ b/packages/sdk/src/cli/commands/setup/templates.ts @@ -10,12 +10,12 @@ export const TEMPLATE_VERSION = 2; export type PackageManager = "pnpm" | "yarn" | "npm" | "bun"; -const HEADER = `# Generated by \`tailor-sdk setup\` — managed by the Tailor SDK. +const HEADER = `# Generated by \`tailor setup\` — managed by the Tailor SDK. # # - Jobs and steps whose id starts with \`tailor-\` are managed by the SDK. # Do not edit or rename them. -# - State is tracked in .github/tailor-sdk.lock (machine-owned: commit it, never edit it). -# - Re-running \`tailor-sdk setup\` regenerates this file. If you have +# - State is tracked in .github/tailor.lock (machine-owned: commit it, never edit it). +# - Re-running \`tailor setup\` regenerates this file. If you have # edited it by hand, regeneration stops and asks for --force (which discards # your edits), so prefer keeping customizations in your own jobs/steps and # re-running setup after SDK updates.`; diff --git a/packages/sdk/src/cli/commands/skills/install.test.ts b/packages/sdk/src/cli/commands/skills/install.test.ts index 3cd3901ca..981f533eb 100644 --- a/packages/sdk/src/cli/commands/skills/install.test.ts +++ b/packages/sdk/src/cli/commands/skills/install.test.ts @@ -9,7 +9,7 @@ describe("resolveBundledSkillsDir", () => { expect(dir.endsWith("/agent-skills")).toBe(true); expect(existsSync(dir)).toBe(true); expect(statSync(dir).isDirectory()).toBe(true); - expect(existsSync(resolve(dir, "tailor-sdk", "SKILL.md"))).toBe(true); + expect(existsSync(resolve(dir, "tailor", "SKILL.md"))).toBe(true); }); }); diff --git a/packages/sdk/src/cli/commands/skills/install.ts b/packages/sdk/src/cli/commands/skills/install.ts index 46f56a16d..3a807db4c 100644 --- a/packages/sdk/src/cli/commands/skills/install.ts +++ b/packages/sdk/src/cli/commands/skills/install.ts @@ -19,7 +19,7 @@ const DEFAULT_AGENT = "claude-code"; export const installCommand = defineAppCommand({ name: "install", - description: "Install the tailor-sdk agent skill from the installed SDK package.", + description: "Install the tailor agent skill from the installed SDK package.", args: z .object({ agent: arg(z.string().default(DEFAULT_AGENT), { diff --git a/packages/sdk/src/cli/commands/tailordb/erd/export.ts b/packages/sdk/src/cli/commands/tailordb/erd/export.ts index b8372a808..798d12377 100644 --- a/packages/sdk/src/cli/commands/tailordb/erd/export.ts +++ b/packages/sdk/src/cli/commands/tailordb/erd/export.ts @@ -10,7 +10,7 @@ import { initErdCommand } from "./utils"; import { writeViewerDist } from "./viewer"; import type { TailorDBNamespaceData } from "#/plugin/types"; -const DEFAULT_ERD_BASE_DIR = ".tailor-sdk/erd"; +const DEFAULT_ERD_BASE_DIR = ".tailor/erd"; interface ResolveTargetsOptions { context: LocalErdSchemaContext; diff --git a/packages/sdk/src/cli/commands/tailordb/erd/schema.ts b/packages/sdk/src/cli/commands/tailordb/erd/schema.ts index 5c91aa04d..494c6aa11 100644 --- a/packages/sdk/src/cli/commands/tailordb/erd/schema.ts +++ b/packages/sdk/src/cli/commands/tailordb/erd/schema.ts @@ -260,7 +260,7 @@ export function buildTailorDbErdSchema(options: BuildTailorDbErdSchemaOptions): namespace: namespaceData.namespace, source: options.source ?? "local", cleanRoom: { - implementation: "tailor-sdk" as const, + implementation: "tailor" as const, notes: CLEAN_ROOM_NOTES, }, tables, diff --git a/packages/sdk/src/cli/commands/tailordb/erd/serve.ts b/packages/sdk/src/cli/commands/tailordb/erd/serve.ts index d184bc12e..c452f7e81 100644 --- a/packages/sdk/src/cli/commands/tailordb/erd/serve.ts +++ b/packages/sdk/src/cli/commands/tailordb/erd/serve.ts @@ -16,7 +16,7 @@ import { prepareErdBuildsFromContext, type ErdBuildResult } from "./export"; import { loadLocalErdSchema, type LocalErdSchemaContext } from "./local-schema"; import { initErdCommand } from "./utils"; -const DEFAULT_ERD_BASE_DIR = ".tailor-sdk/erd"; +const DEFAULT_ERD_BASE_DIR = ".tailor/erd"; const LOCAL_HOST = "127.0.0.1"; interface StaticServerResult { @@ -56,7 +56,7 @@ interface OpenStaticFileResult { const GLOB_CHARS = /[*?[\]{}()!+@]/; function formatServeCommand(namespace: string): string { - return `tailor-sdk tailordb erd serve --namespace ${namespace}`; + return `tailor tailordb erd serve --namespace ${namespace}`; } function getCacheControl(filePath: string): string { diff --git a/packages/sdk/src/cli/commands/tailordb/erd/types.ts b/packages/sdk/src/cli/commands/tailordb/erd/types.ts index f7f6bcba3..e89c0e0ad 100644 --- a/packages/sdk/src/cli/commands/tailordb/erd/types.ts +++ b/packages/sdk/src/cli/commands/tailordb/erd/types.ts @@ -96,7 +96,7 @@ export interface TailorDbErdSchema { revision: string; source: TailorDbErdSource; cleanRoom: { - implementation: "tailor-sdk"; + implementation: "tailor"; notes: string[]; }; tables: TailorDbErdTable[]; diff --git a/packages/sdk/src/cli/commands/tailordb/erd/viewer.test.ts b/packages/sdk/src/cli/commands/tailordb/erd/viewer.test.ts index 137bd3166..7909fefec 100644 --- a/packages/sdk/src/cli/commands/tailordb/erd/viewer.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/erd/viewer.test.ts @@ -9,7 +9,7 @@ function buildSchema(overrides: Partial = {}): TailorDbErdSch generatedAt: "2026-01-01T00:00:00.000Z", revision: "test-revision", source: "local", - cleanRoom: { implementation: "tailor-sdk", notes: [] }, + cleanRoom: { implementation: "tailor", notes: [] }, tables: [ { name: "User", diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/bundler.ts b/packages/sdk/src/cli/commands/tailordb/migrate/bundler.ts index 8f388b5fa..402a4af35 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/bundler.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/bundler.ts @@ -38,7 +38,7 @@ export async function bundleMigrationScript( migrationNumber: number, env: Record = {}, ): Promise { - // Output directory in .tailor-sdk (relative to project root) + // Output directory in .tailor (relative to project root) const outputDir = path.resolve(getDistDir(), "migrations"); fs.mkdirSync(outputDir, { recursive: true }); diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/generate.ts b/packages/sdk/src/cli/commands/tailordb/migrate/generate.ts index 769500a29..54a7aed38 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/generate.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/generate.ts @@ -303,7 +303,7 @@ async function generateDiffFromSnapshot( } logger.newline(); logger.log("A migration script was generated for breaking changes."); - logger.log("Please review and edit the script before running 'tailor-sdk deploy'."); + logger.log("Please review and edit the script before running 'tailor deploy'."); const editor = getConfiguredEditorCommand(); if (!editor) { @@ -330,7 +330,7 @@ async function generateDiffFromSnapshot( `Data loss is possible for this migration but no script was generated. To add a custom migrate.ts, run:`, ); logger.log( - ` ${styles.bold(`tailor-sdk tailordb migration script ${result.migrationNumber.toString().padStart(4, "0")} --namespace ${diff.namespace}`)}`, + ` ${styles.bold(`tailor tailordb migration script ${result.migrationNumber.toString().padStart(4, "0")} --namespace ${diff.namespace}`)}`, ); } } diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/script.ts b/packages/sdk/src/cli/commands/tailordb/migrate/script.ts index 86095ec96..82ac46020 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/script.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/script.ts @@ -100,7 +100,7 @@ async function script(options: ScriptOptions): Promise { logger.newline(); logger.log("Edit the script to implement your data migration logic."); - logger.log("It will be executed by 'tailor-sdk deploy' between Pre and Post phases."); + logger.log("It will be executed by 'tailor deploy' between Pre and Post phases."); const editor = getConfiguredEditorCommand(); if (!editor) return; diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/sync.ts b/packages/sdk/src/cli/commands/tailordb/migrate/sync.ts index 8a4e895fa..0275bc08a 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/sync.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/sync.ts @@ -199,7 +199,7 @@ async function assertMigrationsReproduceLocalTypes( { mode: "plain" }, ); logger.info( - " - Type definitions changed without a new migration — run 'tailor-sdk tailordb migration generate' first.", + " - Type definitions changed without a new migration — run 'tailor tailordb migration generate' first.", { mode: "plain" }, ); logger.newline(); @@ -453,7 +453,7 @@ async function sync(options: SyncOptions): Promise { } catch (error) { handleOptionalToRequiredError(error, [ "The target snapshot marks a field as required, but existing remote records have no value for it.", - "Populate those records first (e.g. with a migration script applied via 'tailor-sdk deploy'), then re-run the sync.", + "Populate those records first (e.g. with a migration script applied via 'tailor deploy'), then re-run the sync.", ]); } await Promise.all( @@ -498,7 +498,7 @@ async function sync(options: SyncOptions): Promise { if (targetVersion < latest) { logger.newline(); logger.info( - `Run 'tailor-sdk deploy' to apply migrations ${formatMigrationNumber( + `Run 'tailor deploy' to apply migrations ${formatMigrationNumber( targetVersion + 1, )}–${formatMigrationNumber(latest)} from the working tree.`, ); diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/template-generator.ts b/packages/sdk/src/cli/commands/tailordb/migrate/template-generator.ts index fd1d4b6ed..1cfe6e98d 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/template-generator.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/template-generator.ts @@ -164,7 +164,7 @@ export function generateMigrationScript(diff: MigrationDiff): string { * Migration script for ${diff.namespace} * * This script runs between the Pre-migration and Post-migration phases of - * 'tailor-sdk deploy'. Use it to transform existing data so that the schema + * 'tailor deploy'. Use it to transform existing data so that the schema * change can complete safely (for breaking changes, this is hard-required; * for warning-tier changes it is optional). Edit this file to implement * your data migration logic. diff --git a/packages/sdk/src/cli/commands/user/current.ts b/packages/sdk/src/cli/commands/user/current.ts index 47910cc50..897b69304 100644 --- a/packages/sdk/src/cli/commands/user/current.ts +++ b/packages/sdk/src/cli/commands/user/current.ts @@ -16,7 +16,7 @@ export const currentCommand = defineAppCommand({ if (!config.current_user) { throw new Error(ml` Current user not set. - Please login first using 'tailor-sdk login' command to register a user. + Please login first using 'tailor login' command to register a user. `); } @@ -24,7 +24,7 @@ export const currentCommand = defineAppCommand({ if (!config.users[config.current_user]) { throw new Error(ml` Current user '${config.current_user}' not found in registered users. - Please login again using 'tailor-sdk login' command to register the user. + Please login again using 'tailor login' command to register the user. `); } diff --git a/packages/sdk/src/cli/commands/user/list.ts b/packages/sdk/src/cli/commands/user/list.ts index 49bd6b17d..61e056c16 100644 --- a/packages/sdk/src/cli/commands/user/list.ts +++ b/packages/sdk/src/cli/commands/user/list.ts @@ -16,7 +16,7 @@ export const listCommand = defineAppCommand({ if (users.length === 0) { logger.info(ml` No users found. - Please login first using 'tailor-sdk login' command to register a user. + Please login first using 'tailor login' command to register a user. `); if (jsonOutput) { logger.out([]); diff --git a/packages/sdk/src/cli/commands/user/pat/create.ts b/packages/sdk/src/cli/commands/user/pat/create.ts index 8919e3085..9d254436e 100644 --- a/packages/sdk/src/cli/commands/user/pat/create.ts +++ b/packages/sdk/src/cli/commands/user/pat/create.ts @@ -29,7 +29,7 @@ export const createCommand = defineAppCommand({ if (!config.current_user) { throw new Error(ml` No user logged in. - Please login first using 'tailor-sdk login' command. + Please login first using 'tailor login' command. `); } diff --git a/packages/sdk/src/cli/commands/user/pat/delete.ts b/packages/sdk/src/cli/commands/user/pat/delete.ts index a0a31e1ac..e8463787e 100644 --- a/packages/sdk/src/cli/commands/user/pat/delete.ts +++ b/packages/sdk/src/cli/commands/user/pat/delete.ts @@ -25,7 +25,7 @@ export const deleteCommand = defineAppCommand({ if (!config.current_user) { throw new Error(ml` No user logged in. - Please login first using 'tailor-sdk login' command. + Please login first using 'tailor login' command. `); } diff --git a/packages/sdk/src/cli/commands/user/pat/list.ts b/packages/sdk/src/cli/commands/user/pat/list.ts index 531bedc4c..df565532d 100644 --- a/packages/sdk/src/cli/commands/user/pat/list.ts +++ b/packages/sdk/src/cli/commands/user/pat/list.ts @@ -18,7 +18,7 @@ export const listCommand = defineAppCommand({ if (!config.current_user) { throw new Error(ml` No user logged in. - Please login first using 'tailor-sdk login' command. + Please login first using 'tailor login' command. `); } @@ -41,7 +41,7 @@ export const listCommand = defineAppCommand({ if (pats.length === 0) { logger.info(ml` No personal access tokens found. - Please create a token using 'tailor-sdk user pat create' command. + Please create a token using 'tailor user pat create' command. `); if (!jsonOutput) { return; diff --git a/packages/sdk/src/cli/commands/user/pat/update.ts b/packages/sdk/src/cli/commands/user/pat/update.ts index e1adee152..fc92ac01e 100644 --- a/packages/sdk/src/cli/commands/user/pat/update.ts +++ b/packages/sdk/src/cli/commands/user/pat/update.ts @@ -29,7 +29,7 @@ export const updateCommand = defineAppCommand({ if (!config.current_user) { throw new Error(ml` No user logged in. - Please login first using 'tailor-sdk login' command. + Please login first using 'tailor login' command. `); } diff --git a/packages/sdk/src/cli/commands/user/switch.ts b/packages/sdk/src/cli/commands/user/switch.ts index d605fc076..53449da0a 100644 --- a/packages/sdk/src/cli/commands/user/switch.ts +++ b/packages/sdk/src/cli/commands/user/switch.ts @@ -23,7 +23,7 @@ export const switchCommand = defineAppCommand({ if (!user) { throw new Error(ml` User "${args.user}" not found. - Please login first using 'tailor-sdk login' command to register this user. + Please login first using 'tailor login' command to register this user. `); } diff --git a/packages/sdk/src/cli/commands/workflow/start.ts b/packages/sdk/src/cli/commands/workflow/start.ts index 69ba6163d..891b08a64 100644 --- a/packages/sdk/src/cli/commands/workflow/start.ts +++ b/packages/sdk/src/cli/commands/workflow/start.ts @@ -169,7 +169,7 @@ async function startWorkflowByName( }); if (!machineUser) { throw new Error( - "Machine user is required. Specify --machine-user, set TAILOR_PLATFORM_MACHINE_USER_NAME, or set a profile default with 'tailor-sdk profile update --machine-user '.", + "Machine user is required. Specify --machine-user, set TAILOR_PLATFORM_MACHINE_USER_NAME, or set a profile default with 'tailor profile update --machine-user '.", ); } diff --git a/packages/sdk/src/cli/commands/workspace/create.ts b/packages/sdk/src/cli/commands/workspace/create.ts index 27386caa0..4ed2116bc 100644 --- a/packages/sdk/src/cli/commands/workspace/create.ts +++ b/packages/sdk/src/cli/commands/workspace/create.ts @@ -148,7 +148,7 @@ export const createCommand = defineAppCommand({ if (!config.users[profileUser]) { throw new Error( - `User "${profileUser}" not found.\nPlease verify your user name and login using 'tailor-sdk login' command.`, + `User "${profileUser}" not found.\nPlease verify your user name and login using 'tailor login' command.`, ); } config.profiles[profileName] = { diff --git a/packages/sdk/src/cli/completion.test.ts b/packages/sdk/src/cli/completion.test.ts index 9a3f8c9e6..295afd18c 100644 --- a/packages/sdk/src/cli/completion.test.ts +++ b/packages/sdk/src/cli/completion.test.ts @@ -143,7 +143,7 @@ describe("shell completion", () => { candidates: readonly { value: string; description?: string }[]; }[]; } { - const data = extractCompletionData(mainCommand, "tailor-sdk"); + const data = extractCompletionData(mainCommand, "tailor"); const apiCmd = data.command.subcommands.find((s) => s.name === "api"); if (!apiCmd) throw new Error("api subcommand missing"); const fieldOpt = apiCmd.options.find((o) => o.name === "field"); @@ -209,7 +209,7 @@ describe("shell completion", () => { // repeated. Confirm both are wired up in the zsh script. const { script } = generateCompletion(mainCommand, { shell: "zsh", - programName: "tailor-sdk", + programName: "tailor", }); expect(script).toMatch(/__tailor_sdk_expand_[a-z_]+__field=/); expect(script).toContain("GetFunctionExecution"); diff --git a/packages/sdk/src/cli/crashreport/index.ts b/packages/sdk/src/cli/crashreport/index.ts index 274b24f0f..aee9be83b 100644 --- a/packages/sdk/src/cli/crashreport/index.ts +++ b/packages/sdk/src/cli/crashreport/index.ts @@ -35,7 +35,7 @@ export async function reportCrash(error: unknown, errorType: ErrorType): Promise ` ${filePath}`, "", "To submit this report:", - ` tailor-sdk crashreport send --file "${filePath}"`, + ` tailor crashreport send --file "${filePath}"`, ].join("\n"), ); } diff --git a/packages/sdk/src/cli/crashreport/sanitize.test.ts b/packages/sdk/src/cli/crashreport/sanitize.test.ts index af9bee65e..593ee93c7 100644 --- a/packages/sdk/src/cli/crashreport/sanitize.test.ts +++ b/packages/sdk/src/cli/crashreport/sanitize.test.ts @@ -131,48 +131,48 @@ describe("sanitizeMessage", () => { describe("sanitizeArgv", () => { test("keeps command and subcommand names", () => { - const argv = ["node", "tailor-sdk", "apply"]; + const argv = ["node", "tailor", "apply"]; const result = sanitizeArgv(argv); - expect(result).toEqual(["node", "tailor-sdk", "apply"]); + expect(result).toEqual(["node", "tailor", "apply"]); }); test("redacts value after any long flag (space format)", () => { - const argv = ["node", "tailor-sdk", "show", "--workspace-id", "some-uuid"]; + const argv = ["node", "tailor", "show", "--workspace-id", "some-uuid"]; const result = sanitizeArgv(argv); - expect(result).toEqual(["node", "tailor-sdk", "show", "--workspace-id", ""]); + expect(result).toEqual(["node", "tailor", "show", "--workspace-id", ""]); }); test("redacts value after any short flag (space format)", () => { - const argv = ["node", "tailor-sdk", "show", "-w", "some-uuid"]; + const argv = ["node", "tailor", "show", "-w", "some-uuid"]; const result = sanitizeArgv(argv); - expect(result).toEqual(["node", "tailor-sdk", "show", "-w", ""]); + expect(result).toEqual(["node", "tailor", "show", "-w", ""]); }); test("redacts --flag=value (equals format)", () => { - const argv = ["node", "tailor-sdk", "show", "--workspace-id=some-uuid"]; + const argv = ["node", "tailor", "show", "--workspace-id=some-uuid"]; const result = sanitizeArgv(argv); expect(result).toContain("--workspace-id="); expect(result).not.toContain("some-uuid"); }); test("redacts value after any flag regardless of flag name", () => { - const argv = ["node", "tailor-sdk", "apply", "--region", "asia-northeast"]; + const argv = ["node", "tailor", "apply", "--region", "asia-northeast"]; const result = sanitizeArgv(argv); - expect(result).toEqual(["node", "tailor-sdk", "apply", "--region", ""]); + expect(result).toEqual(["node", "tailor", "apply", "--region", ""]); }); test("treats consecutive flags correctly (no value between them)", () => { - const argv = ["node", "tailor-sdk", "apply", "--verbose", "--yes"]; + const argv = ["node", "tailor", "apply", "--verbose", "--yes"]; const result = sanitizeArgv(argv); - expect(result).toEqual(["node", "tailor-sdk", "apply", "--verbose", "--yes"]); + expect(result).toEqual(["node", "tailor", "apply", "--verbose", "--yes"]); }); test("redacts value after boolean flag followed by valued flag", () => { - const argv = ["node", "tailor-sdk", "apply", "--verbose", "--workspace-id", "secret"]; + const argv = ["node", "tailor", "apply", "--verbose", "--workspace-id", "secret"]; const result = sanitizeArgv(argv); expect(result).toEqual([ "node", - "tailor-sdk", + "tailor", "apply", "--verbose", "--workspace-id", @@ -181,21 +181,21 @@ describe("sanitizeArgv", () => { }); test("redacts absolute path positional arguments", () => { - const argv = ["node", "tailor-sdk", "/home/user/project/tailor.config.ts"]; + const argv = ["node", "tailor", "/home/user/project/tailor.config.ts"]; const result = sanitizeArgv(argv); expect(result).toContain(""); expect(result).not.toContain("/home/user/"); }); test("redacts Windows-style absolute path positional arguments", () => { - const argv = ["node", "tailor-sdk", "C:\\Users\\admin\\project\\tailor.config.ts"]; + const argv = ["node", "tailor", "C:\\Users\\admin\\project\\tailor.config.ts"]; const result = sanitizeArgv(argv); expect(result).toContain(""); expect(result).not.toContain("C:\\Users\\admin"); }); test("redacts email address positional arguments", () => { - const argv = ["node", "tailor-sdk", "user", "switch", "user@example.com"]; + const argv = ["node", "tailor", "user", "switch", "user@example.com"]; const result = sanitizeArgv(argv); expect(result).toContain(""); expect(result).not.toContain("user@example.com"); diff --git a/packages/sdk/src/cli/crashreport/sender.test.ts b/packages/sdk/src/cli/crashreport/sender.test.ts index a05f8ac8d..30c7adb80 100644 --- a/packages/sdk/src/cli/crashreport/sender.test.ts +++ b/packages/sdk/src/cli/crashreport/sender.test.ts @@ -12,7 +12,7 @@ function makeCrashReport(): CrashReport { osRelease: "25.3.0", arch: "arm64", command: "apply", - argv: ["node", "tailor-sdk", "apply"], + argv: ["node", "tailor", "apply"], errorName: "TypeError", errorMessage: "Cannot read properties of undefined", stackTrace: "TypeError: Cannot read properties of undefined", @@ -47,7 +47,7 @@ describe("sendCrashReport", () => { }); const report = makeCrashReport(); - await sendCrashReport(report, "tailor-sdk/1.0.0"); + await sendCrashReport(report, "tailor/1.0.0"); const call = vi.mocked(globalThis.fetch).mock.calls[0]!; const body = JSON.parse(call[1]!.body as string); @@ -66,7 +66,7 @@ describe("sendCrashReport", () => { }); const report = makeCrashReport(); - await sendCrashReport(report, "tailor-sdk/1.0.0"); + await sendCrashReport(report, "tailor/1.0.0"); const call = vi.mocked(globalThis.fetch).mock.calls[0]!; const { variables } = JSON.parse(call[1]!.body as string); @@ -79,7 +79,7 @@ describe("sendCrashReport", () => { json: () => Promise.resolve({ data: { submitCrashReport: { success: true } } }), }); - const result = await sendCrashReport(makeCrashReport(), "tailor-sdk/1.0.0"); + const result = await sendCrashReport(makeCrashReport(), "tailor/1.0.0"); expect(result).toBe(true); }); @@ -94,7 +94,7 @@ describe("sendCrashReport", () => { }), }); - const result = await sendCrashReport(makeCrashReport(), "tailor-sdk/1.0.0"); + const result = await sendCrashReport(makeCrashReport(), "tailor/1.0.0"); expect(result).toBe(false); }); @@ -109,7 +109,7 @@ describe("sendCrashReport", () => { }), }); - const result = await sendCrashReport(makeCrashReport(), "tailor-sdk/1.0.0"); + const result = await sendCrashReport(makeCrashReport(), "tailor/1.0.0"); expect(result).toBe(true); }); @@ -120,7 +120,7 @@ describe("sendCrashReport", () => { json: () => Promise.resolve({ data: { submitCrashReport: { success: false } } }), }); - const result = await sendCrashReport(makeCrashReport(), "tailor-sdk/1.0.0"); + const result = await sendCrashReport(makeCrashReport(), "tailor/1.0.0"); expect(result).toBe(false); }); @@ -132,7 +132,7 @@ describe("sendCrashReport", () => { json: () => Promise.resolve({}), }); - const result = await sendCrashReport(makeCrashReport(), "tailor-sdk/1.0.0"); + const result = await sendCrashReport(makeCrashReport(), "tailor/1.0.0"); expect(result).toBe(false); }); @@ -140,7 +140,7 @@ describe("sendCrashReport", () => { test("returns false on network error", async () => { globalThis.fetch = vi.fn().mockRejectedValue(new Error("Network error")); - const result = await sendCrashReport(makeCrashReport(), "tailor-sdk/1.0.0"); + const result = await sendCrashReport(makeCrashReport(), "tailor/1.0.0"); expect(result).toBe(false); }); @@ -152,7 +152,7 @@ describe("sendCrashReport", () => { }); process.env.TAILOR_CRASH_REPORT_ENDPOINT = "https://custom.example.com/query"; - await sendCrashReport(makeCrashReport(), "tailor-sdk/1.0.0"); + await sendCrashReport(makeCrashReport(), "tailor/1.0.0"); expect(globalThis.fetch).toHaveBeenCalledWith( "https://custom.example.com/query", @@ -166,14 +166,14 @@ describe("sendCrashReport", () => { json: () => Promise.resolve({ data: { submitCrashReport: { success: true } } }), }); - await sendCrashReport(makeCrashReport(), "tailor-sdk/1.0.0"); + await sendCrashReport(makeCrashReport(), "tailor/1.0.0"); expect(globalThis.fetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ headers: expect.objectContaining({ "Content-Type": "application/json", - "User-Agent": "tailor-sdk/1.0.0", + "User-Agent": "tailor/1.0.0", }), }), ); diff --git a/packages/sdk/src/cli/crashreport/writer.test.ts b/packages/sdk/src/cli/crashreport/writer.test.ts index ae5915565..e4a078364 100644 --- a/packages/sdk/src/cli/crashreport/writer.test.ts +++ b/packages/sdk/src/cli/crashreport/writer.test.ts @@ -15,7 +15,7 @@ function makeCrashReport(overrides?: Partial): CrashReport { osRelease: "25.3.0", arch: "arm64", command: "apply", - argv: ["node", "tailor-sdk", "apply"], + argv: ["node", "tailor", "apply"], errorName: "TypeError", errorMessage: "Cannot read properties of undefined", stackTrace: @@ -45,12 +45,10 @@ describe("formatCrashReport", () => { test("serializes argv as JSON array", () => { const report = makeCrashReport({ - argv: ["node", "tailor-sdk", "apply", "--body", '{"a": "b c"}'], + argv: ["node", "tailor", "apply", "--body", '{"a": "b c"}'], }); const text = formatCrashReport(report); - expect(text).toContain( - 'Arguments: ["node","tailor-sdk","apply","--body","{\\"a\\": \\"b c\\"}"]', - ); + expect(text).toContain('Arguments: ["node","tailor","apply","--body","{\\"a\\": \\"b c\\"}"]'); }); test("handles empty stack trace", () => { diff --git a/packages/sdk/src/cli/index.ts b/packages/sdk/src/cli/index.ts index 17282b302..1482be18e 100644 --- a/packages/sdk/src/cli/index.ts +++ b/packages/sdk/src/cli/index.ts @@ -53,7 +53,7 @@ if (!isNativeTypeScriptRuntime()) { initCrashReporting(); const packageJson = await readPackageJson(); -const cliName = Object.keys(packageJson.bin ?? {})[0] || "tailor-sdk"; +const cliName = Object.keys(packageJson.bin ?? {})[0] || "tailor"; export const mainCommand = withCompletionCommand( defineCommand({ diff --git a/packages/sdk/src/cli/query/errors.ts b/packages/sdk/src/cli/query/errors.ts index 9152f2838..916c9dedc 100644 --- a/packages/sdk/src/cli/query/errors.ts +++ b/packages/sdk/src/cli/query/errors.ts @@ -27,7 +27,7 @@ export function mapQueryExecutionError(args: MapQueryExecutionErrorArgs): Error return CLIError({ code: "not_found", message: `Machine user '${args.machineUser ?? "unknown"}' was not found.`, - suggestion: "Run `tailor-sdk machineuser list` and use an existing name.", + suggestion: "Run `tailor machineuser list` and use an existing name.", }); } diff --git a/packages/sdk/src/cli/query/index.ts b/packages/sdk/src/cli/query/index.ts index 458e833e1..8c1b9f34f 100644 --- a/packages/sdk/src/cli/query/index.ts +++ b/packages/sdk/src/cli/query/index.ts @@ -150,7 +150,7 @@ async function loadOptions(options: QueryBaseOptions) { }); if (!machineUser) { throw new Error( - "Machine user is required. Specify --machine-user, set TAILOR_PLATFORM_MACHINE_USER_NAME, or set a profile default with 'tailor-sdk profile update --machine-user '.", + "Machine user is required. Specify --machine-user, set TAILOR_PLATFORM_MACHINE_USER_NAME, or set a profile default with 'tailor profile update --machine-user '.", ); } diff --git a/packages/sdk/src/cli/services/executor/service.ts b/packages/sdk/src/cli/services/executor/service.ts index 9d3404c5e..e53c9c076 100644 --- a/packages/sdk/src/cli/services/executor/service.ts +++ b/packages/sdk/src/cli/services/executor/service.ts @@ -108,7 +108,7 @@ export function createExecutorService(params: CreateExecutorServiceParams): Exec const executor = await loadExecutorForFile(filePath); if (executor) { // Track as plugin executor (plugin ID is extracted from file path) - // File path format: .tailor-sdk/plugin/{executor-name}.ts + // File path format: .tailor/plugin/{executor-name}.ts pluginExecutors.push({ executor, pluginId: "plugin-generated", diff --git a/packages/sdk/src/cli/shared/context.ts b/packages/sdk/src/cli/shared/context.ts index eca479460..1c57f7ed7 100644 --- a/packages/sdk/src/cli/shared/context.ts +++ b/packages/sdk/src/cli/shared/context.ts @@ -484,7 +484,7 @@ export async function loadMachineUserName( code: "PROFILE_MACHINE_USER_OVERRIDE_DENIED", message: `Profile "${profile}" denies overriding the machine user.`, details: `This profile fixes the machine user to "${entry.machine_user}" for application-data commands.`, - suggestion: `Omit the machine user option, unset TAILOR_PLATFORM_MACHINE_USER_NAME, or run 'tailor-sdk profile update ${profile} --machine-user-override allow'.`, + suggestion: `Omit the machine user option, unset TAILOR_PLATFORM_MACHINE_USER_NAME, or run 'tailor profile update ${profile} --machine-user-override allow'.`, }); } return entry.machine_user; @@ -527,7 +527,7 @@ export async function loadAccessToken(opts?: LoadAccessTokenOptions) { // error throw new Error(ml` Tailor Platform token not found. - Please specify token via TAILOR_PLATFORM_TOKEN environment variable or login using 'tailor-sdk login' command. + Please specify token via TAILOR_PLATFORM_TOKEN environment variable or login using 'tailor login' command. `); } user = u; @@ -551,7 +551,7 @@ export async function resolveTokens( if (!tokens) { throw new Error(ml` Credentials not found in OS keyring for "${user}". - Please run 'tailor-sdk login' and try again. + Please run 'tailor login' and try again. `); } return tokens; @@ -662,7 +662,7 @@ export async function fetchLatestToken( if (!storedUser) { throw new Error(ml` User "${user}" not found. - Please verify your user name and login using 'tailor-sdk login' command. + Please verify your user name and login using 'tailor login' command. `); } @@ -670,7 +670,7 @@ export async function fetchLatestToken( if (!userEntry) { throw new Error(ml` User "${user}" not found. - Please verify your user name and login using 'tailor-sdk login' command. + Please verify your user name and login using 'tailor login' command. `); } @@ -683,7 +683,7 @@ export async function fetchLatestToken( if (!tokens.refreshToken) { throw new Error(ml` Token expired. - Please run 'tailor-sdk login' and try again. + Please run 'tailor login' and try again. `); } @@ -698,7 +698,7 @@ export async function fetchLatestToken( } catch { throw new Error(ml` Failed to refresh token. Your session may have expired. - Please run 'tailor-sdk login' and try again. + Please run 'tailor login' and try again. `); } diff --git a/packages/sdk/src/cli/shared/dist-dir.ts b/packages/sdk/src/cli/shared/dist-dir.ts index 7a8c4cef8..65bd0ff25 100644 --- a/packages/sdk/src/cli/shared/dist-dir.ts +++ b/packages/sdk/src/cli/shared/dist-dir.ts @@ -5,7 +5,7 @@ export const getDistDir = (): string => { if (configured && configured !== distPath) { distPath = configured; } else if (distPath === null) { - distPath = configured || ".tailor-sdk"; + distPath = configured || ".tailor"; } return distPath; }; diff --git a/packages/sdk/src/cli/shared/errors.ts b/packages/sdk/src/cli/shared/errors.ts index 5d0aa170c..9c68c9220 100644 --- a/packages/sdk/src/cli/shared/errors.ts +++ b/packages/sdk/src/cli/shared/errors.ts @@ -50,7 +50,7 @@ function formatError(error: CLIError): string { if (error.command) { parts.push( - `\n ${chalk.gray("Help:")} Run \`tailor-sdk ${error.command} --help\` for usage information.`, + `\n ${chalk.gray("Help:")} Run \`tailor ${error.command} --help\` for usage information.`, ); } diff --git a/packages/sdk/src/cli/shared/readonly-guard.ts b/packages/sdk/src/cli/shared/readonly-guard.ts index dfabff37e..1ba3242fe 100644 --- a/packages/sdk/src/cli/shared/readonly-guard.ts +++ b/packages/sdk/src/cli/shared/readonly-guard.ts @@ -38,6 +38,6 @@ export async function assertWritable(opts?: AssertWritableOptions): Promise { }); test("prefixes dotfile-rooted paths with ./ so they are not mistaken for relative-path markers", () => { - // Regression: paths like `.tailor-sdk/test-run/...` start with `.` but + // Regression: paths like `.tailor/test-run/...` start with `.` but // are not `../` escapes. The display must prefix them with `./` so // users can tell they are cwd-relative. const frames: MappedStackFrame[] = [ { original: { functionName: "main", file: "file:///bundle.js", line: 1, column: 1 }, mapped: { - source: ".tailor-sdk/test-run/test-run--add.entry.js", + source: ".tailor/test-run/test-run--add.entry.js", line: 16, column: 13, name: null, @@ -499,7 +499,7 @@ describe("formatMappedError", () => { const result = formatMappedError("Error: test", frames, null, process.cwd()); const plain = stripVTControlCharacters(result); - expect(plain).toContain("./.tailor-sdk/test-run/test-run--add.entry.js:16:13"); + expect(plain).toContain("./.tailor/test-run/test-run--add.entry.js:16:13"); }); }); diff --git a/packages/sdk/src/cli/shared/stack-trace.ts b/packages/sdk/src/cli/shared/stack-trace.ts index a35d0115c..2f05c7dad 100644 --- a/packages/sdk/src/cli/shared/stack-trace.ts +++ b/packages/sdk/src/cli/shared/stack-trace.ts @@ -295,7 +295,7 @@ export function formatMappedError( const rel = path.relative(process.cwd(), absolutePath); // Only paths escaping cwd (starting with `..`) are shown as-is; all // other relative paths get an explicit `./` prefix so dotfiles like - // `.tailor-sdk/...` are not mistaken for relative-path markers. + // `.tailor/...` are not mistaken for relative-path markers. const displaySource = rel.startsWith("..") ? rel : `./${rel}`; const fnName = name ?? frame.original.functionName; const link = buildSourceLink(displaySource, absolutePath, line, column); diff --git a/packages/sdk/src/cli/shared/type-generator.ts b/packages/sdk/src/cli/shared/type-generator.ts index acaaccedb..7caf1f30d 100644 --- a/packages/sdk/src/cli/shared/type-generator.ts +++ b/packages/sdk/src/cli/shared/type-generator.ts @@ -126,7 +126,7 @@ ${idpNameFields} return ml /* ts */ ` // This file is auto-generated by @tailor-platform/sdk // Do not edit this file manually -// Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' +// Regenerated automatically when running 'tailor deploy' or 'tailor generate' declare module "@tailor-platform/sdk" { interface AttributeMap ${mapBody} diff --git a/packages/sdk/src/cli/shared/user-agent.ts b/packages/sdk/src/cli/shared/user-agent.ts index 38bfb8f78..f9836ed06 100644 --- a/packages/sdk/src/cli/shared/user-agent.ts +++ b/packages/sdk/src/cli/shared/user-agent.ts @@ -8,7 +8,7 @@ import { readPackageJson } from "./package-json"; * @returns User-Agent header value */ export function userAgentFromVersion(version: string): string { - return `tailor-sdk/${version}`; + return `tailor/${version}`; } /** diff --git a/packages/sdk/src/cli/telemetry/index.ts b/packages/sdk/src/cli/telemetry/index.ts index d3466ebeb..ecb7a7d8d 100644 --- a/packages/sdk/src/cli/telemetry/index.ts +++ b/packages/sdk/src/cli/telemetry/index.ts @@ -44,7 +44,7 @@ export async function initTelemetry(): Promise { const version = packageJson.version ?? "unknown"; const resource = resourceFromAttributes({ - [ATTR_SERVICE_NAME]: "tailor-sdk", + [ATTR_SERVICE_NAME]: "tailor", [ATTR_SERVICE_VERSION]: version, }); @@ -79,7 +79,7 @@ export async function shutdownTelemetry(): Promise { * @returns Result of fn */ export async function withSpan(name: string, fn: (span: Span) => Promise): Promise { - const tracer = trace.getTracer("tailor-sdk"); + const tracer = trace.getTracer("tailor"); return tracer.startActiveSpan(name, async (span) => { try { diff --git a/packages/sdk/src/cli/telemetry/interceptor.ts b/packages/sdk/src/cli/telemetry/interceptor.ts index 55f4a8b44..6a5f8f5b7 100644 --- a/packages/sdk/src/cli/telemetry/interceptor.ts +++ b/packages/sdk/src/cli/telemetry/interceptor.ts @@ -9,7 +9,7 @@ import type { Interceptor } from "@connectrpc/connect"; */ export function createTracingInterceptor(): Interceptor { return (next) => async (req) => { - const tracer = trace.getTracer("tailor-sdk"); + const tracer = trace.getTracer("tailor"); return tracer.startActiveSpan(`rpc.${req.method.name}`, async (span) => { span.setAttribute("rpc.method", req.method.name); diff --git a/packages/sdk/src/configure/services/auth/types.ts b/packages/sdk/src/configure/services/auth/types.ts index 7f105fc51..d9075efd4 100644 --- a/packages/sdk/src/configure/services/auth/types.ts +++ b/packages/sdk/src/configure/services/auth/types.ts @@ -34,7 +34,7 @@ export interface MachineUserNameRegistry {} /** * Machine user name. * - * When `tailor.d.ts` is generated (via `tailor-sdk deploy`/`generate`), this is narrowed + * When `tailor.d.ts` is generated (via `tailor deploy`/`generate`), this is narrowed * to the union of defined machine user names. When no machine users are registered yet, * falls back to `string` to avoid blocking editing before the first generate run. */ diff --git a/packages/sdk/src/configure/services/tailordb/permission.ts b/packages/sdk/src/configure/services/tailordb/permission.ts index 5562ecebb..212f3d229 100644 --- a/packages/sdk/src/configure/services/tailordb/permission.ts +++ b/packages/sdk/src/configure/services/tailordb/permission.ts @@ -251,7 +251,7 @@ type HasAnyCondition< /** * Type representing a permission condition that combines user attributes, record fields, and literal values using comparison operators. * - * The User type is extended by `tailor.d.ts`, which is automatically generated when running `tailor-sdk generate`. + * The User type is extended by `tailor.d.ts`, which is automatically generated when running `tailor generate`. * Attributes enabled in the config file's `auth.userProfile.attributes` (or * `auth.machineUserAttributes` when userProfile is omitted) become available as types. * @example diff --git a/packages/sdk/src/configure/types/idp-name.ts b/packages/sdk/src/configure/types/idp-name.ts index a6aa946e8..db0e3908b 100644 --- a/packages/sdk/src/configure/types/idp-name.ts +++ b/packages/sdk/src/configure/types/idp-name.ts @@ -6,7 +6,7 @@ export interface IdpNameRegistry {} /** * IdP namespace name. * - * When `tailor.d.ts` is generated (via `tailor-sdk deploy`/`generate`), this is narrowed + * When `tailor.d.ts` is generated (via `tailor deploy`/`generate`), this is narrowed * to the union of defined IdP names. When no IdPs are registered yet, falls back to * `string` to avoid blocking editing before the first generate run. */ diff --git a/packages/sdk/src/plugin/manager.ts b/packages/sdk/src/plugin/manager.ts index 59518384a..f24428bd7 100644 --- a/packages/sdk/src/plugin/manager.ts +++ b/packages/sdk/src/plugin/manager.ts @@ -563,7 +563,7 @@ export interface PluginTypeGenerationResult { * Parameters for generating plugin files */ export interface GeneratePluginFilesParams { - /** Base output directory (e.g., .tailor-sdk/plugin) */ + /** Base output directory (e.g., .tailor/plugin) */ outputDir: string; /** Map of source type names to their source info */ sourceTypeInfoMap: Map; diff --git a/skills/tailor-sdk/SKILL.md b/skills/tailor/SKILL.md similarity index 98% rename from skills/tailor-sdk/SKILL.md rename to skills/tailor/SKILL.md index 7d1dcdcf2..68c54ad5b 100644 --- a/skills/tailor-sdk/SKILL.md +++ b/skills/tailor/SKILL.md @@ -1,5 +1,5 @@ --- -name: tailor-sdk +name: tailor description: Use this skill when working with @tailor-platform/sdk projects, including service configuration, CLI usage, and docs navigation. --- From 645949ed64bda8b82fc44c0db54928698b12a2eb Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 14:28:54 +0900 Subject: [PATCH 212/618] chore: add changeset for wait-point rename --- .changeset/wait-point-rename.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .changeset/wait-point-rename.md diff --git a/.changeset/wait-point-rename.md b/.changeset/wait-point-rename.md new file mode 100644 index 000000000..510abb478 --- /dev/null +++ b/.changeset/wait-point-rename.md @@ -0,0 +1,19 @@ +--- +"@tailor-platform/sdk": major +"@tailor-platform/create-sdk": major +--- + +Rename `defineWaitPoint` and `defineWaitPoints` to `createWaitPoint` and `createWaitPoints`. + +These functions create runtime instances with `.wait()` and `.resolve()` methods that call the platform API at runtime, so the `create*` prefix is more accurate. Update any usages: + +```diff +-import { defineWaitPoint, defineWaitPoints } from "@tailor-platform/sdk"; ++import { createWaitPoint, createWaitPoints } from "@tailor-platform/sdk"; + +-export const approval = defineWaitPoint("approval"); ++export const approval = createWaitPoint("approval"); + +-export const waitPoints = defineWaitPoints((define) => ({ ... })); ++export const waitPoints = createWaitPoints((define) => ({ ... })); +``` From fdaf44661eb94e708aa0c0c29dd1881481a63604 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 14:57:06 +0900 Subject: [PATCH 213/618] feat(codemod): add v2/wait-point-rename codemod Rename defineWaitPoint and defineWaitPoints to createWaitPoint and createWaitPoints in user code. Handles both non-aliased imports (renames the specifier and all body references) and aliased imports (renames only the imported name, preserving the alias). Includes fixture tests for: basic rename, aliased imports, no-op cases (no matching imports, no SDK import). --- .changeset/wait-point-rename.md | 1 + .../v2/wait-point-rename/codemod.yaml | 7 ++ .../v2/wait-point-rename/scripts/transform.ts | 76 +++++++++++++++++++ .../tests/aliased/expected.ts | 7 ++ .../wait-point-rename/tests/aliased/input.ts | 7 ++ .../wait-point-rename/tests/basic/expected.ts | 17 +++++ .../v2/wait-point-rename/tests/basic/input.ts | 17 +++++ .../wait-point-rename/tests/no-match/input.ts | 4 + .../tests/no-sdk-import/input.ts | 5 ++ packages/sdk-codemod/src/registry.ts | 18 +++++ packages/sdk-codemod/src/transform.test.ts | 4 + packages/sdk/docs/migration/v2.md | 26 +++++++ 12 files changed, 189 insertions(+) create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/aliased/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/aliased/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/basic/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/basic/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/no-match/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/no-sdk-import/input.ts diff --git a/.changeset/wait-point-rename.md b/.changeset/wait-point-rename.md index 510abb478..0621a005a 100644 --- a/.changeset/wait-point-rename.md +++ b/.changeset/wait-point-rename.md @@ -1,6 +1,7 @@ --- "@tailor-platform/sdk": major "@tailor-platform/create-sdk": major +"@tailor-platform/sdk-codemod": patch --- Rename `defineWaitPoint` and `defineWaitPoints` to `createWaitPoint` and `createWaitPoints`. diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/codemod.yaml b/packages/sdk-codemod/codemods/v2/wait-point-rename/codemod.yaml new file mode 100644 index 000000000..35ce27041 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/wait-point-rename" +version: "1.0.0" +description: "Rename defineWaitPoint/defineWaitPoints to createWaitPoint/createWaitPoints" +engine: jssg +language: typescript +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts new file mode 100644 index 000000000..70e74afa3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts @@ -0,0 +1,76 @@ +import { parse, Lang } from "@ast-grep/napi"; +import type { Edit, SgNode } from "@ast-grep/napi"; + +const SDK_MODULE = "@tailor-platform/sdk"; + +const RENAMES: Record = { + defineWaitPoint: "createWaitPoint", + defineWaitPoints: "createWaitPoints", +}; + +function isInsideImportStatement(node: SgNode): boolean { + let current: SgNode | null = node.parent(); + while (current) { + if (current.kind() === "import_statement") return true; + current = current.parent(); + } + return false; +} + +/** + * Rename `defineWaitPoint` and `defineWaitPoints` imported from `@tailor-platform/sdk` + * to `createWaitPoint` and `createWaitPoints`, updating both the import specifiers + * and all usages in the file body. + * @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 { + const hasMatch = Object.keys(RENAMES).some((name) => source.includes(name)); + if (!hasMatch) return null; + if (!source.includes(SDK_MODULE)) return null; + + const lang = source.includes("") ? Lang.Tsx : Lang.TypeScript; + const root = parse(lang, source).root(); + + const edits: Edit[] = []; + // Non-aliased imports need their body references renamed too. + const needsBodyRename = new Set(); + + const importStmts = root.findAll({ + rule: { + kind: "import_statement", + has: { kind: "string", regex: `^["']${SDK_MODULE}["']$` }, + }, + }); + + for (const importStmt of importStmts) { + const specs = importStmt.findAll({ rule: { kind: "import_specifier" } }); + for (const spec of specs) { + const idents = spec.children().filter((c: SgNode) => c.kind() === "identifier"); + if (idents.length === 0) continue; + + const importedName = idents[0]!.text(); + const newName = RENAMES[importedName]; + if (!newName) continue; + + const isAliased = idents.length > 1; + edits.push(idents[0]!.replace(newName)); + if (!isAliased) needsBodyRename.add(importedName); + } + } + + if (edits.length === 0) return null; + + if (needsBodyRename.size > 0) { + const identifiers = root.findAll({ rule: { kind: "identifier" } }); + for (const ident of identifiers) { + const name = ident.text(); + if (!needsBodyRename.has(name)) continue; + if (isInsideImportStatement(ident)) continue; + edits.push(ident.replace(RENAMES[name]!)); + } + } + + return root.commitEdits(edits); +} diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/aliased/expected.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/aliased/expected.ts new file mode 100644 index 000000000..2a7ab0904 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/aliased/expected.ts @@ -0,0 +1,7 @@ +import { createWaitPoints as wp, createWaitPoint as single } from "@tailor-platform/sdk"; + +export const { approval } = wp((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +export const step = single("step"); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/aliased/input.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/aliased/input.ts new file mode 100644 index 000000000..2fcfa6395 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/aliased/input.ts @@ -0,0 +1,7 @@ +import { defineWaitPoints as wp, defineWaitPoint as single } from "@tailor-platform/sdk"; + +export const { approval } = wp((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +export const step = single("step"); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/basic/expected.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/basic/expected.ts new file mode 100644 index 000000000..8c0f6309d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/basic/expected.ts @@ -0,0 +1,17 @@ +import { createWorkflow, createWorkflowJob, createWaitPoint, createWaitPoints } from "@tailor-platform/sdk"; + +export const { approval } = createWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +export const singlePoint = createWaitPoint<{ id: string }, boolean>("my-step"); + +export const processJob = createWorkflowJob({ + name: "process", + body: async (input: { orderId: string }) => { + const result = await approval.wait({ message: `approve ${input.orderId}` }); + return { approved: result.approved }; + }, +}); + +export default createWorkflow({ name: "my-workflow", mainJob: processJob }); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/basic/input.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/basic/input.ts new file mode 100644 index 000000000..6026086f7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/basic/input.ts @@ -0,0 +1,17 @@ +import { createWorkflow, createWorkflowJob, defineWaitPoint, defineWaitPoints } from "@tailor-platform/sdk"; + +export const { approval } = defineWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +export const singlePoint = defineWaitPoint<{ id: string }, boolean>("my-step"); + +export const processJob = createWorkflowJob({ + name: "process", + body: async (input: { orderId: string }) => { + const result = await approval.wait({ message: `approve ${input.orderId}` }); + return { approved: result.approved }; + }, +}); + +export default createWorkflow({ name: "my-workflow", mainJob: processJob }); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/no-match/input.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/no-match/input.ts new file mode 100644 index 000000000..40c9dc885 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/no-match/input.ts @@ -0,0 +1,4 @@ +import { createWorkflow, createWorkflowJob } from "@tailor-platform/sdk"; + +export const job = createWorkflowJob({ name: "job", body: () => ({ ok: true }) }); +export default createWorkflow({ name: "wf", mainJob: job }); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/no-sdk-import/input.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/no-sdk-import/input.ts new file mode 100644 index 000000000..157c91325 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/no-sdk-import/input.ts @@ -0,0 +1,5 @@ +// defineWaitPoints and defineWaitPoint used locally — not from @tailor-platform/sdk +function defineWaitPoints() {} +function defineWaitPoint() {} +defineWaitPoints(); +defineWaitPoint(); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 87bd986b4..97f8d49d5 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -303,6 +303,24 @@ export const allCodemods: CodemodPackage[] = [ }, ], }, + { + id: "v2/wait-point-rename", + name: "defineWaitPoint/defineWaitPoints → createWaitPoint/createWaitPoints", + description: + "Rename `defineWaitPoint` and `defineWaitPoints` to `createWaitPoint` and `createWaitPoints`. The functions create runtime instances with `.wait()` / `.resolve()` methods, so the `create*` prefix is used consistently.", + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/wait-point-rename/scripts/transform.js", + legacyPatterns: ["defineWaitPoint", "defineWaitPoints"], + examples: [ + { + before: + 'import { defineWaitPoints } from "@tailor-platform/sdk";\n\nexport const { approval } = defineWaitPoints((define) => ({\n approval: define<{ message: string }, { approved: boolean }>(),\n}));', + after: + 'import { createWaitPoints } from "@tailor-platform/sdk";\n\nexport const { approval } = createWaitPoints((define) => ({\n approval: define<{ message: string }, { approved: boolean }>(),\n}));', + }, + ], + }, { id: "v2/open-download-stream", name: "openDownloadStream → downloadStream", diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index 5ab9121fa..5aa0cb73e 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -98,4 +98,8 @@ describe("codemod transforms", () => { test("v2/execute-script-arg transforms correctly", async () => { await expect(runFixtureCases("v2/execute-script-arg")).resolves.toBeUndefined(); }); + + test("v2/wait-point-rename transforms correctly", async () => { + await expect(runFixtureCases("v2/wait-point-rename")).resolves.toBeUndefined(); + }); }); diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 15c50f6fa..b0116161a 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -333,6 +333,32 @@ already pass a plain value unchanged. +## defineWaitPoint/defineWaitPoints → createWaitPoint/createWaitPoints + +**Migration:** Partially automatic + +Rename `defineWaitPoint` and `defineWaitPoints` to `createWaitPoint` and `createWaitPoints`. The functions create runtime instances with `.wait()` / `.resolve()` methods, so the `create*` prefix is used consistently. + +Before: + +```ts +import { defineWaitPoints } from "@tailor-platform/sdk"; + +export const { approval } = defineWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); +``` + +After: + +```ts +import { createWaitPoints } from "@tailor-platform/sdk"; + +export const { approval } = createWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); +``` + ## openDownloadStream → downloadStream **Migration:** Manual From ff8ef1c1323daf81812c182e146fd53da20e676e Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 25 Jun 2026 15:31:53 +0900 Subject: [PATCH 214/618] refactor(auth): rename attribute map types --- .changeset/auth-attributes-rename.md | 5 ++ .../create-sdk/templates/executor/tailor.d.ts | 2 +- .../templates/generators/tailor.d.ts | 2 +- .../templates/hello-world/tailor.d.ts | 2 +- .../inventory-management/tailor.d.ts | 2 +- .../create-sdk/templates/resolver/tailor.d.ts | 2 +- .../templates/static-web-site/tailor.d.ts | 2 +- .../create-sdk/templates/tailordb/tailor.d.ts | 2 +- .../create-sdk/templates/workflow/tailor.d.ts | 2 +- packages/sdk-codemod/src/registry.ts | 23 ++++++++ packages/sdk/docs/migration/v2.md | 39 ++++++++++++++ .../sdk/src/cli/shared/type-generator.test.ts | 28 +++++----- packages/sdk/src/cli/shared/type-generator.ts | 54 +++++++++---------- packages/sdk/src/configure/index.ts | 2 +- .../src/configure/services/auth/index.test.ts | 4 +- .../sdk/src/configure/services/auth/index.ts | 34 +++++------- .../sdk/src/configure/services/auth/types.ts | 37 ++++++------- .../src/configure/services/idp/permission.ts | 20 +++---- .../configure/services/tailordb/permission.ts | 27 +++++----- .../src/configure/services/tailordb/schema.ts | 8 +-- .../src/configure/services/tailordb/types.ts | 6 +-- .../sdk/src/parser/service/auth/index.test.ts | 6 +-- packages/sdk/src/runtime/types.ts | 10 ++-- 23 files changed, 187 insertions(+), 132 deletions(-) create mode 100644 .changeset/auth-attributes-rename.md diff --git a/.changeset/auth-attributes-rename.md b/.changeset/auth-attributes-rename.md new file mode 100644 index 000000000..0d1c41a5e --- /dev/null +++ b/.changeset/auth-attributes-rename.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Rename auth attribute module augmentation from `AttributeMap` to `Attributes`. diff --git a/packages/create-sdk/templates/executor/tailor.d.ts b/packages/create-sdk/templates/executor/tailor.d.ts index 9a473cac0..4afdc78cd 100644 --- a/packages/create-sdk/templates/executor/tailor.d.ts +++ b/packages/create-sdk/templates/executor/tailor.d.ts @@ -3,7 +3,7 @@ // Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' declare module "@tailor-platform/sdk" { - interface AttributeMap { + interface Attributes { role: string; } interface AttributeList { diff --git a/packages/create-sdk/templates/generators/tailor.d.ts b/packages/create-sdk/templates/generators/tailor.d.ts index 9a473cac0..4afdc78cd 100644 --- a/packages/create-sdk/templates/generators/tailor.d.ts +++ b/packages/create-sdk/templates/generators/tailor.d.ts @@ -3,7 +3,7 @@ // Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' declare module "@tailor-platform/sdk" { - interface AttributeMap { + interface Attributes { role: string; } interface AttributeList { diff --git a/packages/create-sdk/templates/hello-world/tailor.d.ts b/packages/create-sdk/templates/hello-world/tailor.d.ts index f618bd50c..f9e5e1bc4 100644 --- a/packages/create-sdk/templates/hello-world/tailor.d.ts +++ b/packages/create-sdk/templates/hello-world/tailor.d.ts @@ -3,7 +3,7 @@ // Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' declare module "@tailor-platform/sdk" { - interface AttributeMap {} + interface Attributes {} interface AttributeList { __tuple?: []; } diff --git a/packages/create-sdk/templates/inventory-management/tailor.d.ts b/packages/create-sdk/templates/inventory-management/tailor.d.ts index ee802f4a5..87fc7c835 100644 --- a/packages/create-sdk/templates/inventory-management/tailor.d.ts +++ b/packages/create-sdk/templates/inventory-management/tailor.d.ts @@ -3,7 +3,7 @@ // Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' declare module "@tailor-platform/sdk" { - interface AttributeMap { + interface Attributes { role: "MANAGER" | "STAFF"; } interface AttributeList { diff --git a/packages/create-sdk/templates/resolver/tailor.d.ts b/packages/create-sdk/templates/resolver/tailor.d.ts index 67384d1c5..f5c4cd2e9 100644 --- a/packages/create-sdk/templates/resolver/tailor.d.ts +++ b/packages/create-sdk/templates/resolver/tailor.d.ts @@ -3,7 +3,7 @@ // Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' declare module "@tailor-platform/sdk" { - interface AttributeMap { + interface Attributes { role: string; } interface AttributeList { diff --git a/packages/create-sdk/templates/static-web-site/tailor.d.ts b/packages/create-sdk/templates/static-web-site/tailor.d.ts index 080ae97b4..dcfc80795 100644 --- a/packages/create-sdk/templates/static-web-site/tailor.d.ts +++ b/packages/create-sdk/templates/static-web-site/tailor.d.ts @@ -3,7 +3,7 @@ // Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' declare module "@tailor-platform/sdk" { - interface AttributeMap { + interface Attributes { role: "ADMIN" | "MEMBER"; } interface AttributeList { diff --git a/packages/create-sdk/templates/tailordb/tailor.d.ts b/packages/create-sdk/templates/tailordb/tailor.d.ts index fb3e4de56..daec956ea 100644 --- a/packages/create-sdk/templates/tailordb/tailor.d.ts +++ b/packages/create-sdk/templates/tailordb/tailor.d.ts @@ -3,7 +3,7 @@ // Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' declare module "@tailor-platform/sdk" { - interface AttributeMap { + interface Attributes { role: string; } interface AttributeList { diff --git a/packages/create-sdk/templates/workflow/tailor.d.ts b/packages/create-sdk/templates/workflow/tailor.d.ts index 9016c307d..8da4ee9ab 100644 --- a/packages/create-sdk/templates/workflow/tailor.d.ts +++ b/packages/create-sdk/templates/workflow/tailor.d.ts @@ -3,7 +3,7 @@ // Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' declare module "@tailor-platform/sdk" { - interface AttributeMap { + interface Attributes { role: string; } interface AttributeList { diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 87bd986b4..c0952e99e 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -146,6 +146,29 @@ export const allCodemods: CodemodPackage[] = [ "Use TailorPrincipal for the unified user/actor/invoker type.", ].join("\n"), }, + { + id: "v2/auth-attributes-rename", + name: "AttributeMap → Attributes", + description: + "Rename auth attribute module augmentation and related SDK type names from `AttributeMap` to `Attributes`", + since: "1.0.0", + until: "2.0.0", + legacyPatterns: ["AttributeMap", "interface AttributeMap"], + examples: [ + { + caption: "Module augmentation uses `Attributes`:", + before: + 'declare module "@tailor-platform/sdk" {\n interface AttributeMap {\n role: string;\n }\n}', + after: + 'declare module "@tailor-platform/sdk" {\n interface Attributes {\n role: string;\n }\n}', + }, + ], + prompt: [ + "In Tailor SDK v2, the generated module augmentation interface `AttributeMap`", + "is renamed to `Attributes`. Rename any hand-written declaration merging from", + "`interface AttributeMap` to `interface Attributes`.", + ].join("\n"), + }, { id: "v2/apply-to-deploy", name: "tailor-sdk apply → tailor-sdk deploy", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 15c50f6fa..5409e421b 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -167,6 +167,45 @@ Use TailorPrincipal for the unified user/actor/invoker type. +## AttributeMap → Attributes + +**Migration:** Manual + +Rename auth attribute module augmentation and related SDK type names from `AttributeMap` to `Attributes` + +Module augmentation uses `Attributes`: + +Before: + +```ts +declare module "@tailor-platform/sdk" { + interface AttributeMap { + role: string; + } +} +``` + +After: + +```ts +declare module "@tailor-platform/sdk" { + interface Attributes { + role: string; + } +} +``` + +
+Prompt for an AI agent (to perform this migration) + +```text +In Tailor SDK v2, the generated module augmentation interface `AttributeMap` +is renamed to `Attributes`. Rename any hand-written declaration merging from +`interface AttributeMap` to `interface Attributes`. +``` + +
+ ## tailor-sdk apply → tailor-sdk deploy **Migration:** Automatic diff --git a/packages/sdk/src/cli/shared/type-generator.test.ts b/packages/sdk/src/cli/shared/type-generator.test.ts index d655bcb82..1eaec95ec 100644 --- a/packages/sdk/src/cli/shared/type-generator.test.ts +++ b/packages/sdk/src/cli/shared/type-generator.test.ts @@ -7,7 +7,7 @@ import { generateTypeDefinition, resolveTypeDefinitionPath, } from "./type-generator"; -import type { AttributeListConfig, AttributeMapConfig } from "./type-generator"; +import type { AttributeListConfig, AttributesConfig } from "./type-generator"; describe("generateTypeDefinition", () => { test("should generate tuple type in __tuple property", () => { @@ -19,12 +19,12 @@ describe("generateTypeDefinition", () => { }); test("should generate interface AttributeList for declaration merging", () => { - const attributeMap: AttributeMapConfig = { + const attributes: AttributesConfig = { role: '"MANAGER" | "STAFF"', }; const attributeList: AttributeListConfig = []; - const result = generateTypeDefinition(attributeMap, attributeList); + const result = generateTypeDefinition(attributes, attributeList); // Should use interface instead of type for AttributeList expect(result).toContain("interface AttributeList"); @@ -32,23 +32,23 @@ describe("generateTypeDefinition", () => { expect(result).toContain("__tuple?: []"); }); - test("should generate AttributeMap interface", () => { - const attributeMap: AttributeMapConfig = { + test("should generate Attributes interface", () => { + const attributes: AttributesConfig = { role: '"MANAGER" | "STAFF"', isActive: "boolean", }; - const result = generateTypeDefinition(attributeMap, undefined); + const result = generateTypeDefinition(attributes, undefined); - expect(result).toContain("interface AttributeMap"); + expect(result).toContain("interface Attributes"); expect(result).toContain('role: "MANAGER" | "STAFF"'); expect(result).toContain("isActive: boolean"); }); - test("should generate empty AttributeMap when no attributes", () => { + test("should generate empty Attributes when no attributes", () => { const result = generateTypeDefinition(undefined, undefined); - expect(result).toContain("interface AttributeMap {}"); + expect(result).toContain("interface Attributes {}"); expect(result).toContain("interface AttributeList"); expect(result).toContain("__tuple?: []"); }); @@ -156,7 +156,7 @@ describe("resolveTypeDefinitionPath", () => { }); describe("extractAttributesFromConfig + generateTypeDefinition", () => { - test("renders machineUserAttributes into AttributeMap", () => { + test("renders machineUserAttributes into Attributes", () => { const config = { name: "test-app", auth: defineAuth("auth", { @@ -177,8 +177,8 @@ describe("extractAttributesFromConfig + generateTypeDefinition", () => { }), }; - const { attributeMap } = extractAttributesFromConfig(config); - const content = generateTypeDefinition(attributeMap, undefined); + const { attributes } = extractAttributesFromConfig(config); + const content = generateTypeDefinition(attributes, undefined); expect(content).toContain('role: "ADMIN" | "WORKER";'); expect(content).toContain("isActive: boolean;"); @@ -199,10 +199,10 @@ describe("extractAttributesFromConfig + generateTypeDefinition", () => { }), }; - const { attributeMap, machineUserNames } = extractAttributesFromConfig(config); + const { attributes, machineUserNames } = extractAttributesFromConfig(config); expect(machineUserNames).toEqual(["admin", "worker"]); - const content = generateTypeDefinition(attributeMap, undefined, undefined, machineUserNames); + const content = generateTypeDefinition(attributes, undefined, undefined, machineUserNames); expect(content).toContain("interface MachineUserNameRegistry"); expect(content).toContain("admin: true;"); expect(content).toContain("worker: true;"); diff --git a/packages/sdk/src/cli/shared/type-generator.ts b/packages/sdk/src/cli/shared/type-generator.ts index acaaccedb..477bff36b 100644 --- a/packages/sdk/src/cli/shared/type-generator.ts +++ b/packages/sdk/src/cli/shared/type-generator.ts @@ -4,14 +4,14 @@ import { logger } from "#/cli/shared/logger"; import ml from "#/utils/multiline"; import type { AppConfig } from "#/configure/config/types"; -export interface AttributeMapConfig { +export interface AttributesConfig { [key: string]: string; } export type AttributeListConfig = readonly string[]; interface ExtractedAttributes { - attributeMap?: AttributeMapConfig; + attributes?: AttributesConfig; attributeList?: AttributeListConfig; env?: Record; machineUserNames?: string[]; @@ -29,7 +29,7 @@ type AttributeFieldLike = { /** * Extract attribute definitions from the app config for user-defined typing. * @param config - Application config to inspect - * @returns Extracted attribute map/list and env values + * @returns Extracted attributes/list and env values * @internal */ export function extractAttributesFromConfig(config: AppConfig): ExtractedAttributes { @@ -38,7 +38,7 @@ export function extractAttributesFromConfig(config: AppConfig): ExtractedAttribu /** * Generate the contents of the user-defined type definition file. - * @param attributeMap - Attribute map configuration + * @param attributes - Attribute configuration * @param attributeList - Attribute list configuration * @param env - Environment configuration * @param machineUserNames - Registered machine user names (used to narrow `invoker` strings) @@ -46,25 +46,25 @@ export function extractAttributesFromConfig(config: AppConfig): ExtractedAttribu * @returns Generated type definition source */ export function generateTypeDefinition( - attributeMap: AttributeMapConfig | undefined, + attributes: AttributesConfig | undefined, attributeList: AttributeListConfig | undefined, env?: Record, machineUserNames?: readonly string[], idpNames?: readonly string[], ): string { - // Generate AttributeMap interface - // attributeMap values are type string representations (e.g., "string", "boolean", "string[]") - const mapFields = attributeMap - ? Object.entries(attributeMap) + // Generate Attributes interface + // attributes values are type string representations (e.g., "string", "boolean", "string[]") + const attributeFields = attributes + ? Object.entries(attributes) .map(([key, value]) => ` ${key}: ${value};`) .join("\n") : ""; - const mapBody = - !attributeMap || Object.keys(attributeMap).length === 0 + const attributesBody = + !attributes || Object.keys(attributes).length === 0 ? "{}" : `{ -${mapFields} +${attributeFields} }`; // Generate AttributeList type as a tuple of strings based on the length @@ -129,7 +129,7 @@ ${idpNameFields} // Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' declare module "@tailor-platform/sdk" { - interface AttributeMap ${mapBody} + interface Attributes ${attributesBody} interface AttributeList ${listBody} interface Env ${envBody} interface MachineUserNameRegistry ${machineUserBody} @@ -198,20 +198,20 @@ function collectAttributesFromConfig(config: AppConfig): ExtractedAttributes { } ).userProfile; - const attributes = userProfile?.attributes; + const selectedAttributes = userProfile?.attributes; const fields = userProfile?.type?.fields; const attributeList = userProfile?.attributeList; - // Convert attributes to AttributeMapConfig by inferring types from field metadata - const attributeMap: AttributeMapConfig | undefined = attributes - ? Object.keys(attributes).reduce((acc, key) => { + // Convert attributes to AttributesConfig by inferring types from field metadata + const attributes: AttributesConfig | undefined = selectedAttributes + ? Object.keys(selectedAttributes).reduce((acc, key) => { acc[key] = inferAttributeType(fields?.[key]); return acc; - }, {} as AttributeMapConfig) + }, {} as AttributesConfig) : undefined; return { - attributeMap, + attributes, attributeList, machineUserNames, idpNames, @@ -229,13 +229,13 @@ function collectAttributesFromConfig(config: AppConfig): ExtractedAttributes { return { machineUserNames, idpNames }; } - const attributeMap = Object.entries(machineUserAttributes).reduce((acc, [key, field]) => { + const attributes = Object.entries(machineUserAttributes).reduce((acc, [key, field]) => { acc[key] = inferAttributeType(field); return acc; - }, {} as AttributeMapConfig); + }, {} as AttributesConfig); return { - attributeMap, + attributes, machineUserNames, idpNames, }; @@ -279,14 +279,14 @@ interface GenerateUserTypesOptions { export async function generateUserTypes(options: GenerateUserTypesOptions): Promise { const { config, configPath } = options; try { - const { attributeMap, attributeList, machineUserNames, idpNames } = + const { attributes, attributeList, machineUserNames, idpNames } = extractAttributesFromConfig(config); - if (!attributeMap && !attributeList) { + if (!attributes && !attributeList) { logger.info("No attributes found in configuration", { mode: "plain" }); } - if (attributeMap) { - logger.debug(`Extracted AttributeMap: ${JSON.stringify(attributeMap)}`); + if (attributes) { + logger.debug(`Extracted Attributes: ${JSON.stringify(attributes)}`); } if (attributeList) { logger.debug(`Extracted AttributeList: ${JSON.stringify(attributeList)}`); @@ -305,7 +305,7 @@ export async function generateUserTypes(options: GenerateUserTypesOptions): Prom // Generate type definition const typeDefContent = generateTypeDefinition( - attributeMap, + attributes, attributeList, env, machineUserNames, diff --git a/packages/sdk/src/configure/index.ts b/packages/sdk/src/configure/index.ts index 32bf34251..bf89f4f67 100644 --- a/packages/sdk/src/configure/index.ts +++ b/packages/sdk/src/configure/index.ts @@ -18,7 +18,7 @@ export namespace t { export { type TailorField } from "#/configure/types/type"; export { type TailorPrincipal, - type AttributeMap, + type Attributes, type AttributeList, type Env, } from "#/runtime/types"; diff --git a/packages/sdk/src/configure/services/auth/index.test.ts b/packages/sdk/src/configure/services/auth/index.test.ts index d42645458..a83010b39 100644 --- a/packages/sdk/src/configure/services/auth/index.test.ts +++ b/packages/sdk/src/configure/services/auth/index.test.ts @@ -19,7 +19,7 @@ const userType = db.type("User", { externalId: db.uuid(), }); -type AttributeMap = { +type Attributes = { role: true; isActive: true; tags: true; @@ -28,7 +28,7 @@ type AttributeMap = { type AttributeList = ["externalId"]; -const attributeMapConfig: AttributeMap = { +const attributeMapConfig: Attributes = { role: true, isActive: true, tags: true, diff --git a/packages/sdk/src/configure/services/auth/index.ts b/packages/sdk/src/configure/services/auth/index.ts index 6ecd6c28f..24b5b3822 100644 --- a/packages/sdk/src/configure/services/auth/index.ts +++ b/packages/sdk/src/configure/services/auth/index.ts @@ -5,7 +5,7 @@ import type { AuthServiceInput, DefinedAuth, UserAttributeListKey, - UserAttributeMap, + UserAttributes, } from "#/configure/services/auth/types"; import type { DefinedFieldMetadata, @@ -20,21 +20,21 @@ type MachineUserAttributeFields = Record< >; type PlaceholderUser = TailorDBInstance, Record>; -type PlaceholderAttributeMap = UserAttributeMap; +type PlaceholderAttributes = UserAttributes; type PlaceholderAttributeList = UserAttributeListKey[]; type UserProfileAuthInput< User extends TailorDBInstance, - AttributeMap extends UserAttributeMap, + Attributes extends UserAttributes, AttributeList extends UserAttributeListKey[], MachineUserNames extends string, ConnectionNames extends string = string, > = Omit< - AuthServiceInput, + AuthServiceInput, "userProfile" | "machineUserAttributes" > & { userProfile: NonNullable< - AuthServiceInput["userProfile"] + AuthServiceInput["userProfile"] >; machineUserAttributes?: never; }; @@ -46,7 +46,7 @@ type MachineUserOnlyAuthInput< > = Omit< AuthServiceInput< PlaceholderUser, - PlaceholderAttributeMap, + PlaceholderAttributes, PlaceholderAttributeList, MachineUserNames, MachineUserAttributes, @@ -90,7 +90,7 @@ export type { UsernameFieldKey, UserAttributeKey, UserAttributeListKey, - UserAttributeMap, + UserAttributes, AuthConnectionTokenResult, AuthServiceInput, AuthConfig, @@ -103,7 +103,7 @@ export type { * Define an auth service for the Tailor SDK. * @template Name * @template User - * @template AttributeMap + * @template Attributes * @template AttributeList * @template MachineUserNames * @param name - Auth service name @@ -113,22 +113,16 @@ export type { export function defineAuth< const Name extends string, const User extends TailorDBInstance, - const AttributeMap extends UserAttributeMap, + const Attributes extends UserAttributes, const AttributeList extends UserAttributeListKey[], const MachineUserNames extends string, const ConnectionNames extends string = string, >( name: Name, - config: UserProfileAuthInput< - User, - AttributeMap, - AttributeList, - MachineUserNames, - ConnectionNames - >, + config: UserProfileAuthInput, ): DefinedAuth< Name, - UserProfileAuthInput + UserProfileAuthInput >; export function defineAuth< const Name extends string, @@ -146,7 +140,7 @@ export function defineAuth< export function defineAuth< const Name extends string, const User extends TailorDBInstance, - const AttributeMap extends UserAttributeMap, + const Attributes extends UserAttributes, const AttributeList extends UserAttributeListKey[], const MachineUserAttributes extends MachineUserAttributeFields, const MachineUserNames extends string, @@ -154,7 +148,7 @@ export function defineAuth< >( name: Name, config: - | UserProfileAuthInput + | UserProfileAuthInput | MachineUserOnlyAuthInput, ) { const result = { @@ -164,7 +158,7 @@ export function defineAuth< return tailor.authconnection.getConnectionToken(connectionName); }, } as const satisfies ( - | UserProfileAuthInput + | UserProfileAuthInput | MachineUserOnlyAuthInput ) & { name: string; diff --git a/packages/sdk/src/configure/services/auth/types.ts b/packages/sdk/src/configure/services/auth/types.ts index 7f105fc51..c31a0c142 100644 --- a/packages/sdk/src/configure/services/auth/types.ts +++ b/packages/sdk/src/configure/services/auth/types.ts @@ -121,7 +121,7 @@ export type UserAttributeListKey = { : never; }[UserFieldKeys]; -export type UserAttributeMap = { +export type UserAttributes = { [K in UserAttributeKey]?: true; }; @@ -144,19 +144,19 @@ type AttributeListToTuple< : never; }; -type AttributeMapSelectedKeys< +type SelectedAttributeKeys< User extends TailorDBInstance, - AttributeMap extends UserAttributeMap, + Attributes extends UserAttributes, > = Extract< { - [K in keyof AttributeMap]-?: undefined extends AttributeMap[K] ? never : K; - }[keyof AttributeMap], + [K in keyof Attributes]-?: undefined extends Attributes[K] ? never : K; + }[keyof Attributes], UserAttributeKey >; type UserProfile< User extends TailorDBInstance, - AttributeMap extends UserAttributeMap, + Attributes extends UserAttributes, AttributeList extends UserAttributeListKey[], > = { /** @@ -168,7 +168,7 @@ type UserProfile< namespace?: string; type: User; usernameField: UsernameFieldKey; - attributes?: DisallowExtraKeys>; + attributes?: DisallowExtraKeys>; attributeList?: AttributeList; }; @@ -197,7 +197,7 @@ type MachineUserFromAttributes = type MachineUser< User extends TailorDBInstance, - AttributeMap extends UserAttributeMap = UserAttributeMap, + Attributes extends UserAttributes = UserAttributes, AttributeList extends UserAttributeListKey[] = [], MachineUserAttributes extends MachineUserAttributeFields | undefined = undefined, > = @@ -207,18 +207,15 @@ type MachineUser< attributes: Record; attributeList?: string[]; } - : (AttributeMapSelectedKeys extends never + : (SelectedAttributeKeys extends never ? { attributes?: never } : { attributes: { - [K in AttributeMapSelectedKeys]: K extends keyof output + [K in SelectedAttributeKeys]: K extends keyof output ? output[K] : never; } & { - [K in Exclude< - keyof output, - AttributeMapSelectedKeys - >]?: never; + [K in Exclude, SelectedAttributeKeys>]?: never; }; }) & ([] extends AttributeList @@ -231,17 +228,17 @@ type MachineUser< attributes: Record; attributeList?: string[]; } - : (AttributeMapSelectedKeys extends never + : (SelectedAttributeKeys extends never ? { attributes?: never } : { attributes: { - [K in AttributeMapSelectedKeys]: K extends keyof output + [K in SelectedAttributeKeys]: K extends keyof output ? output[K] : never; } & { [K in Exclude< keyof output, - AttributeMapSelectedKeys + SelectedAttributeKeys >]?: never; }; }) & @@ -309,7 +306,7 @@ export type AuthHooks = { // Input type (before parsing) - used by configure layer export type AuthServiceInput< User extends TailorDBInstance, - AttributeMap extends UserAttributeMap, + Attributes extends UserAttributes, AttributeList extends UserAttributeListKey[], MachineUserNames extends string, MachineUserAttributes extends MachineUserAttributeFields | undefined = @@ -318,11 +315,11 @@ export type AuthServiceInput< ConnectionNames extends string = string, > = { hooks?: AuthHooks; - userProfile?: UserProfile; + userProfile?: UserProfile; machineUserAttributes?: MachineUserAttributes; machineUsers?: Record< MachineUserNames, - MachineUser + MachineUser >; oauth2Clients?: Record; idProvider?: IdProviderConfig; diff --git a/packages/sdk/src/configure/services/idp/permission.ts b/packages/sdk/src/configure/services/idp/permission.ts index 06b381823..aaf841545 100644 --- a/packages/sdk/src/configure/services/idp/permission.ts +++ b/packages/sdk/src/configure/services/idp/permission.ts @@ -1,5 +1,5 @@ import type { IdPUserField } from "#/parser/service/idp/types"; -import type { InferredAttributeMap } from "#/runtime/types"; +import type { InferredAttributes } from "#/runtime/types"; type EqualityOperator = "=" | "!="; type ContainsOperator = "in" | "not in"; @@ -20,19 +20,19 @@ type BooleanArrayFieldKeys = { [K in keyof User]: User[K] extends boolean[] ? K : never; }[keyof User]; -type UserStringOperand = { +type UserStringOperand = { user: StringFieldKeys | "id"; }; -type UserStringArrayOperand = { +type UserStringArrayOperand = { user: StringArrayFieldKeys; }; -type UserBooleanOperand = { +type UserBooleanOperand = { user: BooleanFieldKeys | "_loggedIn"; }; -type UserBooleanArrayOperand = { +type UserBooleanArrayOperand = { user: BooleanArrayFieldKeys; }; @@ -63,7 +63,7 @@ type BooleanEqualityCondition = | readonly [boolean | UserBooleanOperand, EqualityOperator, IdPUserOperand]; type EqualityCondition< - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, Update extends boolean = boolean, > = StringEqualityCondition | BooleanEqualityCondition; @@ -80,17 +80,17 @@ type BooleanContainsCondition = | readonly [IdPUserOperand, ContainsOperator, boolean[] | UserBooleanArrayOperand]; type ContainsCondition< - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, Update extends boolean = boolean, > = StringContainsCondition | BooleanContainsCondition; export type IdPPermissionCondition< - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, Update extends boolean = boolean, > = EqualityCondition | ContainsCondition; type IdPActionPermission< - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, Update extends boolean = boolean, > = | { @@ -124,7 +124,7 @@ type IdPActionPermission< * unenrollMfa: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }], * }; */ -export type IdPPermission = { +export type IdPPermission = { create: readonly IdPActionPermission[]; read: readonly IdPActionPermission[]; update: readonly IdPActionPermission[]; diff --git a/packages/sdk/src/configure/services/tailordb/permission.ts b/packages/sdk/src/configure/services/tailordb/permission.ts index 5562ecebb..ed4bff6fe 100644 --- a/packages/sdk/src/configure/services/tailordb/permission.ts +++ b/packages/sdk/src/configure/services/tailordb/permission.ts @@ -1,4 +1,4 @@ -import type { InferredAttributeMap } from "#/runtime/types"; +import type { InferredAttributes } from "#/runtime/types"; // --- Permission types (UX-focused, for configure layer) --- @@ -19,7 +19,7 @@ import type { InferredAttributeMap } from "#/runtime/types"; * }; */ export type TailorTypePermission< - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, Type extends object = object, > = { create: readonly ActionPermission<"record", User, Type, false>[]; @@ -30,7 +30,7 @@ export type TailorTypePermission< type ActionPermission< Level extends "record" | "gql" = "record" | "gql", - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, Type extends object = object, Update extends boolean = boolean, > = @@ -50,14 +50,11 @@ type ActionPermission< | readonly [...PermissionCondition[], ...([] | [boolean])]; // multiple array condition export type TailorTypeGqlPermission< - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, Type extends object = object, > = readonly GqlPermissionPolicy[]; -type GqlPermissionPolicy< - User extends object = InferredAttributeMap, - Type extends object = object, -> = { +type GqlPermissionPolicy = { conditions: readonly PermissionCondition<"gql", User, boolean, Type>[]; actions: "all" | readonly GqlPermissionAction[]; /** @@ -91,19 +88,19 @@ type BooleanArrayFieldKeys = { [K in keyof User]: User[K] extends boolean[] ? K : never; }[keyof User]; -type UserStringOperand = { +type UserStringOperand = { user: StringFieldKeys | "id"; }; -type UserStringArrayOperand = { +type UserStringArrayOperand = { user: StringArrayFieldKeys; }; -type UserBooleanOperand = { +type UserBooleanOperand = { user: BooleanFieldKeys | "_loggedIn"; }; -type UserBooleanArrayOperand = { +type UserBooleanArrayOperand = { user: BooleanArrayFieldKeys; }; @@ -160,7 +157,7 @@ type BooleanEqualityCondition< type EqualityCondition< Level extends "record" | "gql" = "record", - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, Update extends boolean = boolean, Type extends object = object, > = @@ -216,7 +213,7 @@ type BooleanContainsCondition< type ContainsCondition< Level extends "record" | "gql" = "record", - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, Update extends boolean = boolean, Type extends object = object, > = @@ -270,7 +267,7 @@ type HasAnyCondition< */ export type PermissionCondition< Level extends "record" | "gql" = "record", - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, Update extends boolean = boolean, Type extends object = object, > = diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index a06d94726..f0141384d 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -27,7 +27,7 @@ import type { Validators, } from "#/configure/types/field.types"; import type { PluginAttachment, PluginConfigs } from "#/plugin/types"; -import type { InferredAttributeMap, TailorPrincipal } from "#/runtime/types"; +import type { InferredAttributes, TailorPrincipal } from "#/runtime/types"; import type { output, InferFieldsOutput, Prettify } from "#/types/helpers"; import type { RawPermissions } from "#/types/tailordb.generated"; import type { TailorTypeGqlPermission, TailorTypePermission } from "./permission"; @@ -221,7 +221,7 @@ export interface TailorDBField< export interface TailorDBType< // oxlint-disable-next-line no-explicit-any Fields extends Record = any, - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, > extends TailorDBTypeBase { _description?: string; @@ -271,7 +271,7 @@ export interface TailorDBType< export type TailorDBInstance< // oxlint-disable-next-line no-explicit-any Fields extends Record = any, - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, > = TailorDBType; interface RelationConfig { @@ -900,7 +900,7 @@ function object< function createTailorDBType< // oxlint-disable-next-line no-explicit-any const Fields extends Record = any, - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, >( name: string, fields: Fields, diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index cb1b20b50..bbde489da 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -7,7 +7,7 @@ import type { FieldMetadata, TailorField, } from "#/configure/types/field.types"; -import type { InferredAttributeMap, TailorPrincipal } from "#/runtime/types"; +import type { InferredAttributes, TailorPrincipal } from "#/runtime/types"; import type { InferFieldsOutput, output, Prettify } from "#/types/helpers"; import type { DBFieldMetadata as DBFieldMetadataGenerated, @@ -121,7 +121,7 @@ export interface TailorDBType< Fields extends Record = any, // Generic parameter kept for compatibility with full TailorDBType in configure/ // oxlint-disable-next-line no-unused-vars - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, > { readonly name: string; readonly fields: Fields; @@ -138,7 +138,7 @@ export type TailorDBInstance< // Default kept loose for convenience; callers still get fully inferred types from `db.type()`. // oxlint-disable-next-line no-explicit-any Fields extends Record = any, - User extends object = InferredAttributeMap, + User extends object = InferredAttributes, > = TailorDBType; // --- Hook types (UX-focused, for configure layer) --- diff --git a/packages/sdk/src/parser/service/auth/index.test.ts b/packages/sdk/src/parser/service/auth/index.test.ts index 61b420ea9..dbf8f49d8 100644 --- a/packages/sdk/src/parser/service/auth/index.test.ts +++ b/packages/sdk/src/parser/service/auth/index.test.ts @@ -15,7 +15,7 @@ const userType = db.type("User", { externalId: db.uuid(), }); -type AttributeMap = { +type Attributes = { role: true; isActive: true; tags: true; @@ -24,7 +24,7 @@ type AttributeMap = { type AttributeList = ["externalId"]; -type AuthInput = AuthServiceInput; +type AuthInput = AuthServiceInput; type MachineUserConfig = NonNullable["admin"]; type AuthSchemaInput = Omit, "name">; @@ -76,7 +76,7 @@ describe("AuthServiceInput and AuthConfigSchema type alignment", () => { type SchemaAttributeList = SchemaMachineUser["attributeList"]; type FunctionMachineUser = MachineUserConfig; - type FunctionAttributeKeys = keyof AttributeMap; + type FunctionAttributeKeys = keyof Attributes; type FunctionAttributeValues = FunctionMachineUser["attributes"][FunctionAttributeKeys]; type FunctionAttributeList = FunctionMachineUser["attributeList"]; diff --git a/packages/sdk/src/runtime/types.ts b/packages/sdk/src/runtime/types.ts index 921869e9f..8aa961e2e 100644 --- a/packages/sdk/src/runtime/types.ts +++ b/packages/sdk/src/runtime/types.ts @@ -5,16 +5,16 @@ // without pulling any runtime dependency. // Interfaces for module augmentation -// Users can extend these via: declare module "@tailor-platform/sdk" { interface AttributeMap { ... } } +// Users can extend these via: declare module "@tailor-platform/sdk" { interface Attributes { ... } } // eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface AttributeMap {} +export interface Attributes {} export interface AttributeList { __tuple?: []; // Marker for tuple type } -export type InferredAttributeMap = keyof AttributeMap extends never +export type InferredAttributes = keyof Attributes extends never ? Record - : AttributeMap; + : Attributes; export type InferredAttributeList = AttributeList["__tuple"] extends [] ? string[] @@ -29,7 +29,7 @@ export type TailorPrincipal = { /** The ID of the workspace the principal belongs to. */ workspaceId: string; /** A map of the principal's attributes. */ - attributes: InferredAttributeMap; + attributes: InferredAttributes; /** A list of the principal's attribute IDs. */ attributeList: InferredAttributeList; }; From 461a5fe4694d3ad49058172f54255f16b894e429 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 25 Jun 2026 15:40:55 +0900 Subject: [PATCH 215/618] fix(changeset): release auth rename codemod --- .changeset/auth-attributes-rename.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/auth-attributes-rename.md b/.changeset/auth-attributes-rename.md index 0d1c41a5e..c23cdb316 100644 --- a/.changeset/auth-attributes-rename.md +++ b/.changeset/auth-attributes-rename.md @@ -1,5 +1,6 @@ --- "@tailor-platform/sdk": major +"@tailor-platform/sdk-codemod": patch --- Rename auth attribute module augmentation from `AttributeMap` to `Attributes`. From 51b2417621c9b6be2ab4c8f6184a30e675558a2b Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 25 Jun 2026 15:49:57 +0900 Subject: [PATCH 216/618] fix(codemod): detect renamed auth attribute types --- packages/sdk-codemod/src/registry.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index c0952e99e..be0ecad35 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -153,7 +153,12 @@ export const allCodemods: CodemodPackage[] = [ "Rename auth attribute module augmentation and related SDK type names from `AttributeMap` to `Attributes`", since: "1.0.0", until: "2.0.0", - legacyPatterns: ["AttributeMap", "interface AttributeMap"], + legacyPatterns: [ + "AttributeMap", + "interface AttributeMap", + "UserAttributeMap", + "InferredAttributeMap", + ], examples: [ { caption: "Module augmentation uses `Attributes`:", From 5239afcbacee3a382e5e05875c09b706dd29602b Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 25 Jun 2026 16:22:31 +0900 Subject: [PATCH 217/618] feat(codemod): transform auth attribute renames --- .../v2/auth-attributes-rename/codemod.yaml | 7 + .../scripts/transform.ts | 226 ++++++++++++++++++ .../tests/aliased-type-import/expected.ts | 7 + .../tests/aliased-type-import/input.ts | 7 + .../tests/import-type-reference/expected.ts | 2 + .../tests/import-type-reference/input.ts | 2 + .../tests/local-interface-untouched/input.ts | 5 + .../tests/module-augmentation/expected.ts | 11 + .../tests/module-augmentation/input.ts | 11 + .../tests/namespace-import/expected.ts | 5 + .../tests/namespace-import/input.ts | 5 + .../tests/re-export/expected.ts | 2 + .../tests/re-export/input.ts | 2 + .../runtime-attribute-map-untouched/input.ts | 5 + .../tests/type-imports/expected.ts | 5 + .../tests/type-imports/input.ts | 5 + packages/sdk-codemod/src/registry.ts | 10 +- packages/sdk-codemod/src/transform.test.ts | 4 + packages/sdk-codemod/tsdown.config.ts | 2 + packages/sdk/docs/migration/v2.md | 13 +- 20 files changed, 328 insertions(+), 8 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/aliased-type-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/aliased-type-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/import-type-reference/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/import-type-reference/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/local-interface-untouched/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/module-augmentation/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/module-augmentation/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/re-export/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/re-export/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/runtime-attribute-map-untouched/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/type-imports/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/type-imports/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/codemod.yaml b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/codemod.yaml new file mode 100644 index 000000000..4913a398f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/auth-attributes-rename" +version: "1.0.0" +description: "Rename auth attribute type names from AttributeMap to Attributes" +engine: jssg +language: typescript +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/scripts/transform.ts new file mode 100644 index 000000000..c2c2d2522 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/scripts/transform.ts @@ -0,0 +1,226 @@ +import { parse, Lang } from "@ast-grep/napi"; +import type { Edit, SgNode } from "@ast-grep/napi"; + +const SDK_MODULE = "@tailor-platform/sdk"; + +const TYPE_RENAME_MAP: Record = { + AttributeMap: "Attributes", + UserAttributeMap: "UserAttributes", + InferredAttributeMap: "InferredAttributes", +}; + +function quickFilter(source: string): boolean { + return ( + source.includes(SDK_MODULE) && + Object.keys(TYPE_RENAME_MAP).some((name) => source.includes(name)) + ); +} + +function isSdkModuleLiteral(node: SgNode): boolean { + return node.kind() === "string" && /^["']@tailor-platform\/sdk["']$/.test(node.text()); +} + +function hasSdkModuleLiteral(node: SgNode): boolean { + return node.findAll({ rule: { kind: "string" } }).some(isSdkModuleLiteral); +} + +function identifierChildren(node: SgNode): SgNode[] { + return node.children().filter((child: SgNode) => child.kind() === "identifier"); +} + +function typeIdentifierChildren(node: SgNode): SgNode[] { + return node.children().filter((child: SgNode) => child.kind() === "type_identifier"); +} + +function sameRange(a: SgNode, b: SgNode): boolean { + const ar = a.range(); + const br = b.range(); + return ar.start.index === br.start.index && ar.end.index === br.end.index; +} + +function addReplacement( + edits: Edit[], + editedRanges: Set, + node: SgNode, + replacement: string, +): void { + if (node.text() === replacement) return; + const r = node.range(); + const key = `${r.start.index}:${r.end.index}`; + if (editedRanges.has(key)) return; + editedRanges.add(key); + edits.push(node.replace(replacement)); +} + +function renamedType(name: string): string | undefined { + return TYPE_RENAME_MAP[name]; +} + +function isDeclarationName(node: SgNode): boolean { + const parent = node.parent(); + if ( + !parent || + ![ + "class_declaration", + "enum_declaration", + "interface_declaration", + "type_alias_declaration", + "type_parameter", + ].includes(parent.kind()) + ) { + return false; + } + const name = parent?.field("name"); + return !!name && sameRange(name, node); +} + +function isNestedTypeName(node: SgNode): boolean { + return node.parent()?.kind() === "nested_type_identifier"; +} + +function collectSdkImports( + root: SgNode, + edits: Edit[], + editedRanges: Set, +): { + localTypeRenames: Map; + namespaceNames: Set; +} { + const localTypeRenames = new Map(); + const namespaceNames = new Set(); + const importStmts = root.findAll({ rule: { kind: "import_statement" } }); + + for (const importStmt of importStmts) { + if (!hasSdkModuleLiteral(importStmt)) continue; + + const namespaceImports = importStmt.findAll({ rule: { kind: "namespace_import" } }); + for (const namespaceImport of namespaceImports) { + const localName = identifierChildren(namespaceImport).at(-1)?.text(); + if (localName) namespaceNames.add(localName); + } + + const specs = importStmt.findAll({ rule: { kind: "import_specifier" } }); + for (const spec of specs) { + const identifiers = identifierChildren(spec); + const imported = identifiers[0]; + if (!imported) continue; + const replacement = renamedType(imported.text()); + if (!replacement) continue; + + addReplacement(edits, editedRanges, imported, replacement); + if (identifiers.length === 1) { + localTypeRenames.set(imported.text(), replacement); + } + } + } + + return { localTypeRenames, namespaceNames }; +} + +function rewriteSdkExports(root: SgNode, edits: Edit[], editedRanges: Set): void { + const exportStmts = root.findAll({ rule: { kind: "export_statement" } }); + for (const exportStmt of exportStmts) { + if (!hasSdkModuleLiteral(exportStmt)) continue; + + const specs = exportStmt.findAll({ rule: { kind: "export_specifier" } }); + for (const spec of specs) { + const exported = identifierChildren(spec)[0]; + if (!exported) continue; + const replacement = renamedType(exported.text()); + if (replacement) addReplacement(edits, editedRanges, exported, replacement); + } + } +} + +function rewriteModuleAugmentations(root: SgNode, edits: Edit[], editedRanges: Set): void { + const declarations = root.findAll({ rule: { kind: "ambient_declaration" } }); + for (const declaration of declarations) { + if (!hasSdkModuleLiteral(declaration)) continue; + + const interfaces = declaration.findAll({ rule: { kind: "interface_declaration" } }); + for (const iface of interfaces) { + const name = typeIdentifierChildren(iface)[0]; + if (name?.text() === "AttributeMap") { + addReplacement(edits, editedRanges, name, "Attributes"); + } + } + } +} + +function rewriteLocalTypeReferences( + root: SgNode, + edits: Edit[], + editedRanges: Set, + localTypeRenames: Map, +): void { + if (localTypeRenames.size === 0) return; + + const typeIdentifiers = root.findAll({ rule: { kind: "type_identifier" } }); + for (const typeIdentifier of typeIdentifiers) { + if (isDeclarationName(typeIdentifier) || isNestedTypeName(typeIdentifier)) continue; + const replacement = localTypeRenames.get(typeIdentifier.text()); + if (replacement) addReplacement(edits, editedRanges, typeIdentifier, replacement); + } +} + +function rewriteNamespaceTypeReferences( + root: SgNode, + edits: Edit[], + editedRanges: Set, + namespaceNames: Set, +): void { + if (namespaceNames.size === 0) return; + + const nestedTypes = root.findAll({ rule: { kind: "nested_type_identifier" } }); + for (const nestedType of nestedTypes) { + const namespaceName = identifierChildren(nestedType)[0]?.text(); + if (!namespaceName || !namespaceNames.has(namespaceName)) continue; + const typeName = typeIdentifierChildren(nestedType).at(-1); + if (!typeName) continue; + const replacement = renamedType(typeName.text()); + if (replacement) addReplacement(edits, editedRanges, typeName, replacement); + } +} + +function isSdkImportCall(node: SgNode): boolean { + return node.kind() === "call_expression" && hasSdkModuleLiteral(node); +} + +function rewriteImportTypeReferences(root: SgNode, edits: Edit[], editedRanges: Set): void { + const members = root.findAll({ rule: { kind: "member_expression" } }); + for (const member of members) { + const object = member.field("object"); + if (!object || !isSdkImportCall(object)) continue; + const property = member.field("property"); + if (!property || property.kind() !== "property_identifier") continue; + const replacement = renamedType(property.text()); + if (replacement) addReplacement(edits, editedRanges, property, replacement); + } +} + +/** + * Rename the v1 auth attribute type API to its v2 names only when a reference + * can be tied to `@tailor-platform/sdk`. + * @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 (!quickFilter(source)) return null; + + const lang = source.includes("") ? Lang.Tsx : Lang.TypeScript; + const root = parse(lang, source).root(); + + const edits: Edit[] = []; + const editedRanges = new Set(); + + const { localTypeRenames, namespaceNames } = collectSdkImports(root, edits, editedRanges); + rewriteSdkExports(root, edits, editedRanges); + rewriteModuleAugmentations(root, edits, editedRanges); + rewriteLocalTypeReferences(root, edits, editedRanges, localTypeRenames); + rewriteNamespaceTypeReferences(root, edits, editedRanges, namespaceNames); + rewriteImportTypeReferences(root, edits, editedRanges); + + if (edits.length === 0) return null; + return root.commitEdits(edits); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/aliased-type-import/expected.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/aliased-type-import/expected.ts new file mode 100644 index 000000000..2a0d7226b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/aliased-type-import/expected.ts @@ -0,0 +1,7 @@ +import type { + Attributes as AuthAttributes, + UserAttributes as AuthUserAttributes, +} from "@tailor-platform/sdk"; + +type Auth = AuthAttributes; +type User = AuthUserAttributes; diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/aliased-type-import/input.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/aliased-type-import/input.ts new file mode 100644 index 000000000..17a948308 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/aliased-type-import/input.ts @@ -0,0 +1,7 @@ +import type { + AttributeMap as AuthAttributes, + UserAttributeMap as AuthUserAttributes, +} from "@tailor-platform/sdk"; + +type Auth = AuthAttributes; +type User = AuthUserAttributes; diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/import-type-reference/expected.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/import-type-reference/expected.ts new file mode 100644 index 000000000..5aebaba69 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/import-type-reference/expected.ts @@ -0,0 +1,2 @@ +type Auth = import("@tailor-platform/sdk").Attributes; +type User = import("@tailor-platform/sdk").UserAttributes; diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/import-type-reference/input.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/import-type-reference/input.ts new file mode 100644 index 000000000..5dfb88239 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/import-type-reference/input.ts @@ -0,0 +1,2 @@ +type Auth = import("@tailor-platform/sdk").AttributeMap; +type User = import("@tailor-platform/sdk").UserAttributeMap; diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/local-interface-untouched/input.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/local-interface-untouched/input.ts new file mode 100644 index 000000000..9d197530c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/local-interface-untouched/input.ts @@ -0,0 +1,5 @@ +interface AttributeMap { + role: string; +} + +type UserAttributeMap = AttributeMap; diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/module-augmentation/expected.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/module-augmentation/expected.ts new file mode 100644 index 000000000..cabd68b47 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/module-augmentation/expected.ts @@ -0,0 +1,11 @@ +declare module "@tailor-platform/sdk" { + interface Attributes { + role: string; + } +} + +declare module "other" { + interface AttributeMap { + role: string; + } +} diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/module-augmentation/input.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/module-augmentation/input.ts new file mode 100644 index 000000000..38db5e5b6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/module-augmentation/input.ts @@ -0,0 +1,11 @@ +declare module "@tailor-platform/sdk" { + interface AttributeMap { + role: string; + } +} + +declare module "other" { + interface AttributeMap { + role: string; + } +} diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-import/expected.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-import/expected.ts new file mode 100644 index 000000000..79b7a127f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-import/expected.ts @@ -0,0 +1,5 @@ +import type * as sdk from "@tailor-platform/sdk"; + +type Auth = sdk.Attributes; +type User = sdk.UserAttributes; +type Inferred = sdk.InferredAttributes<"auth">; diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-import/input.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-import/input.ts new file mode 100644 index 000000000..c79434923 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-import/input.ts @@ -0,0 +1,5 @@ +import type * as sdk from "@tailor-platform/sdk"; + +type Auth = sdk.AttributeMap; +type User = sdk.UserAttributeMap; +type Inferred = sdk.InferredAttributeMap<"auth">; diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/re-export/expected.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/re-export/expected.ts new file mode 100644 index 000000000..5f5578485 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/re-export/expected.ts @@ -0,0 +1,2 @@ +export type { Attributes, InferredAttributes, UserAttributes as AuthUserAttributeMap } from "@tailor-platform/sdk"; +export type { AttributeMap as OtherAttributeMap } from "other"; diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/re-export/input.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/re-export/input.ts new file mode 100644 index 000000000..98ce0945a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/re-export/input.ts @@ -0,0 +1,2 @@ +export type { AttributeMap, InferredAttributeMap, UserAttributeMap as AuthUserAttributeMap } from "@tailor-platform/sdk"; +export type { AttributeMap as OtherAttributeMap } from "other"; diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/runtime-attribute-map-untouched/input.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/runtime-attribute-map-untouched/input.ts new file mode 100644 index 000000000..5f9ef5eda --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/runtime-attribute-map-untouched/input.ts @@ -0,0 +1,5 @@ +import { protoMachineUserAttributeMap } from "@tailor-platform/sdk/internal"; + +const attributeMap = { role: "admin" }; + +protoMachineUserAttributeMap(attributeMap); diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/type-imports/expected.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/type-imports/expected.ts new file mode 100644 index 000000000..d3cbf9e46 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/type-imports/expected.ts @@ -0,0 +1,5 @@ +import type { Attributes, InferredAttributes, UserAttributes } from "@tailor-platform/sdk"; + +type AuthAttributes = Attributes; +type User = UserAttributes; +type Inferred = InferredAttributes<"auth">; diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/type-imports/input.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/type-imports/input.ts new file mode 100644 index 000000000..a5665ee21 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/type-imports/input.ts @@ -0,0 +1,5 @@ +import type { AttributeMap, InferredAttributeMap, UserAttributeMap } from "@tailor-platform/sdk"; + +type AuthAttributes = AttributeMap; +type User = UserAttributeMap; +type Inferred = InferredAttributeMap<"auth">; diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index be0ecad35..dcc9408b9 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -153,6 +153,7 @@ export const allCodemods: CodemodPackage[] = [ "Rename auth attribute module augmentation and related SDK type names from `AttributeMap` to `Attributes`", since: "1.0.0", until: "2.0.0", + scriptPath: "v2/auth-attributes-rename/scripts/transform.js", legacyPatterns: [ "AttributeMap", "interface AttributeMap", @@ -169,9 +170,12 @@ export const allCodemods: CodemodPackage[] = [ }, ], prompt: [ - "In Tailor SDK v2, the generated module augmentation interface `AttributeMap`", - "is renamed to `Attributes`. Rename any hand-written declaration merging from", - "`interface AttributeMap` to `interface Attributes`.", + "In Tailor SDK v2, the auth attribute type API is renamed from `AttributeMap`", + "to `Attributes`; related SDK types are renamed to `UserAttributes` and", + "`InferredAttributes`. The codemod rewrites SDK imports, re-exports,", + "namespace-qualified references, import() type references, and module", + "augmentations. Review any remaining matches manually and leave unrelated", + "local names or deploy/proto wire field names unchanged.", ].join("\n"), }, { diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index 5ab9121fa..f903f57cd 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -79,6 +79,10 @@ describe("codemod transforms", () => { await expect(runFixtureCases("v2/principal-unify")).resolves.toBeUndefined(); }); + test("v2/auth-attributes-rename transforms correctly", async () => { + await expect(runFixtureCases("v2/auth-attributes-rename")).resolves.toBeUndefined(); + }); + test("v2/apply-to-deploy transforms correctly", async () => { await expect(runFixtureCases("v2/apply-to-deploy")).resolves.toBeUndefined(); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index aa8ede142..6a3f2fcaa 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -23,6 +23,8 @@ export default defineConfig([ "codemods/v2/test-run-arg-input/scripts/transform.ts", "v2/sdk-skills-shim/scripts/transform": "codemods/v2/sdk-skills-shim/scripts/transform.ts", "v2/principal-unify/scripts/transform": "codemods/v2/principal-unify/scripts/transform.ts", + "v2/auth-attributes-rename/scripts/transform": + "codemods/v2/auth-attributes-rename/scripts/transform.ts", "v2/apply-to-deploy/scripts/transform": "codemods/v2/apply-to-deploy/scripts/transform.ts", "v2/cli-rename/scripts/transform": "codemods/v2/cli-rename/scripts/transform.ts", "v2/auth-invoker-unwrap/scripts/transform": diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 5409e421b..b62158bb1 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -169,7 +169,7 @@ Use TailorPrincipal for the unified user/actor/invoker type. ## AttributeMap → Attributes -**Migration:** Manual +**Migration:** Partially automatic Rename auth attribute module augmentation and related SDK type names from `AttributeMap` to `Attributes` @@ -196,12 +196,15 @@ declare module "@tailor-platform/sdk" { ```
-Prompt for an AI agent (to perform this migration) +Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text -In Tailor SDK v2, the generated module augmentation interface `AttributeMap` -is renamed to `Attributes`. Rename any hand-written declaration merging from -`interface AttributeMap` to `interface Attributes`. +In Tailor SDK v2, the auth attribute type API is renamed from `AttributeMap` +to `Attributes`; related SDK types are renamed to `UserAttributes` and +`InferredAttributes`. The codemod rewrites SDK imports, re-exports, +namespace-qualified references, import() type references, and module +augmentations. Review any remaining matches manually and leave unrelated +local names or deploy/proto wire field names unchanged. ```
From dc76726bcc8003a1046985d00f62a5ae88a93e8b Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 25 Jun 2026 16:31:57 +0900 Subject: [PATCH 218/618] fix(codemod): avoid unsafe auth attribute rewrites --- .changeset/auth-attributes-rename.md | 1 + .../scripts/transform.ts | 89 ++++++++++++++++++- .../tests/module-string-untouched/input.ts | 7 ++ .../namespace-local-interface/expected.ts | 11 +++ .../tests/namespace-local-interface/input.ts | 11 +++ .../typescript-type-assertion/expected.ts | 4 + .../tests/typescript-type-assertion/input.ts | 4 + 7 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/module-string-untouched/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-local-interface/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-local-interface/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/typescript-type-assertion/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/typescript-type-assertion/input.ts diff --git a/.changeset/auth-attributes-rename.md b/.changeset/auth-attributes-rename.md index c23cdb316..45744f30f 100644 --- a/.changeset/auth-attributes-rename.md +++ b/.changeset/auth-attributes-rename.md @@ -1,5 +1,6 @@ --- "@tailor-platform/sdk": major +"@tailor-platform/create-sdk": patch "@tailor-platform/sdk-codemod": patch --- diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/scripts/transform.ts index c2c2d2522..96c5db5d4 100644 --- a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/scripts/transform.ts @@ -24,6 +24,18 @@ function hasSdkModuleLiteral(node: SgNode): boolean { return node.findAll({ rule: { kind: "string" } }).some(isSdkModuleLiteral); } +function moduleSpecifierLiteral(node: SgNode): SgNode | undefined { + const directLiteral = node.children().find((child: SgNode) => child.kind() === "string"); + if (directLiteral) return directLiteral; + const moduleNode = node.children().find((child: SgNode) => child.kind() === "module"); + return moduleNode?.children().find((child: SgNode) => child.kind() === "string"); +} + +function hasSdkModuleSpecifier(node: SgNode): boolean { + const literal = moduleSpecifierLiteral(node); + return literal ? isSdkModuleLiteral(literal) : false; +} + function identifierChildren(node: SgNode): SgNode[] { return node.children().filter((child: SgNode) => child.kind() === "identifier"); } @@ -78,6 +90,66 @@ function isNestedTypeName(node: SgNode): boolean { return node.parent()?.kind() === "nested_type_identifier"; } +function declarationName(node: SgNode): SgNode | undefined { + if ( + [ + "class_declaration", + "enum_declaration", + "interface_declaration", + "type_alias_declaration", + ].includes(node.kind()) + ) { + return node.field("name") ?? undefined; + } + if (node.kind() !== "export_statement") return undefined; + const declaration = node + .children() + .find((child: SgNode) => + [ + "class_declaration", + "enum_declaration", + "interface_declaration", + "type_alias_declaration", + ].includes(child.kind()), + ); + return declaration?.field("name") ?? undefined; +} + +function scopeDeclaresType(scope: SgNode, name: string, reference: SgNode): boolean { + return scope.children().some((child: SgNode) => { + const declaredName = declarationName(child); + return !!declaredName && declaredName.text() === name && !sameRange(declaredName, reference); + }); +} + +function hasTypeParameterShadow(node: SgNode, name: string): boolean { + const parameters = node.findAll({ rule: { kind: "type_parameter" } }); + return parameters.some((parameter) => typeIdentifierChildren(parameter)[0]?.text() === name); +} + +function isShadowedTypeReference(node: SgNode, name: string): boolean { + let current = node.parent(); + while (current) { + if (current.kind() === "statement_block" && scopeDeclaresType(current, name, node)) { + return true; + } + if ( + [ + "class_declaration", + "function_declaration", + "interface_declaration", + "method_definition", + "type_alias_declaration", + ].includes(current.kind()) && + hasTypeParameterShadow(current, name) + ) { + return true; + } + current = current.parent(); + } + return false; +} + function collectSdkImports( root: SgNode, edits: Edit[], @@ -91,7 +163,7 @@ function collectSdkImports( const importStmts = root.findAll({ rule: { kind: "import_statement" } }); for (const importStmt of importStmts) { - if (!hasSdkModuleLiteral(importStmt)) continue; + if (!hasSdkModuleSpecifier(importStmt)) continue; const namespaceImports = importStmt.findAll({ rule: { kind: "namespace_import" } }); for (const namespaceImport of namespaceImports) { @@ -120,7 +192,7 @@ function collectSdkImports( function rewriteSdkExports(root: SgNode, edits: Edit[], editedRanges: Set): void { const exportStmts = root.findAll({ rule: { kind: "export_statement" } }); for (const exportStmt of exportStmts) { - if (!hasSdkModuleLiteral(exportStmt)) continue; + if (!hasSdkModuleSpecifier(exportStmt)) continue; const specs = exportStmt.findAll({ rule: { kind: "export_specifier" } }); for (const spec of specs) { @@ -135,7 +207,7 @@ function rewriteSdkExports(root: SgNode, edits: Edit[], editedRanges: Set): void { const declarations = root.findAll({ rule: { kind: "ambient_declaration" } }); for (const declaration of declarations) { - if (!hasSdkModuleLiteral(declaration)) continue; + if (!hasSdkModuleSpecifier(declaration)) continue; const interfaces = declaration.findAll({ rule: { kind: "interface_declaration" } }); for (const iface of interfaces) { @@ -159,6 +231,7 @@ function rewriteLocalTypeReferences( for (const typeIdentifier of typeIdentifiers) { if (isDeclarationName(typeIdentifier) || isNestedTypeName(typeIdentifier)) continue; const replacement = localTypeRenames.get(typeIdentifier.text()); + if (replacement && isShadowedTypeReference(typeIdentifier, typeIdentifier.text())) continue; if (replacement) addReplacement(edits, editedRanges, typeIdentifier, replacement); } } @@ -208,7 +281,15 @@ function rewriteImportTypeReferences(root: SgNode, edits: Edit[], editedRanges: export default function transform(source: string, _filePath?: string): string | null { if (!quickFilter(source)) return null; - const lang = source.includes("") ? Lang.Tsx : Lang.TypeScript; + const filePath = _filePath?.toLowerCase(); + const lang = + filePath?.endsWith(".tsx") || filePath?.endsWith(".jsx") + ? Lang.Tsx + : filePath + ? Lang.TypeScript + : source.includes("") + ? Lang.Tsx + : Lang.TypeScript; const root = parse(lang, source).root(); const edits: Edit[] = []; diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/module-string-untouched/input.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/module-string-untouched/input.ts new file mode 100644 index 000000000..2cbcab067 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/module-string-untouched/input.ts @@ -0,0 +1,7 @@ +declare module "other" { + type SdkModule = "@tailor-platform/sdk"; + + interface AttributeMap { + role: string; + } +} diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-local-interface/expected.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-local-interface/expected.ts new file mode 100644 index 000000000..9537630fb --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-local-interface/expected.ts @@ -0,0 +1,11 @@ +import type { Attributes } from "@tailor-platform/sdk"; + +namespace Local { + export interface AttributeMap { + local: string; + } + + export type LocalAttrs = AttributeMap; +} + +type SdkAttrs = Attributes; diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-local-interface/input.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-local-interface/input.ts new file mode 100644 index 000000000..77b1d1a0f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/namespace-local-interface/input.ts @@ -0,0 +1,11 @@ +import type { AttributeMap } from "@tailor-platform/sdk"; + +namespace Local { + export interface AttributeMap { + local: string; + } + + export type LocalAttrs = AttributeMap; +} + +type SdkAttrs = AttributeMap; diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/typescript-type-assertion/expected.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/typescript-type-assertion/expected.ts new file mode 100644 index 000000000..85b45d4f5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/typescript-type-assertion/expected.ts @@ -0,0 +1,4 @@ +import type { Attributes } from "@tailor-platform/sdk"; + +const marker = "/>"; +const attrs = {}; diff --git a/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/typescript-type-assertion/input.ts b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/typescript-type-assertion/input.ts new file mode 100644 index 000000000..ae0af5685 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-attributes-rename/tests/typescript-type-assertion/input.ts @@ -0,0 +1,4 @@ +import type { AttributeMap } from "@tailor-platform/sdk"; + +const marker = "/>"; +const attrs = {}; From 501e8bfdd2bca7201a1c9b036bf72087476da416 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 25 Jun 2026 14:35:53 +0900 Subject: [PATCH 219/618] feat!: rename SDK environment variables --- .changeset/rename-tailor-cli-env.md | 9 ++ .github/workflows/deploy.yml | 2 +- .github/workflows/pkg-pr-new.yml | 2 +- example/e2e/utils.ts | 2 +- example/tailor.config.ts | 2 +- example/tests/scripts/generate_files.ts | 6 +- example/vitest.config.ts | 2 +- .../create-sdk/scripts/prepare-templates.js | 4 +- .../templates/workflow/e2e/globalSetup.ts | 2 +- .../codemods/v2/env-var-rename/codemod.yaml | 7 ++ .../v2/env-var-rename/scripts/transform.ts | 63 ++++++++++ .../tests/basic-env/expected.env | 13 +++ .../env-var-rename/tests/basic-env/input.env | 13 +++ .../tests/basic-markdown/expected.md | 5 + .../tests/basic-markdown/input.md | 5 + .../tests/basic-package-json/expected.json | 10 ++ .../tests/basic-package-json/input.json | 10 ++ .../tests/basic-source/expected.ts | 8 ++ .../tests/basic-source/input.ts | 8 ++ .../tests/basic-yaml/expected.yml | 8 ++ .../env-var-rename/tests/basic-yaml/input.yml | 8 ++ .../v2/env-var-rename/tests/no-match/input.sh | 4 + packages/sdk-codemod/src/registry.test.ts | 17 +++ packages/sdk-codemod/src/registry.ts | 42 +++++++ packages/sdk-codemod/src/transform.test.ts | 4 + packages/sdk-codemod/tsdown.config.ts | 1 + packages/sdk/docs/cli-reference.md | 14 +-- packages/sdk/docs/cli-reference.template.md | 14 +-- packages/sdk/docs/cli/application.md | 68 +++++------ packages/sdk/docs/cli/auth.md | 48 ++++---- packages/sdk/docs/cli/query.md | 2 +- packages/sdk/docs/cli/tailordb.md | 110 +++++++++--------- packages/sdk/docs/cli/workflow.md | 2 +- packages/sdk/docs/configuration.md | 6 +- packages/sdk/docs/github-actions.md | 2 +- packages/sdk/docs/migration/v2.md | 30 +++++ packages/sdk/docs/multi-environment.md | 4 +- packages/sdk/postinstall.mjs | 4 +- packages/sdk/scripts/perf/runtime-perf.sh | 4 +- .../cli/bundler/query/query-bundler.test.ts | 6 +- .../__test_fixtures__/integration.test.ts | 2 +- .../deploy/__test_fixtures__/prepare.ts | 2 +- .../deploy/__test_fixtures__/tailor.config.ts | 2 +- .../deploy/config-id-ci-guard.test.ts | 4 +- .../cli/commands/deploy/config-id-injector.ts | 5 +- .../sdk/src/cli/commands/deploy/deploy.ts | 2 +- .../src/cli/commands/function/bundle.test.ts | 4 +- .../commands/generate/seed/bundler.test.ts | 6 +- .../sdk/src/cli/commands/generate/service.ts | 4 +- .../commands/tailordb/migrate/bundler.test.ts | 6 +- packages/sdk/src/cli/query/index.ts | 4 +- .../sdk/src/cli/services/auth/bundler.test.ts | 6 +- packages/sdk/src/cli/services/auth/bundler.ts | 2 +- .../src/cli/services/workflow/bundler.test.ts | 4 +- packages/sdk/src/cli/shared/args.ts | 4 +- packages/sdk/src/cli/shared/client.test.ts | 14 +++ packages/sdk/src/cli/shared/client.ts | 4 +- packages/sdk/src/cli/shared/context.test.ts | 22 +--- packages/sdk/src/cli/shared/context.ts | 12 +- packages/sdk/src/cli/shared/dist-dir.ts | 2 +- .../sdk/src/cli/shared/inline-sourcemap.ts | 4 +- .../cli/shared/platform-bundle-plugin.test.ts | 6 +- .../src/cli/shared/platform-bundle-plugin.ts | 6 +- .../src/cli/shared/skills-installer.test.ts | 4 +- .../sdk/src/cli/shared/skills-installer.ts | 2 +- .../sdk/src/cli/shared/type-generator.test.ts | 16 +-- packages/sdk/src/cli/shared/type-generator.ts | 4 +- .../sdk/src/configure/config/index.test.ts | 2 +- .../src/configure/services/workflow/job.ts | 6 +- .../configure/services/workflow/workflow.ts | 2 +- 70 files changed, 497 insertions(+), 237 deletions(-) create mode 100644 .changeset/rename-tailor-cli-env.md create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-env/expected.env create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-env/input.env create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-markdown/expected.md create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-markdown/input.md create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-package-json/expected.json create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-package-json/input.json create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-yaml/expected.yml create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-yaml/input.yml create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/tests/no-match/input.sh diff --git a/.changeset/rename-tailor-cli-env.md b/.changeset/rename-tailor-cli-env.md new file mode 100644 index 000000000..fe94eb4ea --- /dev/null +++ b/.changeset/rename-tailor-cli-env.md @@ -0,0 +1,9 @@ +--- +"@tailor-platform/sdk": major +"@tailor-platform/create-sdk": patch +"@tailor-platform/sdk-codemod": patch +--- + +Standardize SDK-owned environment variables on the `TAILOR_*` namespace. + +Replace the removed SDK-specific environment variables with their new names: `TAILOR_CONFIG_PATH`, `TAILOR_DTS_PATH`, `TAILOR_CI_ALLOW_ID_INJECTION`, `TAILOR_DEPLOY_BUILD_ONLY`, `TAILOR_BUILD_OUTPUT_DIR`, `TAILOR_SKILLS_SOURCE`, `TAILOR_TEMPLATE_SDK_VERSION`, `TAILOR_PLATFORM_URL`, `TAILOR_PLATFORM_OAUTH2_CLIENT_ID`, `TAILOR_INLINE_SOURCEMAP`, `TAILOR_QUERY_NEWLINE_ON_ENTER`, and `TAILOR_APP_LOG_LEVEL`. The deprecated `TAILOR_TOKEN` fallback is removed; use `TAILOR_PLATFORM_TOKEN`. The v2 codemod rewrites the removed environment variable names to their replacements. diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7377434b1..8f84785db 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -104,7 +104,7 @@ jobs: working-directory: example run: pnpm run deploy env: - TAILOR_PLATFORM_SDK_BUILD_ONLY: ${{ runner.os == 'Windows' && 'true' || '' }} + TAILOR_DEPLOY_BUILD_ONLY: ${{ runner.os == 'Windows' && 'true' || '' }} - name: Verify all migrations applied if: runner.os != 'Windows' diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml index b82625952..c8058404f 100644 --- a/.github/workflows/pkg-pr-new.yml +++ b/.github/workflows/pkg-pr-new.yml @@ -64,7 +64,7 @@ jobs: # the build and hang the job. The build itself only needs create-sdk's own deps. - name: Build create-sdk with pkg.pr.new SDK env: - TAILOR_SDK_VERSION: ${{ steps.urls.outputs.sdk_url }} + TAILOR_TEMPLATE_SDK_VERSION: ${{ steps.urls.outputs.sdk_url }} run: | node packages/create-sdk/scripts/prepare-templates.js pnpm --config.verify-deps-before-run=false --filter @tailor-platform/create-sdk build diff --git a/example/e2e/utils.ts b/example/e2e/utils.ts index 7f70ec2ee..ec0580e2c 100644 --- a/example/e2e/utils.ts +++ b/example/e2e/utils.ts @@ -5,7 +5,7 @@ import { GraphQLClient } from "graphql-request"; import { inject } from "vitest"; export function createOperatorClient() { - const baseUrl = process.env.PLATFORM_URL ?? "https://api.tailor.tech"; + const baseUrl = process.env.TAILOR_PLATFORM_URL ?? "https://api.tailor.tech"; const workspaceId = inject("workspaceId"); const platformToken = inject("platformToken"); diff --git a/example/tailor.config.ts b/example/tailor.config.ts index 234fc0fed..078e726ae 100644 --- a/example/tailor.config.ts +++ b/example/tailor.config.ts @@ -112,7 +112,7 @@ export default defineConfig({ // SDK-managed app id — do not edit, except when copying this config to a separate app. id: "d0a3398a-f79c-4c2e-be1e-b81469bb0a43", name: "my-app", - logLevel: process.env.LOG_LEVEL ?? "DEBUG", + logLevel: process.env.TAILOR_APP_LOG_LEVEL ?? "DEBUG", env: { foo: 1, bar: "hello", diff --git a/example/tests/scripts/generate_files.ts b/example/tests/scripts/generate_files.ts index 33e4f5cf7..d29daba8e 100644 --- a/example/tests/scripts/generate_files.ts +++ b/example/tests/scripts/generate_files.ts @@ -6,7 +6,7 @@ import { generate, deploy } from "@tailor-platform/sdk/cli"; // Disable inline sourcemaps during test fixture generation so that snapshot // comparisons remain stable across environments. -process.env.TAILOR_ENABLE_INLINE_SOURCEMAP ??= "false"; +process.env.TAILOR_INLINE_SOURCEMAP ??= "false"; const __filename = url.fileURLToPath(import.meta.url); @@ -52,7 +52,7 @@ export async function generateExpectedFiles(): Promise { console.log("Removed existing expected directory"); } - process.env.TAILOR_SDK_OUTPUT_DIR = expectedDir; + process.env.TAILOR_BUILD_OUTPUT_DIR = expectedDir; await generate({ configPath: "./tests/tailor.config.expected.ts", }); @@ -101,7 +101,7 @@ export async function generateCompatFiles(): Promise { await generate({ configPath: "./tests/tailor.config.plugins-compat.ts" }); // Also run deploy --buildOnly for plugins-compat (used by bundled_execution tests) - process.env.TAILOR_SDK_OUTPUT_DIR = pluginsCompatDir; + process.env.TAILOR_BUILD_OUTPUT_DIR = pluginsCompatDir; const result = await deploy({ configPath: "./tests/tailor.config.plugins-compat.ts", buildOnly: true, diff --git a/example/vitest.config.ts b/example/vitest.config.ts index d9abe067b..3d48ee0a1 100644 --- a/example/vitest.config.ts +++ b/example/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ // Disable inline sourcemaps during tests to keep bundled output stable // for size and fixture comparisons. env: { - TAILOR_ENABLE_INLINE_SOURCEMAP: "false", + TAILOR_INLINE_SOURCEMAP: "false", }, projects: [ { diff --git a/packages/create-sdk/scripts/prepare-templates.js b/packages/create-sdk/scripts/prepare-templates.js index 92bab0dd9..1c5dd7574 100644 --- a/packages/create-sdk/scripts/prepare-templates.js +++ b/packages/create-sdk/scripts/prepare-templates.js @@ -4,11 +4,11 @@ import { readFileSync, writeFileSync, readdirSync, existsSync, copyFileSync } fr import { resolve } from "node:path"; // Get SDK version or URL from environment variable or package.json -const sdkVersionOrUrl = process.env.TAILOR_SDK_VERSION; +const sdkVersionOrUrl = process.env.TAILOR_TEMPLATE_SDK_VERSION; let version; if (sdkVersionOrUrl) { - // If TAILOR_SDK_VERSION is set, use it (can be version string or pkg-pr-new URL) + // If TAILOR_TEMPLATE_SDK_VERSION is set, use it (can be version string or pkg-pr-new URL) version = sdkVersionOrUrl; console.log(`Using SDK version from environment: ${version}`); } else { diff --git a/packages/create-sdk/templates/workflow/e2e/globalSetup.ts b/packages/create-sdk/templates/workflow/e2e/globalSetup.ts index 6af16a754..3ad71e969 100644 --- a/packages/create-sdk/templates/workflow/e2e/globalSetup.ts +++ b/packages/create-sdk/templates/workflow/e2e/globalSetup.ts @@ -55,7 +55,7 @@ export async function setup(project: TestProject): Promise { process.env.TAILOR_PLATFORM_WORKSPACE_ID = createdWorkspace.id; // This pipeline deploys a fresh app into a per-run workspace, so let the // SDK inject a missing app id even in CI (normally a hard error there). - process.env.TAILOR_PLATFORM_SDK_ALLOW_CI_ID_INJECTION = "true"; + process.env.TAILOR_CI_ALLOW_ID_INJECTION = "true"; await deployApplication(); } diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/codemod.yaml b/packages/sdk-codemod/codemods/v2/env-var-rename/codemod.yaml new file mode 100644 index 000000000..33ec20299 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/env-var-rename" +version: "1.0.0" +description: "Rewrite removed SDK environment variable names to their v2 `TAILOR_*` names" +engine: jssg +language: text +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts new file mode 100644 index 000000000..63199ac3b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts @@ -0,0 +1,63 @@ +import * as path from "pathe"; + +const ENV_RENAMES: ReadonlyArray = [ + ["TAILOR_PLATFORM_SDK_CONFIG_PATH", "TAILOR_CONFIG_PATH"], + ["TAILOR_PLATFORM_SDK_DTS_PATH", "TAILOR_DTS_PATH"], + ["TAILOR_PLATFORM_SDK_ALLOW_CI_ID_INJECTION", "TAILOR_CI_ALLOW_ID_INJECTION"], + ["TAILOR_PLATFORM_SDK_BUILD_ONLY", "TAILOR_DEPLOY_BUILD_ONLY"], + ["TAILOR_SDK_OUTPUT_DIR", "TAILOR_BUILD_OUTPUT_DIR"], + ["TAILOR_SDK_SKILLS_SOURCE", "TAILOR_SKILLS_SOURCE"], + ["TAILOR_SDK_VERSION", "TAILOR_TEMPLATE_SDK_VERSION"], + ["PLATFORM_URL", "TAILOR_PLATFORM_URL"], + ["PLATFORM_OAUTH2_CLIENT_ID", "TAILOR_PLATFORM_OAUTH2_CLIENT_ID"], + ["TAILOR_ENABLE_INLINE_SOURCEMAP", "TAILOR_INLINE_SOURCEMAP"], + ["TAILOR_PLATFORM_QUERY_NEWLINE_ON_ENTER", "TAILOR_QUERY_NEWLINE_ON_ENTER"], + ["LOG_LEVEL", "TAILOR_APP_LOG_LEVEL"], + ["TAILOR_TOKEN", "TAILOR_PLATFORM_TOKEN"], +]; + +const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]); +const ENV_BOUNDARY = "[A-Z0-9_]"; +const RENAME_PATTERNS = ENV_RENAMES.map(([from, to]) => ({ + pattern: new RegExp(`(? { ); }); + test("env-var-rename scans env files, CI configs, and source files", () => { + const envVarRename = getApplicableCodemods("1.67.1", "2.0.0").find( + (codemod) => codemod.id === "v2/env-var-rename", + ); + + expect(envVarRename?.filePatterns).toEqual( + expect.arrayContaining([ + "**/.env", + "**/.env.*", + "**/*.{env,sh,bash,zsh,yml,yaml,json,md}", + "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}", + ]), + ); + expect(envVarRename?.legacyPatterns).toContain("TAILOR_PLATFORM_SDK_CONFIG_PATH"); + expect(envVarRename?.legacyPatterns).toContain("TAILOR_TOKEN"); + }); + test("flags CommonJS TypeScript files for runtime globals review", () => { const codemod = allCodemods.find((entry) => entry.id === "v2/runtime-globals-opt-in"); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 87bd986b4..c31ae8dc5 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -192,6 +192,48 @@ export const allCodemods: CodemodPackage[] = [ "happen to use `--machineuser` alone.", ].join("\n"), }, + { + id: "v2/env-var-rename", + name: "SDK environment variable rename", + description: + "Rewrite removed SDK environment variable names to their v2 `TAILOR_*` names across scripts, CI configs, docs, env files, and source files", + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/env-var-rename/scripts/transform.js", + filePatterns: [ + "**/package.json", + "**/.env", + "**/.env.*", + "**/*.{env,sh,bash,zsh,yml,yaml,json,md}", + "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}", + ], + legacyPatterns: [ + "TAILOR_PLATFORM_SDK_CONFIG_PATH", + "TAILOR_PLATFORM_SDK_DTS_PATH", + "TAILOR_PLATFORM_SDK_ALLOW_CI_ID_INJECTION", + "TAILOR_PLATFORM_SDK_BUILD_ONLY", + "TAILOR_SDK_OUTPUT_DIR", + "TAILOR_SDK_SKILLS_SOURCE", + "TAILOR_SDK_VERSION", + "PLATFORM_URL", + "PLATFORM_OAUTH2_CLIENT_ID", + "TAILOR_ENABLE_INLINE_SOURCEMAP", + "TAILOR_PLATFORM_QUERY_NEWLINE_ON_ENTER", + "LOG_LEVEL", + "TAILOR_TOKEN", + ], + examples: [ + { + lang: "sh", + before: "TAILOR_PLATFORM_SDK_BUILD_ONLY=true LOG_LEVEL=DEBUG tailor-sdk deploy", + after: "TAILOR_DEPLOY_BUILD_ONLY=true TAILOR_APP_LOG_LEVEL=DEBUG tailor-sdk deploy", + }, + { + before: "const apiUrl = process.env.PLATFORM_URL;", + after: "const apiUrl = process.env.TAILOR_PLATFORM_URL;", + }, + ], + }, { id: "v2/auth-invoker-unwrap", name: 'auth.invoker("name") → invoker: "name"', diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index 5ab9121fa..22cc70b97 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -87,6 +87,10 @@ describe("codemod transforms", () => { await expect(runFixtureCases("v2/cli-rename")).resolves.toBeUndefined(); }); + test("v2/env-var-rename transforms correctly", async () => { + await expect(runFixtureCases("v2/env-var-rename")).resolves.toBeUndefined(); + }); + test("v2/auth-invoker-unwrap transforms correctly", async () => { await expect(runFixtureCases("v2/auth-invoker-unwrap")).resolves.toBeUndefined(); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index aa8ede142..bc25e5678 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -25,6 +25,7 @@ export default defineConfig([ "v2/principal-unify/scripts/transform": "codemods/v2/principal-unify/scripts/transform.ts", "v2/apply-to-deploy/scripts/transform": "codemods/v2/apply-to-deploy/scripts/transform.ts", "v2/cli-rename/scripts/transform": "codemods/v2/cli-rename/scripts/transform.ts", + "v2/env-var-rename/scripts/transform": "codemods/v2/env-var-rename/scripts/transform.ts", "v2/auth-invoker-unwrap/scripts/transform": "codemods/v2/auth-invoker-unwrap/scripts/transform.ts", "v2/tailordb-namespace/scripts/transform": diff --git a/packages/sdk/docs/cli-reference.md b/packages/sdk/docs/cli-reference.md index 2bf4113d8..c5beb583a 100644 --- a/packages/sdk/docs/cli-reference.md +++ b/packages/sdk/docs/cli-reference.md @@ -38,7 +38,7 @@ The following options are available for most commands: | ---------------- | ----- | -------------------------------------- | | `--workspace-id` | `-w` | Workspace ID (for deployment commands) | | `--profile` | `-p` | Workspace profile | -| `--config` | `-c` | Path to SDK config file | +| `--config` | `-c` | Path to Tailor config file | | `--yes` | `-y` | Skip confirmation prompts | ### Environment File Loading @@ -67,15 +67,16 @@ You can use environment variables to configure workspace and authentication: | `TAILOR_PLATFORM_ORGANIZATION_ID` | Organization ID for organization commands | | `TAILOR_PLATFORM_FOLDER_ID` | Folder ID for folder commands | | `TAILOR_PLATFORM_TOKEN` | Authentication token (alternative to `login`) | -| `TAILOR_TOKEN` | **Deprecated.** Use `TAILOR_PLATFORM_TOKEN` instead | | `TAILOR_PLATFORM_PROFILE` | Workspace profile name | -| `TAILOR_PLATFORM_SDK_CONFIG_PATH` | Path to SDK config file | -| `TAILOR_PLATFORM_SDK_DTS_PATH` | Output path for generated `tailor.d.ts` type definition file | +| `TAILOR_CONFIG_PATH` | Path to Tailor config file | +| `TAILOR_DTS_PATH` | Output path for generated `tailor.d.ts` type definition file | | `TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID` | Client ID for `login --machine-user` | | `TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET` | Client secret for `login --machine-user` | | `TAILOR_PLATFORM_MACHINE_USER_NAME` | Default machine user name for `query`, `workflow start`, `function test-run`, `machineuser token` | | `TAILOR_BUNDLE_CONCURRENCY` | Max concurrent bundle workers for `deploy` (resolvers/executors/workflows). Defaults to CPU count | | `TAILOR_APPLY_CONCURRENCY` | Max concurrent unary platform RPCs during `apply`/`deploy` (streaming uploads are not gated). Defaults to 16 | +| `TAILOR_PLATFORM_URL` | Tailor Platform API endpoint override | +| `TAILOR_PLATFORM_OAUTH2_CLIENT_ID` | OAuth2 client ID override for Tailor Platform login | | `VISUAL` / `EDITOR` | Preferred editor for commands that open files (e.g., `vim`, `code`, `nano`) | | `TAILOR_CRASH_REPORTS_LOCAL` | Local crash log writing: `on` (default) or `off` | | `TAILOR_CRASH_REPORTS_REMOTE` | Automatic crash report submission: `off` (default) or `on` | @@ -85,9 +86,8 @@ You can use environment variables to configure workspace and authentication: Token resolution follows this priority order: 1. `TAILOR_PLATFORM_TOKEN` environment variable -2. `TAILOR_TOKEN` environment variable (deprecated) -3. Profile specified via `--profile` option or `TAILOR_PLATFORM_PROFILE` -4. Current user from platform config (`~/.config/tailor-platform/config.yaml`) +2. Profile specified via `--profile` option or `TAILOR_PLATFORM_PROFILE` +3. Current user from platform config (`~/.config/tailor-platform/config.yaml`) ### Workspace ID Priority diff --git a/packages/sdk/docs/cli-reference.template.md b/packages/sdk/docs/cli-reference.template.md index f8b2c4c95..fe3ba559c 100644 --- a/packages/sdk/docs/cli-reference.template.md +++ b/packages/sdk/docs/cli-reference.template.md @@ -32,7 +32,7 @@ The following options are available for most commands: | ---------------- | ----- | -------------------------------------- | | `--workspace-id` | `-w` | Workspace ID (for deployment commands) | | `--profile` | `-p` | Workspace profile | -| `--config` | `-c` | Path to SDK config file | +| `--config` | `-c` | Path to Tailor config file | | `--yes` | `-y` | Skip confirmation prompts | ### Environment File Loading @@ -61,15 +61,16 @@ You can use environment variables to configure workspace and authentication: | `TAILOR_PLATFORM_ORGANIZATION_ID` | Organization ID for organization commands | | `TAILOR_PLATFORM_FOLDER_ID` | Folder ID for folder commands | | `TAILOR_PLATFORM_TOKEN` | Authentication token (alternative to `login`) | -| `TAILOR_TOKEN` | **Deprecated.** Use `TAILOR_PLATFORM_TOKEN` instead | | `TAILOR_PLATFORM_PROFILE` | Workspace profile name | -| `TAILOR_PLATFORM_SDK_CONFIG_PATH` | Path to SDK config file | -| `TAILOR_PLATFORM_SDK_DTS_PATH` | Output path for generated `tailor.d.ts` type definition file | +| `TAILOR_CONFIG_PATH` | Path to Tailor config file | +| `TAILOR_DTS_PATH` | Output path for generated `tailor.d.ts` type definition file | | `TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID` | Client ID for `login --machine-user` | | `TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET` | Client secret for `login --machine-user` | | `TAILOR_PLATFORM_MACHINE_USER_NAME` | Default machine user name for `query`, `workflow start`, `function test-run`, `machineuser token` | | `TAILOR_BUNDLE_CONCURRENCY` | Max concurrent bundle workers for `deploy` (resolvers/executors/workflows). Defaults to CPU count | | `TAILOR_APPLY_CONCURRENCY` | Max concurrent unary platform RPCs during `apply`/`deploy` (streaming uploads are not gated). Defaults to 16 | +| `TAILOR_PLATFORM_URL` | Tailor Platform API endpoint override | +| `TAILOR_PLATFORM_OAUTH2_CLIENT_ID` | OAuth2 client ID override for Tailor Platform login | | `VISUAL` / `EDITOR` | Preferred editor for commands that open files (e.g., `vim`, `code`, `nano`) | | `TAILOR_CRASH_REPORTS_LOCAL` | Local crash log writing: `on` (default) or `off` | | `TAILOR_CRASH_REPORTS_REMOTE` | Automatic crash report submission: `off` (default) or `on` | @@ -79,9 +80,8 @@ You can use environment variables to configure workspace and authentication: Token resolution follows this priority order: 1. `TAILOR_PLATFORM_TOKEN` environment variable -2. `TAILOR_TOKEN` environment variable (deprecated) -3. Profile specified via `--profile` option or `TAILOR_PLATFORM_PROFILE` -4. Current user from platform config (`~/.config/tailor-platform/config.yaml`) +2. Profile specified via `--profile` option or `TAILOR_PLATFORM_PROFILE` +3. Current user from platform config (`~/.config/tailor-platform/config.yaml`) ### Workspace ID Priority diff --git a/packages/sdk/docs/cli/application.md b/packages/sdk/docs/cli/application.md index 34e6e5af9..12769ee46 100644 --- a/packages/sdk/docs/cli/application.md +++ b/packages/sdk/docs/cli/application.md @@ -57,17 +57,17 @@ tailor-sdk deploy [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | ----------------------------------------------------------------- | -------- | -------------------- | --------------------------------- | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | -| `--yes` | `-y` | Skip confirmation prompts | No | `false` | - | -| `--dry-run` | `-d` | Run the command without making any changes | No | - | - | -| `--no-schema-check` | - | Skip schema diff check against migration snapshots | No | - | - | -| `--no-validate` | - | Skip client-side validation against platform resource constraints | No | - | - | -| `--no-cache` | - | Disable bundle caching for this run | No | - | - | -| `--clean-cache` | - | Clean the bundle cache before building | No | - | - | +| Option | Alias | Description | Required | Default | Env | +| ------------------------------- | ----- | ----------------------------------------------------------------- | -------- | -------------------- | ------------------------------ | +| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | +| `--yes` | `-y` | Skip confirmation prompts | No | `false` | - | +| `--dry-run` | `-d` | Run the command without making any changes | No | - | - | +| `--no-schema-check` | - | Skip schema diff check against migration snapshots | No | - | - | +| `--no-validate` | - | Skip client-side validation against platform resource constraints | No | - | - | +| `--no-cache` | - | Disable bundle caching for this run | No | - | - | +| `--clean-cache` | - | Clean the bundle cache before building | No | - | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. **Config File Modification:** @@ -125,12 +125,12 @@ tailor-sdk remove [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | ------------------------- | -------- | -------------------- | --------------------------------- | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | -| `--yes` | `-y` | Skip confirmation prompts | No | `false` | - | +| Option | Alias | Description | Required | Default | Env | +| ------------------------------- | ----- | -------------------------- | -------- | -------------------- | ------------------------------ | +| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | +| `--yes` | `-y` | Skip confirmation prompts | No | `false` | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -146,11 +146,11 @@ tailor-sdk show [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | ----------------------- | -------- | -------------------- | --------------------------------- | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | +| Option | Alias | Description | Required | Default | Env | +| ------------------------------- | ----- | -------------------------- | -------- | -------------------- | ------------------------------ | +| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -166,11 +166,11 @@ tailor-sdk open [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | ----------------------- | -------- | -------------------- | --------------------------------- | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | +| Option | Alias | Description | Required | Default | Env | +| ------------------------------- | ----- | -------------------------- | -------- | -------------------- | ------------------------------ | +| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -192,13 +192,13 @@ tailor-sdk api [options] [command] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | --------------------------------------------------------------------------------- | -------- | -------------------- | --------------------------------- | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | -| `--body ` | `-b` | Request body as JSON. | No | `"{}"` | - | -| `--field ` | `-f` | Set a body field as `key=value` (repeatable; dotted keys nest). Overrides --body. | No | - | - | +| Option | Alias | Description | Required | Default | Env | +| ------------------------------- | ----- | --------------------------------------------------------------------------------- | -------- | -------------------- | ------------------------------ | +| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | +| `--body ` | `-b` | Request body as JSON. | No | `"{}"` | - | +| `--field ` | `-f` | Set a body field as `key=value` (repeatable; dotted keys nest). Overrides --body. | No | - | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. diff --git a/packages/sdk/docs/cli/auth.md b/packages/sdk/docs/cli/auth.md index 30cab7c5b..af0184537 100644 --- a/packages/sdk/docs/cli/auth.md +++ b/packages/sdk/docs/cli/auth.md @@ -164,13 +164,13 @@ tailor-sdk machineuser list [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | -------------------------------------------------------- | -------- | -------------------- | --------------------------------- | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | -| `--order ` | - | Sort order (asc or desc) | No | `"desc"` | - | -| `--limit ` | `-l` | Maximum number of items to return (0 or omit: unlimited) | No | - | - | +| Option | Alias | Description | Required | Default | Env | +| ------------------------------- | ----- | -------------------------------------------------------- | -------- | -------------------- | ------------------------------ | +| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | +| `--order ` | - | Sort order (asc or desc) | No | `"desc"` | - | +| `--limit ` | `-l` | Maximum number of items to return (0 or omit: unlimited) | No | - | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -192,11 +192,11 @@ tailor-sdk machineuser token [options] [name] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | ----------------------- | -------- | -------------------- | --------------------------------- | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | +| Option | Alias | Description | Required | Default | Env | +| ------------------------------- | ----- | -------------------------- | -------- | -------------------- | ------------------------------ | +| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -231,13 +231,13 @@ tailor-sdk oauth2client list [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | -------------------------------------------------------- | -------- | -------------------- | --------------------------------- | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | -| `--order ` | - | Sort order (asc or desc) | No | `"desc"` | - | -| `--limit ` | `-l` | Maximum number of items to return (0 or omit: unlimited) | No | - | - | +| Option | Alias | Description | Required | Default | Env | +| ------------------------------- | ----- | -------------------------------------------------------- | -------- | -------------------- | ------------------------------ | +| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | +| `--order ` | - | Sort order (asc or desc) | No | `"desc"` | - | +| `--limit ` | `-l` | Maximum number of items to return (0 or omit: unlimited) | No | - | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -270,11 +270,11 @@ tailor-sdk oauth2client get [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | ----------------------- | -------- | -------------------- | --------------------------------- | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | +| Option | Alias | Description | Required | Default | Env | +| ------------------------------- | ----- | -------------------------- | -------- | -------------------- | ------------------------------ | +| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | See [Global Options](../cli-reference.md#global-options) for options available to all commands. diff --git a/packages/sdk/docs/cli/query.md b/packages/sdk/docs/cli/query.md index 567f99425..62b4adeee 100644 --- a/packages/sdk/docs/cli/query.md +++ b/packages/sdk/docs/cli/query.md @@ -14,7 +14,7 @@ tailor-sdk query [options] | ------------------------------- | ----- | ---------------------------------------------------------------------------------------------------- | -------- | -------------------- | ----------------------------------- | | `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | | `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | | `--engine ` | - | Query engine (sql or gql) | Yes | - | - | | `--query ` | `-q` | Query string to execute directly; omit to start REPL mode | No | - | - | | `--file ` | `-f` | Read query string from file; omit to start REPL mode | No | - | - | diff --git a/packages/sdk/docs/cli/tailordb.md b/packages/sdk/docs/cli/tailordb.md index 08688fad9..f3cddfdbc 100644 --- a/packages/sdk/docs/cli/tailordb.md +++ b/packages/sdk/docs/cli/tailordb.md @@ -40,14 +40,14 @@ tailor-sdk tailordb truncate [options] [types] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | -------------------------------------------------------------------------- | -------- | -------------------- | --------------------------------- | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | -| `--yes` | `-y` | Skip confirmation prompts | No | `false` | - | -| `--all` | `-a` | Truncate all tables in all owned namespaces (excludes external namespaces) | No | `false` | - | -| `--namespace ` | `-n` | Truncate all tables in specified namespace | No | - | - | +| Option | Alias | Description | Required | Default | Env | +| ------------------------------- | ----- | -------------------------------------------------------------------------- | -------- | -------------------- | ------------------------------ | +| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | +| `--yes` | `-y` | Skip confirmation prompts | No | `false` | - | +| `--all` | `-a` | Truncate all tables in all owned namespaces (excludes external namespaces) | No | `false` | - | +| `--namespace ` | `-n` | Truncate all tables in specified namespace | No | - | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -117,12 +117,12 @@ tailor-sdk tailordb migration generate [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------- | ----- | ------------------------------------------ | -------- | -------------------- | --------------------------------- | -| `--yes` | `-y` | Skip confirmation prompts | No | `false` | - | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | -| `--name ` | `-n` | Optional description for the migration | No | - | - | -| `--init` | - | Delete existing migrations and start fresh | No | `false` | - | +| Option | Alias | Description | Required | Default | Env | +| ------------------- | ----- | ------------------------------------------ | -------- | -------------------- | -------------------- | +| `--yes` | `-y` | Skip confirmation prompts | No | `false` | - | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | +| `--name ` | `-n` | Optional description for the migration | No | - | - | +| `--init` | - | Delete existing migrations and start fresh | No | `false` | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -144,10 +144,10 @@ tailor-sdk tailordb migration script [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------- | ----- | ----------------------------------------------------------------- | -------- | -------------------- | --------------------------------- | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | -| `--namespace ` | `-n` | Target TailorDB namespace (required if multiple namespaces exist) | No | - | - | +| Option | Alias | Description | Required | Default | Env | +| ------------------------- | ----- | ----------------------------------------------------------------- | -------- | -------------------- | -------------------- | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | +| `--namespace ` | `-n` | Target TailorDB namespace (required if multiple namespaces exist) | No | - | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -169,13 +169,13 @@ tailor-sdk tailordb migration set [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | ----------------------------------------------------------------- | -------- | -------------------- | --------------------------------- | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | -| `--yes` | `-y` | Skip confirmation prompts | No | `false` | - | -| `--namespace ` | `-n` | Target TailorDB namespace (required if multiple namespaces exist) | No | - | - | +| Option | Alias | Description | Required | Default | Env | +| ------------------------------- | ----- | ----------------------------------------------------------------- | -------- | -------------------- | ------------------------------ | +| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | +| `--yes` | `-y` | Skip confirmation prompts | No | `false` | - | +| `--namespace ` | `-n` | Target TailorDB namespace (required if multiple namespaces exist) | No | - | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -191,12 +191,12 @@ tailor-sdk tailordb migration status [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | ----------------------------------------------------------------- | -------- | -------------------- | --------------------------------- | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | -| `--namespace ` | `-n` | Target TailorDB namespace (shows all namespaces if not specified) | No | - | - | +| Option | Alias | Description | Required | Default | Env | +| ------------------------------- | ----- | ----------------------------------------------------------------- | -------- | -------------------- | ------------------------------ | +| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | +| `--namespace ` | `-n` | Target TailorDB namespace (shows all namespaces if not specified) | No | - | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -218,13 +218,13 @@ tailor-sdk tailordb migration sync [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | ----------------------------------------------------------------- | -------- | -------------------- | --------------------------------- | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | -| `--yes` | `-y` | Skip confirmation prompts | No | `false` | - | -| `--namespace ` | `-n` | Target TailorDB namespace (required if multiple namespaces exist) | No | - | - | +| Option | Alias | Description | Required | Default | Env | +| ------------------------------- | ----- | ----------------------------------------------------------------- | -------- | -------------------- | ------------------------------ | +| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | +| `--yes` | `-y` | Skip confirmation prompts | No | `false` | - | +| `--namespace ` | `-n` | Target TailorDB namespace (required if multiple namespaces exist) | No | - | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -262,11 +262,11 @@ tailor-sdk tailordb erd export [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------- | ----- | ---------------------------------------------------------------------------------------------- | -------- | -------------------- | --------------------------------- | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | -| `--namespace ` | `-n` | TailorDB namespace name (optional if only one namespace is defined in config) | No | - | - | -| `--output ` | `-o` | Output directory path for TailorDB ERD viewer files (writes to `//dist`) | No | `".tailor-sdk/erd"` | - | +| Option | Alias | Description | Required | Default | Env | +| ------------------------- | ----- | ---------------------------------------------------------------------------------------------- | -------- | -------------------- | -------------------- | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | +| `--namespace ` | `-n` | TailorDB namespace name (optional if only one namespace is defined in config) | No | - | - | +| `--output ` | `-o` | Output directory path for TailorDB ERD viewer files (writes to `//dist`) | No | `".tailor-sdk/erd"` | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -282,12 +282,12 @@ tailor-sdk tailordb erd serve [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------- | ----- | ------------------------------------------------------------------------- | -------- | -------------------- | --------------------------------- | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | -| `--namespace ` | `-n` | TailorDB namespace name (uses first namespace in config if not specified) | No | - | - | -| `--port ` | - | Local server port (0 selects a free port) | No | `0` | - | -| `--open` | - | Open the ERD viewer in the default browser | No | `false` | - | +| Option | Alias | Description | Required | Default | Env | +| ------------------------- | ----- | ------------------------------------------------------------------------- | -------- | -------------------- | -------------------- | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | +| `--namespace ` | `-n` | TailorDB namespace name (uses first namespace in config if not specified) | No | - | - | +| `--port ` | - | Local server port (0 selects a free port) | No | `0` | - | +| `--open` | - | Open the ERD viewer in the default browser | No | `false` | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. @@ -303,12 +303,12 @@ tailor-sdk tailordb erd deploy [options] **Options** -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | ----------------------------------------------------------------------------------- | -------- | -------------------- | --------------------------------- | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | -| `--namespace ` | `-n` | TailorDB namespace name (optional - deploys all namespaces with erdSite if omitted) | No | - | - | +| Option | Alias | Description | Required | Default | Env | +| ------------------------------- | ----- | ----------------------------------------------------------------------------------- | -------- | -------------------- | ------------------------------ | +| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | +| `--namespace ` | `-n` | TailorDB namespace name (optional - deploys all namespaces with erdSite if omitted) | No | - | - | See [Global Options](../cli-reference.md#global-options) for options available to all commands. diff --git a/packages/sdk/docs/cli/workflow.md b/packages/sdk/docs/cli/workflow.md index ee6c590f5..8ef462f41 100644 --- a/packages/sdk/docs/cli/workflow.md +++ b/packages/sdk/docs/cli/workflow.md @@ -93,7 +93,7 @@ tailor-sdk workflow start [options] | ------------------------------- | ----- | --------------------------------------------------------------------------- | -------- | -------------------- | ----------------------------------- | | `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | | `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | `TAILOR_PLATFORM_SDK_CONFIG_PATH` | +| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | | `--machine-user ` | `-m` | Machine user name. Falls back to the active profile's default machine user. | No | - | `TAILOR_PLATFORM_MACHINE_USER_NAME` | | `--arg ` | `-a` | Workflow argument (JSON string) | No | - | - | | `--wait` | `-W` | Wait for execution to complete | No | `false` | - | diff --git a/packages/sdk/docs/configuration.md b/packages/sdk/docs/configuration.md index 9785cfc0b..c5fc2d8db 100644 --- a/packages/sdk/docs/configuration.md +++ b/packages/sdk/docs/configuration.md @@ -27,7 +27,7 @@ export default defineConfig({ cors: ["https://example.com"], allowedIpAddresses: ["192.168.1.0/24"], disableIntrospection: false, - logLevel: process.env.LOG_LEVEL ?? "DEBUG", + logLevel: process.env.TAILOR_APP_LOG_LEVEL ?? "DEBUG", }); ``` @@ -46,11 +46,11 @@ export default defineConfig({ ```typescript export default defineConfig({ name: "my-app", - logLevel: process.env.LOG_LEVEL ?? "DEBUG", + logLevel: process.env.TAILOR_APP_LOG_LEVEL ?? "DEBUG", }); ``` -This is a bundle-time setting. Changing `LOG_LEVEL` affects newly bundled deployments; already deployed functions must be redeployed. +This is a bundle-time setting. Changing `TAILOR_APP_LOG_LEVEL` affects newly bundled deployments; already deployed functions must be redeployed. ### Service Configuration diff --git a/packages/sdk/docs/github-actions.md b/packages/sdk/docs/github-actions.md index f8da796a4..7f54eaf1d 100644 --- a/packages/sdk/docs/github-actions.md +++ b/packages/sdk/docs/github-actions.md @@ -178,7 +178,7 @@ previously deployed resources. If your pipeline intentionally deploys a fresh, throwaway application on every run (for example an end-to-end test harness that creates and deletes its own -workspace), set `TAILOR_PLATFORM_SDK_ALLOW_CI_ID_INJECTION=true` to opt back +workspace), set `TAILOR_CI_ALLOW_ID_INJECTION=true` to opt back into automatic id injection for that pipeline. ## Secrets diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 15c50f6fa..edc17a0c8 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -217,6 +217,36 @@ happen to use `--machineuser` alone. +## SDK environment variable rename + +**Migration:** Partially automatic + +Rewrite removed SDK environment variable names to their v2 `TAILOR_*` names across scripts, CI configs, docs, env files, and source files + +Before: + +```sh +TAILOR_PLATFORM_SDK_BUILD_ONLY=true LOG_LEVEL=DEBUG tailor-sdk deploy +``` + +After: + +```sh +TAILOR_DEPLOY_BUILD_ONLY=true TAILOR_APP_LOG_LEVEL=DEBUG tailor-sdk deploy +``` + +Before: + +```ts +const apiUrl = process.env.PLATFORM_URL; +``` + +After: + +```ts +const apiUrl = process.env.TAILOR_PLATFORM_URL; +``` + ## auth.invoker("name") → invoker: "name" **Migration:** Partially automatic diff --git a/packages/sdk/docs/multi-environment.md b/packages/sdk/docs/multi-environment.md index 2f4b54b36..736b46d0a 100644 --- a/packages/sdk/docs/multi-environment.md +++ b/packages/sdk/docs/multi-environment.md @@ -32,7 +32,7 @@ Profiles are created with `write` permission by default. The production profile ```ini # .env.production APP_ENV=production -LOG_LEVEL=WARN +TAILOR_APP_LOG_LEVEL=WARN ``` ```bash @@ -42,7 +42,7 @@ tailor-sdk deploy -w --env-file .env.production ```typescript export default defineConfig({ name: "my-app", - logLevel: process.env.LOG_LEVEL ?? "DEBUG", + logLevel: process.env.TAILOR_APP_LOG_LEVEL ?? "DEBUG", }); ``` diff --git a/packages/sdk/postinstall.mjs b/packages/sdk/postinstall.mjs index 7ac8733d6..ad7e19d18 100644 --- a/packages/sdk/postinstall.mjs +++ b/packages/sdk/postinstall.mjs @@ -22,8 +22,8 @@ async function install() { // Try to find and load the user's tailor.config.ts // Priority: env/config > search parent directories - const configPath = process.env.TAILOR_PLATFORM_SDK_CONFIG_PATH - ? resolve(cwd, process.env.TAILOR_PLATFORM_SDK_CONFIG_PATH) + const configPath = process.env.TAILOR_CONFIG_PATH + ? resolve(cwd, process.env.TAILOR_CONFIG_PATH) : findUpSync(DEFAULT_CONFIG_FILENAME, { cwd }); if (!configPath || !existsSync(configPath)) { diff --git a/packages/sdk/scripts/perf/runtime-perf.sh b/packages/sdk/scripts/perf/runtime-perf.sh index 14f65b3dc..7c1fc5b48 100644 --- a/packages/sdk/scripts/perf/runtime-perf.sh +++ b/packages/sdk/scripts/perf/runtime-perf.sh @@ -46,7 +46,7 @@ if ! pnpm generate > "${LOG_DIR}/generate-warmup.log" 2>&1; then fi echo "Warmup: Running apply (build-only)..." -if ! TAILOR_PLATFORM_SDK_BUILD_ONLY=true pnpm exec tailor-sdk deploy -c tailor.config.ts > "${LOG_DIR}/apply-warmup.log" 2>&1; then +if ! TAILOR_DEPLOY_BUILD_ONLY=true pnpm exec tailor-sdk deploy -c tailor.config.ts > "${LOG_DIR}/apply-warmup.log" 2>&1; then echo "ERROR: apply warmup failed" cat "${LOG_DIR}/apply-warmup.log" exit 1 @@ -78,7 +78,7 @@ echo "Measuring apply (build-only) command..." for i in $(seq 1 $ITERATIONS); do echo " apply iteration $i/$ITERATIONS..." START=$(get_timestamp_ms) - if ! TAILOR_PLATFORM_SDK_BUILD_ONLY=true pnpm exec tailor-sdk deploy -c tailor.config.ts > "${LOG_DIR}/apply-iter-${i}.log" 2>&1; then + if ! TAILOR_DEPLOY_BUILD_ONLY=true pnpm exec tailor-sdk deploy -c tailor.config.ts > "${LOG_DIR}/apply-iter-${i}.log" 2>&1; then echo "ERROR: apply iteration $i failed" cat "${LOG_DIR}/apply-iter-${i}.log" exit 1 diff --git a/packages/sdk/src/cli/bundler/query/query-bundler.test.ts b/packages/sdk/src/cli/bundler/query/query-bundler.test.ts index 1fa2c515e..8322b6034 100644 --- a/packages/sdk/src/cli/bundler/query/query-bundler.test.ts +++ b/packages/sdk/src/cli/bundler/query/query-bundler.test.ts @@ -12,11 +12,11 @@ describe("query-bundler", () => { `test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, ); fs.mkdirSync(testDir, { recursive: true }); - process.env.TAILOR_SDK_OUTPUT_DIR = testDir; + process.env.TAILOR_BUILD_OUTPUT_DIR = testDir; }); afterAll(() => { - delete process.env.TAILOR_SDK_OUTPUT_DIR; + delete process.env.TAILOR_BUILD_OUTPUT_DIR; try { fs.rmSync(TEST_BUNDLER_BASE, { recursive: true, force: true }); } catch { @@ -63,7 +63,7 @@ describe("query-bundler", () => { }); test("writes entry files to query output directory (bundle output is in-memory only)", async () => { - const outputDir = path.join(process.env.TAILOR_SDK_OUTPUT_DIR!, "query"); + const outputDir = path.join(process.env.TAILOR_BUILD_OUTPUT_DIR!, "query"); await bundleQueryScript("sql"); await bundleQueryScript("gql"); diff --git a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/integration.test.ts b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/integration.test.ts index 69b5dc1fc..8cf1f4b26 100644 --- a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/integration.test.ts +++ b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/integration.test.ts @@ -61,7 +61,7 @@ describe("deploy command integration tests", () => { }, 120000); afterAll(() => { - delete process.env.TAILOR_SDK_OUTPUT_DIR; + delete process.env.TAILOR_BUILD_OUTPUT_DIR; vi.useRealTimers(); }); diff --git a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/prepare.ts b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/prepare.ts index 3fb31169e..e115a54a0 100644 --- a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/prepare.ts +++ b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/prepare.ts @@ -25,7 +25,7 @@ export async function prepareFixtures(): Promise<{ const configPath = path.join(fixtureDir, "tailor.config.ts"); - process.env.TAILOR_SDK_OUTPUT_DIR = outputDir; + process.env.TAILOR_BUILD_OUTPUT_DIR = outputDir; // Generate plugin output (db.ts, enums.ts) await generate({ configPath }); diff --git a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailor.config.ts b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailor.config.ts index 68bd0ce06..3bec3f5ac 100644 --- a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailor.config.ts +++ b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailor.config.ts @@ -6,7 +6,7 @@ import { enumConstantsPlugin } from "@tailor-platform/sdk/plugin/enum-constants" import { kyselyTypePlugin } from "@tailor-platform/sdk/plugin/kysely-type"; const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); -const outDir = process.env.TAILOR_SDK_OUTPUT_DIR ?? path.join(__dirname, "dist"); +const outDir = process.env.TAILOR_BUILD_OUTPUT_DIR ?? path.join(__dirname, "dist"); export default defineConfig({ // SDK-managed app id — do not edit, except when copying this config to a separate app. diff --git a/packages/sdk/src/cli/commands/deploy/config-id-ci-guard.test.ts b/packages/sdk/src/cli/commands/deploy/config-id-ci-guard.test.ts index a32b75d34..2ba1849e4 100644 --- a/packages/sdk/src/cli/commands/deploy/config-id-ci-guard.test.ts +++ b/packages/sdk/src/cli/commands/deploy/config-id-ci-guard.test.ts @@ -100,8 +100,8 @@ export default defineConfig({ ).rejects.toThrow(/must be a UUID/); }); - test("CI + missing id + TAILOR_PLATFORM_SDK_ALLOW_CI_ID_INJECTION: injects an id", async () => { - vi.stubEnv("TAILOR_PLATFORM_SDK_ALLOW_CI_ID_INJECTION", "true"); + test("CI + missing id + TAILOR_CI_ALLOW_ID_INJECTION: injects an id", async () => { + vi.stubEnv("TAILOR_CI_ALLOW_ID_INJECTION", "true"); const filePath = await writeConfig(configWithoutId); const { ensureConfigIdForDeploy } = await load(true); await ensureConfigIdForDeploy({ configPath: filePath, dryRun: false, buildOnly: false }); diff --git a/packages/sdk/src/cli/commands/deploy/config-id-injector.ts b/packages/sdk/src/cli/commands/deploy/config-id-injector.ts index 3a32d6db5..c7043e944 100644 --- a/packages/sdk/src/cli/commands/deploy/config-id-injector.ts +++ b/packages/sdk/src/cli/commands/deploy/config-id-injector.ts @@ -190,7 +190,7 @@ async function assertConfigIdInCI(configPath: string): Promise { * ownership. CI dry-runs (plan) perform the same check read-only, so a * forgotten id fails at PR time instead of at deploy. Ephemeral pipelines that * intentionally deploy a fresh app per run (such as e2e harnesses) can opt - * back into injection with `TAILOR_PLATFORM_SDK_ALLOW_CI_ID_INJECTION=true`. + * back into injection with `TAILOR_CI_ALLOW_ID_INJECTION=true`. * Local dry-run and build-only flows skip both injection and the check (no * on-disk side effects are expected, and build-only never talks to the * platform). @@ -207,8 +207,7 @@ export async function ensureConfigIdForDeploy(obj: { const { configPath, dryRun, buildOnly } = obj; if (buildOnly) return; - const allowCIInjection = - parseBoolean(process.env.TAILOR_PLATFORM_SDK_ALLOW_CI_ID_INJECTION) === true; + const allowCIInjection = parseBoolean(process.env.TAILOR_CI_ALLOW_ID_INJECTION) === true; const strictCI = isCI && !allowCIInjection; if (dryRun) { diff --git a/packages/sdk/src/cli/commands/deploy/deploy.ts b/packages/sdk/src/cli/commands/deploy/deploy.ts index dd85d0f7d..f16989d55 100644 --- a/packages/sdk/src/cli/commands/deploy/deploy.ts +++ b/packages/sdk/src/cli/commands/deploy/deploy.ts @@ -394,7 +394,7 @@ export async function deploy(options?: DeployOptions) { } = await withSpan("build", async () => { const dryRun = options?.dryRun ?? false; const buildOnly = - options?.buildOnly ?? parseBoolean(process.env.TAILOR_PLATFORM_SDK_BUILD_ONLY) === true; + options?.buildOnly ?? parseBoolean(process.env.TAILOR_DEPLOY_BUILD_ONLY) === true; const { config, plugins } = await withSpan("build.loadConfig", async () => { const foundPath = loadConfigPath(options?.configPath); diff --git a/packages/sdk/src/cli/commands/function/bundle.test.ts b/packages/sdk/src/cli/commands/function/bundle.test.ts index 84d9bae0d..426fb0e10 100644 --- a/packages/sdk/src/cli/commands/function/bundle.test.ts +++ b/packages/sdk/src/cli/commands/function/bundle.test.ts @@ -22,11 +22,11 @@ describe("bundleForTestRun", () => { beforeEach(() => { testDir = path.join(TEST_BASE, `test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`); fs.mkdirSync(testDir, { recursive: true }); - process.env.TAILOR_SDK_OUTPUT_DIR = testDir; + process.env.TAILOR_BUILD_OUTPUT_DIR = testDir; }); afterAll(() => { - delete process.env.TAILOR_SDK_OUTPUT_DIR; + delete process.env.TAILOR_BUILD_OUTPUT_DIR; try { fs.rmSync(TEST_BASE, { recursive: true, force: true }); } catch { diff --git a/packages/sdk/src/cli/commands/generate/seed/bundler.test.ts b/packages/sdk/src/cli/commands/generate/seed/bundler.test.ts index 2a4637baf..888bce183 100644 --- a/packages/sdk/src/cli/commands/generate/seed/bundler.test.ts +++ b/packages/sdk/src/cli/commands/generate/seed/bundler.test.ts @@ -7,17 +7,17 @@ const TEST_BUNDLER_BASE = path.join(__dirname, "__test_bundler__"); describe("seed-bundler", () => { beforeEach(() => { - // Set TAILOR_SDK_OUTPUT_DIR to test directory so bundled output goes into test directory + // Set TAILOR_BUILD_OUTPUT_DIR to test directory so bundled output goes into test directory const testDir = path.join( TEST_BUNDLER_BASE, `test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, ); fs.mkdirSync(testDir, { recursive: true }); - process.env.TAILOR_SDK_OUTPUT_DIR = testDir; + process.env.TAILOR_BUILD_OUTPUT_DIR = testDir; }); afterAll(() => { - delete process.env.TAILOR_SDK_OUTPUT_DIR; + delete process.env.TAILOR_BUILD_OUTPUT_DIR; try { fs.rmSync(TEST_BUNDLER_BASE, { recursive: true, force: true }); } catch { diff --git a/packages/sdk/src/cli/commands/generate/service.ts b/packages/sdk/src/cli/commands/generate/service.ts index 62bbb751b..3a1768db9 100644 --- a/packages/sdk/src/cli/commands/generate/service.ts +++ b/packages/sdk/src/cli/commands/generate/service.ts @@ -294,8 +294,8 @@ export function createGenerationManager(params: { const args = process.argv.slice(2); const env = { ...process.env, - TAILOR_WATCH_GENERATION: ( - parseInt(process.env.TAILOR_WATCH_GENERATION || "0", 10) + 1 + __TAILOR_WATCH_GENERATION: ( + parseInt(process.env.__TAILOR_WATCH_GENERATION || "0", 10) + 1 ).toString(), }; diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/bundler.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/bundler.test.ts index 9eb377169..60cc85525 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/bundler.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/bundler.test.ts @@ -14,13 +14,13 @@ describe("migration-bundler", () => { `test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, ); fs.mkdirSync(testDir, { recursive: true }); - // Set TAILOR_SDK_OUTPUT_DIR to testDir so bundled output goes into test directory - process.env.TAILOR_SDK_OUTPUT_DIR = testDir; + // Set TAILOR_BUILD_OUTPUT_DIR to testDir so bundled output goes into test directory + process.env.TAILOR_BUILD_OUTPUT_DIR = testDir; }); afterAll(() => { // Clean up environment variable - delete process.env.TAILOR_SDK_OUTPUT_DIR; + delete process.env.TAILOR_BUILD_OUTPUT_DIR; try { fs.rmSync(TEST_BUNDLER_BASE, { recursive: true, force: true }); } catch { diff --git a/packages/sdk/src/cli/query/index.ts b/packages/sdk/src/cli/query/index.ts index 458e833e1..1f160dcc3 100644 --- a/packages/sdk/src/cli/query/index.ts +++ b/packages/sdk/src/cli/query/index.ts @@ -822,9 +822,7 @@ export const queryCommand = defineAppCommand({ if (mode.mode === "repl") { const newlineOnEnter = - args["newline-on-enter"] ?? - parseBoolean(process.env.TAILOR_PLATFORM_QUERY_NEWLINE_ON_ENTER) ?? - true; + args["newline-on-enter"] ?? parseBoolean(process.env.TAILOR_QUERY_NEWLINE_ON_ENTER) ?? true; await runRepl({ ...sharedOptions, json: args.json, diff --git a/packages/sdk/src/cli/services/auth/bundler.test.ts b/packages/sdk/src/cli/services/auth/bundler.test.ts index 37d43cdb3..fded87af8 100644 --- a/packages/sdk/src/cli/services/auth/bundler.test.ts +++ b/packages/sdk/src/cli/services/auth/bundler.test.ts @@ -65,7 +65,7 @@ export default { expect(code).toContain("env"); }); - test("inlines LOG_LEVEL references from config during bundling", async () => { + test("inlines TAILOR_APP_LOG_LEVEL references from config during bundling", async () => { tmpDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "auth-bundler-test-"))); const configFile = path.join(tmpDir, "tailor.config.ts"); fs.writeFileSync( @@ -74,7 +74,7 @@ export default { const handler = async () => ({ ok: true }); export default { - logLevel: process.env.LOG_LEVEL ?? "DEBUG", + logLevel: process.env.TAILOR_APP_LOG_LEVEL ?? "DEBUG", auth: { hooks: { beforeLogin: { handler } } }, }; `, @@ -89,6 +89,6 @@ export default { const code = bundled.get("auth-hook--my-auth--before-login"); expect(code).toBeDefined(); - expect(code).not.toContain("process.env.LOG_LEVEL"); + expect(code).not.toContain("process.env.TAILOR_APP_LOG_LEVEL"); }); }); diff --git a/packages/sdk/src/cli/services/auth/bundler.ts b/packages/sdk/src/cli/services/auth/bundler.ts index e2f4c8add..c636dedf2 100644 --- a/packages/sdk/src/cli/services/auth/bundler.ts +++ b/packages/sdk/src/cli/services/auth/bundler.ts @@ -137,7 +137,7 @@ export async function bundleAuthHooks( plugins, transform: { define: { - "process.env.LOG_LEVEL": JSON.stringify(bundleLogLevel), + "process.env.TAILOR_APP_LOG_LEVEL": JSON.stringify(bundleLogLevel), }, }, treeshake: composeFunctionTreeshakeOptions([ diff --git a/packages/sdk/src/cli/services/workflow/bundler.test.ts b/packages/sdk/src/cli/services/workflow/bundler.test.ts index b0987f302..72736f12c 100644 --- a/packages/sdk/src/cli/services/workflow/bundler.test.ts +++ b/packages/sdk/src/cli/services/workflow/bundler.test.ts @@ -102,11 +102,11 @@ export default createWorkflow({ // The raw simpleWorkflow.trigger() should NOT remain in the bundle expect(callerCode).not.toContain("simpleWorkflow.trigger"); - // The platform bundle must fold away the TAILOR_PLATFORM_BUNDLE gate and + // The platform bundle must fold away the __TAILOR_PLATFORM_BUNDLE gate and // tree-shake every test-only symbol; otherwise an unsubstituted process.env.* // reaches the Platform Web runtime (no `process`) and crashes. for (const code of result.bundledCode.values()) { - expect(code).not.toContain("process.env.TAILOR_PLATFORM_BUNDLE"); + expect(code).not.toContain("process.env.__TAILOR_PLATFORM_BUNDLE"); expect(code).not.toContain("job-registry"); expect(code).not.toContain("registerJob"); expect(code).not.toContain("platformSerialize"); diff --git a/packages/sdk/src/cli/shared/args.ts b/packages/sdk/src/cli/shared/args.ts index 440b69127..cb65dda6c 100644 --- a/packages/sdk/src/cli/shared/args.ts +++ b/packages/sdk/src/cli/shared/args.ts @@ -201,8 +201,8 @@ export const workspaceArgs = { export const configArg = { config: arg(z.string().default("tailor.config.ts"), { alias: "c", - description: "Path to SDK config file", - env: "TAILOR_PLATFORM_SDK_CONFIG_PATH", + description: "Path to Tailor config file", + env: "TAILOR_CONFIG_PATH", completion: { type: "file", extensions: ["ts"] }, }), } satisfies ArgsShape; diff --git a/packages/sdk/src/cli/shared/client.test.ts b/packages/sdk/src/cli/shared/client.test.ts index 442fdcf51..042415f24 100644 --- a/packages/sdk/src/cli/shared/client.test.ts +++ b/packages/sdk/src/cli/shared/client.test.ts @@ -27,6 +27,20 @@ vi.mock("#/cli/crashreport/index", () => ({ reportCrash: vi.fn(), })); +describe("client environment configuration", () => { + afterEach(() => { + vi.unstubAllEnvs(); + vi.resetModules(); + }); + + test("uses TAILOR_PLATFORM_URL for the platform base URL", async () => { + vi.resetModules(); + vi.stubEnv("TAILOR_PLATFORM_URL", "https://api.staging.tailor.test"); + const client = await import("./client"); + expect(client.platformBaseUrl).toBe("https://api.staging.tailor.test"); + }); +}); + describe("createTransport", () => { afterEach(() => { vi.clearAllMocks(); diff --git a/packages/sdk/src/cli/shared/client.ts b/packages/sdk/src/cli/shared/client.ts index 54bb4e422..b6ca1a2f7 100644 --- a/packages/sdk/src/cli/shared/client.ts +++ b/packages/sdk/src/cli/shared/client.ts @@ -17,10 +17,10 @@ import { logger } from "./logger"; import { userAgent } from "./user-agent"; import type { OperatorService } from "@tailor-platform/tailor-proto/service_pb"; -export const platformBaseUrl = process.env.PLATFORM_URL ?? "https://api.tailor.tech"; +export const platformBaseUrl = process.env.TAILOR_PLATFORM_URL ?? "https://api.tailor.tech"; const oauth2ClientId = - process.env.PLATFORM_OAUTH2_CLIENT_ID ?? "cpoc_0Iudir72fqSpqC6GQ58ri1cLAqcq5vJl"; + process.env.TAILOR_PLATFORM_OAUTH2_CLIENT_ID ?? "cpoc_0Iudir72fqSpqC6GQ58ri1cLAqcq5vJl"; const oauth2DiscoveryEndpoint = "/.well-known/oauth-authorization-server/oauth2/platform"; /** diff --git a/packages/sdk/src/cli/shared/context.test.ts b/packages/sdk/src/cli/shared/context.test.ts index 1df3cda1c..1807ccf0a 100644 --- a/packages/sdk/src/cli/shared/context.test.ts +++ b/packages/sdk/src/cli/shared/context.test.ts @@ -75,7 +75,7 @@ describe("loadConfigPath", () => { beforeEach(() => { vi.resetModules(); process.env = { ...originalEnv }; - delete process.env.TAILOR_PLATFORM_SDK_CONFIG_PATH; + delete process.env.TAILOR_CONFIG_PATH; tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tailor-test-")); vi.spyOn(process, "cwd").mockReturnValue(tempDir); }); @@ -92,7 +92,7 @@ describe("loadConfigPath", () => { }); test("returns env config path when set", () => { - process.env.TAILOR_PLATFORM_SDK_CONFIG_PATH = "/env/path/config.ts"; + process.env.TAILOR_CONFIG_PATH = "/env/path/config.ts"; const result = loadConfigPath(); expect(result).toBe("/env/path/config.ts"); }); @@ -527,13 +527,6 @@ describe("loadAccessToken", () => { expect(result).toBe(validToken); }); - test("TAILOR_PLATFORM_TOKEN takes precedence over TAILOR_TOKEN", async () => { - vi.stubEnv("TAILOR_PLATFORM_TOKEN", validToken); - vi.stubEnv("TAILOR_TOKEN", otherToken); - const result = await loadAccessToken(); - expect(result).toBe(validToken); - }); - test("TAILOR_PLATFORM_TOKEN takes precedence over profile", async () => { vi.stubEnv("TAILOR_PLATFORM_TOKEN", validToken); writePlatformConfig({ @@ -557,15 +550,12 @@ describe("loadAccessToken", () => { }); }); - describe("env.TAILOR_TOKEN (deprecated)", () => { - test("returns token from TAILOR_TOKEN when TAILOR_PLATFORM_TOKEN not set", async () => { + describe("env.TAILOR_TOKEN", () => { + test("ignores the removed TAILOR_TOKEN fallback", async () => { vi.stubEnv("TAILOR_TOKEN", validToken); using warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); - const result = await loadAccessToken(); - expect(result).toBe(validToken); - expect(warnSpy).toHaveBeenCalledWith( - "TAILOR_TOKEN is deprecated. Please use TAILOR_PLATFORM_TOKEN instead.", - ); + await expect(loadAccessToken()).rejects.toThrow(/Tailor Platform token not found/); + expect(warnSpy).not.toHaveBeenCalled(); }); }); diff --git a/packages/sdk/src/cli/shared/context.ts b/packages/sdk/src/cli/shared/context.ts index eca479460..166ead756 100644 --- a/packages/sdk/src/cli/shared/context.ts +++ b/packages/sdk/src/cli/shared/context.ts @@ -496,7 +496,7 @@ export async function loadMachineUserName( /** * Load access token from environment variables, command options, or platform config. * In CLI context, profile env fallback is also handled by politty's arg env option. - * Priority: env/TAILOR_PLATFORM_TOKEN > env/TAILOR_TOKEN (deprecated) > opts/profile > env/profile > config/currentUser > error + * Priority: env/TAILOR_PLATFORM_TOKEN > opts/profile > env/profile > config/currentUser > error * @param opts - Profile options * @returns Resolved access token */ @@ -505,12 +505,6 @@ export async function loadAccessToken(opts?: LoadAccessTokenOptions) { if (process.env.TAILOR_PLATFORM_TOKEN) { return process.env.TAILOR_PLATFORM_TOKEN; } - // TAILOR_TOKEN is deprecated - if (process.env.TAILOR_TOKEN) { - logger.warn("TAILOR_TOKEN is deprecated. Please use TAILOR_PLATFORM_TOKEN instead."); - return process.env.TAILOR_TOKEN; - } - const pfConfig = await readPlatformConfig(); let user; const profile = opts?.profile || process.env.TAILOR_PLATFORM_PROFILE; @@ -750,8 +744,8 @@ export function loadConfigPath(configPath?: string): string | undefined { if (configPath) { return configPath; } - if (process.env.TAILOR_PLATFORM_SDK_CONFIG_PATH) { - return process.env.TAILOR_PLATFORM_SDK_CONFIG_PATH; + if (process.env.TAILOR_CONFIG_PATH) { + return process.env.TAILOR_CONFIG_PATH; } // Search for config file in current directory and parent directories diff --git a/packages/sdk/src/cli/shared/dist-dir.ts b/packages/sdk/src/cli/shared/dist-dir.ts index 7a8c4cef8..5dae1882f 100644 --- a/packages/sdk/src/cli/shared/dist-dir.ts +++ b/packages/sdk/src/cli/shared/dist-dir.ts @@ -1,7 +1,7 @@ let distPath: string | null = null; export const getDistDir = (): string => { - const configured = process.env.TAILOR_SDK_OUTPUT_DIR; + const configured = process.env.TAILOR_BUILD_OUTPUT_DIR; if (configured && configured !== distPath) { distPath = configured; } else if (distPath === null) { diff --git a/packages/sdk/src/cli/shared/inline-sourcemap.ts b/packages/sdk/src/cli/shared/inline-sourcemap.ts index c11785f46..4725d75ee 100644 --- a/packages/sdk/src/cli/shared/inline-sourcemap.ts +++ b/packages/sdk/src/cli/shared/inline-sourcemap.ts @@ -5,14 +5,14 @@ import { parseBoolean } from "./parse-boolean"; * * Resolution order: * 1. Config value (`inlineSourcemap` in defineConfig) — if explicitly set - * 2. Environment variable `TAILOR_ENABLE_INLINE_SOURCEMAP` — if explicitly set + * 2. Environment variable `TAILOR_INLINE_SOURCEMAP` — if explicitly set * 3. Default: `true` * @param configValue - The `inlineSourcemap` value from AppConfig * @returns Whether inline sourcemaps should be enabled */ export function resolveInlineSourcemap(configValue?: boolean): boolean { if (configValue !== undefined) return configValue; - const envValue = parseBoolean(process.env.TAILOR_ENABLE_INLINE_SOURCEMAP); + const envValue = parseBoolean(process.env.TAILOR_INLINE_SOURCEMAP); if (envValue !== undefined) return envValue; return true; } diff --git a/packages/sdk/src/cli/shared/platform-bundle-plugin.test.ts b/packages/sdk/src/cli/shared/platform-bundle-plugin.test.ts index e12ad5030..5f8f158d3 100644 --- a/packages/sdk/src/cli/shared/platform-bundle-plugin.test.ts +++ b/packages/sdk/src/cli/shared/platform-bundle-plugin.test.ts @@ -10,7 +10,7 @@ const run = (code: string): string | null => { describe("platformBundleDefinePlugin", () => { test("folds the gate member-expression to true", () => { - expect(run("if (!process.env.TAILOR_PLATFORM_BUNDLE) register();")).toBe( + expect(run("if (!process.env.__TAILOR_PLATFORM_BUNDLE) register();")).toBe( "if (!true) register();", ); }); @@ -20,9 +20,9 @@ describe("platformBundleDefinePlugin", () => { }); test("does not rewrite a longer key or a different owner", () => { - const longerKey = "read(process.env.TAILOR_PLATFORM_BUNDLE_MODE);"; + const longerKey = "read(process.env.__TAILOR_PLATFORM_BUNDLE_MODE);"; expect(run(longerKey)).toBe(longerKey); - const otherOwner = "read(self.process.env.TAILOR_PLATFORM_BUNDLE);"; + const otherOwner = "read(self.process.env.__TAILOR_PLATFORM_BUNDLE);"; expect(run(otherOwner)).toBe(otherOwner); }); }); diff --git a/packages/sdk/src/cli/shared/platform-bundle-plugin.ts b/packages/sdk/src/cli/shared/platform-bundle-plugin.ts index bd784ceea..b87233443 100644 --- a/packages/sdk/src/cli/shared/platform-bundle-plugin.ts +++ b/packages/sdk/src/cli/shared/platform-bundle-plugin.ts @@ -1,9 +1,9 @@ import type * as rolldown from "rolldown"; -// Match the exact `process.env.TAILOR_PLATFORM_BUNDLE` member-expression: the +// Match the exact `process.env.__TAILOR_PLATFORM_BUNDLE` member-expression: the // leading lookbehind rejects a longer owner (`foo.process.env…`) or identifier, // the trailing `\b` rejects a longer key (`…_BUNDLE_MODE`). -const GATE = /(? { ]); }); - test("prefers TAILOR_SDK_SKILLS_SOURCE env var over the passed source", () => { - vi.stubEnv("TAILOR_SDK_SKILLS_SOURCE", "/override/skills"); + test("prefers TAILOR_SKILLS_SOURCE env var over the passed source", () => { + vi.stubEnv("TAILOR_SKILLS_SOURCE", "/override/skills"); try { expect(buildSkillsAddArgs({ source: TEST_SOURCE })[2]).toBe("/override/skills"); } finally { diff --git a/packages/sdk/src/cli/shared/skills-installer.ts b/packages/sdk/src/cli/shared/skills-installer.ts index aacd57d79..77790824d 100644 --- a/packages/sdk/src/cli/shared/skills-installer.ts +++ b/packages/sdk/src/cli/shared/skills-installer.ts @@ -1,7 +1,7 @@ import { spawn } from "node:child_process"; export const SKILL_NAME = "tailor-sdk"; -const SKILLS_SOURCE_ENV_KEY = "TAILOR_SDK_SKILLS_SOURCE"; +const SKILLS_SOURCE_ENV_KEY = "TAILOR_SKILLS_SOURCE"; interface ChildProcessLike { on(event: "close", listener: (code: number | null) => void): ChildProcessLike; diff --git a/packages/sdk/src/cli/shared/type-generator.test.ts b/packages/sdk/src/cli/shared/type-generator.test.ts index d655bcb82..d60b48498 100644 --- a/packages/sdk/src/cli/shared/type-generator.test.ts +++ b/packages/sdk/src/cli/shared/type-generator.test.ts @@ -123,17 +123,17 @@ describe("generateTypeDefinition", () => { }); describe("resolveTypeDefinitionPath", () => { - const originalEnv = process.env.TAILOR_PLATFORM_SDK_DTS_PATH; + const originalEnv = process.env.TAILOR_DTS_PATH; beforeEach(() => { - delete process.env.TAILOR_PLATFORM_SDK_DTS_PATH; + delete process.env.TAILOR_DTS_PATH; }); afterEach(() => { if (originalEnv !== undefined) { - process.env.TAILOR_PLATFORM_SDK_DTS_PATH = originalEnv; + process.env.TAILOR_DTS_PATH = originalEnv; } else { - delete process.env.TAILOR_PLATFORM_SDK_DTS_PATH; + delete process.env.TAILOR_DTS_PATH; } }); @@ -142,14 +142,14 @@ describe("resolveTypeDefinitionPath", () => { expect(result).toBe(path.resolve("/project", "tailor.d.ts")); }); - test("should use TAILOR_PLATFORM_SDK_DTS_PATH when set to an absolute path", () => { - process.env.TAILOR_PLATFORM_SDK_DTS_PATH = "/custom/output/types.d.ts"; + test("should use TAILOR_DTS_PATH when set to an absolute path", () => { + process.env.TAILOR_DTS_PATH = "/custom/output/types.d.ts"; const result = resolveTypeDefinitionPath("/project/tailor.config.ts"); expect(result).toBe("/custom/output/types.d.ts"); }); - test("should resolve TAILOR_PLATFORM_SDK_DTS_PATH relative to cwd when relative", () => { - process.env.TAILOR_PLATFORM_SDK_DTS_PATH = "custom/types.d.ts"; + test("should resolve TAILOR_DTS_PATH relative to cwd when relative", () => { + process.env.TAILOR_DTS_PATH = "custom/types.d.ts"; const result = resolveTypeDefinitionPath("/project/tailor.config.ts"); expect(result).toBe(path.resolve("custom/types.d.ts")); }); diff --git a/packages/sdk/src/cli/shared/type-generator.ts b/packages/sdk/src/cli/shared/type-generator.ts index acaaccedb..1915d21a7 100644 --- a/packages/sdk/src/cli/shared/type-generator.ts +++ b/packages/sdk/src/cli/shared/type-generator.ts @@ -247,14 +247,14 @@ function collectAttributesFromConfig(config: AppConfig): ExtractedAttributes { /** * Resolve the output path for the generated type definition file. * - * When the `TAILOR_PLATFORM_SDK_DTS_PATH` environment variable is set, the value is + * When the `TAILOR_DTS_PATH` environment variable is set, the value is * used as the output path (resolved relative to cwd when relative). * Otherwise, the file is written next to the config file as `tailor.d.ts`. * @param configPath - Path to Tailor config file * @returns Absolute path to the type definition file */ export function resolveTypeDefinitionPath(configPath: string): string { - const envPath = process.env.TAILOR_PLATFORM_SDK_DTS_PATH; + const envPath = process.env.TAILOR_DTS_PATH; if (envPath) { return path.resolve(envPath); } diff --git a/packages/sdk/src/configure/config/index.test.ts b/packages/sdk/src/configure/config/index.test.ts index 55fb02492..1c68dad10 100644 --- a/packages/sdk/src/configure/config/index.test.ts +++ b/packages/sdk/src/configure/config/index.test.ts @@ -14,7 +14,7 @@ describe("defineConfig", () => { test("accepts logLevel from an environment variable fallback", () => { defineConfig({ name: "my-app", - logLevel: process.env.LOG_LEVEL ?? "DEBUG", + logLevel: process.env.TAILOR_APP_LOG_LEVEL ?? "DEBUG", }); }); }); diff --git a/packages/sdk/src/configure/services/workflow/job.ts b/packages/sdk/src/configure/services/workflow/job.ts index f87ed8d8a..b706370f6 100644 --- a/packages/sdk/src/configure/services/workflow/job.ts +++ b/packages/sdk/src/configure/services/workflow/job.ts @@ -96,17 +96,17 @@ export function createWorkflowJob, ): WorkflowJob> { const userBody = config.body as (input: I, context: WorkflowJobContext) => O | Promise; - const body = process.env.TAILOR_PLATFORM_BUNDLE + const body = process.env.__TAILOR_PLATFORM_BUNDLE ? userBody : (input: I, context: WorkflowJobContext): O | Promise => withWorkflowTestInvoker(context.invoker, () => userBody(input, context)); // Test-only local runner registry; the platform bundle sets the flag so it is DCE'd. - if (!process.env.TAILOR_PLATFORM_BUNDLE) { + if (!process.env.__TAILOR_PLATFORM_BUNDLE) { registerJob(config.name, body as RegisteredJobBody); } - const trigger = process.env.TAILOR_PLATFORM_BUNDLE + const trigger = process.env.__TAILOR_PLATFORM_BUNDLE ? () => { throw new Error( "This workflow job's .trigger() is rewritten at build time and is unavailable in the bundle", diff --git a/packages/sdk/src/configure/services/workflow/workflow.ts b/packages/sdk/src/configure/services/workflow/workflow.ts index 57d760273..e0917fe11 100644 --- a/packages/sdk/src/configure/services/workflow/workflow.ts +++ b/packages/sdk/src/configure/services/workflow/workflow.ts @@ -65,7 +65,7 @@ export function createWorkflow>( return brandValue( { ...config, - trigger: process.env.TAILOR_PLATFORM_BUNDLE + trigger: process.env.__TAILOR_PLATFORM_BUNDLE ? async () => { throw new Error( "workflow.trigger() is rewritten at build time and unavailable in the bundle", From c44da394b918bbe539962727349edb6ff08a0d64 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 25 Jun 2026 17:05:45 +0900 Subject: [PATCH 220/618] docs(codemod): add env rename follow-up guidance --- packages/sdk-codemod/src/registry.ts | 7 +++++++ packages/sdk/docs/migration/v2.md | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index c31ae8dc5..e66d4a205 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -233,6 +233,13 @@ export const allCodemods: CodemodPackage[] = [ after: "const apiUrl = process.env.TAILOR_PLATFORM_URL;", }, ], + prompt: [ + "Review any remaining removed SDK environment variable names after the codemod", + "runs. Replace actual environment variable usages with their v2 names, including", + "`TAILOR_TOKEN` -> `TAILOR_PLATFORM_TOKEN`. If a remaining match is an unrelated", + "local identifier, fixture label, or historical documentation that intentionally", + "does not configure the SDK, leave it unchanged.", + ].join("\n"), }, { id: "v2/auth-invoker-unwrap", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index edc17a0c8..e15ce5fb2 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -247,6 +247,19 @@ After: const apiUrl = process.env.TAILOR_PLATFORM_URL; ``` +
+Prompt for an AI agent (to finish the cases the codemod could not migrate) + +```text +Review any remaining removed SDK environment variable names after the codemod +runs. Replace actual environment variable usages with their v2 names, including +`TAILOR_TOKEN` -> `TAILOR_PLATFORM_TOKEN`. If a remaining match is an unrelated +local identifier, fixture label, or historical documentation that intentionally +does not configure the SDK, leave it unchanged. +``` + +
+ ## auth.invoker("name") → invoker: "name" **Migration:** Partially automatic From e8ce6a6197d1034b9377d77746ff137d76ecb1d2 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 25 Jun 2026 17:15:45 +0900 Subject: [PATCH 221/618] fix(codemod): avoid partial env var replacements --- .../codemods/v2/env-var-rename/scripts/transform.ts | 2 +- .../codemods/v2/env-var-rename/tests/no-match/input.sh | 4 +++- .../v2/env-var-rename/tests/shell-expansion/expected.sh | 4 ++++ .../codemods/v2/env-var-rename/tests/shell-expansion/input.sh | 4 ++++ 4 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/tests/shell-expansion/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/env-var-rename/tests/shell-expansion/input.sh diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts index 63199ac3b..a694314fb 100644 --- a/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts @@ -17,7 +17,7 @@ const ENV_RENAMES: ReadonlyArray = [ ]; const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]); -const ENV_BOUNDARY = "[A-Z0-9_]"; +const ENV_BOUNDARY = "[A-Za-z0-9_]"; const RENAME_PATTERNS = ENV_RENAMES.map(([from, to]) => ({ pattern: new RegExp(`(? Date: Thu, 25 Jun 2026 17:24:29 +0900 Subject: [PATCH 222/618] fix(codemod): leave generic env names for review --- .changeset/rename-tailor-cli-env.md | 2 +- .../v2/env-var-rename/scripts/transform.ts | 3 --- .../tests/basic-env/expected.env | 6 +++--- .../tests/basic-markdown/expected.md | 2 +- .../tests/basic-package-json/expected.json | 2 +- .../tests/basic-source/expected.ts | 6 +++--- .../tests/basic-yaml/expected.yml | 2 +- .../tests/shell-expansion/expected.sh | 4 ++-- packages/sdk-codemod/src/registry.ts | 20 ++++++++++--------- packages/sdk/docs/migration/v2.md | 20 ++++++++++--------- 10 files changed, 34 insertions(+), 33 deletions(-) diff --git a/.changeset/rename-tailor-cli-env.md b/.changeset/rename-tailor-cli-env.md index fe94eb4ea..1fe743e4d 100644 --- a/.changeset/rename-tailor-cli-env.md +++ b/.changeset/rename-tailor-cli-env.md @@ -6,4 +6,4 @@ Standardize SDK-owned environment variables on the `TAILOR_*` namespace. -Replace the removed SDK-specific environment variables with their new names: `TAILOR_CONFIG_PATH`, `TAILOR_DTS_PATH`, `TAILOR_CI_ALLOW_ID_INJECTION`, `TAILOR_DEPLOY_BUILD_ONLY`, `TAILOR_BUILD_OUTPUT_DIR`, `TAILOR_SKILLS_SOURCE`, `TAILOR_TEMPLATE_SDK_VERSION`, `TAILOR_PLATFORM_URL`, `TAILOR_PLATFORM_OAUTH2_CLIENT_ID`, `TAILOR_INLINE_SOURCEMAP`, `TAILOR_QUERY_NEWLINE_ON_ENTER`, and `TAILOR_APP_LOG_LEVEL`. The deprecated `TAILOR_TOKEN` fallback is removed; use `TAILOR_PLATFORM_TOKEN`. The v2 codemod rewrites the removed environment variable names to their replacements. +Replace the removed SDK-specific environment variables with their new names: `TAILOR_CONFIG_PATH`, `TAILOR_DTS_PATH`, `TAILOR_CI_ALLOW_ID_INJECTION`, `TAILOR_DEPLOY_BUILD_ONLY`, `TAILOR_BUILD_OUTPUT_DIR`, `TAILOR_SKILLS_SOURCE`, `TAILOR_TEMPLATE_SDK_VERSION`, `TAILOR_PLATFORM_URL`, `TAILOR_PLATFORM_OAUTH2_CLIENT_ID`, `TAILOR_INLINE_SOURCEMAP`, `TAILOR_QUERY_NEWLINE_ON_ENTER`, and `TAILOR_APP_LOG_LEVEL`. The deprecated `TAILOR_TOKEN` fallback is removed; use `TAILOR_PLATFORM_TOKEN`. The v2 codemod rewrites unambiguous removed SDK environment variable names and flags generic names such as `LOG_LEVEL` and `PLATFORM_URL` for manual review. diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts index a694314fb..3e93f9776 100644 --- a/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts @@ -8,11 +8,8 @@ const ENV_RENAMES: ReadonlyArray = [ ["TAILOR_SDK_OUTPUT_DIR", "TAILOR_BUILD_OUTPUT_DIR"], ["TAILOR_SDK_SKILLS_SOURCE", "TAILOR_SKILLS_SOURCE"], ["TAILOR_SDK_VERSION", "TAILOR_TEMPLATE_SDK_VERSION"], - ["PLATFORM_URL", "TAILOR_PLATFORM_URL"], - ["PLATFORM_OAUTH2_CLIENT_ID", "TAILOR_PLATFORM_OAUTH2_CLIENT_ID"], ["TAILOR_ENABLE_INLINE_SOURCEMAP", "TAILOR_INLINE_SOURCEMAP"], ["TAILOR_PLATFORM_QUERY_NEWLINE_ON_ENTER", "TAILOR_QUERY_NEWLINE_ON_ENTER"], - ["LOG_LEVEL", "TAILOR_APP_LOG_LEVEL"], ["TAILOR_TOKEN", "TAILOR_PLATFORM_TOKEN"], ]; diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-env/expected.env b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-env/expected.env index 69d3440dc..40665a69a 100644 --- a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-env/expected.env +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-env/expected.env @@ -5,9 +5,9 @@ TAILOR_DEPLOY_BUILD_ONLY=true TAILOR_BUILD_OUTPUT_DIR=.tailor-sdk TAILOR_SKILLS_SOURCE=./skills TAILOR_TEMPLATE_SDK_VERSION=2.0.0-next.2 -TAILOR_PLATFORM_URL=https://api.staging.tailor.tech -TAILOR_PLATFORM_OAUTH2_CLIENT_ID=client-id +PLATFORM_URL=https://api.staging.tailor.tech +PLATFORM_OAUTH2_CLIENT_ID=client-id TAILOR_INLINE_SOURCEMAP=false TAILOR_QUERY_NEWLINE_ON_ENTER=false -TAILOR_APP_LOG_LEVEL=DEBUG +LOG_LEVEL=DEBUG TAILOR_PLATFORM_TOKEN=token diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-markdown/expected.md b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-markdown/expected.md index 9ba18037e..8a66a45f5 100644 --- a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-markdown/expected.md +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-markdown/expected.md @@ -1,5 +1,5 @@ Configure the SDK with `TAILOR_CONFIG_PATH`. ```sh -TAILOR_BUILD_OUTPUT_DIR=.tailor-sdk TAILOR_APP_LOG_LEVEL=DEBUG tailor-sdk generate +TAILOR_BUILD_OUTPUT_DIR=.tailor-sdk LOG_LEVEL=DEBUG tailor-sdk generate ``` diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-package-json/expected.json b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-package-json/expected.json index 0d38dbe8e..f921b7ad7 100644 --- a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-package-json/expected.json +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-package-json/expected.json @@ -5,6 +5,6 @@ }, "config": { "TAILOR_TEMPLATE_SDK_VERSION": "workspace:*", - "TAILOR_PLATFORM_URL": "https://api.tailor.test" + "PLATFORM_URL": "https://api.tailor.test" } } diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/expected.ts b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/expected.ts index c97133b21..f892bb336 100644 --- a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/expected.ts +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/expected.ts @@ -1,8 +1,8 @@ const configPath = process.env.TAILOR_CONFIG_PATH; const dtsPath = process.env["TAILOR_DTS_PATH"]; -const baseUrl = process.env.TAILOR_PLATFORM_URL ?? "https://api.tailor.tech"; -const logLevel = process.env.TAILOR_APP_LOG_LEVEL ?? "DEBUG"; +const baseUrl = process.env.PLATFORM_URL ?? "https://api.tailor.tech"; +const logLevel = process.env.LOG_LEVEL ?? "DEBUG"; const token = process.env.TAILOR_PLATFORM_TOKEN; -const env = { TAILOR_APP_LOG_LEVEL: "DEBUG", TAILOR_PLATFORM_TOKEN: token }; +const env = { LOG_LEVEL: "DEBUG", TAILOR_PLATFORM_TOKEN: token }; const LOG_LEVEL = "local"; const unchanged = process.env.MY_LOG_LEVEL ?? process.env.TAILOR_TOKEN_BACKUP; diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-yaml/expected.yml b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-yaml/expected.yml index 0280c0c4d..e8611b398 100644 --- a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-yaml/expected.yml +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-yaml/expected.yml @@ -5,4 +5,4 @@ jobs: TAILOR_DEPLOY_BUILD_ONLY: "true" TAILOR_CI_ALLOW_ID_INJECTION: "true" steps: - - run: TAILOR_PLATFORM_OAUTH2_CLIENT_ID=client TAILOR_PLATFORM_URL=https://api.tailor.test tailor-sdk login + - run: PLATFORM_OAUTH2_CLIENT_ID=client PLATFORM_URL=https://api.tailor.test tailor-sdk login diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/shell-expansion/expected.sh b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/shell-expansion/expected.sh index dacceb44a..eff853131 100644 --- a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/shell-expansion/expected.sh +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/shell-expansion/expected.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -echo "$TAILOR_APP_LOG_LEVEL" -echo "${TAILOR_PLATFORM_URL}" +echo "$LOG_LEVEL" +echo "${PLATFORM_URL}" echo "${TAILOR_PLATFORM_TOKEN:-missing}" diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index e66d4a205..06abdf8bd 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -196,7 +196,7 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/env-var-rename", name: "SDK environment variable rename", description: - "Rewrite removed SDK environment variable names to their v2 `TAILOR_*` names across scripts, CI configs, docs, env files, and source files", + "Rewrite unambiguous removed SDK environment variable names to their v2 `TAILOR_*` names and flag generic names for manual review", since: "1.0.0", until: "2.0.0", scriptPath: "v2/env-var-rename/scripts/transform.js", @@ -225,20 +225,22 @@ export const allCodemods: CodemodPackage[] = [ examples: [ { lang: "sh", - before: "TAILOR_PLATFORM_SDK_BUILD_ONLY=true LOG_LEVEL=DEBUG tailor-sdk deploy", - after: "TAILOR_DEPLOY_BUILD_ONLY=true TAILOR_APP_LOG_LEVEL=DEBUG tailor-sdk deploy", + before: "TAILOR_PLATFORM_SDK_BUILD_ONLY=true tailor-sdk deploy", + after: "TAILOR_DEPLOY_BUILD_ONLY=true tailor-sdk deploy", }, { - before: "const apiUrl = process.env.PLATFORM_URL;", - after: "const apiUrl = process.env.TAILOR_PLATFORM_URL;", + before: "const token = process.env.TAILOR_TOKEN;", + after: "const token = process.env.TAILOR_PLATFORM_TOKEN;", }, ], prompt: [ "Review any remaining removed SDK environment variable names after the codemod", - "runs. Replace actual environment variable usages with their v2 names, including", - "`TAILOR_TOKEN` -> `TAILOR_PLATFORM_TOKEN`. If a remaining match is an unrelated", - "local identifier, fixture label, or historical documentation that intentionally", - "does not configure the SDK, leave it unchanged.", + "runs. The codemod intentionally leaves generic names such as `LOG_LEVEL`,", + "`PLATFORM_URL`, and `PLATFORM_OAUTH2_CLIENT_ID` for manual review because", + "they can configure non-SDK tools. Replace only actual SDK usages with their", + "v2 names. If a remaining match is an unrelated local identifier, fixture", + "label, or historical documentation that intentionally does not configure the", + "SDK, leave it unchanged.", ].join("\n"), }, { diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index e15ce5fb2..8a4c57b2f 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -221,30 +221,30 @@ happen to use `--machineuser` alone. **Migration:** Partially automatic -Rewrite removed SDK environment variable names to their v2 `TAILOR_*` names across scripts, CI configs, docs, env files, and source files +Rewrite unambiguous removed SDK environment variable names to their v2 `TAILOR_*` names and flag generic names for manual review Before: ```sh -TAILOR_PLATFORM_SDK_BUILD_ONLY=true LOG_LEVEL=DEBUG tailor-sdk deploy +TAILOR_PLATFORM_SDK_BUILD_ONLY=true tailor-sdk deploy ``` After: ```sh -TAILOR_DEPLOY_BUILD_ONLY=true TAILOR_APP_LOG_LEVEL=DEBUG tailor-sdk deploy +TAILOR_DEPLOY_BUILD_ONLY=true tailor-sdk deploy ``` Before: ```ts -const apiUrl = process.env.PLATFORM_URL; +const token = process.env.TAILOR_TOKEN; ``` After: ```ts -const apiUrl = process.env.TAILOR_PLATFORM_URL; +const token = process.env.TAILOR_PLATFORM_TOKEN; ```
@@ -252,10 +252,12 @@ const apiUrl = process.env.TAILOR_PLATFORM_URL; ```text Review any remaining removed SDK environment variable names after the codemod -runs. Replace actual environment variable usages with their v2 names, including -`TAILOR_TOKEN` -> `TAILOR_PLATFORM_TOKEN`. If a remaining match is an unrelated -local identifier, fixture label, or historical documentation that intentionally -does not configure the SDK, leave it unchanged. +runs. The codemod intentionally leaves generic names such as `LOG_LEVEL`, +`PLATFORM_URL`, and `PLATFORM_OAUTH2_CLIENT_ID` for manual review because +they can configure non-SDK tools. Replace only actual SDK usages with their +v2 names. If a remaining match is an unrelated local identifier, fixture +label, or historical documentation that intentionally does not configure the +SDK, leave it unchanged. ```
From 000940b91a95eeb567aaa4d9084f790e794bbd4b Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 25 Jun 2026 17:34:39 +0900 Subject: [PATCH 223/618] fix(codemod): rewrite env vars in source strings --- .../codemods/v2/env-var-rename/scripts/transform.ts | 6 +++++- .../v2/env-var-rename/tests/basic-source/expected.ts | 1 + .../codemods/v2/env-var-rename/tests/basic-source/input.ts | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts index 3e93f9776..eecfe5390 100644 --- a/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts @@ -19,6 +19,7 @@ const RENAME_PATTERNS = ENV_RENAMES.map(([from, to]) => ({ pattern: new RegExp(`(? { + const replaced = replaceTextTokens(body); + return replaced === body ? match : `${quote}${replaced}${quote}`; + }); } export default function transform(source: string, filePath: string): string | null { diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/expected.ts b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/expected.ts index f892bb336..7563b73c8 100644 --- a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/expected.ts +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/expected.ts @@ -4,5 +4,6 @@ const baseUrl = process.env.PLATFORM_URL ?? "https://api.tailor.tech"; const logLevel = process.env.LOG_LEVEL ?? "DEBUG"; const token = process.env.TAILOR_PLATFORM_TOKEN; const env = { LOG_LEVEL: "DEBUG", TAILOR_PLATFORM_TOKEN: token }; +const command = "TAILOR_DEPLOY_BUILD_ONLY=true tailor-sdk deploy"; const LOG_LEVEL = "local"; const unchanged = process.env.MY_LOG_LEVEL ?? process.env.TAILOR_TOKEN_BACKUP; diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/input.ts b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/input.ts index e13ebdbb4..c7dd9209d 100644 --- a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/input.ts +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/input.ts @@ -4,5 +4,6 @@ const baseUrl = process.env.PLATFORM_URL ?? "https://api.tailor.tech"; const logLevel = process.env.LOG_LEVEL ?? "DEBUG"; const token = process.env.TAILOR_TOKEN; const env = { LOG_LEVEL: "DEBUG", TAILOR_TOKEN: token }; +const command = "TAILOR_PLATFORM_SDK_BUILD_ONLY=true tailor-sdk deploy"; const LOG_LEVEL = "local"; const unchanged = process.env.MY_LOG_LEVEL ?? process.env.TAILOR_TOKEN_BACKUP; From 2aebe36eeb777ad4624c6fe94e89ed139b2972de Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 25 Jun 2026 17:48:22 +0900 Subject: [PATCH 224/618] fix(codemod): preserve template expressions during env rename --- .../v2/env-var-rename/scripts/transform.ts | 52 ++++++++++++++++--- .../tests/basic-source/expected.ts | 2 + .../tests/basic-source/input.ts | 2 + packages/sdk-codemod/src/runner.test.ts | 34 ++++++++++++ packages/sdk-codemod/src/runner.ts | 10 ++++ 5 files changed, 92 insertions(+), 8 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts index eecfe5390..8ad057c0b 100644 --- a/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/scripts/transform.ts @@ -1,4 +1,6 @@ +import { parse, Lang } from "@ast-grep/napi"; import * as path from "pathe"; +import type { SgNode } from "@ast-grep/napi"; const ENV_RENAMES: ReadonlyArray = [ ["TAILOR_PLATFORM_SDK_CONFIG_PATH", "TAILOR_CONFIG_PATH"], @@ -19,7 +21,6 @@ const RENAME_PATTERNS = ENV_RENAMES.map(([from, to]) => ({ pattern: new RegExp(`(? { + const edits: Array<[number, number, string]> = []; + const visit = (node: SgNode): void => { + if (node.kind() === "string_fragment") { + const range = node.range(); + const text = source.slice(range.start.index, range.end.index); + const replacement = replaceTextTokens(text); + if (replacement !== text) edits.push([range.start.index, range.end.index, replacement]); + return; + } + for (const child of node.children()) { + visit(child); + } + }; + visit(root); + return edits; +} + +function replaceSourceStringFragments(source: string, filePath: string): string { + let root: SgNode; + try { + root = parse(sourceLang(filePath), source).root(); + } catch { + return source; + } + + let updated = source; + const edits = collectStringFragmentEdits(root, source).toSorted(([a], [b]) => b - a); + for (const [start, end, replacement] of edits) { + updated = `${updated.slice(0, start)}${replacement}${updated.slice(end)}`; + } + return updated; +} + +function replaceSourceTokens(source: string, filePath: string): string { let updated = source; for (const [from, to] of ENV_RENAMES) { const escaped = escapeRegExp(from); @@ -46,19 +86,15 @@ function replaceSourceTokens(source: string): string { new RegExp(`\\bprocess\\.env\\[(["'\`])${escaped}\\1\\]`, "g"), `process.env[$1${to}$1]`, ) - .replace(new RegExp(`(["'\`])${escaped}\\1`, "g"), `$1${to}$1`) .replace(new RegExp(`([,{]\\s*)${escaped}(?=\\s*:)`, "g"), `$1${to}`); } - return updated.replace(STRING_LITERAL_PATTERN, (match, quote: string, body: string) => { - const replaced = replaceTextTokens(body); - return replaced === body ? match : `${quote}${replaced}${quote}`; - }); + return replaceSourceStringFragments(updated, filePath); } export default function transform(source: string, filePath: string): string | null { const ext = path.extname(filePath).toLowerCase(); const updated = SOURCE_EXTENSIONS.has(ext) - ? replaceSourceTokens(source) + ? replaceSourceTokens(source, filePath) : replaceTextTokens(source); return updated === source ? null : updated; } diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/expected.ts b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/expected.ts index 7563b73c8..10e19de4a 100644 --- a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/expected.ts +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/expected.ts @@ -5,5 +5,7 @@ const logLevel = process.env.LOG_LEVEL ?? "DEBUG"; const token = process.env.TAILOR_PLATFORM_TOKEN; const env = { LOG_LEVEL: "DEBUG", TAILOR_PLATFORM_TOKEN: token }; const command = "TAILOR_DEPLOY_BUILD_ONLY=true tailor-sdk deploy"; +const templateCommand = `TAILOR_DEPLOY_BUILD_ONLY=${buildOnly}`; +const localInterpolation = `${TAILOR_TOKEN}`; const LOG_LEVEL = "local"; const unchanged = process.env.MY_LOG_LEVEL ?? process.env.TAILOR_TOKEN_BACKUP; diff --git a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/input.ts b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/input.ts index c7dd9209d..c518f8301 100644 --- a/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/input.ts +++ b/packages/sdk-codemod/codemods/v2/env-var-rename/tests/basic-source/input.ts @@ -5,5 +5,7 @@ const logLevel = process.env.LOG_LEVEL ?? "DEBUG"; const token = process.env.TAILOR_TOKEN; const env = { LOG_LEVEL: "DEBUG", TAILOR_TOKEN: token }; const command = "TAILOR_PLATFORM_SDK_BUILD_ONLY=true tailor-sdk deploy"; +const templateCommand = `TAILOR_PLATFORM_SDK_BUILD_ONLY=${buildOnly}`; +const localInterpolation = `${TAILOR_TOKEN}`; const LOG_LEVEL = "local"; const unchanged = process.env.MY_LOG_LEVEL ?? process.env.TAILOR_TOKEN_BACKUP; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 37c8b1893..564696ef2 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -428,6 +428,40 @@ describe("runCodemods", () => { ]); }); + test("keeps legacy warnings for process.env bracket keys in source files", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "env.ts"), + [ + 'const platformUrl = process.env["PLATFORM_URL"];', + "const logLevel = process.env[`LOG_LEVEL`];", + 'const unrelated = "LOG_LEVEL";', + ].join("\n"), + "utf-8", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod( + "test/env", + partialTransformPath, + ["**/*.ts"], + ["PLATFORM_URL", "LOG_LEVEL"], + ), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toEqual([ + "env.ts: contains PLATFORM_URL, LOG_LEVEL but was not migrated automatically (rule: test/env). Manual migration may be needed.", + ]); + }); + test("flags files matching a suspicious pattern for LLM review", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 3c4887141..91ebb454f 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -150,10 +150,20 @@ function sourceLang(relative: string): Lang { return ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : Lang.TypeScript; } +function isProcessEnvSubscriptKey(node: SgNode): boolean { + const stringNode = node.kind() === "string_fragment" ? node.parent() : node; + if (stringNode == null || !["string", "template_string"].includes(stringNode.kind())) { + return false; + } + const parent = stringNode.parent(); + return parent?.kind() === "subscript_expression" && /^process\.env\s*\[/.test(parent.text()); +} + function collectMaskedRanges(root: SgNode): Array<[number, number]> { const ranges: Array<[number, number]> = []; const visit = (node: SgNode): void => { if (MASKED_SOURCE_NODE_KINDS.has(node.kind())) { + if (isProcessEnvSubscriptKey(node)) return; const range = node.range(); ranges.push([range.start.index, range.end.index]); return; From 684a1d1ff42704c2c35b161de1ae166f3489e54e Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 25 Jun 2026 18:02:19 +0900 Subject: [PATCH 225/618] fix(codemod): warn on generic env names in source strings --- packages/sdk-codemod/src/migration-doc.ts | 4 +- packages/sdk-codemod/src/registry.test.ts | 3 ++ packages/sdk-codemod/src/registry.ts | 1 + packages/sdk-codemod/src/runner.test.ts | 33 ++++++++++++++- packages/sdk-codemod/src/runner.ts | 51 ++++++++++++++++++++--- packages/sdk-codemod/src/types.ts | 7 ++++ 6 files changed, 91 insertions(+), 8 deletions(-) diff --git a/packages/sdk-codemod/src/migration-doc.ts b/packages/sdk-codemod/src/migration-doc.ts index b12be0743..8c3cd7d9a 100644 --- a/packages/sdk-codemod/src/migration-doc.ts +++ b/packages/sdk-codemod/src/migration-doc.ts @@ -6,7 +6,8 @@ export type AutomationLevel = "Automatic" | "Partially automatic" | "Manual"; * Classify how much of a migration the codemod automates. * - `Automatic`: a transform fully covers it, with no residual to flag. * - `Partially automatic`: a transform covers the common cases but flags - * residuals (via `legacyPatterns`/`suspiciousPatterns`/`prompt`) to finish. + * residuals (via `legacyPatterns`/`sourceStringLegacyPatterns`/ + * `suspiciousPatterns`/`prompt`) to finish. * - `Manual`: no transform; the change is migrated by hand (optionally guided * by a `prompt`). Whether a person or an LLM does it does not matter here. * @param codemod - The codemod registry entry @@ -16,6 +17,7 @@ export function automationLevel(codemod: CodemodPackage): AutomationLevel { if (!codemod.scriptPath) return "Manual"; const flagsResidual = (codemod.legacyPatterns?.length ?? 0) > 0 || + (codemod.sourceStringLegacyPatterns?.length ?? 0) > 0 || (codemod.suspiciousPatterns?.length ?? 0) > 0 || codemod.prompt != null; return flagsResidual ? "Partially automatic" : "Automatic"; diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 5803135b2..301045f83 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -50,6 +50,9 @@ describe("getApplicableCodemods", () => { ); expect(envVarRename?.legacyPatterns).toContain("TAILOR_PLATFORM_SDK_CONFIG_PATH"); expect(envVarRename?.legacyPatterns).toContain("TAILOR_TOKEN"); + expect(envVarRename?.sourceStringLegacyPatterns).toEqual( + expect.arrayContaining(["PLATFORM_URL", "PLATFORM_OAUTH2_CLIENT_ID", "LOG_LEVEL"]), + ); }); test("flags CommonJS TypeScript files for runtime globals review", () => { diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 06abdf8bd..9b2a7cd79 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -222,6 +222,7 @@ export const allCodemods: CodemodPackage[] = [ "LOG_LEVEL", "TAILOR_TOKEN", ], + sourceStringLegacyPatterns: ["PLATFORM_URL", "PLATFORM_OAUTH2_CLIENT_ID", "LOG_LEVEL"], examples: [ { lang: "sh", diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 564696ef2..457ff906f 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -26,7 +26,7 @@ function makeCodemod( scriptPath?: string, filePatterns?: string[], legacyPatterns?: Array, - extra?: Pick, + extra?: Pick, ): CodemodPackage { return { id, @@ -462,6 +462,37 @@ describe("runCodemods", () => { ]); }); + test("keeps opt-in legacy warnings for source string fragments", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "env.ts"), + [ + 'import { execSync } from "node:child_process";', + 'execSync("PLATFORM_URL=https://api.test LOG_LEVEL=DEBUG tailor-sdk login");', + "// PLATFORM_URL in a comment stays ignored.", + ].join("\n"), + "utf-8", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/env", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: ["PLATFORM_URL", "LOG_LEVEL"], + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toEqual([ + "env.ts: contains PLATFORM_URL, LOG_LEVEL but was not migrated automatically (rule: test/env). Manual migration may be needed.", + ]); + }); + test("flags files matching a suspicious pattern for LLM review", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 91ebb454f..26720e266 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -136,6 +136,7 @@ interface LoadedTransform { transform?: TransformFn; matches: (relativePath: string) => boolean; legacyPatterns: CodemodPatternGroup[]; + sourceStringLegacyPatterns: CodemodPatternGroup[]; suspiciousPatterns: CodemodPatternGroup[]; prompt?: string; } @@ -145,6 +146,31 @@ function contentForResidualMatching(relative: string, content: string): string { return SOURCE_EXTENSIONS.has(ext) ? maskSourceNonCode(relative, content) : content; } +function sourceStringContentForResidualMatching(relative: string, content: string): string | null { + const ext = path.extname(relative).toLowerCase(); + if (!SOURCE_EXTENSIONS.has(ext)) return null; + + let root: SgNode; + try { + root = parse(sourceLang(relative), content).root(); + } catch { + return null; + } + + const fragments: string[] = []; + const visit = (node: SgNode): void => { + if (node.kind() === "string_fragment") { + fragments.push(node.text()); + return; + } + for (const child of node.children()) { + visit(child); + } + }; + visit(root); + return fragments.join("\n"); +} + function sourceLang(relative: string): Lang { const ext = path.extname(relative).toLowerCase(); return ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : Lang.TypeScript; @@ -235,15 +261,24 @@ function matchResidualPattern(content: string, pattern: CodemodPatternGroup): st function legacyPatternWarnings( relative: string, content: string, + sourceStringContent: string | null, transforms: LoadedTransform[], ): string[] { return transforms.flatMap((lt) => { - const found = lt.legacyPatterns - .map((p) => matchResidualPattern(content, p)) - .filter((label): label is string => label !== null); - if (found.length === 0) return []; + const found = new Set( + lt.legacyPatterns + .map((p) => matchResidualPattern(content, p)) + .filter((label): label is string => label !== null), + ); + if (sourceStringContent != null) { + for (const pattern of lt.sourceStringLegacyPatterns) { + const label = matchResidualPattern(sourceStringContent, pattern); + if (label != null) found.add(label); + } + } + if (found.size === 0) return []; return [ - `${relative}: contains ${found.join(", ")} but was not migrated automatically (rule: ${lt.id}). Manual migration may be needed.`, + `${relative}: contains ${Array.from(found).join(", ")} but was not migrated automatically (rule: ${lt.id}). Manual migration may be needed.`, ]; }); } @@ -273,6 +308,7 @@ export async function runCodemods( transform: scriptPath ? await loadTransform(scriptPath) : undefined, matches: picomatch(patterns, { dot: true }), legacyPatterns: codemod.legacyPatterns ?? [], + sourceStringLegacyPatterns: codemod.sourceStringLegacyPatterns ?? [], suspiciousPatterns: codemod.suspiciousPatterns ?? [], prompt: codemod.prompt, }); @@ -320,7 +356,10 @@ export async function runCodemods( } const residualContent = contentForResidualMatching(relative, current); - warnings.push(...legacyPatternWarnings(relative, residualContent, matchedTransforms)); + const sourceStringContent = sourceStringContentForResidualMatching(relative, current); + warnings.push( + ...legacyPatternWarnings(relative, residualContent, sourceStringContent, matchedTransforms), + ); for (const lt of matchedTransforms) { if (!lt.prompt || lt.suspiciousPatterns.length === 0) continue; diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index 9dc9efbd6..34965ebd6 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -48,6 +48,13 @@ export interface CodemodPackage { * string patterns must match token boundaries. */ legacyPatterns?: CodemodPatternGroup[]; + /** + * Patterns to detect only inside string/template fragments of source files + * after a transform runs. Use this when a migration normally masks source + * strings for residual matching, but selected string content still needs a + * manual follow-up warning. + */ + sourceStringLegacyPatterns?: CodemodPatternGroup[]; /** * Patterns that, when present in a file's post-transform content, mark it * as a candidate for LLM-assisted review. Use this for migrations the From 368ad9164b5baa1801657ec5f03d978730d1c193 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 18:12:45 +0900 Subject: [PATCH 226/618] fix(sdk): handle tsconfig path aliases and directory barrel imports in ts-hook - Resolve tsconfig path aliases (e.g. @/tailordb/user) by walking up the directory tree to find tsconfig.json, following extends chains - Handle ERR_UNSUPPORTED_DIR_IMPORT to resolve ./models -> ./models/index.ts - Update example/migrations/0004/diff.json to remove spurious field_modified entries caused by whitespace differences between tsx and amaro output - Update README and changeset to reflect Node.js 22.15.0 minimum --- .changeset/remove-tsx.md | 8 +- example/migrations/0004/diff.json | 185 +-------------------------- packages/sdk/README.md | 2 +- packages/sdk/src/cli/ts-hook.mjs | 185 ++++++++++++++++++++++++++- packages/sdk/src/cli/ts-hook.test.ts | 76 +++++++++++ 5 files changed, 261 insertions(+), 195 deletions(-) diff --git a/.changeset/remove-tsx.md b/.changeset/remove-tsx.md index 9f19d8dd8..3ca872d9c 100644 --- a/.changeset/remove-tsx.md +++ b/.changeset/remove-tsx.md @@ -8,9 +8,11 @@ Removes `tsx` (which pulled in esbuild's native binaries, ~10.5 MB) from `dependencies` and replaces it with `amaro` (~3.8 MB, zero transitive deps). A small `ts-hook.mjs` provides the Node.js module hook with both a resolver -(`.ts` extension fallback) and a load hook (`amaro` for full TypeScript -support including enums). Dev-only scripts now use -`node --experimental-strip-types` instead. +and a load hook (`amaro` for full TypeScript support including enums). +The resolver handles `.ts` extension fallback, directory barrel imports +(`./models` → `./models/index.ts`), and tsconfig `paths` aliases (reads +`tsconfig.json` following `extends` chains). +Dev-only scripts now use `node --experimental-strip-types` instead. Raises the minimum Node.js version to 22.15.0 (from >=22) to use `module.registerHooks()`, which allows synchronous hook registration directly diff --git a/example/migrations/0004/diff.json b/example/migrations/0004/diff.json index a38bf0c38..0c6caac98 100644 --- a/example/migrations/0004/diff.json +++ b/example/migrations/0004/diff.json @@ -2,190 +2,7 @@ "version": 1, "namespace": "tailordb", "createdAt": "2026-06-24T04:44:47.663Z", - "changes": [ - { - "kind": "field_modified", - "typeName": "Customer", - "fieldName": "name", - "before": { - "type": "string", - "required": true, - "validate": [ - { - "script": { - "expr": "(({value})=>value.length>5)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" - }, - "errorMessage": "Name must be longer than 5 characters" - } - ] - }, - "after": { - "type": "string", - "required": true, - "validate": [ - { - "script": { - "expr": "(({ value })=>value.length > 5)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" - }, - "errorMessage": "Name must be longer than 5 characters" - } - ] - } - }, - { - "kind": "field_modified", - "typeName": "Customer", - "fieldName": "city", - "before": { - "type": "string", - "required": false, - "validate": [ - { - "script": { - "expr": "(({value})=>value?value.length>1:true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" - }, - "errorMessage": "failed by `({value})=>value?value.length>1:true`" - }, - { - "script": { - "expr": "(({value})=>value?value.length<100:true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" - }, - "errorMessage": "failed by `({value})=>value?value.length<100:true`" - } - ] - }, - "after": { - "type": "string", - "required": false, - "validate": [ - { - "script": { - "expr": "(({ value })=>value ? value.length > 1 : true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" - }, - "errorMessage": "failed by `({ value })=>value ? value.length > 1 : true`" - }, - { - "script": { - "expr": "(({ value })=>value ? value.length < 100 : true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" - }, - "errorMessage": "failed by `({ value })=>value ? value.length < 100 : true`" - } - ] - } - }, - { - "kind": "field_modified", - "typeName": "Customer", - "fieldName": "fullAddress", - "before": { - "type": "string", - "required": true, - "hooks": { - "create": { - "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" - }, - "update": { - "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" - } - } - }, - "after": { - "type": "string", - "required": true, - "hooks": { - "create": { - "expr": "(({ data })=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" - }, - "update": { - "expr": "(({ data })=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "PurchaseOrder", - "fieldName": "attachedFiles", - "before": { - "type": "nested", - "required": true, - "array": true, - "fields": { - "id": { - "type": "uuid", - "required": true - }, - "name": { - "type": "string", - "required": true - }, - "size": { - "type": "integer", - "required": true, - "validate": [ - { - "script": { - "expr": "(({value})=>value>0)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" - }, - "errorMessage": "failed by `({value})=>value>0`" - } - ] - }, - "type": { - "type": "enum", - "required": true, - "allowedValues": [ - { - "value": "text" - }, - { - "value": "image" - } - ] - } - } - }, - "after": { - "type": "nested", - "required": true, - "array": true, - "fields": { - "id": { - "type": "uuid", - "required": true - }, - "name": { - "type": "string", - "required": true - }, - "size": { - "type": "integer", - "required": true, - "validate": [ - { - "script": { - "expr": "(({ value })=>value > 0)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" - }, - "errorMessage": "failed by `({ value })=>value > 0`" - } - ] - }, - "type": { - "type": "enum", - "required": true, - "allowedValues": [ - { - "value": "text" - }, - { - "value": "image" - } - ] - } - } - } - } - ], + "changes": [], "hasBreakingChanges": false, "breakingChanges": [], "hasWarnings": false, diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 71e4bc1a7..72ff02168 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -96,5 +96,5 @@ See [Create Tailor Platform SDK](https://github.com/tailor-platform/sdk/tree/mai ## Requirements -- Node.js 22 or later (or Bun) +- Node.js 22.15.0 or later (or Bun) - A Tailor Platform account ([request access](https://www.tailor.tech/demo)) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 45fbf0791..90648ddd1 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -1,24 +1,164 @@ import { readFileSync } from "node:fs"; import { readFile } from "node:fs/promises"; -import { fileURLToPath } from "node:url"; +import { dirname, join, resolve as resolvePath } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; // Node.js module hook: TypeScript resolver + type stripper via amaro. // Registered programmatically so the CLI can import user .ts files without // requiring --experimental-strip-types at process startup. import { transformSync } from "amaro"; -const TS_EXTENSIONS = [".ts", ".tsx", ".mts"]; +const TS_EXTENSIONS = [".ts", ".mts"]; const JS_TO_TS = new Map([ [".js", ".ts"], - [".jsx", ".tsx"], [".mjs", ".mts"], ]); +// --- tsconfig paths resolution --- + +const tsconfigPathsCache = new Map(); + +function collectPathsInto(out, baseDir, content, visited) { + const id = join(baseDir, "tsconfig.json"); + if (visited.has(id)) return; + visited.add(id); + + const extendsField = content.extends; + if (typeof extendsField === "string") { + const base = resolvePath(baseDir, extendsField); + const extendsPath = base.endsWith(".json") ? base : base + ".json"; + try { + const sub = JSON.parse(readFileSync(extendsPath, "utf-8")); + collectPathsInto(out, dirname(extendsPath), sub, visited); + } catch { + // extends file not readable; skip + } + } + + const opts = content.compilerOptions ?? {}; + const rawPaths = opts.paths; + const baseUrl = opts.baseUrl; + if (rawPaths && baseUrl) { + const absBase = resolvePath(baseDir, baseUrl); + for (const [alias, targets] of Object.entries(rawPaths)) { + out[alias] = targets.map((t) => resolvePath(absBase, t)); + } + } +} + +function loadTsconfigPaths(startDir) { + if (tsconfigPathsCache.has(startDir)) return tsconfigPathsCache.get(startDir); + + const paths = {}; + let dir = startDir; + let prev = ""; + while (dir !== prev) { + try { + const content = JSON.parse(readFileSync(join(dir, "tsconfig.json"), "utf-8")); + collectPathsInto(paths, dir, content, new Set()); + break; + } catch { + // tsconfig.json not found in this directory; walk up + } + prev = dir; + dir = dirname(dir); + } + + tsconfigPathsCache.set(startDir, paths); + return paths; +} + +function matchTsconfigPath(specifier, paths) { + for (const [alias, targets] of Object.entries(paths)) { + if (alias.endsWith("/*")) { + const prefix = alias.slice(0, -2); + if (specifier.startsWith(prefix + "/")) { + const rest = specifier.slice(prefix.length + 1); + for (const target of targets) { + return target.endsWith("/*") ? target.slice(0, -2) + "/" + rest : target; + } + } + } else if (alias === specifier && targets.length > 0) { + return targets[0]; + } + } + return null; +} + +async function tryResolveWithExtensions(base, context, nextResolve) { + for (const ext of TS_EXTENSIONS) { + for (const suffix of ["", "/index"]) { + try { + return await nextResolve(base + suffix + ext, context); + } catch (e) { + if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + } + } + } + try { + return await nextResolve(base, context); + } catch (e) { + if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + } + return null; +} + +function tryResolveWithExtensionsSync(base, context, nextResolve) { + for (const ext of TS_EXTENSIONS) { + for (const suffix of ["", "/index"]) { + try { + return nextResolve(base + suffix + ext, context); + } catch (e) { + if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + } + } + } + try { + return nextResolve(base, context); + } catch (e) { + if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + } + return null; +} + +// --- module hooks --- + export async function resolve(specifier, context, nextResolve) { try { return await nextResolve(specifier, context); } catch (err) { - if (err.code !== "ERR_MODULE_NOT_FOUND") throw err; - if (!specifier.startsWith(".") && !specifier.startsWith("/")) throw err; + if (err.code !== "ERR_MODULE_NOT_FOUND" && err.code !== "ERR_UNSUPPORTED_DIR_IMPORT") throw err; + + if ( + err.code === "ERR_UNSUPPORTED_DIR_IMPORT" && + (specifier.startsWith(".") || specifier.startsWith("/")) + ) { + for (const ext of TS_EXTENSIONS) { + try { + return await nextResolve(specifier + "/index" + ext, context); + } catch (e) { + if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + } + } + throw err; + } + + if (!specifier.startsWith(".") && !specifier.startsWith("/")) { + // Non-relative: try tsconfig path aliases + if (context.parentURL?.startsWith("file://")) { + const parentDir = dirname(fileURLToPath(context.parentURL)); + const tsconfigPaths = loadTsconfigPaths(parentDir); + const mapped = matchTsconfigPath(specifier, tsconfigPaths); + if (mapped) { + const result = await tryResolveWithExtensions( + pathToFileURL(mapped).href, + context, + nextResolve, + ); + if (result) return result; + } + } + throw err; + } const lastSegment = specifier.split("/").pop() ?? ""; if (!lastSegment.includes(".")) { @@ -65,8 +205,39 @@ export function resolveSync(specifier, context, nextResolve) { try { return nextResolve(specifier, context); } catch (err) { - if (err.code !== "ERR_MODULE_NOT_FOUND") throw err; - if (!specifier.startsWith(".") && !specifier.startsWith("/")) throw err; + if (err.code !== "ERR_MODULE_NOT_FOUND" && err.code !== "ERR_UNSUPPORTED_DIR_IMPORT") throw err; + + if ( + err.code === "ERR_UNSUPPORTED_DIR_IMPORT" && + (specifier.startsWith(".") || specifier.startsWith("/")) + ) { + for (const ext of TS_EXTENSIONS) { + try { + return nextResolve(specifier + "/index" + ext, context); + } catch (e) { + if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + } + } + throw err; + } + + if (!specifier.startsWith(".") && !specifier.startsWith("/")) { + // Non-relative: try tsconfig path aliases + if (context.parentURL?.startsWith("file://")) { + const parentDir = dirname(fileURLToPath(context.parentURL)); + const tsconfigPaths = loadTsconfigPaths(parentDir); + const mapped = matchTsconfigPath(specifier, tsconfigPaths); + if (mapped) { + const result = tryResolveWithExtensionsSync( + pathToFileURL(mapped).href, + context, + nextResolve, + ); + if (result) return result; + } + } + throw err; + } const lastSegment = specifier.split("/").pop() ?? ""; if (!lastSegment.includes(".")) { diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index 4272982eb..bea1e60d1 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -1,3 +1,4 @@ +import { readFileSync } from "node:fs"; import { describe, expect, test, vi } from "vitest"; import { load, loadSync, resolve, resolveSync } from "./ts-hook.mjs"; @@ -65,7 +66,23 @@ describe("loadSync", () => { const notFound = (specifier: string) => Object.assign(new Error(`Cannot find '${specifier}'`), { code: "ERR_MODULE_NOT_FOUND" }); +const dirImport = (specifier: string) => + Object.assign(new Error(`Directory import not allowed for '${specifier}'`), { + code: "ERR_UNSUPPORTED_DIR_IMPORT", + }); + describe("resolve", () => { + test("retries with /index.ts for ERR_UNSUPPORTED_DIR_IMPORT on relative directory specifier", async () => { + const resolved = { url: "file:///path/to/models/index.ts" }; + const nextResolve = vi + .fn() + .mockRejectedValueOnce(dirImport("./models")) + .mockResolvedValueOnce(resolved); + const result = await resolve("./models", {}, nextResolve); + expect(result).toEqual(resolved); + expect(nextResolve).toHaveBeenCalledWith("./models/index.ts", {}); + }); + test("retries with .ts extension for extensionless relative specifier on ERR_MODULE_NOT_FOUND", async () => { const resolved = { url: "file:///path/to/foo.ts" }; const nextResolve = vi @@ -103,9 +120,44 @@ describe("resolve", () => { }); expect(nextResolve).toHaveBeenCalledTimes(1); }); + + test("resolves non-relative specifier via tsconfig path alias", async () => { + const tsconfig = JSON.stringify({ + compilerOptions: { baseUrl: ".", paths: { "@/*": ["./*"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///alias-project/tailordb/user.ts" }; + const nextResolve = vi + .fn() + .mockRejectedValueOnce(notFound("@/tailordb/user")) + .mockResolvedValueOnce(resolved); + const result = await resolve( + "@/tailordb/user", + { parentURL: "file:///alias-project/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); }); describe("resolveSync", () => { + test("retries with /index.ts for ERR_UNSUPPORTED_DIR_IMPORT on relative directory specifier", () => { + const resolved = { url: "file:///path/to/models/index.ts" }; + const nextResolve = vi + .fn() + .mockImplementationOnce(() => { + throw dirImport("./models"); + }) + .mockReturnValueOnce(resolved); + const result = resolveSync("./models", {}, nextResolve); + expect(result).toEqual(resolved); + expect(nextResolve).toHaveBeenCalledWith("./models/index.ts", {}); + }); + test("retries with .ts extension for extensionless relative specifier on ERR_MODULE_NOT_FOUND", () => { const resolved = { url: "file:///path/to/foo.ts" }; const nextResolve = vi @@ -139,4 +191,28 @@ describe("resolveSync", () => { expect(() => resolveSync("./foo.ts", {}, nextResolve)).toThrow("Cannot find './foo.ts'"); expect(nextResolve).toHaveBeenCalledTimes(1); }); + + test("resolves non-relative specifier via tsconfig path alias", () => { + const tsconfig = JSON.stringify({ + compilerOptions: { baseUrl: ".", paths: { "@/*": ["./*"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///alias-sync-project/tailordb/user.ts" }; + const nextResolve = vi + .fn() + .mockImplementationOnce(() => { + throw notFound("@/tailordb/user"); + }) + .mockReturnValueOnce(resolved); + const result = resolveSync( + "@/tailordb/user", + { parentURL: "file:///alias-sync-project/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); }); From 25bcc52e821039b0020c85193eeec76b7e71f4f4 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 18:24:48 +0900 Subject: [PATCH 227/618] fix(sdk): strip query params from parentURL before fileURLToPath in resolve hooks Fixes ERR_INVALID_FILE_URL_PATH when parentURL includes a watch-mode query string (e.g. ?tailorImportNonce=...). Adds regression tests for both resolve() and resolveSync(). Also updates quickstart.md to reference the correct minimum Node.js version (22.15.0). --- packages/sdk/docs/quickstart.md | 2 +- packages/sdk/src/cli/ts-hook.mjs | 10 ++++-- packages/sdk/src/cli/ts-hook.test.ts | 46 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/sdk/docs/quickstart.md b/packages/sdk/docs/quickstart.md index b3071e772..27896da5f 100644 --- a/packages/sdk/docs/quickstart.md +++ b/packages/sdk/docs/quickstart.md @@ -12,7 +12,7 @@ Contact us [here](https://www.tailor.tech/demo) to get started. ### Install Node.js -The SDK requires Node.js 22 or later. Install Node.js via your package manager by following the official Node.js instructions. +The SDK requires Node.js 22.15.0 or later. Install Node.js via your package manager by following the official Node.js instructions. Alternatively, you can use [Bun](https://bun.sh/) as the runtime. diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 90648ddd1..0e9889230 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -145,7 +145,10 @@ export async function resolve(specifier, context, nextResolve) { if (!specifier.startsWith(".") && !specifier.startsWith("/")) { // Non-relative: try tsconfig path aliases if (context.parentURL?.startsWith("file://")) { - const parentDir = dirname(fileURLToPath(context.parentURL)); + const parentParsed = new URL(context.parentURL); + parentParsed.search = ""; + parentParsed.hash = ""; + const parentDir = dirname(fileURLToPath(parentParsed)); const tsconfigPaths = loadTsconfigPaths(parentDir); const mapped = matchTsconfigPath(specifier, tsconfigPaths); if (mapped) { @@ -224,7 +227,10 @@ export function resolveSync(specifier, context, nextResolve) { if (!specifier.startsWith(".") && !specifier.startsWith("/")) { // Non-relative: try tsconfig path aliases if (context.parentURL?.startsWith("file://")) { - const parentDir = dirname(fileURLToPath(context.parentURL)); + const parentParsed = new URL(context.parentURL); + parentParsed.search = ""; + parentParsed.hash = ""; + const parentDir = dirname(fileURLToPath(parentParsed)); const tsconfigPaths = loadTsconfigPaths(parentDir); const mapped = matchTsconfigPath(specifier, tsconfigPaths); if (mapped) { diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index bea1e60d1..43aea199e 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -142,6 +142,28 @@ describe("resolve", () => { expect(result).toEqual(resolved); vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + + test("resolves tsconfig path alias when parentURL has tailorImportNonce query string", async () => { + const tsconfig = JSON.stringify({ + compilerOptions: { baseUrl: ".", paths: { "@/*": ["./*"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///alias-project/tailordb/user.ts" }; + const nextResolve = vi + .fn() + .mockRejectedValueOnce(notFound("@/tailordb/user")) + .mockResolvedValueOnce(resolved); + const result = await resolve( + "@/tailordb/user", + { parentURL: "file:///alias-project/tailor.config.ts?tailorImportNonce=1" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); }); describe("resolveSync", () => { @@ -215,4 +237,28 @@ describe("resolveSync", () => { expect(result).toEqual(resolved); vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + + test("resolves tsconfig path alias when parentURL has tailorImportNonce query string", () => { + const tsconfig = JSON.stringify({ + compilerOptions: { baseUrl: ".", paths: { "@/*": ["./*"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///alias-sync-project/tailordb/user.ts" }; + const nextResolve = vi + .fn() + .mockImplementationOnce(() => { + throw notFound("@/tailordb/user"); + }) + .mockReturnValueOnce(resolved); + const result = resolveSync( + "@/tailordb/user", + { parentURL: "file:///alias-sync-project/tailor.config.ts?tailorImportNonce=1" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); }); From 5fa05c05fe0a2d6847cc31f9c47b60b9b4636c2f Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 18:34:01 +0900 Subject: [PATCH 228/618] fix(sdk): resolve all tsconfig path alias targets and swallow ERR_UNSUPPORTED_DIR_IMPORT in alias fallback matchTsconfigPath() was returning only the first target, but TypeScript resolves path alias targets in order until one succeeds. Rename to matchTsconfigPaths() and return all candidates; resolve() and resolveSync() now iterate over the list. tryResolveWithExtensions() and its sync variant were re-throwing ERR_UNSUPPORTED_DIR_IMPORT from the final nextResolve(base) fallback, aborting alias resolution for directory-mapped targets. Treat the error as a failed candidate (same as ERR_MODULE_NOT_FOUND). --- packages/sdk/src/cli/ts-hook.mjs | 50 +++++++++++++++++--------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 0e9889230..8ee186272 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -67,18 +67,16 @@ function loadTsconfigPaths(startDir) { return paths; } -function matchTsconfigPath(specifier, paths) { +function matchTsconfigPaths(specifier, paths) { for (const [alias, targets] of Object.entries(paths)) { if (alias.endsWith("/*")) { const prefix = alias.slice(0, -2); if (specifier.startsWith(prefix + "/")) { const rest = specifier.slice(prefix.length + 1); - for (const target of targets) { - return target.endsWith("/*") ? target.slice(0, -2) + "/" + rest : target; - } + return targets.map((t) => (t.endsWith("/*") ? t.slice(0, -2) + "/" + rest : t)); } } else if (alias === specifier && targets.length > 0) { - return targets[0]; + return targets; } } return null; @@ -97,7 +95,8 @@ async function tryResolveWithExtensions(base, context, nextResolve) { try { return await nextResolve(base, context); } catch (e) { - if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + const code = e?.code; + if (code !== "ERR_MODULE_NOT_FOUND" && code !== "ERR_UNSUPPORTED_DIR_IMPORT") throw e; } return null; } @@ -115,7 +114,8 @@ function tryResolveWithExtensionsSync(base, context, nextResolve) { try { return nextResolve(base, context); } catch (e) { - if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + const code = e?.code; + if (code !== "ERR_MODULE_NOT_FOUND" && code !== "ERR_UNSUPPORTED_DIR_IMPORT") throw e; } return null; } @@ -150,14 +150,16 @@ export async function resolve(specifier, context, nextResolve) { parentParsed.hash = ""; const parentDir = dirname(fileURLToPath(parentParsed)); const tsconfigPaths = loadTsconfigPaths(parentDir); - const mapped = matchTsconfigPath(specifier, tsconfigPaths); - if (mapped) { - const result = await tryResolveWithExtensions( - pathToFileURL(mapped).href, - context, - nextResolve, - ); - if (result) return result; + const candidates = matchTsconfigPaths(specifier, tsconfigPaths); + if (candidates) { + for (const candidate of candidates) { + const result = await tryResolveWithExtensions( + pathToFileURL(candidate).href, + context, + nextResolve, + ); + if (result) return result; + } } } throw err; @@ -232,14 +234,16 @@ export function resolveSync(specifier, context, nextResolve) { parentParsed.hash = ""; const parentDir = dirname(fileURLToPath(parentParsed)); const tsconfigPaths = loadTsconfigPaths(parentDir); - const mapped = matchTsconfigPath(specifier, tsconfigPaths); - if (mapped) { - const result = tryResolveWithExtensionsSync( - pathToFileURL(mapped).href, - context, - nextResolve, - ); - if (result) return result; + const candidates = matchTsconfigPaths(specifier, tsconfigPaths); + if (candidates) { + for (const candidate of candidates) { + const result = tryResolveWithExtensionsSync( + pathToFileURL(candidate).href, + context, + nextResolve, + ); + if (result) return result; + } } } throw err; From e0cc389f9a40aefcb12d1afc396c18dad6243f27 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 18:48:59 +0900 Subject: [PATCH 229/618] fix(cli): fix tsconfig path resolution for same-dir extends and alias specificity - collectPathsInto() tracks visited configs by file path (not directory), fixing a bug where extends: ./tsconfig.base.json in the same directory was skipped because join(baseDir, tsconfig.json) immediately hit the visited set - loadTsconfigPaths() passes the config file path to the initial collectPathsInto() - matchTsconfigPaths() checks exact-match aliases first, then wildcard aliases sorted by length descending, aligning with TypeScript most-specific-wins behavior - Add regression tests for same-directory extends and alias specificity ordering --- packages/sdk/src/cli/ts-hook.mjs | 36 ++++++++++--------- packages/sdk/src/cli/ts-hook.test.ts | 54 ++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 16 deletions(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 8ee186272..f1cdc26fc 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -17,10 +17,11 @@ const JS_TO_TS = new Map([ const tsconfigPathsCache = new Map(); -function collectPathsInto(out, baseDir, content, visited) { - const id = join(baseDir, "tsconfig.json"); - if (visited.has(id)) return; - visited.add(id); +function collectPathsInto(out, configFilePath, content, visited) { + if (visited.has(configFilePath)) return; + visited.add(configFilePath); + + const baseDir = dirname(configFilePath); const extendsField = content.extends; if (typeof extendsField === "string") { @@ -28,7 +29,7 @@ function collectPathsInto(out, baseDir, content, visited) { const extendsPath = base.endsWith(".json") ? base : base + ".json"; try { const sub = JSON.parse(readFileSync(extendsPath, "utf-8")); - collectPathsInto(out, dirname(extendsPath), sub, visited); + collectPathsInto(out, extendsPath, sub, visited); } catch { // extends file not readable; skip } @@ -53,8 +54,9 @@ function loadTsconfigPaths(startDir) { let prev = ""; while (dir !== prev) { try { - const content = JSON.parse(readFileSync(join(dir, "tsconfig.json"), "utf-8")); - collectPathsInto(paths, dir, content, new Set()); + const configFilePath = join(dir, "tsconfig.json"); + const content = JSON.parse(readFileSync(configFilePath, "utf-8")); + collectPathsInto(paths, configFilePath, content, new Set()); break; } catch { // tsconfig.json not found in this directory; walk up @@ -68,15 +70,17 @@ function loadTsconfigPaths(startDir) { } function matchTsconfigPaths(specifier, paths) { - for (const [alias, targets] of Object.entries(paths)) { - if (alias.endsWith("/*")) { - const prefix = alias.slice(0, -2); - if (specifier.startsWith(prefix + "/")) { - const rest = specifier.slice(prefix.length + 1); - return targets.map((t) => (t.endsWith("/*") ? t.slice(0, -2) + "/" + rest : t)); - } - } else if (alias === specifier && targets.length > 0) { - return targets; + if (paths[specifier]?.length > 0) { + return paths[specifier]; + } + const wildcardEntries = Object.entries(paths) + .filter(([alias]) => alias.endsWith("/*")) + .toSorted((a, b) => b[0].length - a[0].length); + for (const [alias, targets] of wildcardEntries) { + const prefix = alias.slice(0, -2); + if (specifier.startsWith(prefix + "/")) { + const rest = specifier.slice(prefix.length + 1); + return targets.map((t) => (t.endsWith("/*") ? t.slice(0, -2) + "/" + rest : t)); } } return null; diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index 43aea199e..5812a15b9 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -164,6 +164,60 @@ describe("resolve", () => { expect(result).toEqual(resolved); vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + + test("collects paths from same-directory extends (visited key tracks file path, not dir)", async () => { + const baseConfig = JSON.stringify({ + compilerOptions: { baseUrl: ".", paths: { "@/*": ["./*"] } }, + }); + const rootConfig = JSON.stringify({ extends: "./tsconfig.base.json" }); + vi.mocked(readFileSync).mockImplementation((path) => { + const p = String(path); + if (p.endsWith("tsconfig.base.json")) return baseConfig as unknown as string; + if (p.endsWith("tsconfig.json")) return rootConfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///extends-project/tailordb/user.ts" }; + const nextResolve = vi + .fn() + .mockRejectedValueOnce(notFound("@/tailordb/user")) + .mockResolvedValueOnce(resolved); + const result = await resolve( + "@/tailordb/user", + { parentURL: "file:///extends-project/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + + test("prefers more specific wildcard alias over less specific", async () => { + const tsconfig = JSON.stringify({ + compilerOptions: { + baseUrl: ".", + paths: { "@/*": ["./*"], "@foo/*": ["./foo-pkg/*"] }, + }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///specificity-project/foo-pkg/bar.ts" }; + const nextResolve = vi + .fn() + .mockRejectedValueOnce(notFound("@foo/bar")) + .mockResolvedValueOnce(resolved); + const result = await resolve( + "@foo/bar", + { parentURL: "file:///specificity-project/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + expect(nextResolve).toHaveBeenLastCalledWith( + expect.stringContaining("foo-pkg/bar"), + expect.anything(), + ); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); }); describe("resolveSync", () => { From 0fcde2ff6d6385b9ba9833de4034173a300c9bb5 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 19:51:46 +0900 Subject: [PATCH 230/618] fix(example): restore field_modified entries in migration 0004 diff amaro (SWC) and tsx (esbuild) produce different whitespace in compiled function text. This restores the before/after entries documenting the change from esbuild-style to SWC-style output. --- example/migrations/0004/diff.json | 185 +++++++++++++++++++++++++++++- 1 file changed, 184 insertions(+), 1 deletion(-) diff --git a/example/migrations/0004/diff.json b/example/migrations/0004/diff.json index 0c6caac98..a38bf0c38 100644 --- a/example/migrations/0004/diff.json +++ b/example/migrations/0004/diff.json @@ -2,7 +2,190 @@ "version": 1, "namespace": "tailordb", "createdAt": "2026-06-24T04:44:47.663Z", - "changes": [], + "changes": [ + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "name", + "before": { + "type": "string", + "required": true, + "validate": [ + { + "script": { + "expr": "(({value})=>value.length>5)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "Name must be longer than 5 characters" + } + ] + }, + "after": { + "type": "string", + "required": true, + "validate": [ + { + "script": { + "expr": "(({ value })=>value.length > 5)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "Name must be longer than 5 characters" + } + ] + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "city", + "before": { + "type": "string", + "required": false, + "validate": [ + { + "script": { + "expr": "(({value})=>value?value.length>1:true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({value})=>value?value.length>1:true`" + }, + { + "script": { + "expr": "(({value})=>value?value.length<100:true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({value})=>value?value.length<100:true`" + } + ] + }, + "after": { + "type": "string", + "required": false, + "validate": [ + { + "script": { + "expr": "(({ value })=>value ? value.length > 1 : true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({ value })=>value ? value.length > 1 : true`" + }, + { + "script": { + "expr": "(({ value })=>value ? value.length < 100 : true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({ value })=>value ? value.length < 100 : true`" + } + ] + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "fullAddress", + "before": { + "type": "string", + "required": true, + "hooks": { + "create": { + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "string", + "required": true, + "hooks": { + "create": { + "expr": "(({ data })=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(({ data })=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "attachedFiles", + "before": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "size": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({value})=>value>0)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({value})=>value>0`" + } + ] + }, + "type": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "text" + }, + { + "value": "image" + } + ] + } + } + }, + "after": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "size": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({ value })=>value > 0)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({ value })=>value > 0`" + } + ] + }, + "type": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "text" + }, + { + "value": "image" + } + ] + } + } + } + } + ], "hasBreakingChanges": false, "breakingChanges": [], "hasWarnings": false, From 4f1317e6489dc36d3734040c11ac965f0190886b Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 20:00:43 +0900 Subject: [PATCH 231/618] fix(sdk): tighten error handling in tsconfig paths loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - collectPathsInto(): only swallow ENOENT when reading extends file; rethrow JSON parse errors and permission errors to avoid silent misconfiguration - loadTsconfigPaths(): same — only swallow ENOENT when searching for tsconfig.json; stop treating a malformed tsconfig as 'not found' - Use Object.create(null) for the paths map to prevent prototype pollution from user-controlled tsconfig alias keys --- packages/sdk/src/cli/ts-hook.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index f1cdc26fc..3b0b063f1 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -30,8 +30,8 @@ function collectPathsInto(out, configFilePath, content, visited) { try { const sub = JSON.parse(readFileSync(extendsPath, "utf-8")); collectPathsInto(out, extendsPath, sub, visited); - } catch { - // extends file not readable; skip + } catch (e) { + if (e?.code !== "ENOENT") throw e; } } @@ -49,7 +49,7 @@ function collectPathsInto(out, configFilePath, content, visited) { function loadTsconfigPaths(startDir) { if (tsconfigPathsCache.has(startDir)) return tsconfigPathsCache.get(startDir); - const paths = {}; + const paths = Object.create(null); let dir = startDir; let prev = ""; while (dir !== prev) { @@ -58,8 +58,8 @@ function loadTsconfigPaths(startDir) { const content = JSON.parse(readFileSync(configFilePath, "utf-8")); collectPathsInto(paths, configFilePath, content, new Set()); break; - } catch { - // tsconfig.json not found in this directory; walk up + } catch (e) { + if (e?.code !== "ENOENT") throw e; } prev = dir; dir = dirname(dir); From 479a9a9a19558fdc14c7d42b6a6d18699273ff41 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 20:10:47 +0900 Subject: [PATCH 232/618] fix(sdk): also swallow SyntaxError for JSONC tsconfig files tsconfig.json is commonly JSONC (comments, trailing commas from tsc --init). Swallow SyntaxError in both loadTsconfigPaths() and collectPathsInto() alongside ENOENT, so a JSONC tsconfig is treated as 'no paths' rather than crashing the resolver. --- packages/sdk/src/cli/ts-hook.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 3b0b063f1..5a9fecf71 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -31,7 +31,7 @@ function collectPathsInto(out, configFilePath, content, visited) { const sub = JSON.parse(readFileSync(extendsPath, "utf-8")); collectPathsInto(out, extendsPath, sub, visited); } catch (e) { - if (e?.code !== "ENOENT") throw e; + if (e?.code !== "ENOENT" && !(e instanceof SyntaxError)) throw e; } } @@ -59,7 +59,8 @@ function loadTsconfigPaths(startDir) { collectPathsInto(paths, configFilePath, content, new Set()); break; } catch (e) { - if (e?.code !== "ENOENT") throw e; + if (e?.code !== "ENOENT" && !(e instanceof SyntaxError)) throw e; + if (e instanceof SyntaxError) break; } prev = dir; dir = dirname(dir); From 2c263af14c91fd2be28a15faf8def4b9b0a12b4c Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 20:23:42 +0900 Subject: [PATCH 233/618] fix(sdk): skip extension appending when tsconfig path target already has a TS extension tryResolveWithExtensions() and tryResolveWithExtensionsSync() appended .ts/.mts (and /index variants) even when the base URL already ended with a TS extension (e.g. an exact-match tsconfig paths target like "./utils/index.ts"). This caused redundant lookups (.ts.ts, .ts/index.ts) before the correct URL was tried. Guard with a TS_EXTENSIONS check. --- packages/sdk/src/cli/ts-hook.mjs | 28 ++++++++++++++----------- packages/sdk/src/cli/ts-hook.test.ts | 31 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 5a9fecf71..062736a41 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -88,12 +88,14 @@ function matchTsconfigPaths(specifier, paths) { } async function tryResolveWithExtensions(base, context, nextResolve) { - for (const ext of TS_EXTENSIONS) { - for (const suffix of ["", "/index"]) { - try { - return await nextResolve(base + suffix + ext, context); - } catch (e) { - if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + if (!TS_EXTENSIONS.some((ext) => base.endsWith(ext))) { + for (const ext of TS_EXTENSIONS) { + for (const suffix of ["", "/index"]) { + try { + return await nextResolve(base + suffix + ext, context); + } catch (e) { + if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + } } } } @@ -107,12 +109,14 @@ async function tryResolveWithExtensions(base, context, nextResolve) { } function tryResolveWithExtensionsSync(base, context, nextResolve) { - for (const ext of TS_EXTENSIONS) { - for (const suffix of ["", "/index"]) { - try { - return nextResolve(base + suffix + ext, context); - } catch (e) { - if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + if (!TS_EXTENSIONS.some((ext) => base.endsWith(ext))) { + for (const ext of TS_EXTENSIONS) { + for (const suffix of ["", "/index"]) { + try { + return nextResolve(base + suffix + ext, context); + } catch (e) { + if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; + } } } } diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index 5812a15b9..8b756b891 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -218,6 +218,37 @@ describe("resolve", () => { ); vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + + test("does not append extensions when tsconfig path target already has a .ts extension", async () => { + const tsconfig = JSON.stringify({ + compilerOptions: { baseUrl: ".", paths: { "@/utils": ["./utils/index.ts"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///ext-project/utils/index.ts" }; + const nextResolve = vi + .fn() + .mockRejectedValueOnce(notFound("@/utils")) + .mockResolvedValueOnce(resolved); + const result = await resolve( + "@/utils", + { parentURL: "file:///ext-project/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + expect(nextResolve).toHaveBeenCalledTimes(2); + expect(nextResolve).toHaveBeenLastCalledWith( + expect.stringContaining("utils/index.ts"), + expect.anything(), + ); + expect(nextResolve).not.toHaveBeenCalledWith( + expect.stringContaining("index.ts.ts"), + expect.anything(), + ); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); }); describe("resolveSync", () => { From 85b307cc69be3874414630e60cc08f29971c64c3 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 20:34:55 +0900 Subject: [PATCH 234/618] fix(sdk): store tsconfig path targets as file URLs to fix wildcard resolution on Windows resolvePath() on Windows produces targets ending with '\*' (e.g. C:\proj\src\*), so the t.endsWith('/\*') guard in matchTsconfigPaths() never fired and wildcards like '@/*': ['./*'] silently fell through. Store targets as pathToFileURL() hrefs at collection time; the /*-wildcard marker remains a POSIX slash so the guard and string concatenation work on all platforms. --- packages/sdk/src/cli/ts-hook.mjs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 062736a41..026266ff9 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -41,7 +41,11 @@ function collectPathsInto(out, configFilePath, content, visited) { if (rawPaths && baseUrl) { const absBase = resolvePath(baseDir, baseUrl); for (const [alias, targets] of Object.entries(rawPaths)) { - out[alias] = targets.map((t) => resolvePath(absBase, t)); + out[alias] = targets.map((t) => { + const isWildcard = t.endsWith("/*"); + const resolved = resolvePath(absBase, isWildcard ? t.slice(0, -2) : t); + return pathToFileURL(resolved).href + (isWildcard ? "/*" : ""); + }); } } } @@ -162,11 +166,7 @@ export async function resolve(specifier, context, nextResolve) { const candidates = matchTsconfigPaths(specifier, tsconfigPaths); if (candidates) { for (const candidate of candidates) { - const result = await tryResolveWithExtensions( - pathToFileURL(candidate).href, - context, - nextResolve, - ); + const result = await tryResolveWithExtensions(candidate, context, nextResolve); if (result) return result; } } @@ -246,11 +246,7 @@ export function resolveSync(specifier, context, nextResolve) { const candidates = matchTsconfigPaths(specifier, tsconfigPaths); if (candidates) { for (const candidate of candidates) { - const result = tryResolveWithExtensionsSync( - pathToFileURL(candidate).href, - context, - nextResolve, - ); + const result = tryResolveWithExtensionsSync(candidate, context, nextResolve); if (result) return result; } } From 07155f003c18cdd6d4cf45f5ce4198d3beb21e49 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 20:43:38 +0900 Subject: [PATCH 235/618] fix(sdk): exclude .d.ts/.d.mts declaration files from amaro transform in load/loadSync TS_EXTENSIONS includes .ts/.mts, which also matches declaration files (.d.ts, .d.mts). These are type-only and must not be passed to amaro; delegating them to nextLoad() surfaces a clearer error if they are ever imported at runtime. --- packages/sdk/src/cli/ts-hook.mjs | 12 ++++++++++-- packages/sdk/src/cli/ts-hook.test.ts | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 026266ff9..ee48593b1 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -202,7 +202,11 @@ export async function resolve(specifier, context, nextResolve) { export async function load(url, context, nextLoad) { if (url.startsWith("file:")) { const parsedUrl = new URL(url); - if (TS_EXTENSIONS.some((ext) => parsedUrl.pathname.endsWith(ext))) { + if ( + TS_EXTENSIONS.some((ext) => parsedUrl.pathname.endsWith(ext)) && + !parsedUrl.pathname.endsWith(".d.ts") && + !parsedUrl.pathname.endsWith(".d.mts") + ) { parsedUrl.search = ""; parsedUrl.hash = ""; const filePath = fileURLToPath(parsedUrl); @@ -282,7 +286,11 @@ export function resolveSync(specifier, context, nextResolve) { export function loadSync(url, context, nextLoad) { if (url.startsWith("file:")) { const parsedUrl = new URL(url); - if (TS_EXTENSIONS.some((ext) => parsedUrl.pathname.endsWith(ext))) { + if ( + TS_EXTENSIONS.some((ext) => parsedUrl.pathname.endsWith(ext)) && + !parsedUrl.pathname.endsWith(".d.ts") && + !parsedUrl.pathname.endsWith(".d.mts") + ) { parsedUrl.search = ""; parsedUrl.hash = ""; const filePath = fileURLToPath(parsedUrl); diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index 8b756b891..261d001ae 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -40,6 +40,18 @@ describe("load", () => { await load("file:///path/to/foo.js", {}, nextLoad); expect(nextLoad).toHaveBeenCalledWith("file:///path/to/foo.js", {}); }); + + test("delegates .d.ts declaration files to nextLoad without transforming", async () => { + const nextLoad = vi.fn(); + await load("file:///path/to/foo.d.ts", {}, nextLoad); + expect(nextLoad).toHaveBeenCalledWith("file:///path/to/foo.d.ts", {}); + }); + + test("delegates .d.mts declaration files to nextLoad without transforming", async () => { + const nextLoad = vi.fn(); + await load("file:///path/to/foo.d.mts", {}, nextLoad); + expect(nextLoad).toHaveBeenCalledWith("file:///path/to/foo.d.mts", {}); + }); }); describe("loadSync", () => { @@ -61,6 +73,12 @@ describe("loadSync", () => { loadSync("file:///path/to/foo.js", {}, nextLoad); expect(nextLoad).toHaveBeenCalledWith("file:///path/to/foo.js", {}); }); + + test("delegates .d.ts declaration files to nextLoad without transforming", () => { + const nextLoad = vi.fn(); + loadSync("file:///path/to/foo.d.ts", {}, nextLoad); + expect(nextLoad).toHaveBeenCalledWith("file:///path/to/foo.d.ts", {}); + }); }); const notFound = (specifier: string) => From dbd7e9af8142c137aa970211af5a5df247d0b45a Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 25 Jun 2026 21:22:31 +0900 Subject: [PATCH 236/618] fix(codemod): satisfy runner typecheck --- packages/sdk-codemod/src/runner.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 26720e266..4bf94a544 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -178,7 +178,9 @@ function sourceLang(relative: string): Lang { function isProcessEnvSubscriptKey(node: SgNode): boolean { const stringNode = node.kind() === "string_fragment" ? node.parent() : node; - if (stringNode == null || !["string", "template_string"].includes(stringNode.kind())) { + if (stringNode == null) return false; + const stringNodeKind = stringNode.kind(); + if (stringNodeKind !== "string" && stringNodeKind !== "template_string") { return false; } const parent = stringNode.parent(); From 1e94537e5cec5e63a883bd097cd643e47a532c72 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 21:58:21 +0900 Subject: [PATCH 237/618] test(sdk): fix completion test assertion for renamed binary --- packages/sdk/src/cli/completion.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/cli/completion.test.ts b/packages/sdk/src/cli/completion.test.ts index 295afd18c..263606343 100644 --- a/packages/sdk/src/cli/completion.test.ts +++ b/packages/sdk/src/cli/completion.test.ts @@ -211,7 +211,7 @@ describe("shell completion", () => { shell: "zsh", programName: "tailor", }); - expect(script).toMatch(/__tailor_sdk_expand_[a-z_]+__field=/); + expect(script).toMatch(/__tailor_expand_[a-z_]+__field=/); expect(script).toContain("GetFunctionExecution"); expect(script).toContain("_used_field_keys"); }); From 261099e709ecb99025e99107d5d0e912b6edfd66 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 22:24:27 +0900 Subject: [PATCH 238/618] fix(sdk-codemod): restore tailor-sdk binary references in registry metadata Bulk rename incorrectly converted text strings in sdk-skills-shim, apply-to-deploy, and cli-rename registry entries that should have remained as tailor-sdk-* (those codemods migrate tailor-sdk v1 CLI invocations, not the binary name itself). Update rename-bin description to note .tailor-sdk/ to .tailor/ manual path migration. Rename tailorSdkBin to tailorBin and runTailorSdk to runTailor in migration_e2e.ts. Regenerate v2.md from updated registry. --- example/tests/scripts/migration_e2e.ts | 10 +++---- packages/sdk-codemod/src/registry.ts | 41 +++++++++++++------------- packages/sdk/docs/migration/v2.md | 36 +++++++++++----------- 3 files changed, 44 insertions(+), 43 deletions(-) diff --git a/example/tests/scripts/migration_e2e.ts b/example/tests/scripts/migration_e2e.ts index 2d0586324..5fb48ac00 100644 --- a/example/tests/scripts/migration_e2e.ts +++ b/example/tests/scripts/migration_e2e.ts @@ -29,15 +29,15 @@ const templateMigrationsDir = path.resolve(fixtureRoot, "templates"); const namespace = "migrationdb"; const machineUserName = "manager-machine-user"; -const tailorSdkBin = path.resolve( +const tailorBin = path.resolve( exampleDir, "node_modules", ".bin", process.platform === "win32" ? "tailor.cmd" : "tailor", ); -const runTailorSdk = (args: string[]) => { - execFileSync(tailorSdkBin, args, { +const runTailor = (args: string[]) => { + execFileSync(tailorBin, args, { cwd: appDir, env: { ...process.env, @@ -47,11 +47,11 @@ const runTailorSdk = (args: string[]) => { }; const runDeploy = () => { - runTailorSdk(["deploy", "-c", configPath, "--yes"]); + runTailor(["deploy", "-c", configPath, "--yes"]); }; const runMigrateGenerate = () => { - runTailorSdk(["tailordb", "migration", "generate", "-c", configPath, "--yes"]); + runTailor(["tailordb", "migration", "generate", "-c", configPath, "--yes"]); }; const resetMigrations = () => { diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 51b7793a9..ebb488a3e 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -75,25 +75,26 @@ export const allCodemods: CodemodPackage[] = [ }, { id: "v2/sdk-skills-shim", - name: "tailor-skills → tailor skills install", - description: "Replace deprecated `tailor-skills` invocations with `tailor skills install`", + name: "tailor-sdk-skills → tailor-sdk skills install", + description: + "Replace deprecated `tailor-sdk-skills` invocations with `tailor-sdk skills install`", since: "1.0.0", until: "2.0.0", scriptPath: "v2/sdk-skills-shim/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], - legacyPatterns: ["tailor-skills"], + legacyPatterns: ["tailor-sdk-skills"], examples: [ { lang: "sh", - before: "npx tailor-skills", - after: "tailor skills install", + before: "npx tailor-sdk-skills", + after: "tailor-sdk skills install", }, ], prompt: [ - "The standalone tailor-skills binary is removed in v2; call the skills install", - "subcommand on the main tailor CLI instead. Replace any remaining", - "tailor-skills invocations the codemod did not rewrite with", - "`tailor skills install`.", + "The standalone tailor-sdk-skills binary is removed in v2; call the skills install", + "subcommand on the main tailor-sdk CLI instead. Replace any remaining", + "tailor-sdk-skills invocations the codemod did not rewrite with", + "`tailor-sdk skills install`.", ].join("\n"), }, { @@ -147,9 +148,9 @@ export const allCodemods: CodemodPackage[] = [ }, { id: "v2/apply-to-deploy", - name: "tailor apply → tailor deploy", + name: "tailor-sdk apply → tailor-sdk deploy", description: - "Rewrite `tailor apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor deploy` command", + "Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor-sdk deploy` command", since: "1.0.0", until: "2.0.0", scriptPath: "v2/apply-to-deploy/scripts/transform.js", @@ -162,8 +163,8 @@ export const allCodemods: CodemodPackage[] = [ examples: [ { lang: "sh", - before: "tailor apply --profile prod", - after: "tailor deploy --profile prod", + before: "tailor-sdk apply --profile prod", + after: "tailor-sdk deploy --profile prod", }, ], }, @@ -171,22 +172,22 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/cli-rename", name: "v2 CLI rename", description: - "Rewrite `tailor crash-report` to `tailor crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs", + "Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs", since: "1.0.0", until: "2.0.0", scriptPath: "v2/cli-rename/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], - legacyPatterns: ["tailor crash-report", "--machineuser"], + legacyPatterns: ["tailor-sdk crash-report", "--machineuser"], examples: [ { lang: "sh", - before: "tailor crash-report list\ntailor-sdk login --machineuser", - after: "tailor crashreport list\ntailor-sdk login --machine-user", + before: "tailor-sdk crash-report list\ntailor-sdk login --machineuser", + after: "tailor-sdk crashreport list\ntailor-sdk login --machine-user", }, ], prompt: [ - "Apply the v2 CLI renames the codemod did not reach (only `tailor`-prefixed", - "invocations are rewritten): `tailor crash-report` -> `tailor crashreport`", + "Apply the v2 CLI renames the codemod did not reach (only `tailor-sdk`-prefixed", + "invocations are rewritten): `tailor-sdk crash-report` -> `tailor-sdk crashreport`", "and the `--machineuser` option -> `--machine-user`. Leave unrelated commands that", "happen to use `--machineuser` alone.", ].join("\n"), @@ -440,7 +441,7 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/rename-bin", name: "tailor-sdk binary → tailor", description: - "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package.", + "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock` — update `.gitignore` entries and any lock-file references manually.", since: "1.0.0", until: "2.0.0", scriptPath: "v2/rename-bin/scripts/transform.js", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index d149d8a5a..56091f0b8 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -82,32 +82,32 @@ After: tailor function test-run resolvers/add.ts --arg '{"a":1}' ``` -## tailor-skills → tailor skills install +## tailor-sdk-skills → tailor-sdk skills install **Migration:** Partially automatic -Replace deprecated `tailor-skills` invocations with `tailor skills install` +Replace deprecated `tailor-sdk-skills` invocations with `tailor-sdk skills install` Before: ```sh -npx tailor-skills +npx tailor-sdk-skills ``` After: ```sh -tailor skills install +tailor-sdk skills install ```
Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text -The standalone tailor-skills binary is removed in v2; call the skills install -subcommand on the main tailor CLI instead. Replace any remaining -tailor-skills invocations the codemod did not rewrite with -`tailor skills install`. +The standalone tailor-sdk-skills binary is removed in v2; call the skills install +subcommand on the main tailor-sdk CLI instead. Replace any remaining +tailor-sdk-skills invocations the codemod did not rewrite with +`tailor-sdk skills install`. ```
@@ -167,41 +167,41 @@ Use TailorPrincipal for the unified user/actor/invoker type. -## tailor apply → tailor deploy +## tailor-sdk apply → tailor-sdk deploy **Migration:** Automatic -Rewrite `tailor apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor deploy` command +Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor-sdk deploy` command Before: ```sh -tailor apply --profile prod +tailor-sdk apply --profile prod ``` After: ```sh -tailor deploy --profile prod +tailor-sdk deploy --profile prod ``` ## v2 CLI rename **Migration:** Partially automatic -Rewrite `tailor crash-report` to `tailor crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs +Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs Before: ```sh -tailor crash-report list +tailor-sdk crash-report list tailor-sdk login --machineuser ``` After: ```sh -tailor crashreport list +tailor-sdk crashreport list tailor-sdk login --machine-user ``` @@ -209,8 +209,8 @@ tailor-sdk login --machine-user Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text -Apply the v2 CLI renames the codemod did not reach (only `tailor`-prefixed -invocations are rewritten): `tailor crash-report` -> `tailor crashreport` +Apply the v2 CLI renames the codemod did not reach (only `tailor-sdk`-prefixed +invocations are rewritten): `tailor-sdk crash-report` -> `tailor-sdk crashreport` and the `--machineuser` option -> `--machine-user`. Leave unrelated commands that happen to use `--machineuser` alone. ``` @@ -464,7 +464,7 @@ trigger result as the job output directly (no Promise wrapper to unwrap). **Migration:** Partially automatic -Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. +Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock` — update `.gitignore` entries and any lock-file references manually. Before: From 1f4db920b6079f16102addc730bd0dc55a6aa017 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 22:36:20 +0900 Subject: [PATCH 239/618] fix(sdk-codemod): tighten rename-bin regex and clarify .gitignore note Add lookahead `(?![\w-])` to TAILOR_SDK_RE so that hyphenated identifiers like `tailor-sdk-skills` are not partially matched and incorrectly rewritten to `tailor-skills`. Aligns with the pattern used in apply-to-deploy and cli-rename transforms. Clarify the rename-bin description: only `.gitignore` entries need manual update (the codemod skips paths preceded by a dot); references to lock-file paths in YAML and Markdown are rewritten automatically. Regenerate v2.md. --- .../codemods/v2/rename-bin/scripts/transform.ts | 7 ++++--- packages/sdk-codemod/src/registry.ts | 2 +- packages/sdk/docs/migration/v2.md | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 3dd999a02..23578fdde 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -1,9 +1,10 @@ import * as path from "pathe"; // Match the `tailor-sdk` binary, optionally with a version pin (`@latest`, -// `@2.0.0`, etc.). Lookbehind excludes `.tailor-sdk` (directory path preceded -// by `.`) and `create-tailor-sdk` (package name preceded by `-`). -const TAILOR_SDK_RE = /(? diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index ebb488a3e..7fac36e9f 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -441,7 +441,7 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/rename-bin", name: "tailor-sdk binary → tailor", description: - "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock` — update `.gitignore` entries and any lock-file references manually.", + "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock` — update `.gitignore` entries manually (the codemod skips paths preceded by a dot).", since: "1.0.0", until: "2.0.0", scriptPath: "v2/rename-bin/scripts/transform.js", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 56091f0b8..299846cfe 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -464,7 +464,7 @@ trigger result as the job output directly (no Promise wrapper to unwrap). **Migration:** Partially automatic -Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock` — update `.gitignore` entries and any lock-file references manually. +Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock` — update `.gitignore` entries manually (the codemod skips paths preceded by a dot). Before: From 558708bf53717b16c66afbd0dff5c93bec860513 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 22:47:27 +0900 Subject: [PATCH 240/618] fix(codemod): guard body rename against shadowed local declarations Skip body-identifier rename for any name that is also declared as a function or variable in the file body, preventing incorrect rewrites of locals that happen to share the SDK import name (e.g. a helper that re-declares defineWaitPoints in an inner scope). Adds a local-shadow fixture to verify the import specifier is still renamed while the shadowed body usages are left unchanged. --- .../v2/wait-point-rename/scripts/transform.ts | 13 +++++++++++++ .../tests/local-shadow/expected.ts | 8 ++++++++ .../wait-point-rename/tests/local-shadow/input.ts | 8 ++++++++ 3 files changed, 29 insertions(+) create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/local-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/local-shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts index 70e74afa3..0669fe28e 100644 --- a/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts @@ -63,6 +63,19 @@ export default function transform(source: string, _filePath?: string): string | if (edits.length === 0) return null; if (needsBodyRename.size > 0) { + // Skip body rename for any name that is also declared locally (function or variable), + // to avoid incorrectly renaming shadowed identifiers unrelated to the SDK import. + const localDecls = root.findAll({ + rule: { any: [{ kind: "function_declaration" }, { kind: "variable_declarator" }] }, + }); + for (const decl of localDecls) { + if (isInsideImportStatement(decl)) continue; + const nameChild = decl.children().find((c: SgNode) => c.kind() === "identifier"); + if (nameChild && needsBodyRename.has(nameChild.text())) { + needsBodyRename.delete(nameChild.text()); + } + } + const identifiers = root.findAll({ rule: { kind: "identifier" } }); for (const ident of identifiers) { const name = ident.text(); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/local-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/local-shadow/expected.ts new file mode 100644 index 000000000..f0ff1cd3e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/local-shadow/expected.ts @@ -0,0 +1,8 @@ +import { createWorkflow, createWaitPoints } from "@tailor-platform/sdk"; + +// Local declaration shadows the SDK import — body usages should NOT be renamed +function defineWaitPoints() {} + +defineWaitPoints(); + +export default createWorkflow({ name: "wf", mainJob: {} as never }); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/local-shadow/input.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/local-shadow/input.ts new file mode 100644 index 000000000..69061524d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/local-shadow/input.ts @@ -0,0 +1,8 @@ +import { createWorkflow, defineWaitPoints } from "@tailor-platform/sdk"; + +// Local declaration shadows the SDK import — body usages should NOT be renamed +function defineWaitPoints() {} + +defineWaitPoints(); + +export default createWorkflow({ name: "wf", mainJob: {} as never }); From 2a9308dff2fd9e0c900461fa97249fc0499564dd Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 23:33:51 +0900 Subject: [PATCH 241/618] fix(codemod): use byte-range scope tracking for shadow detection Replace the over-broad needsBodyRename.delete() guard with a per-scope byte-range map. For each local declaration that shadows an SDK import name, record the byte range of its containing statement_block (or program for top-level). Body identifiers are only skipped when their position falls within one of those ranges, so top-level SDK usages are correctly renamed even when the same name is re-declared in a nested function. Adds a nested-shadow fixture to verify: top-level defineWaitPoints is renamed to createWaitPoints while the re-declared local inside a helper function and its calls are left unchanged. --- .../v2/wait-point-rename/scripts/transform.ts | 33 ++++++++++++++++--- .../tests/nested-shadow/expected.ts | 14 ++++++++ .../tests/nested-shadow/input.ts | 14 ++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/nested-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/nested-shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts index 0669fe28e..3c56c927e 100644 --- a/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts @@ -63,17 +63,34 @@ export default function transform(source: string, _filePath?: string): string | if (edits.length === 0) return null; if (needsBodyRename.size > 0) { - // Skip body rename for any name that is also declared locally (function or variable), - // to avoid incorrectly renaming shadowed identifiers unrelated to the SDK import. + // Build byte-range maps for scopes where each name is shadowed by a local declaration. + // Only identifiers outside these ranges should be renamed. + const shadowedRanges = new Map>(); + const localDecls = root.findAll({ rule: { any: [{ kind: "function_declaration" }, { kind: "variable_declarator" }] }, }); for (const decl of localDecls) { if (isInsideImportStatement(decl)) continue; const nameChild = decl.children().find((c: SgNode) => c.kind() === "identifier"); - if (nameChild && needsBodyRename.has(nameChild.text())) { - needsBodyRename.delete(nameChild.text()); + if (!nameChild || !needsBodyRename.has(nameChild.text())) continue; + + // Walk up to the nearest statement_block or program — that is the scope + // where this declaration shadows the imported name. + let scopeNode: SgNode = root; + let p: SgNode | null = decl.parent(); + while (p) { + if (p.kind() === "statement_block" || p.kind() === "program") { + scopeNode = p; + break; + } + p = p.parent(); } + + const r = scopeNode.range(); + const name = nameChild.text(); + if (!shadowedRanges.has(name)) shadowedRanges.set(name, []); + shadowedRanges.get(name)!.push({ start: r.start.index, end: r.end.index }); } const identifiers = root.findAll({ rule: { kind: "identifier" } }); @@ -81,6 +98,14 @@ export default function transform(source: string, _filePath?: string): string | const name = ident.text(); if (!needsBodyRename.has(name)) continue; if (isInsideImportStatement(ident)) continue; + + // Skip identifiers that fall inside a scope where this name is locally declared. + const ranges = shadowedRanges.get(name); + if (ranges) { + const pos = ident.range().start.index; + if (ranges.some((r) => pos >= r.start && pos <= r.end)) continue; + } + edits.push(ident.replace(RENAMES[name]!)); } } diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/nested-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/nested-shadow/expected.ts new file mode 100644 index 000000000..d0da7227d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/nested-shadow/expected.ts @@ -0,0 +1,14 @@ +import { createWorkflow, createWaitPoints } from "@tailor-platform/sdk"; + +// Top-level SDK usage — should be renamed +export const { approval } = createWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +function helper() { + // Nested local shadow — these should NOT be renamed + function defineWaitPoints() {} + defineWaitPoints(); +} + +export default createWorkflow({ name: "wf", mainJob: {} as never }); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/nested-shadow/input.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/nested-shadow/input.ts new file mode 100644 index 000000000..70bd01d08 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/nested-shadow/input.ts @@ -0,0 +1,14 @@ +import { createWorkflow, defineWaitPoints } from "@tailor-platform/sdk"; + +// Top-level SDK usage — should be renamed +export const { approval } = defineWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +function helper() { + // Nested local shadow — these should NOT be renamed + function defineWaitPoints() {} + defineWaitPoints(); +} + +export default createWorkflow({ name: "wf", mainJob: {} as never }); From 26ace2cba0aa768e15b5357f43e375947cf1ce33 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 23:44:18 +0900 Subject: [PATCH 242/618] fix(codemod): include for_statement in scope boundary detection Add for_statement and for_in_statement to the scope-kind check when walking up the AST to find the containing scope of a local declaration. Without this, a variable_declarator in a for-loop initializer (for (let defineWaitPoints = ...)) walked all the way up to program, causing the entire file to be treated as shadowed and preventing top-level SDK usages from being renamed. Adds a for-loop-shadow fixture to verify that top-level defineWaitPoints is renamed while the loop counter and body references are left unchanged. --- .../v2/wait-point-rename/scripts/transform.ts | 8 +++++++- .../tests/for-loop-shadow/expected.ts | 13 +++++++++++++ .../tests/for-loop-shadow/input.ts | 13 +++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-loop-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-loop-shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts index 3c56c927e..e417bc50e 100644 --- a/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts @@ -80,7 +80,13 @@ export default function transform(source: string, _filePath?: string): string | let scopeNode: SgNode = root; let p: SgNode | null = decl.parent(); while (p) { - if (p.kind() === "statement_block" || p.kind() === "program") { + const k = p.kind(); + if ( + k === "statement_block" || + k === "program" || + k === "for_statement" || + k === "for_in_statement" + ) { scopeNode = p; break; } diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-loop-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-loop-shadow/expected.ts new file mode 100644 index 000000000..33428ceab --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-loop-shadow/expected.ts @@ -0,0 +1,13 @@ +import { createWorkflow, createWaitPoints } from "@tailor-platform/sdk"; + +// Top-level SDK usage — should be renamed +export const { approval } = createWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +// for-loop counter shares the name — everything inside the for is NOT renamed +for (let defineWaitPoints = 0; defineWaitPoints < 3; defineWaitPoints++) { + void defineWaitPoints; +} + +export default createWorkflow({ name: "wf", mainJob: {} as never }); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-loop-shadow/input.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-loop-shadow/input.ts new file mode 100644 index 000000000..3cf7fbc06 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-loop-shadow/input.ts @@ -0,0 +1,13 @@ +import { createWorkflow, defineWaitPoints } from "@tailor-platform/sdk"; + +// Top-level SDK usage — should be renamed +export const { approval } = defineWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +// for-loop counter shares the name — everything inside the for is NOT renamed +for (let defineWaitPoints = 0; defineWaitPoints < 3; defineWaitPoints++) { + void defineWaitPoints; +} + +export default createWorkflow({ name: "wf", mainJob: {} as never }); From ddec77366f60a58ebb1bec3e581f2f5c89f91180 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 26 Jun 2026 00:05:19 +0900 Subject: [PATCH 243/618] fix(codemod): detect parameter and for-of binding shadows in wait-point-rename --- .../v2/wait-point-rename/scripts/transform.ts | 69 +++++++++++++++++-- .../tests/for-of-shadow/expected.ts | 13 ++++ .../tests/for-of-shadow/input.ts | 13 ++++ .../tests/param-shadow/expected.ts | 18 +++++ .../tests/param-shadow/input.ts | 18 +++++ packages/sdk-codemod/tsdown.config.ts | 2 + 6 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-of-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-of-shadow/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/param-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/param-shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts index e417bc50e..5e0c74d89 100644 --- a/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts @@ -67,6 +67,13 @@ export default function transform(source: string, _filePath?: string): string | // Only identifiers outside these ranges should be renamed. const shadowedRanges = new Map>(); + const addShadowedRange = (name: string, scopeNode: SgNode) => { + const r = scopeNode.range(); + if (!shadowedRanges.has(name)) shadowedRanges.set(name, []); + shadowedRanges.get(name)!.push({ start: r.start.index, end: r.end.index }); + }; + + // Variable declarations and local function declarations. const localDecls = root.findAll({ rule: { any: [{ kind: "function_declaration" }, { kind: "variable_declarator" }] }, }); @@ -93,10 +100,64 @@ export default function transform(source: string, _filePath?: string): string | p = p.parent(); } - const r = scopeNode.range(); - const name = nameChild.text(); - if (!shadowedRanges.has(name)) shadowedRanges.set(name, []); - shadowedRanges.get(name)!.push({ start: r.start.index, end: r.end.index }); + addShadowedRange(nameChild.text(), scopeNode); + } + + // Function/arrow parameters (required_parameter covers both regular and rest patterns). + const paramNodes = root.findAll({ rule: { kind: "required_parameter" } }); + for (const param of paramNodes) { + if (isInsideImportStatement(param)) continue; + // The name identifier may be a direct child or wrapped in rest_pattern. + const nameChild = param + .children() + .flatMap((c: SgNode) => + c.kind() === "rest_pattern" + ? c.children().filter((cc: SgNode) => cc.kind() === "identifier") + : c.kind() === "identifier" + ? [c] + : [], + ) + .find((c: SgNode) => needsBodyRename.has(c.text())); + if (!nameChild) continue; + + // Walk up past formal_parameters to the enclosing function/arrow, then use its body. + let scopeNode: SgNode = root; + let p: SgNode | null = param.parent(); + while (p) { + const k = p.kind(); + if (k === "formal_parameters") { + p = p.parent(); + continue; + } + if ( + k === "function_declaration" || + k === "function_expression" || + k === "arrow_function" || + k === "method_definition" + ) { + // Use the whole function node so the parameter list itself is also covered. + scopeNode = p; + break; + } + break; + } + + addShadowedRange(nameChild.text(), scopeNode); + } + + // for...of / for...in binding identifiers (direct children of for_in_statement, + // appearing before the 'of' or 'in' keyword). + const forInStmts = root.findAll({ rule: { kind: "for_in_statement" } }); + for (const stmt of forInStmts) { + const children = stmt.children(); + const keywordIdx = children.findIndex((c: SgNode) => c.kind() === "of" || c.kind() === "in"); + if (keywordIdx < 0) continue; + for (let i = 0; i < keywordIdx; i++) { + const child = children[i]!; + if (child.kind() === "identifier" && needsBodyRename.has(child.text())) { + addShadowedRange(child.text(), stmt); + } + } } const identifiers = root.findAll({ rule: { kind: "identifier" } }); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-of-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-of-shadow/expected.ts new file mode 100644 index 000000000..6f7234a5c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-of-shadow/expected.ts @@ -0,0 +1,13 @@ +import { createWorkflow, createWaitPoints } from "@tailor-platform/sdk"; + +// Top-level SDK usage — should be renamed +export const { approval } = createWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +// for...of loop variable shadows the import — everything inside the loop is NOT renamed +for (const defineWaitPoints of []) { + void defineWaitPoints; +} + +export default createWorkflow({ name: "wf", mainJob: {} as never }); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-of-shadow/input.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-of-shadow/input.ts new file mode 100644 index 000000000..4462004e6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/for-of-shadow/input.ts @@ -0,0 +1,13 @@ +import { createWorkflow, defineWaitPoints } from "@tailor-platform/sdk"; + +// Top-level SDK usage — should be renamed +export const { approval } = defineWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +// for...of loop variable shadows the import — everything inside the loop is NOT renamed +for (const defineWaitPoints of []) { + void defineWaitPoints; +} + +export default createWorkflow({ name: "wf", mainJob: {} as never }); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/param-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/param-shadow/expected.ts new file mode 100644 index 000000000..b2383ff65 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/param-shadow/expected.ts @@ -0,0 +1,18 @@ +import { createWorkflow, createWaitPoints } from "@tailor-platform/sdk"; + +// Top-level SDK usage — should be renamed +export const { approval } = createWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +// Function parameter shadows the import — should NOT rename inside +function processAll(defineWaitPoints: unknown[]) { + return defineWaitPoints.length; +} + +// Arrow function parameter shadows the import — should NOT rename inside +const processEach = (defineWaitPoints: string) => { + return defineWaitPoints.trim(); +}; + +export default createWorkflow({ name: "wf", mainJob: {} as never }); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/param-shadow/input.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/param-shadow/input.ts new file mode 100644 index 000000000..18dbade5e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/param-shadow/input.ts @@ -0,0 +1,18 @@ +import { createWorkflow, defineWaitPoints } from "@tailor-platform/sdk"; + +// Top-level SDK usage — should be renamed +export const { approval } = defineWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +// Function parameter shadows the import — should NOT rename inside +function processAll(defineWaitPoints: unknown[]) { + return defineWaitPoints.length; +} + +// Arrow function parameter shadows the import — should NOT rename inside +const processEach = (defineWaitPoints: string) => { + return defineWaitPoints.trim(); +}; + +export default createWorkflow({ name: "wf", mainJob: {} as never }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index aa8ede142..0c6e71bd2 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -31,6 +31,8 @@ export default defineConfig([ "codemods/v2/tailordb-namespace/scripts/transform.ts", "v2/execute-script-arg/scripts/transform": "codemods/v2/execute-script-arg/scripts/transform.ts", + "v2/wait-point-rename/scripts/transform": + "codemods/v2/wait-point-rename/scripts/transform.ts", }, format: ["esm"], target: "node18", From 6f98aafc7508f6982ef872deba80fc69a4336218 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 26 Jun 2026 09:46:19 +0900 Subject: [PATCH 244/618] fix(codemod): rename object-literal shorthands and detect destructuring shadows in wait-point-rename --- .../v2/wait-point-rename/scripts/transform.ts | 41 ++++++++++++++----- .../tests/destructuring-shadow/expected.ts | 14 +++++++ .../tests/destructuring-shadow/input.ts | 14 +++++++ .../tests/shorthand-property/expected.ts | 6 +++ .../tests/shorthand-property/input.ts | 6 +++ 5 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/destructuring-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/destructuring-shadow/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/shorthand-property/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/shorthand-property/input.ts diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts index 5e0c74d89..310c8e334 100644 --- a/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts @@ -79,7 +79,21 @@ export default function transform(source: string, _filePath?: string): string | }); for (const decl of localDecls) { if (isInsideImportStatement(decl)) continue; - const nameChild = decl.children().find((c: SgNode) => c.kind() === "identifier"); + // For variable_declarator: check direct identifier bindings and shorthand + // destructuring patterns (const { x } = ...) — both create local shadows. + const nameChild = + decl + .children() + .filter((c: SgNode) => c.kind() === "identifier") + .find((c: SgNode) => needsBodyRename.has(c.text())) ?? + decl + .children() + .find((c: SgNode) => c.kind() === "object_pattern") + ?.children() + .find( + (c: SgNode) => + c.kind() === "shorthand_property_identifier_pattern" && needsBodyRename.has(c.text()), + ); if (!nameChild || !needsBodyRename.has(nameChild.text())) continue; // Walk up to the nearest statement_block or program — that is the scope @@ -160,20 +174,25 @@ export default function transform(source: string, _filePath?: string): string | } } - const identifiers = root.findAll({ rule: { kind: "identifier" } }); - for (const ident of identifiers) { - const name = ident.text(); - if (!needsBodyRename.has(name)) continue; - if (isInsideImportStatement(ident)) continue; - - // Skip identifiers that fall inside a scope where this name is locally declared. + const renameNode = (node: SgNode) => { + const name = node.text(); + if (!needsBodyRename.has(name)) return; + if (isInsideImportStatement(node)) return; const ranges = shadowedRanges.get(name); if (ranges) { - const pos = ident.range().start.index; - if (ranges.some((r) => pos >= r.start && pos <= r.end)) continue; + const pos = node.range().start.index; + if (ranges.some((r) => pos >= r.start && pos <= r.end)) return; } + edits.push(node.replace(RENAMES[name]!)); + }; - edits.push(ident.replace(RENAMES[name]!)); + for (const ident of root.findAll({ rule: { kind: "identifier" } })) { + renameNode(ident); + } + // Shorthand property references in object literals ({ defineWaitPoints }) use a + // distinct AST node kind and must be renamed separately. + for (const prop of root.findAll({ rule: { kind: "shorthand_property_identifier" } })) { + renameNode(prop); } } diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/destructuring-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/destructuring-shadow/expected.ts new file mode 100644 index 000000000..c03f074e7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/destructuring-shadow/expected.ts @@ -0,0 +1,14 @@ +import { createWorkflow, createWaitPoints } from "@tailor-platform/sdk"; + +// Top-level SDK usage — should be renamed +export const { approval } = createWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +// Object destructuring shadows the import — calls inside should NOT be renamed +function helper(source: Record void>) { + const { defineWaitPoints } = source; + return defineWaitPoints(); +} + +export default createWorkflow({ name: "wf", mainJob: {} as never }); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/destructuring-shadow/input.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/destructuring-shadow/input.ts new file mode 100644 index 000000000..a07ac53ec --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/destructuring-shadow/input.ts @@ -0,0 +1,14 @@ +import { createWorkflow, defineWaitPoints } from "@tailor-platform/sdk"; + +// Top-level SDK usage — should be renamed +export const { approval } = defineWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +// Object destructuring shadows the import — calls inside should NOT be renamed +function helper(source: Record void>) { + const { defineWaitPoints } = source; + return defineWaitPoints(); +} + +export default createWorkflow({ name: "wf", mainJob: {} as never }); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/shorthand-property/expected.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/shorthand-property/expected.ts new file mode 100644 index 000000000..e9f7bf3d0 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/shorthand-property/expected.ts @@ -0,0 +1,6 @@ +import { createWorkflow, createWaitPoints } from "@tailor-platform/sdk"; + +// Object shorthand usage — should be renamed alongside the import +const api = { createWaitPoints }; + +export default createWorkflow({ name: "wf", mainJob: {} as never }); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/shorthand-property/input.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/shorthand-property/input.ts new file mode 100644 index 000000000..6263adc92 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/shorthand-property/input.ts @@ -0,0 +1,6 @@ +import { createWorkflow, defineWaitPoints } from "@tailor-platform/sdk"; + +// Object shorthand usage — should be renamed alongside the import +const api = { defineWaitPoints }; + +export default createWorkflow({ name: "wf", mainJob: {} as never }); From 955b4335294f9b5edd3000e24e1c0f045fb93bda Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 26 Jun 2026 09:58:22 +0900 Subject: [PATCH 245/618] fix(codemod): detect optional parameters and fix off-by-one in scope range check --- .../v2/wait-point-rename/scripts/transform.ts | 8 +++++--- .../tests/optional-param-shadow/expected.ts | 13 +++++++++++++ .../tests/optional-param-shadow/input.ts | 13 +++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/optional-param-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/wait-point-rename/tests/optional-param-shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts index 310c8e334..dfc2cf463 100644 --- a/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/scripts/transform.ts @@ -117,8 +117,10 @@ export default function transform(source: string, _filePath?: string): string | addShadowedRange(nameChild.text(), scopeNode); } - // Function/arrow parameters (required_parameter covers both regular and rest patterns). - const paramNodes = root.findAll({ rule: { kind: "required_parameter" } }); + // Function/arrow parameters — covers required (param: T) and optional (param?: T). + const paramNodes = root.findAll({ + rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] }, + }); for (const param of paramNodes) { if (isInsideImportStatement(param)) continue; // The name identifier may be a direct child or wrapped in rest_pattern. @@ -181,7 +183,7 @@ export default function transform(source: string, _filePath?: string): string | const ranges = shadowedRanges.get(name); if (ranges) { const pos = node.range().start.index; - if (ranges.some((r) => pos >= r.start && pos <= r.end)) return; + if (ranges.some((r) => pos >= r.start && pos < r.end)) return; } edits.push(node.replace(RENAMES[name]!)); }; diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/optional-param-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/optional-param-shadow/expected.ts new file mode 100644 index 000000000..204ce53e8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/optional-param-shadow/expected.ts @@ -0,0 +1,13 @@ +import { createWorkflow, createWaitPoints } from "@tailor-platform/sdk"; + +// Top-level SDK usage — should be renamed +export const { approval } = createWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +// Optional parameter shadows the import — should NOT rename inside +function processAll(defineWaitPoints?: unknown[]) { + return defineWaitPoints?.length; +} + +export default createWorkflow({ name: "wf", mainJob: {} as never }); diff --git a/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/optional-param-shadow/input.ts b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/optional-param-shadow/input.ts new file mode 100644 index 000000000..8e77ef8af --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/wait-point-rename/tests/optional-param-shadow/input.ts @@ -0,0 +1,13 @@ +import { createWorkflow, defineWaitPoints } from "@tailor-platform/sdk"; + +// Top-level SDK usage — should be renamed +export const { approval } = defineWaitPoints((define) => ({ + approval: define<{ message: string }, { approved: boolean }>(), +})); + +// Optional parameter shadows the import — should NOT rename inside +function processAll(defineWaitPoints?: unknown[]) { + return defineWaitPoints?.length; +} + +export default createWorkflow({ name: "wf", mainJob: {} as never }); From cdfa2c715bdfa158d4a8cf29dd23a6911f254ff6 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 26 Jun 2026 16:10:30 +0900 Subject: [PATCH 246/618] fix(sdk-codemod): add rename-bin build entry, fix npx form, update symlinks and tests - Add v2/rename-bin transform entry to tsdown.config.ts so the script is included in the published package - Rewrite `npx tailor-sdk@...` to `npx @tailor-platform/sdk@...` to avoid resolving the unrelated npm `tailor` package - Add NPX_RE regex to handle npx invocations separately from the general binary rename - Update test fixtures to reflect correct npx rewrite (basic-shell, basic-yaml, version-qualified) - Add v2/rename-bin to transform.test.ts fixture test suite - Rename .agents/skills/tailor-sdk and .claude/skills/tailor-sdk symlinks to tailor - Regenerate v2 migration docs --- .agents/skills/tailor | 1 + .agents/skills/tailor-sdk | 1 - .claude/skills/tailor | 1 + .claude/skills/tailor-sdk | 1 - .../codemods/v2/rename-bin/scripts/transform.ts | 13 +++++++++++-- .../v2/rename-bin/tests/basic-shell/expected.sh | 2 +- .../v2/rename-bin/tests/basic-yaml/expected.yml | 2 +- .../rename-bin/tests/version-qualified/expected.sh | 2 ++ .../v2/rename-bin/tests/version-qualified/input.sh | 2 ++ packages/sdk-codemod/src/registry.ts | 2 +- packages/sdk-codemod/src/transform.test.ts | 4 ++++ packages/sdk-codemod/tsdown.config.ts | 1 + packages/sdk/docs/migration/v2.md | 2 +- 13 files changed, 26 insertions(+), 8 deletions(-) create mode 120000 .agents/skills/tailor delete mode 120000 .agents/skills/tailor-sdk create mode 120000 .claude/skills/tailor delete mode 120000 .claude/skills/tailor-sdk diff --git a/.agents/skills/tailor b/.agents/skills/tailor new file mode 120000 index 000000000..2165e68b1 --- /dev/null +++ b/.agents/skills/tailor @@ -0,0 +1 @@ +../../packages/sdk/agent-skills/tailor \ No newline at end of file diff --git a/.agents/skills/tailor-sdk b/.agents/skills/tailor-sdk deleted file mode 120000 index 167d322cb..000000000 --- a/.agents/skills/tailor-sdk +++ /dev/null @@ -1 +0,0 @@ -../../packages/sdk/agent-skills/tailor-sdk \ No newline at end of file diff --git a/.claude/skills/tailor b/.claude/skills/tailor new file mode 120000 index 000000000..9f908626d --- /dev/null +++ b/.claude/skills/tailor @@ -0,0 +1 @@ +../../.agents/skills/tailor \ No newline at end of file diff --git a/.claude/skills/tailor-sdk b/.claude/skills/tailor-sdk deleted file mode 120000 index 5b9c704ab..000000000 --- a/.claude/skills/tailor-sdk +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/tailor-sdk \ No newline at end of file diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 23578fdde..d54983f2c 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -1,5 +1,9 @@ import * as path from "pathe"; +// `npx tailor-sdk@...` must become `npx @tailor-platform/sdk@...` — rewriting +// to `npx tailor@...` would resolve the unrelated npm `tailor` package. +const NPX_RE = /\bnpx\s+tailor-sdk(?![\w-])(@[^\s'"`;|&)]+)?/g; + // Match the `tailor-sdk` binary, optionally with a version pin (`@latest`, // `@2.0.0`, etc.). Lookbehind excludes `.tailor-sdk` (preceded by `.`) and // `create-tailor-sdk` (preceded by `-`). Lookahead excludes trailing `-word` @@ -7,7 +11,10 @@ import * as path from "pathe"; const TAILOR_SDK_RE = /(? + const withNpx = value.replace(NPX_RE, (_, version?: string) => + version ? `npx @tailor-platform/sdk${version}` : "npx @tailor-platform/sdk", + ); + return withNpx.replace(TAILOR_SDK_RE, (_match, version?: string) => version ? `tailor${version}` : "tailor", ); } @@ -42,7 +49,9 @@ function transformPackageJson(source: string): string | null { /** * Rename `tailor-sdk` binary references to `tailor`. * - * Handles optional `@version` pins (`tailor-sdk@latest` → `tailor@latest`). + * Handles optional `@version` pins: + * - `npx tailor-sdk@latest` → `npx @tailor-platform/sdk@latest` (package-manager form) + * - `tailor-sdk@latest` elsewhere → `tailor@latest` * Does not rewrite `.tailor-sdk` directory paths or `create-tailor-sdk`. * @param source - File contents * @param filePath - Absolute path to the file (used to dispatch package.json vs text) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh index 027c87b73..eb5ca3b6f 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh @@ -4,4 +4,4 @@ set -euo pipefail pnpm exec tailor deploy tailor login tailor@latest deploy -npx tailor@2.0.0 workspace list +npx @tailor-platform/sdk@2.0.0 workspace list diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-yaml/expected.yml b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-yaml/expected.yml index eda750943..91cb025a0 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-yaml/expected.yml +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-yaml/expected.yml @@ -2,4 +2,4 @@ steps: - name: Deploy run: tailor deploy -w ${{ secrets.WORKSPACE_ID }} - name: Login check - run: npx tailor@latest login --help + run: npx @tailor-platform/sdk@latest login --help diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh index f00720db0..b65f3c37d 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh @@ -1,2 +1,4 @@ tailor@latest deploy -w my-workspace pnpm dlx tailor@2.0.0 login +npx @tailor-platform/sdk@latest login +npx @tailor-platform/sdk login diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh index 559cd868f..ff7d60073 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh @@ -1,2 +1,4 @@ tailor-sdk@latest deploy -w my-workspace pnpm dlx tailor-sdk@2.0.0 login +npx tailor-sdk@latest login +npx tailor-sdk login diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 7fac36e9f..7870dd6c7 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -451,7 +451,7 @@ export const allCodemods: CodemodPackage[] = [ { lang: "sh", before: "tailor-sdk deploy\nnpx tailor-sdk@latest login", - after: "tailor deploy\nnpx tailor@latest login", + after: "tailor deploy\nnpx @tailor-platform/sdk@latest login", }, ], prompt: [ diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index 5ab9121fa..6df74739c 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -98,4 +98,8 @@ describe("codemod transforms", () => { test("v2/execute-script-arg transforms correctly", async () => { await expect(runFixtureCases("v2/execute-script-arg")).resolves.toBeUndefined(); }); + + test("v2/rename-bin transforms correctly", async () => { + await expect(runFixtureCases("v2/rename-bin")).resolves.toBeUndefined(); + }); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index aa8ede142..4bed559ef 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -31,6 +31,7 @@ export default defineConfig([ "codemods/v2/tailordb-namespace/scripts/transform.ts", "v2/execute-script-arg/scripts/transform": "codemods/v2/execute-script-arg/scripts/transform.ts", + "v2/rename-bin/scripts/transform": "codemods/v2/rename-bin/scripts/transform.ts", }, format: ["esm"], target: "node18", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 299846cfe..d71c12004 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -477,7 +477,7 @@ After: ```sh tailor deploy -npx tailor@latest login +npx @tailor-platform/sdk@latest login ```
From 965c81b5fc1bc54b6d9a618754de8041d3dc0143 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 26 Jun 2026 16:27:51 +0900 Subject: [PATCH 247/618] fix(sdk-codemod): extend package-runner rewrite to pnpm dlx, yarn dlx, and bunx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Like `npx`, `pnpm dlx` / `yarn dlx` / `bunx` resolve npm package names — rewriting `tailor-sdk@...` to `tailor@...` would download the unrelated CSS Sprites Generator. Extend PKG_RUNNER_RE (renamed from NPX_RE) to cover all package-runner forms so ` tailor-sdk@...` becomes ` @tailor-platform/sdk@...`. Add yarn dlx and bunx cases to the version-qualified fixture. --- .../codemods/v2/rename-bin/scripts/transform.ts | 16 +++++++++------- .../tests/version-qualified/expected.sh | 4 +++- .../rename-bin/tests/version-qualified/input.sh | 2 ++ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index d54983f2c..e40aafff9 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -1,8 +1,9 @@ import * as path from "pathe"; -// `npx tailor-sdk@...` must become `npx @tailor-platform/sdk@...` — rewriting -// to `npx tailor@...` would resolve the unrelated npm `tailor` package. -const NPX_RE = /\bnpx\s+tailor-sdk(?![\w-])(@[^\s'"`;|&)]+)?/g; +// Package-runner forms (`npx`, `pnpm dlx`, `yarn dlx`, `bunx`) resolve npm package +// names, so `tailor-sdk@...` must become `@tailor-platform/sdk@...` — rewriting +// to `tailor@...` would download the unrelated CSS Sprites Generator instead. +const PKG_RUNNER_RE = /\b(npx|pnpm\s+dlx|yarn\s+dlx|bunx)\s+tailor-sdk(?![\w-])(@[^\s'"`;|&)]+)?/g; // Match the `tailor-sdk` binary, optionally with a version pin (`@latest`, // `@2.0.0`, etc.). Lookbehind excludes `.tailor-sdk` (preceded by `.`) and @@ -11,10 +12,10 @@ const NPX_RE = /\bnpx\s+tailor-sdk(?![\w-])(@[^\s'"`;|&)]+)?/g; const TAILOR_SDK_RE = /(? - version ? `npx @tailor-platform/sdk${version}` : "npx @tailor-platform/sdk", + const withRunners = value.replace(PKG_RUNNER_RE, (_, runner: string, version?: string) => + version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, ); - return withNpx.replace(TAILOR_SDK_RE, (_match, version?: string) => + return withRunners.replace(TAILOR_SDK_RE, (_match, version?: string) => version ? `tailor${version}` : "tailor", ); } @@ -50,7 +51,8 @@ function transformPackageJson(source: string): string | null { * Rename `tailor-sdk` binary references to `tailor`. * * Handles optional `@version` pins: - * - `npx tailor-sdk@latest` → `npx @tailor-platform/sdk@latest` (package-manager form) + * - `npx tailor-sdk@latest` → `npx @tailor-platform/sdk@latest` (package-runner form) + * - `pnpm dlx tailor-sdk@latest` → `pnpm dlx @tailor-platform/sdk@latest` (package-runner form) * - `tailor-sdk@latest` elsewhere → `tailor@latest` * Does not rewrite `.tailor-sdk` directory paths or `create-tailor-sdk`. * @param source - File contents diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh index b65f3c37d..84037491e 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh @@ -1,4 +1,6 @@ tailor@latest deploy -w my-workspace -pnpm dlx tailor@2.0.0 login +pnpm dlx @tailor-platform/sdk@2.0.0 login +yarn dlx @tailor-platform/sdk@latest login +bunx @tailor-platform/sdk@1.0.0 login npx @tailor-platform/sdk@latest login npx @tailor-platform/sdk login diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh index ff7d60073..3a0baa0fe 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh @@ -1,4 +1,6 @@ tailor-sdk@latest deploy -w my-workspace pnpm dlx tailor-sdk@2.0.0 login +yarn dlx tailor-sdk@latest login +bunx tailor-sdk@1.0.0 login npx tailor-sdk@latest login npx tailor-sdk login From e1b2fa00b9973b7bacc9139274b0776233e0afce Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 26 Jun 2026 16:44:09 +0900 Subject: [PATCH 248/618] docs(sdk): add cd example-app step before workspace creation in quickstart --- packages/sdk/docs/quickstart.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sdk/docs/quickstart.md b/packages/sdk/docs/quickstart.md index a0b85bd01..5bd5bb831 100644 --- a/packages/sdk/docs/quickstart.md +++ b/packages/sdk/docs/quickstart.md @@ -24,8 +24,10 @@ The following command creates a new project with the required configuration file ```bash npm create @tailor-platform/sdk -- --template hello-world example-app +cd example-app # Or with Bun: # bun create @tailor-platform/sdk --template hello-world example-app +# cd example-app ``` Before deploying your app, you need to create a workspace: @@ -49,7 +51,6 @@ npx tailor workspace list Run the deploy command to deploy your project: ```bash -cd example-app npm run deploy -- --workspace-id # Or with Bun: # bun run deploy --workspace-id From 11be4c7b193410f28494789333ff8ca1afed36f8 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 26 Jun 2026 16:53:54 +0900 Subject: [PATCH 249/618] docs(sdk-codemod): note secrets-state.json migration in rename-bin v2 guide --- packages/sdk-codemod/src/registry.ts | 2 +- packages/sdk/docs/migration/v2.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 278ef1254..a0b7bcc79 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -525,7 +525,7 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/rename-bin", name: "tailor-sdk binary → tailor", description: - "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock` — update `.gitignore` entries manually (the codemod skips paths preceded by a dot).", + "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock` — update `.gitignore` entries manually (the codemod skips paths preceded by a dot). If you use auth connections (`defineAuth()` → `connections`), copy `.tailor-sdk/secrets-state.json` to `.tailor/secrets-state.json` to preserve existing connection state; without it v2 will treat connections as new and revoke their tokens.", since: "1.0.0", until: "2.0.0", scriptPath: "v2/rename-bin/scripts/transform.js", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 93ca2054b..c5370e311 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -551,7 +551,7 @@ trigger result as the job output directly (no Promise wrapper to unwrap). **Migration:** Partially automatic -Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock` — update `.gitignore` entries manually (the codemod skips paths preceded by a dot). +Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock` — update `.gitignore` entries manually (the codemod skips paths preceded by a dot). If you use auth connections (`defineAuth()` → `connections`), copy `.tailor-sdk/secrets-state.json` to `.tailor/secrets-state.json` to preserve existing connection state; without it v2 will treat connections as new and revoke their tokens. Before: From 4049e86d4ca1f459d0f89f20796cdd5ba01e0ade Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 26 Jun 2026 17:07:44 +0900 Subject: [PATCH 250/618] fix(sdk): update postinstall message and add lock file rename to v2 migration guide --- packages/sdk-codemod/src/registry.ts | 2 +- packages/sdk/docs/migration/v2.md | 2 +- packages/sdk/postinstall.mjs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index a0b7bcc79..6ea0424d1 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -525,7 +525,7 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/rename-bin", name: "tailor-sdk binary → tailor", description: - "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock` — update `.gitignore` entries manually (the codemod skips paths preceded by a dot). If you use auth connections (`defineAuth()` → `connections`), copy `.tailor-sdk/secrets-state.json` to `.tailor/secrets-state.json` to preserve existing connection state; without it v2 will treat connections as new and revoke their tokens.", + "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot). If you use auth connections (`defineAuth()` → `connections`), copy `.tailor-sdk/secrets-state.json` to `.tailor/secrets-state.json` to preserve existing connection state; without it v2 will treat connections as new and revoke their tokens.", since: "1.0.0", until: "2.0.0", scriptPath: "v2/rename-bin/scripts/transform.js", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index c5370e311..0a849dc11 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -551,7 +551,7 @@ trigger result as the job output directly (no Promise wrapper to unwrap). **Migration:** Partially automatic -Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock` — update `.gitignore` entries manually (the codemod skips paths preceded by a dot). If you use auth connections (`defineAuth()` → `connections`), copy `.tailor-sdk/secrets-state.json` to `.tailor/secrets-state.json` to preserve existing connection state; without it v2 will treat connections as new and revoke their tokens. +Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot). If you use auth connections (`defineAuth()` → `connections`), copy `.tailor-sdk/secrets-state.json` to `.tailor/secrets-state.json` to preserve existing connection state; without it v2 will treat connections as new and revoke their tokens. Before: diff --git a/packages/sdk/postinstall.mjs b/packages/sdk/postinstall.mjs index 3fb0ddecb..76e72bf74 100644 --- a/packages/sdk/postinstall.mjs +++ b/packages/sdk/postinstall.mjs @@ -11,9 +11,9 @@ const DEFAULT_CONFIG_FILENAME = "tailor.config.ts"; async function install() { const cwd = process.env.INIT_CWD || process.cwd(); - // Skip if running in the tailor-sdk package itself + // Skip if running in the @tailor-platform/sdk package itself if (cwd === __dirname || cwd === resolve(__dirname, "..", "..")) { - console.log("⚠️ Skipping postinstall in tailor-sdk package itself"); + console.log("⚠️ Skipping postinstall in @tailor-platform/sdk package itself"); return; } From d1fc5c5c89d9a55afa68171e509c061ef7ffb596 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 26 Jun 2026 20:18:30 +0900 Subject: [PATCH 251/618] docs(sdk-codemod): simplify migration guide to mv .tailor-sdk .tailor --- packages/sdk-codemod/src/registry.ts | 2 +- packages/sdk/docs/migration/v2.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 6ea0424d1..43289dce4 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -525,7 +525,7 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/rename-bin", name: "tailor-sdk binary → tailor", description: - "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot). If you use auth connections (`defineAuth()` → `connections`), copy `.tailor-sdk/secrets-state.json` to `.tailor/secrets-state.json` to preserve existing connection state; without it v2 will treat connections as new and revoke their tokens.", + "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot).", since: "1.0.0", until: "2.0.0", scriptPath: "v2/rename-bin/scripts/transform.js", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 0a849dc11..6a525cfe3 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -551,7 +551,7 @@ trigger result as the job output directly (no Promise wrapper to unwrap). **Migration:** Partially automatic -Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot). If you use auth connections (`defineAuth()` → `connections`), copy `.tailor-sdk/secrets-state.json` to `.tailor/secrets-state.json` to preserve existing connection state; without it v2 will treat connections as new and revoke their tokens. +Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot). Before: From 266203c3185387dcf482cf5861bd5b55e0594bbe Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 26 Jun 2026 20:39:48 +0900 Subject: [PATCH 252/618] test(sdk-codemod): add tailor-sdk-skills boundary fixture to no-match --- .../sdk-codemod/codemods/v2/rename-bin/tests/no-match/input.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/no-match/input.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/no-match/input.sh index 4a32df489..75da395ed 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/no-match/input.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/no-match/input.sh @@ -5,3 +5,6 @@ npx create-tailor-sdk@latest my-app # .tailor-sdk directory paths are not the binary ls .tailor-sdk/cache cat .tailor-sdk/config.json + +# tailor-sdk-skills has a trailing hyphen+word after the base token — not a binary invocation +tailor-sdk-skills From b216c23365f6ee46fc8efba2a276ba582584281a Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 26 Jun 2026 20:52:09 +0900 Subject: [PATCH 253/618] fix(sdk-codemod): rewrite version-qualified tailor-sdk refs to @tailor-platform/sdk Previously `tailor-sdk@latest` without a package-runner prefix was rewritten to `tailor@latest`, which resolves the unrelated CSS Sprites Generator package. Now any version-pinned form falls back to `@tailor-platform/sdk@version`, so `npx -y tailor-sdk@latest` and bare `tailor-sdk@1.0.0` both produce the correct package name. Unversioned `tailor-sdk` still becomes `tailor`. --- .../sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts | 4 ++-- .../v2/rename-bin/tests/basic-package-json/expected.json | 2 +- .../codemods/v2/rename-bin/tests/basic-shell/expected.sh | 2 +- .../v2/rename-bin/tests/version-qualified/expected.sh | 3 ++- .../codemods/v2/rename-bin/tests/version-qualified/input.sh | 1 + 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index e40aafff9..b247e32fa 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -16,7 +16,7 @@ function renameBinary(value: string): string { version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, ); return withRunners.replace(TAILOR_SDK_RE, (_match, version?: string) => - version ? `tailor${version}` : "tailor", + version ? `@tailor-platform/sdk${version}` : "tailor", ); } @@ -53,7 +53,7 @@ function transformPackageJson(source: string): string | null { * Handles optional `@version` pins: * - `npx tailor-sdk@latest` → `npx @tailor-platform/sdk@latest` (package-runner form) * - `pnpm dlx tailor-sdk@latest` → `pnpm dlx @tailor-platform/sdk@latest` (package-runner form) - * - `tailor-sdk@latest` elsewhere → `tailor@latest` + * - `tailor-sdk@latest` elsewhere → `@tailor-platform/sdk@latest` * Does not rewrite `.tailor-sdk` directory paths or `create-tailor-sdk`. * @param source - File contents * @param filePath - Absolute path to the file (used to dispatch package.json vs text) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-package-json/expected.json b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-package-json/expected.json index 1bedc183b..d8d47ae29 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-package-json/expected.json +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-package-json/expected.json @@ -4,7 +4,7 @@ "scripts": { "deploy": "tailor deploy", "login": "pnpm exec tailor login", - "generate": "tailor@latest generate", + "generate": "@tailor-platform/sdk@latest generate", "build": "tsc" } } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh index eb5ca3b6f..62283d810 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh @@ -3,5 +3,5 @@ set -euo pipefail pnpm exec tailor deploy tailor login -tailor@latest deploy +@tailor-platform/sdk@latest deploy npx @tailor-platform/sdk@2.0.0 workspace list diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh index 84037491e..5cf703319 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/expected.sh @@ -1,6 +1,7 @@ -tailor@latest deploy -w my-workspace +@tailor-platform/sdk@latest deploy -w my-workspace pnpm dlx @tailor-platform/sdk@2.0.0 login yarn dlx @tailor-platform/sdk@latest login bunx @tailor-platform/sdk@1.0.0 login npx @tailor-platform/sdk@latest login npx @tailor-platform/sdk login +npx -y @tailor-platform/sdk@latest login diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh index 3a0baa0fe..25ef731c5 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/version-qualified/input.sh @@ -4,3 +4,4 @@ yarn dlx tailor-sdk@latest login bunx tailor-sdk@1.0.0 login npx tailor-sdk@latest login npx tailor-sdk login +npx -y tailor-sdk@latest login From 1b7336fc41aee7188f60484a7ffb9b457a38a35e Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 26 Jun 2026 21:04:27 +0900 Subject: [PATCH 254/618] fix(sdk-codemod): match runner flags in PKG_RUNNER_RE for correct package rewrite `npx -y tailor-sdk login` was falling through to TAILOR_SDK_RE, producing `npx -y tailor login`, which downloads the unrelated npm `tailor` package. Extend PKG_RUNNER_RE to include optional short/long flags in the runner capture group so the replacement preserves them and produces `npx -y @tailor-platform/sdk login` as expected. --- .../sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts | 6 +++++- .../codemods/v2/rename-bin/tests/basic-shell/expected.sh | 1 + .../codemods/v2/rename-bin/tests/basic-shell/input.sh | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index b247e32fa..0512d48f7 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -3,7 +3,10 @@ import * as path from "pathe"; // Package-runner forms (`npx`, `pnpm dlx`, `yarn dlx`, `bunx`) resolve npm package // names, so `tailor-sdk@...` must become `@tailor-platform/sdk@...` — rewriting // to `tailor@...` would download the unrelated CSS Sprites Generator instead. -const PKG_RUNNER_RE = /\b(npx|pnpm\s+dlx|yarn\s+dlx|bunx)\s+tailor-sdk(?![\w-])(@[^\s'"`;|&)]+)?/g; +// Optional flags (e.g. `-y`, `--yes`) between the runner and the package name are +// captured as part of the runner group so the replacement preserves them. +const PKG_RUNNER_RE = + /\b((?:npx|pnpm\s+dlx|yarn\s+dlx|bunx)(?:\s+(?:-\w+|--\w[\w-]*))*)\s+tailor-sdk(?![\w-])(@[^\s'"`;|&)]+)?/g; // Match the `tailor-sdk` binary, optionally with a version pin (`@latest`, // `@2.0.0`, etc.). Lookbehind excludes `.tailor-sdk` (preceded by `.`) and @@ -52,6 +55,7 @@ function transformPackageJson(source: string): string | null { * * Handles optional `@version` pins: * - `npx tailor-sdk@latest` → `npx @tailor-platform/sdk@latest` (package-runner form) + * - `npx -y tailor-sdk login` → `npx -y @tailor-platform/sdk login` (runner flags preserved) * - `pnpm dlx tailor-sdk@latest` → `pnpm dlx @tailor-platform/sdk@latest` (package-runner form) * - `tailor-sdk@latest` elsewhere → `@tailor-platform/sdk@latest` * Does not rewrite `.tailor-sdk` directory paths or `create-tailor-sdk`. diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh index 62283d810..1752dc093 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh @@ -5,3 +5,4 @@ pnpm exec tailor deploy tailor login @tailor-platform/sdk@latest deploy npx @tailor-platform/sdk@2.0.0 workspace list +npx -y @tailor-platform/sdk login diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh index 70d126faf..e7743b6b7 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh @@ -5,3 +5,4 @@ pnpm exec tailor-sdk deploy tailor-sdk login tailor-sdk@latest deploy npx tailor-sdk@2.0.0 workspace list +npx -y tailor-sdk login From 579cb4705cb295c1fcf9bff948d205fb245ff4e5 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 02:17:28 +0900 Subject: [PATCH 255/618] fix(sdk-codemod): apply v2 codemods to prerelease targets --- .changeset/fix-v2-prerelease-codemods.md | 5 +++++ packages/sdk-codemod/src/registry.test.ts | 13 ++++++++++++ packages/sdk-codemod/src/registry.ts | 25 ++++++++++++++++++++--- 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-v2-prerelease-codemods.md diff --git a/.changeset/fix-v2-prerelease-codemods.md b/.changeset/fix-v2-prerelease-codemods.md new file mode 100644 index 000000000..90ba3dcf1 --- /dev/null +++ b/.changeset/fix-v2-prerelease-codemods.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Run v2 codemods when the target version is a v2 prerelease. diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 301045f83..cebd319d1 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -8,10 +8,23 @@ describe("getApplicableCodemods", () => { expect(codemods[0]!.id).toBe("v2/define-generators-to-plugins"); }); + test("returns codemods when upgrading to a prerelease at their version boundary", () => { + const stableCodemods = getApplicableCodemods("1.67.1", "2.0.0"); + const prereleaseCodemods = getApplicableCodemods("1.67.1", "2.0.0-next.2"); + + expect(prereleaseCodemods.map((codemod) => codemod.id)).toEqual( + stableCodemods.map((codemod) => codemod.id), + ); + }); + test("returns empty when both versions are before the codemod boundary", () => { expect(getApplicableCodemods("1.0.0", "1.5.0")).toEqual([]); }); + test("returns empty when the target prerelease is before the codemod boundary", () => { + expect(getApplicableCodemods("1.67.1", "1.99.0-next.1")).toEqual([]); + }); + test("returns empty when both versions are after the codemod boundary", () => { expect(getApplicableCodemods("2.0.0", "3.0.0")).toEqual([]); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 43289dce4..a3137cb46 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -1,6 +1,6 @@ import * as url from "node:url"; import * as path from "pathe"; -import { lt, gte, valid } from "semver"; +import { gte, lt, parse, valid } from "semver"; import type { CodemodPackage } from "./types"; const CODEMODS_ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "codemods"); @@ -564,9 +564,28 @@ export function resolveCodemodScript(scriptPath: string): string { return path.resolve(CODEMODS_ROOT, scriptPath); } +function reachesCodemodBoundary(toVersion: string, boundaryVersion: string): boolean { + if (gte(toVersion, boundaryVersion)) { + return true; + } + + const target = parse(toVersion); + const boundary = parse(boundaryVersion); + + return ( + target !== null && + boundary !== null && + target.prerelease.length > 0 && + target.major === boundary.major && + target.minor === boundary.minor && + target.patch === boundary.patch + ); +} + /** * Get codemod packages applicable for a version range. - * A codemod applies when: since <= fromVersion < until <= toVersion + * A codemod applies when: since <= fromVersion < until <= toVersion. + * A target prerelease for `until` also reaches that boundary. * @param fromVersion - Current SDK version (semver) * @param toVersion - Target SDK version (semver) * @returns Array of applicable codemod packages in registration order @@ -583,6 +602,6 @@ export function getApplicableCodemods(fromVersion: string, toVersion: string): C (codemod) => gte(fromVersion, codemod.since) && lt(fromVersion, codemod.until) && - gte(toVersion, codemod.until), + reachesCodemodBoundary(toVersion, codemod.until), ); } From 0cdfc5dab153ad5df0dda535b0c261a6fe1ef947 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 02:29:17 +0900 Subject: [PATCH 256/618] fix(sdk-codemod): honor prerelease codemod boundaries --- packages/sdk-codemod/src/registry.test.ts | 8 +++++ packages/sdk-codemod/src/registry.ts | 42 ++++++++++++++++++++--- packages/sdk-codemod/src/types.ts | 2 ++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index cebd319d1..7e50ab49b 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -21,6 +21,14 @@ describe("getApplicableCodemods", () => { expect(getApplicableCodemods("1.0.0", "1.5.0")).toEqual([]); }); + test("uses each codemod's prerelease boundary", () => { + const ids = getApplicableCodemods("1.67.1", "2.0.0-next.1").map((codemod) => codemod.id); + + expect(ids).toContain("v2/test-run-arg-input"); + expect(ids).not.toContain("v2/execute-script-arg"); + expect(ids).not.toContain("v2/principal-unify"); + }); + test("returns empty when the target prerelease is before the codemod boundary", () => { expect(getApplicableCodemods("1.67.1", "1.99.0-next.1")).toEqual([]); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index a3137cb46..32f599dd2 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -4,6 +4,8 @@ import { gte, lt, parse, valid } from "semver"; import type { CodemodPackage } from "./types"; const CODEMODS_ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "codemods"); +const V2_NEXT_1 = "2.0.0-next.1"; +const V2_NEXT_2 = "2.0.0-next.2"; /** All registered codemods, in registration order. */ export const allCodemods: CodemodPackage[] = [ @@ -14,6 +16,7 @@ export const allCodemods: CodemodPackage[] = [ "Migrate defineGenerators() tuple syntax to definePlugins() with explicit plugin imports", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_1, scriptPath: "v2/define-generators-to-plugins/scripts/transform.js", legacyPatterns: ["defineGenerators"], examples: [ @@ -48,6 +51,7 @@ export const allCodemods: CodemodPackage[] = [ "Rewrite deprecated plugin re-export imports (kyselyTypePlugin, enumConstantsPlugin, fileUtilsPlugin, seedPlugin) from `@tailor-platform/sdk/cli` to their dedicated plugin subpaths", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_1, scriptPath: "v2/plugin-cli-import/scripts/transform.js", examples: [ { @@ -63,6 +67,7 @@ export const allCodemods: CodemodPackage[] = [ "Strip the deprecated {input: ...} wrapper from `tailor function test-run --arg` JSON in scripts and docs", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_1, scriptPath: "v2/test-run-arg-input/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh}", "**/*.md"], examples: [ @@ -80,6 +85,7 @@ export const allCodemods: CodemodPackage[] = [ "Replace deprecated `tailor-sdk-skills` invocations with `tailor-sdk skills install`", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_1, scriptPath: "v2/sdk-skills-shim/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], legacyPatterns: ["tailor-sdk-skills"], @@ -104,6 +110,7 @@ export const allCodemods: CodemodPackage[] = [ "Rename TailorUser/TailorActor/TailorActorType/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, rename resolver body `user` to `caller`, and rename TailorDB callback `user` to `invoker`", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_2, scriptPath: "v2/principal-unify/scripts/transform.js", legacyPatterns: [ "TailorUser", @@ -153,6 +160,7 @@ export const allCodemods: CodemodPackage[] = [ "Rename auth attribute module augmentation and related SDK type names from `AttributeMap` to `Attributes`", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_2, scriptPath: "v2/auth-attributes-rename/scripts/transform.js", legacyPatterns: [ "AttributeMap", @@ -185,6 +193,7 @@ export const allCodemods: CodemodPackage[] = [ "Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor-sdk deploy` command", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_1, scriptPath: "v2/apply-to-deploy/scripts/transform.js", filePatterns: [ "**/package.json", @@ -207,6 +216,7 @@ export const allCodemods: CodemodPackage[] = [ "Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_1, scriptPath: "v2/cli-rename/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], legacyPatterns: ["tailor-sdk crash-report", "--machineuser"], @@ -231,6 +241,7 @@ export const allCodemods: CodemodPackage[] = [ "Rewrite unambiguous removed SDK environment variable names to their v2 `TAILOR_*` names and flag generic names for manual review", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_2, scriptPath: "v2/env-var-rename/scripts/transform.js", filePatterns: [ "**/package.json", @@ -283,6 +294,7 @@ export const allCodemods: CodemodPackage[] = [ 'Rename statically identified SDK `authInvoker` options to `invoker`, replace `auth.invoker("name")` there with the bare `"name"` string, and drop the `auth` import when no other reference remains. Ambiguous workflow `.trigger()` calls are left for manual review. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle.', since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_2, scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js", suspiciousPatterns: [ "auth.invoker", @@ -334,6 +346,7 @@ export const allCodemods: CodemodPackage[] = [ 'Rewrite references to the removed capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the lowercase `tailordb.*` namespace exposed by `@tailor-platform/sdk/runtime/globals`. Because v2 no longer activates ambient declarations automatically, each file that contains `tailordb.*` references after the rewrite must also add `import "@tailor-platform/sdk/runtime/globals"`.', since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_1, scriptPath: "v2/tailordb-namespace/scripts/transform.js", legacyPatterns: ["Tailordb."], examples: [ @@ -361,6 +374,7 @@ export const allCodemods: CodemodPackage[] = [ "Unwrap `JSON.stringify(...)` passed as the `executeScript` `arg` option. In v2 `arg` takes a JSON-serializable value and is serialized internally, so a pre-stringified argument double-encodes.", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_2, scriptPath: "v2/execute-script-arg/scripts/transform.js", filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"], suspiciousPatterns: [ @@ -394,6 +408,7 @@ export const allCodemods: CodemodPackage[] = [ "The deprecated `openDownloadStream` file-streaming API is removed in v2. Use `downloadStream` for streamed file downloads. The generated file utilities now emit `downloadFileStream` (which calls `downloadStream` and returns `FileDownloadStreamResponse`) instead of the removed `openFileDownloadStream` helper.", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_2, // No scriptPath: this is a codemod-less ("manual") migration. filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"], suspiciousPatterns: ["openDownloadStream", "openFileDownloadStream"], @@ -417,6 +432,7 @@ export const allCodemods: CodemodPackage[] = [ 'Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Normal SDK development does not need them — use the SDK APIs and the typed wrappers from `@tailor-platform/sdk/runtime`. Only if you relied on the ambient globals directly, add `import "@tailor-platform/sdk/runtime/globals"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.)', since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_1, filePatterns: ["**/*.{ts,tsx,mts,cts}"], suspiciousPatterns: [ "tailor.context", @@ -476,6 +492,7 @@ export const allCodemods: CodemodPackage[] = [ "Workflow job `.trigger()` now aligns with the platform runtime: it returns the job result directly instead of a Promise wrapper, and tests no longer run job bodies locally. Mock trigger responses with `mockWorkflow()` (`setJobHandler` / `enqueueResult`, assert via `triggeredJobs`), or use `runWorkflowLocally()` for a full-chain local run.", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_1, suspiciousPatterns: [".trigger("], examples: [ { @@ -501,6 +518,7 @@ export const allCodemods: CodemodPackage[] = [ "CLI login tokens are stored in the OS keyring by default when available, falling back to the platform config file when it is not. No source change is required; re-login if you need tokens moved into the keyring.", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_2, notice: true, }, { @@ -510,6 +528,7 @@ export const allCodemods: CodemodPackage[] = [ "The CLI stores human users by their stable subject ID instead of email (email is kept for display). Legacy email-keyed entries are migrated automatically on the next login or token refresh. No source change is required.", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_1, notice: true, }, { @@ -519,6 +538,7 @@ export const allCodemods: CodemodPackage[] = [ "`tailor function logs` maps stack traces against the function bundle only when the execution recorded a `contentHash`. Executions without one now show raw stack traces instead of mapped frames. No source change is required.", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_1, notice: true, }, { @@ -528,6 +548,7 @@ export const allCodemods: CodemodPackage[] = [ "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot).", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_2, scriptPath: "v2/rename-bin/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], legacyPatterns: ["tailor-sdk"], @@ -551,6 +572,7 @@ export const allCodemods: CodemodPackage[] = [ "v2 requires Node.js **22.15.0** or later. This is the first version that includes `module.registerHooks()`, which the SDK uses to register its TypeScript loader hook synchronously in the main thread. No source change is required; ensure your environment runs Node.js 22.15.0+.", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_2, notice: true, }, ]; @@ -564,21 +586,31 @@ export function resolveCodemodScript(scriptPath: string): string { return path.resolve(CODEMODS_ROOT, scriptPath); } -function reachesCodemodBoundary(toVersion: string, boundaryVersion: string): boolean { - if (gte(toVersion, boundaryVersion)) { +function reachesCodemodBoundary(toVersion: string, codemod: CodemodPackage): boolean { + if (gte(toVersion, codemod.until)) { return true; } + if (codemod.prereleaseUntil === undefined || !gte(toVersion, codemod.prereleaseUntil)) { + return false; + } const target = parse(toVersion); - const boundary = parse(boundaryVersion); + const boundary = parse(codemod.until); + const prereleaseBoundary = parse(codemod.prereleaseUntil); return ( target !== null && boundary !== null && + prereleaseBoundary !== null && target.prerelease.length > 0 && + boundary.prerelease.length === 0 && + prereleaseBoundary.prerelease.length > 0 && target.major === boundary.major && target.minor === boundary.minor && - target.patch === boundary.patch + target.patch === boundary.patch && + prereleaseBoundary.major === boundary.major && + prereleaseBoundary.minor === boundary.minor && + prereleaseBoundary.patch === boundary.patch ); } @@ -602,6 +634,6 @@ export function getApplicableCodemods(fromVersion: string, toVersion: string): C (codemod) => gte(fromVersion, codemod.since) && lt(fromVersion, codemod.until) && - reachesCodemodBoundary(toVersion, codemod.until), + reachesCodemodBoundary(toVersion, codemod), ); } diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index 34965ebd6..31c40ef5c 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -28,6 +28,8 @@ export interface CodemodPackage { since: string; /** Target version this codemod upgrades to (semver, exclusive upper bound) */ until: string; + /** Earliest prerelease target that should apply this codemod before `until` is stable. */ + prereleaseUntil?: string; /** * Path to the jssg transform script relative to the codemods root. Omit for a * codemod-less ("manual") migration that ships only guidance — `prompt`, From 3a0b36fc59aacf5113bb06063c01ee2be0b6df32 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 02:39:07 +0900 Subject: [PATCH 257/618] fix(sdk-codemod): avoid repeated prerelease migrations --- packages/sdk-codemod/src/registry.test.ts | 19 +++++++++++++++++-- packages/sdk-codemod/src/registry.ts | 14 +++++++------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 7e50ab49b..adbd8c38e 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -8,12 +8,23 @@ describe("getApplicableCodemods", () => { expect(codemods[0]!.id).toBe("v2/define-generators-to-plugins"); }); + test("returns all v2 codemods when upgrading to the stable boundary", () => { + expect(getApplicableCodemods("1.67.1", "2.0.0").map((codemod) => codemod.id)).toEqual( + allCodemods.map((codemod) => codemod.id), + ); + }); + test("returns codemods when upgrading to a prerelease at their version boundary", () => { - const stableCodemods = getApplicableCodemods("1.67.1", "2.0.0"); const prereleaseCodemods = getApplicableCodemods("1.67.1", "2.0.0-next.2"); expect(prereleaseCodemods.map((codemod) => codemod.id)).toEqual( - stableCodemods.map((codemod) => codemod.id), + allCodemods + .filter( + (codemod) => + codemod.prereleaseUntil === "2.0.0-next.1" || + codemod.prereleaseUntil === "2.0.0-next.2", + ) + .map((codemod) => codemod.id), ); }); @@ -29,6 +40,10 @@ describe("getApplicableCodemods", () => { expect(ids).not.toContain("v2/principal-unify"); }); + test("returns empty when the source prerelease already reached the codemod boundary", () => { + expect(getApplicableCodemods("2.0.0-next.2", "2.0.0-next.2")).toEqual([]); + }); + test("returns empty when the target prerelease is before the codemod boundary", () => { expect(getApplicableCodemods("1.67.1", "1.99.0-next.1")).toEqual([]); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 32f599dd2..d1c6ee955 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -160,7 +160,6 @@ export const allCodemods: CodemodPackage[] = [ "Rename auth attribute module augmentation and related SDK type names from `AttributeMap` to `Attributes`", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_2, scriptPath: "v2/auth-attributes-rename/scripts/transform.js", legacyPatterns: [ "AttributeMap", @@ -241,7 +240,6 @@ export const allCodemods: CodemodPackage[] = [ "Rewrite unambiguous removed SDK environment variable names to their v2 `TAILOR_*` names and flag generic names for manual review", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_2, scriptPath: "v2/env-var-rename/scripts/transform.js", filePatterns: [ "**/package.json", @@ -548,7 +546,6 @@ export const allCodemods: CodemodPackage[] = [ "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot).", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_2, scriptPath: "v2/rename-bin/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], legacyPatterns: ["tailor-sdk"], @@ -572,7 +569,6 @@ export const allCodemods: CodemodPackage[] = [ "v2 requires Node.js **22.15.0** or later. This is the first version that includes `module.registerHooks()`, which the SDK uses to register its TypeScript loader hook synchronously in the main thread. No source change is required; ensure your environment runs Node.js 22.15.0+.", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_2, notice: true, }, ]; @@ -614,10 +610,14 @@ function reachesCodemodBoundary(toVersion: string, codemod: CodemodPackage): boo ); } +function effectiveCodemodBoundary(codemod: CodemodPackage): string { + return codemod.prereleaseUntil ?? codemod.until; +} + /** * Get codemod packages applicable for a version range. - * A codemod applies when: since <= fromVersion < until <= toVersion. - * A target prerelease for `until` also reaches that boundary. + * A codemod applies when: since <= fromVersion < boundary <= toVersion. + * A target prerelease reaches `until` only when the codemod declares `prereleaseUntil`. * @param fromVersion - Current SDK version (semver) * @param toVersion - Target SDK version (semver) * @returns Array of applicable codemod packages in registration order @@ -633,7 +633,7 @@ export function getApplicableCodemods(fromVersion: string, toVersion: string): C return allCodemods.filter( (codemod) => gte(fromVersion, codemod.since) && - lt(fromVersion, codemod.until) && + lt(fromVersion, effectiveCodemodBoundary(codemod)) && reachesCodemodBoundary(toVersion, codemod), ); } From 13e5b2cb877d4ddd6521a209b6f53b11d39a7faa Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 02:48:24 +0900 Subject: [PATCH 258/618] fix(sdk-codemod): split auth invoker prerelease migration --- .../v2/auth-invoker-call-unwrap/codemod.yaml | 4 ++ .../scripts/transform.ts | 5 ++ .../tests/helper-call/expected.ts | 8 +++ .../tests/helper-call/input.ts | 8 +++ .../tests/key-only/input.ts | 4 ++ .../auth-invoker-unwrap/scripts/transform.ts | 34 +++++++++---- packages/sdk-codemod/src/registry.test.ts | 1 + packages/sdk-codemod/src/registry.ts | 51 +++++++++++++++++++ packages/sdk-codemod/src/transform.test.ts | 4 ++ packages/sdk-codemod/tsdown.config.ts | 2 + packages/sdk/docs/migration/v2.md | 42 +++++++++++++++ 11 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/key-only/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/codemod.yaml b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/codemod.yaml new file mode 100644 index 000000000..d0dbdc776 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/codemod.yaml @@ -0,0 +1,4 @@ +name: "@tailor-platform/auth-invoker-call-unwrap" +version: 0.1.0 +description: 'Replace auth.invoker("name") with the bare string "name" while preserving the authInvoker option key' +language: typescript diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/scripts/transform.ts new file mode 100644 index 000000000..236865ec5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/scripts/transform.ts @@ -0,0 +1,5 @@ +import { transformAuthInvoker } from "../../auth-invoker-unwrap/scripts/transform"; + +export default function transform(source: string, filePath: string): string | null { + return transformAuthInvoker(source, filePath, { renameOptionKeys: false }); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/expected.ts new file mode 100644 index 000000000..7d62ccf47 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/expected.ts @@ -0,0 +1,8 @@ +import { db } from "../tailor.config"; + +createResolver({ + name: "orders", + operation: "query", + authInvoker: "kiosk", + body: () => db.type("Order"), +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/input.ts new file mode 100644 index 000000000..6a340cdae --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/input.ts @@ -0,0 +1,8 @@ +import { auth, db } from "../tailor.config"; + +createResolver({ + name: "orders", + operation: "query", + authInvoker: auth.invoker("kiosk"), + body: () => db.type("Order"), +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/key-only/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/key-only/input.ts new file mode 100644 index 000000000..10df309cf --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/key-only/input.ts @@ -0,0 +1,4 @@ +startWorkflow({ + workflow, + authInvoker: "kiosk", +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts index 379742507..cabe0e8eb 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts @@ -257,9 +257,13 @@ function renameQuotedKey(node: SgNode): string { return `${quote}invoker${quote}`; } +export interface AuthInvokerTransformOptions { + renameOptionKeys?: boolean; +} + /** * Replace `auth.invoker("name")` calls with the bare `"name"` string literal - * and rename `authInvoker:` option keys to `invoker:`. + * and optionally rename `authInvoker:` option keys to `invoker:`. * If no other `auth` references remain after the rewrite, drop the `auth` * specifier (or the entire import line when `auth` was its sole specifier). * @@ -268,23 +272,31 @@ function renameQuotedKey(node: SgNode): string { * would otherwise pull config-layer modules into runtime bundles. * @param source - File contents * @param filePath - Absolute path to the file (kept for the runner signature) + * @param options - Transform behavior flags * @returns Transformed source or null when nothing matched. */ -export default function transform(source: string, _filePath: string): string | null { +export function transformAuthInvoker( + source: string, + _filePath: string, + options: AuthInvokerTransformOptions = {}, +): string | null { if (!quickFilter(source)) return null; + const renameOptionKeys = options.renameOptionKeys ?? true; const lang = source.includes("") ? Lang.Tsx : Lang.TypeScript; const root = parse(lang, source).root(); const calls = findInvokerCalls(root).filter((c) => isSupportedInvokerValueCall(c.callNode)); const edits: Edit[] = calls.map((c) => c.callNode.replace(c.argText)); - edits.push(...findAuthInvokerPropertyKeys(root).map((node) => node.replace("invoker"))); - edits.push( - ...findQuotedAuthInvokerPropertyKeys(root).map((node) => node.replace(renameQuotedKey(node))), - ); - edits.push( - ...findAuthInvokerShorthands(root).map((node) => node.replace("invoker: authInvoker")), - ); + if (renameOptionKeys) { + edits.push(...findAuthInvokerPropertyKeys(root).map((node) => node.replace("invoker"))); + edits.push( + ...findQuotedAuthInvokerPropertyKeys(root).map((node) => node.replace(renameQuotedKey(node))), + ); + edits.push( + ...findAuthInvokerShorthands(root).map((node) => node.replace("invoker: authInvoker")), + ); + } if ( calls.length > 0 && @@ -307,3 +319,7 @@ export default function transform(source: string, _filePath: string): string | n return result === source ? null : result; } + +export default function transform(source: string, filePath: string): string | null { + return transformAuthInvoker(source, filePath); +} diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index adbd8c38e..251aacb7c 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -36,6 +36,7 @@ describe("getApplicableCodemods", () => { const ids = getApplicableCodemods("1.67.1", "2.0.0-next.1").map((codemod) => codemod.id); expect(ids).toContain("v2/test-run-arg-input"); + expect(ids).toContain("v2/auth-invoker-call-unwrap"); expect(ids).not.toContain("v2/execute-script-arg"); expect(ids).not.toContain("v2/principal-unify"); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index d1c6ee955..4ae1a0d27 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -285,6 +285,57 @@ export const allCodemods: CodemodPackage[] = [ "SDK, leave it unchanged.", ].join("\n"), }, + { + id: "v2/auth-invoker-call-unwrap", + name: 'auth.invoker("name") → "name"', + description: + 'Replace statically identified SDK `auth.invoker("name")` option values with the bare `"name"` string while preserving the `authInvoker` key for SDK versions before the option rename.', + since: "1.0.0", + until: "2.0.0", + prereleaseUntil: V2_NEXT_1, + scriptPath: "v2/auth-invoker-call-unwrap/scripts/transform.js", + suspiciousPatterns: [ + "auth.invoker", + "authInvoker:", + "authInvoker :", + "authInvoker?", + "{ authInvoker", + ", authInvoker", + "\n authInvoker", + "\n authInvoker", + "\n authInvoker", + '"authInvoker":', + '"authInvoker" :', + '"authInvoker"?', + "'authInvoker':", + "'authInvoker' :", + "'authInvoker'?", + ], + prompt: [ + "In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the", + "machine user name passed directly as a string. The codemod already rewrote the", + 'statically identified SDK option form authInvoker: auth.invoker("name") to authInvoker: "name". These files still contain', + "auth.invoker(...) calls or authInvoker keys that need manual review.", + "", + "For each remaining auth.invoker() call:", + "1. Replace the whole call with only where the target option expects a", + " machine user name string; platform/runtime authInvoker payloads still expect", + " the object form.", + "2. Keep the authInvoker key when targeting SDK versions before the invoker", + " option rename; later v2 targets run a separate codemod for that key rename.", + "3. After removing every auth.invoker usage in a file, delete the now-unused auth", + " import (keeping it pulls Node-only config modules into runtime bundles); leave", + " the import if auth is still referenced elsewhere.", + "", + "Do not change behavior beyond the auth.invoker() removal.", + ].join("\n"), + examples: [ + { + before: 'createResolver({ authInvoker: auth.invoker("manager") });', + after: 'createResolver({ authInvoker: "manager" });', + }, + ], + }, { id: "v2/auth-invoker-unwrap", name: 'auth.invoker("name") → invoker: "name"', diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index 41e9f9a5f..fbfdfed45 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -95,6 +95,10 @@ describe("codemod transforms", () => { await expect(runFixtureCases("v2/env-var-rename")).resolves.toBeUndefined(); }); + test("v2/auth-invoker-call-unwrap transforms correctly", async () => { + await expect(runFixtureCases("v2/auth-invoker-call-unwrap")).resolves.toBeUndefined(); + }); + test("v2/auth-invoker-unwrap transforms correctly", async () => { await expect(runFixtureCases("v2/auth-invoker-unwrap")).resolves.toBeUndefined(); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index 228194e94..8e7ce6db2 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -28,6 +28,8 @@ export default defineConfig([ "v2/apply-to-deploy/scripts/transform": "codemods/v2/apply-to-deploy/scripts/transform.ts", "v2/cli-rename/scripts/transform": "codemods/v2/cli-rename/scripts/transform.ts", "v2/env-var-rename/scripts/transform": "codemods/v2/env-var-rename/scripts/transform.ts", + "v2/auth-invoker-call-unwrap/scripts/transform": + "codemods/v2/auth-invoker-call-unwrap/scripts/transform.ts", "v2/auth-invoker-unwrap/scripts/transform": "codemods/v2/auth-invoker-unwrap/scripts/transform.ts", "v2/tailordb-namespace/scripts/transform": diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 6a525cfe3..321e37ce4 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -304,6 +304,48 @@ SDK, leave it unchanged.
+## auth.invoker("name") → "name" + +**Migration:** Partially automatic + +Replace statically identified SDK `auth.invoker("name")` option values with the bare `"name"` string while preserving the `authInvoker` key for SDK versions before the option rename. + +Before: + +```ts +createResolver({ authInvoker: auth.invoker("manager") }); +``` + +After: + +```ts +createResolver({ authInvoker: "manager" }); +``` + +
+Prompt for an AI agent (to finish the cases the codemod could not migrate) + +```text +In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the +machine user name passed directly as a string. The codemod already rewrote the +statically identified SDK option form authInvoker: auth.invoker("name") to authInvoker: "name". These files still contain +auth.invoker(...) calls or authInvoker keys that need manual review. + +For each remaining auth.invoker() call: +1. Replace the whole call with only where the target option expects a + machine user name string; platform/runtime authInvoker payloads still expect + the object form. +2. Keep the authInvoker key when targeting SDK versions before the invoker + option rename; later v2 targets run a separate codemod for that key rename. +3. After removing every auth.invoker usage in a file, delete the now-unused auth + import (keeping it pulls Node-only config modules into runtime bundles); leave + the import if auth is still referenced elsewhere. + +Do not change behavior beyond the auth.invoker() removal. +``` + +
+ ## auth.invoker("name") → invoker: "name" **Migration:** Partially automatic From 6190c498e3e09d684b52cc06e5a3c2b50f1ea288 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 02:55:46 +0900 Subject: [PATCH 259/618] fix(sdk-codemod): narrow auth invoker review signal --- packages/sdk-codemod/src/registry.test.ts | 4 ++++ packages/sdk-codemod/src/registry.ts | 20 ++------------------ packages/sdk/docs/migration/v2.md | 2 +- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 251aacb7c..a929a4658 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -34,9 +34,13 @@ describe("getApplicableCodemods", () => { test("uses each codemod's prerelease boundary", () => { const ids = getApplicableCodemods("1.67.1", "2.0.0-next.1").map((codemod) => codemod.id); + const authInvokerCallUnwrap = getApplicableCodemods("1.67.1", "2.0.0-next.1").find( + (codemod) => codemod.id === "v2/auth-invoker-call-unwrap", + ); expect(ids).toContain("v2/test-run-arg-input"); expect(ids).toContain("v2/auth-invoker-call-unwrap"); + expect(authInvokerCallUnwrap?.suspiciousPatterns).toEqual(["auth.invoker"]); expect(ids).not.toContain("v2/execute-script-arg"); expect(ids).not.toContain("v2/principal-unify"); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 4ae1a0d27..74d786dbf 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -294,28 +294,12 @@ export const allCodemods: CodemodPackage[] = [ until: "2.0.0", prereleaseUntil: V2_NEXT_1, scriptPath: "v2/auth-invoker-call-unwrap/scripts/transform.js", - suspiciousPatterns: [ - "auth.invoker", - "authInvoker:", - "authInvoker :", - "authInvoker?", - "{ authInvoker", - ", authInvoker", - "\n authInvoker", - "\n authInvoker", - "\n authInvoker", - '"authInvoker":', - '"authInvoker" :', - '"authInvoker"?', - "'authInvoker':", - "'authInvoker' :", - "'authInvoker'?", - ], + suspiciousPatterns: ["auth.invoker"], prompt: [ "In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the", "machine user name passed directly as a string. The codemod already rewrote the", 'statically identified SDK option form authInvoker: auth.invoker("name") to authInvoker: "name". These files still contain', - "auth.invoker(...) calls or authInvoker keys that need manual review.", + "auth.invoker(...) calls that need manual review.", "", "For each remaining auth.invoker() call:", "1. Replace the whole call with only where the target option expects a", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 321e37ce4..9931991bb 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -329,7 +329,7 @@ createResolver({ authInvoker: "manager" }); In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the machine user name passed directly as a string. The codemod already rewrote the statically identified SDK option form authInvoker: auth.invoker("name") to authInvoker: "name". These files still contain -auth.invoker(...) calls or authInvoker keys that need manual review. +auth.invoker(...) calls that need manual review. For each remaining auth.invoker() call: 1. Replace the whole call with only where the target option expects a From 1bebbd32c6c916d59413d413a18c1deec825871c Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 03:07:08 +0900 Subject: [PATCH 260/618] fix(sdk-codemod): complete auth invoker manifest --- .../codemods/v2/auth-invoker-call-unwrap/codemod.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/codemod.yaml b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/codemod.yaml index d0dbdc776..412acdd9b 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/codemod.yaml +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/codemod.yaml @@ -1,4 +1,7 @@ name: "@tailor-platform/auth-invoker-call-unwrap" -version: 0.1.0 +version: "1.0.0" description: 'Replace auth.invoker("name") with the bare string "name" while preserving the authInvoker option key' +engine: jssg language: typescript +since: "1.0.0" +until: "2.0.0" From 771105871f0fd244959e8ce6b0dcf065dab5d489 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 03:15:46 +0900 Subject: [PATCH 261/618] fix(sdk-codemod): suppress superseded review prompts --- packages/sdk-codemod/src/registry.test.ts | 1 + packages/sdk-codemod/src/registry.ts | 1 + packages/sdk-codemod/src/runner.test.ts | 42 ++++++++++++++++++++++- packages/sdk-codemod/src/runner.ts | 4 +++ packages/sdk-codemod/src/types.ts | 2 ++ 5 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index a929a4658..2228c35cf 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -41,6 +41,7 @@ describe("getApplicableCodemods", () => { expect(ids).toContain("v2/test-run-arg-input"); expect(ids).toContain("v2/auth-invoker-call-unwrap"); expect(authInvokerCallUnwrap?.suspiciousPatterns).toEqual(["auth.invoker"]); + expect(authInvokerCallUnwrap?.reviewSupersededBy).toEqual(["v2/auth-invoker-unwrap"]); expect(ids).not.toContain("v2/execute-script-arg"); expect(ids).not.toContain("v2/principal-unify"); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 74d786dbf..b671ce7ed 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -295,6 +295,7 @@ export const allCodemods: CodemodPackage[] = [ prereleaseUntil: V2_NEXT_1, scriptPath: "v2/auth-invoker-call-unwrap/scripts/transform.js", suspiciousPatterns: ["auth.invoker"], + reviewSupersededBy: ["v2/auth-invoker-unwrap"], prompt: [ "In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the", "machine user name passed directly as a string. The codemod already rewrote the", diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 080ae1173..9baeb8276 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -26,7 +26,10 @@ function makeCodemod( scriptPath?: string, filePatterns?: string[], legacyPatterns?: Array, - extra?: Pick, + extra?: Pick< + CodemodPackage, + "sourceStringLegacyPatterns" | "suspiciousPatterns" | "prompt" | "reviewSupersededBy" + >, ): CodemodPackage { return { id, @@ -522,6 +525,43 @@ describe("runCodemods", () => { ]); }); + test("suppresses LLM review when a superseding codemod is selected", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-superseded-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "a.ts"), + "createResolver({ authInvoker: auth.invoker(machineUserName) });\n", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/helper", undefined, ["**/*.ts"], undefined, { + suspiciousPatterns: ["auth.invoker"], + prompt: "Keep authInvoker and unwrap auth.invoker.", + reviewSupersededBy: ["test/rename"], + }), + }, + { + codemod: makeCodemod("test/rename", undefined, ["**/*.ts"], undefined, { + suspiciousPatterns: ["auth.invoker"], + prompt: "Rename authInvoker to invoker and unwrap auth.invoker.", + }), + }, + ], + dir, + true, + ); + + expect(result.llmReviews).toEqual([ + { + codemodId: "test/rename", + prompt: "Rename authInvoker to invoker and unwrap auth.invoker.", + files: ["a.ts"], + }, + ]); + }); + test("AND-group suspicious pattern flags only when every substring co-occurs", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-and-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 4bf94a544..64737f750 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -139,6 +139,7 @@ interface LoadedTransform { sourceStringLegacyPatterns: CodemodPatternGroup[]; suspiciousPatterns: CodemodPatternGroup[]; prompt?: string; + reviewSupersededBy: string[]; } function contentForResidualMatching(relative: string, content: string): string { @@ -313,6 +314,7 @@ export async function runCodemods( sourceStringLegacyPatterns: codemod.sourceStringLegacyPatterns ?? [], suspiciousPatterns: codemod.suspiciousPatterns ?? [], prompt: codemod.prompt, + reviewSupersededBy: codemod.reviewSupersededBy ?? [], }); } @@ -374,8 +376,10 @@ export async function runCodemods( } const llmReviews: LlmReview[] = []; + const loadedIds = new Set(loaded.map((lt) => lt.id)); for (const lt of loaded) { if (!lt.prompt) continue; + if (lt.reviewSupersededBy.some((id) => loadedIds.has(id))) continue; if (lt.suspiciousPatterns.length > 0) { // File-scoped: only surface when a suspicious pattern actually matched. const files = suspiciousByCodemod.get(lt.id); diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index 31c40ef5c..66e5ff3e6 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -74,6 +74,8 @@ export interface CodemodPackage { * by `suspiciousPatterns`. */ prompt?: string; + /** Codemod ids whose LLM review prompt supersedes this prompt when both are selected. */ + reviewSupersededBy?: string[]; /** Before/after examples shown in the generated migration doc. */ examples?: CodemodExample[]; /** From f831bc006b591f0f3e273d3c62126b468fe704ae Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 03:28:39 +0900 Subject: [PATCH 262/618] fix(sdk-codemod): include shipped prerelease migrations --- packages/sdk-codemod/src/registry.test.ts | 8 ++++++++ packages/sdk-codemod/src/registry.ts | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 2228c35cf..9d53c35b3 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -26,6 +26,14 @@ describe("getApplicableCodemods", () => { ) .map((codemod) => codemod.id), ); + expect(prereleaseCodemods.map((codemod) => codemod.id)).toEqual( + expect.arrayContaining([ + "v2/auth-attributes-rename", + "v2/env-var-rename", + "v2/rename-bin", + "v2/node-minimum-22-15-0", + ]), + ); }); test("returns empty when both versions are before the codemod boundary", () => { diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index b671ce7ed..f8356acf6 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -160,6 +160,7 @@ export const allCodemods: CodemodPackage[] = [ "Rename auth attribute module augmentation and related SDK type names from `AttributeMap` to `Attributes`", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_2, scriptPath: "v2/auth-attributes-rename/scripts/transform.js", legacyPatterns: [ "AttributeMap", @@ -240,6 +241,7 @@ export const allCodemods: CodemodPackage[] = [ "Rewrite unambiguous removed SDK environment variable names to their v2 `TAILOR_*` names and flag generic names for manual review", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_2, scriptPath: "v2/env-var-rename/scripts/transform.js", filePatterns: [ "**/package.json", @@ -582,6 +584,7 @@ export const allCodemods: CodemodPackage[] = [ "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot).", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_2, scriptPath: "v2/rename-bin/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], legacyPatterns: ["tailor-sdk"], @@ -605,6 +608,7 @@ export const allCodemods: CodemodPackage[] = [ "v2 requires Node.js **22.15.0** or later. This is the first version that includes `module.registerHooks()`, which the SDK uses to register its TypeScript loader hook synchronously in the main thread. No source change is required; ensure your environment runs Node.js 22.15.0+.", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_2, notice: true, }, ]; From a4ea4c147bad092381a16658555f924e8bdfb026 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 03:40:14 +0900 Subject: [PATCH 263/618] fix(sdk-codemod): keep unpublished migrations stable-only --- packages/sdk-codemod/src/registry.test.ts | 15 ++++++--------- packages/sdk-codemod/src/registry.ts | 4 ---- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 9d53c35b3..cfad2204a 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -16,8 +16,9 @@ describe("getApplicableCodemods", () => { test("returns codemods when upgrading to a prerelease at their version boundary", () => { const prereleaseCodemods = getApplicableCodemods("1.67.1", "2.0.0-next.2"); + const prereleaseIds = prereleaseCodemods.map((codemod) => codemod.id); - expect(prereleaseCodemods.map((codemod) => codemod.id)).toEqual( + expect(prereleaseIds).toEqual( allCodemods .filter( (codemod) => @@ -26,14 +27,10 @@ describe("getApplicableCodemods", () => { ) .map((codemod) => codemod.id), ); - expect(prereleaseCodemods.map((codemod) => codemod.id)).toEqual( - expect.arrayContaining([ - "v2/auth-attributes-rename", - "v2/env-var-rename", - "v2/rename-bin", - "v2/node-minimum-22-15-0", - ]), - ); + expect(prereleaseIds).not.toContain("v2/auth-attributes-rename"); + expect(prereleaseIds).not.toContain("v2/env-var-rename"); + expect(prereleaseIds).not.toContain("v2/rename-bin"); + expect(prereleaseIds).not.toContain("v2/node-minimum-22-15-0"); }); test("returns empty when both versions are before the codemod boundary", () => { diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index f8356acf6..b671ce7ed 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -160,7 +160,6 @@ export const allCodemods: CodemodPackage[] = [ "Rename auth attribute module augmentation and related SDK type names from `AttributeMap` to `Attributes`", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_2, scriptPath: "v2/auth-attributes-rename/scripts/transform.js", legacyPatterns: [ "AttributeMap", @@ -241,7 +240,6 @@ export const allCodemods: CodemodPackage[] = [ "Rewrite unambiguous removed SDK environment variable names to their v2 `TAILOR_*` names and flag generic names for manual review", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_2, scriptPath: "v2/env-var-rename/scripts/transform.js", filePatterns: [ "**/package.json", @@ -584,7 +582,6 @@ export const allCodemods: CodemodPackage[] = [ "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot).", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_2, scriptPath: "v2/rename-bin/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], legacyPatterns: ["tailor-sdk"], @@ -608,7 +605,6 @@ export const allCodemods: CodemodPackage[] = [ "v2 requires Node.js **22.15.0** or later. This is the first version that includes `module.registerHooks()`, which the SDK uses to register its TypeScript loader hook synchronously in the main thread. No source change is required; ensure your environment runs Node.js 22.15.0+.", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_2, notice: true, }, ]; From 5fa1d210145b8be5ccc6715ca8919de1bad738b2 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 03:49:03 +0900 Subject: [PATCH 264/618] test(sdk-codemod): cover prerelease to stable migrations --- packages/sdk-codemod/src/registry.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index cfad2204a..8843984ec 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -55,6 +55,17 @@ describe("getApplicableCodemods", () => { expect(getApplicableCodemods("2.0.0-next.2", "2.0.0-next.2")).toEqual([]); }); + test("runs stable-only codemods when upgrading from a prerelease to stable", () => { + const ids = getApplicableCodemods("2.0.0-next.2", "2.0.0").map((codemod) => codemod.id); + + expect(ids).toContain("v2/auth-attributes-rename"); + expect(ids).toContain("v2/env-var-rename"); + expect(ids).toContain("v2/rename-bin"); + expect(ids).toContain("v2/node-minimum-22-15-0"); + expect(ids).not.toContain("v2/principal-unify"); + expect(ids).not.toContain("v2/auth-invoker-unwrap"); + }); + test("returns empty when the target prerelease is before the codemod boundary", () => { expect(getApplicableCodemods("1.67.1", "1.99.0-next.1")).toEqual([]); }); From b88f6a2e1c6d8e25a797bec6ca90428f5be3b1b9 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 04:04:52 +0900 Subject: [PATCH 265/618] fix(sdk-codemod): scan source files for rename bin --- .changeset/fix-rename-bin-source-files.md | 5 +++++ .../sdk-codemod/codemods/v2/rename-bin/codemod.yaml | 2 +- .../rename-bin/tests/declaration-comment/expected.d.ts | 6 ++++++ .../v2/rename-bin/tests/declaration-comment/input.d.ts | 6 ++++++ .../v2/rename-bin/tests/source-js-string/expected.js | 3 +++ .../v2/rename-bin/tests/source-js-string/input.js | 3 +++ .../v2/rename-bin/tests/source-template/expected.ts | 7 +++++++ .../v2/rename-bin/tests/source-template/input.ts | 7 +++++++ packages/sdk-codemod/src/registry.test.ts | 10 ++++++++++ packages/sdk-codemod/src/registry.ts | 9 +++++++-- packages/sdk/docs/migration/v2.md | 2 +- 11 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 .changeset/fix-rename-bin-source-files.md create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/expected.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/input.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts diff --git a/.changeset/fix-rename-bin-source-files.md b/.changeset/fix-rename-bin-source-files.md new file mode 100644 index 000000000..0ce99f44c --- /dev/null +++ b/.changeset/fix-rename-bin-source-files.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Apply the v2 `rename-bin` codemod to SDK CLI command strings in TypeScript and JavaScript source files. diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/codemod.yaml b/packages/sdk-codemod/codemods/v2/rename-bin/codemod.yaml index 03de46a1e..3c7789c8c 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/codemod.yaml +++ b/packages/sdk-codemod/codemods/v2/rename-bin/codemod.yaml @@ -1,6 +1,6 @@ name: "@tailor-platform/rename-bin" version: "1.0.0" -description: "Rename the CLI binary from tailor-sdk to tailor in scripts, CI workflows, and documentation" +description: "Rename the CLI binary from tailor-sdk to tailor in scripts, source files, CI workflows, and documentation" engine: jssg language: text since: "1.0.0" diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/expected.d.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/expected.d.ts new file mode 100644 index 000000000..fe27b2eb3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/expected.d.ts @@ -0,0 +1,6 @@ +/** + * Run `tailor deploy` after editing the app config. + * Regenerate types with `tailor generate`. + * Keep the generated output directory `.tailor-sdk/` in place until v2 setup runs. + */ +export {}; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/input.d.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/input.d.ts new file mode 100644 index 000000000..61fd5bfaf --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/input.d.ts @@ -0,0 +1,6 @@ +/** + * Run `tailor-sdk deploy` after editing the app config. + * Regenerate types with `tailor-sdk generate`. + * Keep the generated output directory `.tailor-sdk/` in place until v2 setup runs. + */ +export {}; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js new file mode 100644 index 000000000..d4657966e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -0,0 +1,3 @@ +const migrate = "bunx @tailor-platform/sdk@2.0.0-next.2 generate"; +const script = ["tailor", "deploy"].join(" "); +const skills = "tailor-sdk-skills"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js new file mode 100644 index 000000000..c215d6e06 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -0,0 +1,3 @@ +const migrate = "bunx tailor-sdk@2.0.0-next.2 generate"; +const script = ["tailor-sdk", "deploy"].join(" "); +const skills = "tailor-sdk-skills"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts new file mode 100644 index 000000000..6c8b38639 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -0,0 +1,7 @@ +const siteName = "portal"; +const setup = `pnpm tailor staticwebsite deploy --name ${siteName}`; +const deploy = "tailor deploy"; +const latest = "npx @tailor-platform/sdk@latest login"; +const outputDir = ".tailor-sdk/"; +const scaffold = "create-tailor-sdk"; +const skills = "tailor-sdk-skills install"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts new file mode 100644 index 000000000..5b39513b6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -0,0 +1,7 @@ +const siteName = "portal"; +const setup = `pnpm tailor-sdk staticwebsite deploy --name ${siteName}`; +const deploy = "tailor-sdk deploy"; +const latest = "npx tailor-sdk@latest login"; +const outputDir = ".tailor-sdk/"; +const scaffold = "create-tailor-sdk"; +const skills = "tailor-sdk-skills install"; diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 301045f83..c0a72af00 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -55,6 +55,16 @@ describe("getApplicableCodemods", () => { ); }); + test("rename-bin scans source files and declaration comments", () => { + const renameBin = getApplicableCodemods("1.67.1", "2.0.0").find( + (codemod) => codemod.id === "v2/rename-bin", + ); + + expect(renameBin?.filePatterns).toEqual( + expect.arrayContaining(["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"]), + ); + }); + test("flags CommonJS TypeScript files for runtime globals review", () => { const codemod = allCodemods.find((entry) => entry.id === "v2/runtime-globals-opt-in"); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 43289dce4..84e6dd9a4 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -525,11 +525,16 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/rename-bin", name: "tailor-sdk binary → tailor", description: - "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot).", + "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, source files, generated declaration comments, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot).", since: "1.0.0", until: "2.0.0", scriptPath: "v2/rename-bin/scripts/transform.js", - filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], + filePatterns: [ + "**/package.json", + "**/*.{sh,bash,zsh,yml,yaml}", + "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}", + "**/*.md", + ], legacyPatterns: ["tailor-sdk"], examples: [ { diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 6a525cfe3..407334850 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -551,7 +551,7 @@ trigger result as the job output directly (no Promise wrapper to unwrap). **Migration:** Partially automatic -Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot). +Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, source files, generated declaration comments, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot). Before: From 02e77cb2a91c74376bef86a6ea41b2c9da376670 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 04:07:09 +0900 Subject: [PATCH 266/618] test(sdk-codemod): cover rename bin source globs --- packages/sdk-codemod/src/registry.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index c0a72af00..0cb66ba8c 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -1,3 +1,4 @@ +import picomatch from "picomatch"; import { describe, expect, test } from "vitest"; import { allCodemods, getApplicableCodemods } from "./registry"; @@ -63,6 +64,9 @@ describe("getApplicableCodemods", () => { expect(renameBin?.filePatterns).toEqual( expect.arrayContaining(["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"]), ); + const matches = picomatch(renameBin?.filePatterns ?? [], { dot: true }); + expect(matches("packages/app/frontend/e2e/global-setup.ts")).toBe(true); + expect(matches("tailor.d.ts")).toBe(true); }); test("flags CommonJS TypeScript files for runtime globals review", () => { From 4956175f85ddbe7db9de9e1531c35d86f80b81de Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 04:17:55 +0900 Subject: [PATCH 267/618] fix(sdk-codemod): scan source files for cli rename --- .../codemods/v2/cli-rename/codemod.yaml | 2 +- .../tests/declaration-comment/expected.d.ts | 4 ++++ .../tests/declaration-comment/input.d.ts | 4 ++++ .../tests/source-template/expected.ts | 4 ++++ .../cli-rename/tests/source-template/input.ts | 4 ++++ packages/sdk-codemod/src/registry.test.ts | 18 +++++++++++------- packages/sdk-codemod/src/registry.ts | 9 +++++++-- packages/sdk/docs/migration/v2.md | 2 +- 8 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/expected.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/input.d.ts create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/codemod.yaml b/packages/sdk-codemod/codemods/v2/cli-rename/codemod.yaml index 2937f57aa..8cdb2f535 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/codemod.yaml +++ b/packages/sdk-codemod/codemods/v2/cli-rename/codemod.yaml @@ -1,6 +1,6 @@ name: "@tailor-platform/cli-rename" version: "1.0.0" -description: "Apply v2 CLI naming conventions: single-word command names and kebab-case option names" +description: "Apply v2 CLI naming conventions in scripts, source files, CI workflows, and documentation" engine: jssg language: text since: "1.0.0" diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/expected.d.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/expected.d.ts new file mode 100644 index 000000000..7d6b77937 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/expected.d.ts @@ -0,0 +1,4 @@ +/** + * Inspect crash reports with `tailor-sdk crashreport list --machine-user ci`. + */ +export {}; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/input.d.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/input.d.ts new file mode 100644 index 000000000..c0364eca7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/input.d.ts @@ -0,0 +1,4 @@ +/** + * Inspect crash reports with `tailor-sdk crash-report list --machineuser ci`. + */ +export {}; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts new file mode 100644 index 000000000..856d2b12d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts @@ -0,0 +1,4 @@ +const machineUser = "ci"; +const report = `tailor-sdk crashreport list --machine-user ${machineUser}`; +const login = "tailor-sdk login --machine-user ci"; +const other = "other-cli --machineuser ci"; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts new file mode 100644 index 000000000..1f0d0527a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts @@ -0,0 +1,4 @@ +const machineUser = "ci"; +const report = `tailor-sdk crash-report list --machineuser ${machineUser}`; +const login = "tailor-sdk login --machineuser ci"; +const other = "other-cli --machineuser ci"; diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 0cb66ba8c..7c13ec50c 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -56,17 +56,21 @@ describe("getApplicableCodemods", () => { ); }); - test("rename-bin scans source files and declaration comments", () => { + test("CLI command codemods scan source files and declaration comments", () => { + const sourcePattern = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"; + const cliRename = getApplicableCodemods("1.67.1", "2.0.0").find( + (codemod) => codemod.id === "v2/cli-rename", + ); const renameBin = getApplicableCodemods("1.67.1", "2.0.0").find( (codemod) => codemod.id === "v2/rename-bin", ); - expect(renameBin?.filePatterns).toEqual( - expect.arrayContaining(["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"]), - ); - const matches = picomatch(renameBin?.filePatterns ?? [], { dot: true }); - expect(matches("packages/app/frontend/e2e/global-setup.ts")).toBe(true); - expect(matches("tailor.d.ts")).toBe(true); + for (const codemod of [cliRename, renameBin]) { + expect(codemod?.filePatterns).toEqual(expect.arrayContaining([sourcePattern])); + const matches = picomatch(codemod?.filePatterns ?? [], { dot: true }); + expect(matches("packages/app/frontend/e2e/global-setup.ts")).toBe(true); + expect(matches("tailor.d.ts")).toBe(true); + } }); test("flags CommonJS TypeScript files for runtime globals review", () => { diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 84e6dd9a4..972e652d9 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -204,11 +204,16 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/cli-rename", name: "v2 CLI rename", description: - "Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs", + "Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, source files, and docs", since: "1.0.0", until: "2.0.0", scriptPath: "v2/cli-rename/scripts/transform.js", - filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], + filePatterns: [ + "**/package.json", + "**/*.{sh,bash,zsh,yml,yaml}", + "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}", + "**/*.md", + ], legacyPatterns: ["tailor-sdk crash-report", "--machineuser"], examples: [ { diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 407334850..a6066606b 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -231,7 +231,7 @@ tailor-sdk deploy --profile prod **Migration:** Partially automatic -Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs +Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, source files, and docs Before: From d214c752227a7e71e08bb67e1a667e633e1e688e Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 04:28:30 +0900 Subject: [PATCH 268/618] fix(sdk-codemod): handle tokenized cli source strings --- .changeset/fix-rename-bin-source-files.md | 2 +- .../v2/cli-rename/scripts/transform.ts | 1135 +++++++++++++- .../cli-rename/tests/basic-shell/expected.sh | 1 + .../v2/cli-rename/tests/basic-shell/input.sh | 1 + .../tests/declaration-comment/expected.d.ts | 5 + .../tests/declaration-comment/input.d.ts | 5 + .../tests/source-template/expected.ts | 36 + .../cli-rename/tests/source-template/input.ts | 36 + .../tests/source-token-array/expected.ts | 48 + .../tests/source-token-array/input.ts | 48 + .../cli-rename/tests/source-tsx/expected.tsx | 6 + .../v2/cli-rename/tests/source-tsx/input.tsx | 6 + .../v2/rename-bin/scripts/transform.ts | 1319 ++++++++++++++++- .../rename-bin/tests/basic-shell/expected.sh | 1 + .../v2/rename-bin/tests/basic-shell/input.sh | 1 + .../tests/declaration-comment/expected.d.ts | 1 + .../tests/declaration-comment/input.d.ts | 1 + .../tests/source-js-string/expected.js | 14 + .../tests/source-js-string/input.js | 14 + .../tests/source-template/expected.ts | 25 + .../rename-bin/tests/source-template/input.ts | 25 + .../tests/source-token-runner/expected.ts | 54 + .../tests/source-token-runner/input.ts | 54 + .../rename-bin/tests/source-tsx/expected.tsx | 7 + .../v2/rename-bin/tests/source-tsx/input.tsx | 7 + packages/sdk-codemod/src/registry.test.ts | 7 + packages/sdk-codemod/src/registry.ts | 5 + packages/sdk-codemod/src/runner.test.ts | 39 + 28 files changed, 2878 insertions(+), 25 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/expected.tsx create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/input.tsx create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/expected.tsx create mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/input.tsx diff --git a/.changeset/fix-rename-bin-source-files.md b/.changeset/fix-rename-bin-source-files.md index 0ce99f44c..f9936f110 100644 --- a/.changeset/fix-rename-bin-source-files.md +++ b/.changeset/fix-rename-bin-source-files.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk-codemod": patch --- -Apply the v2 `rename-bin` codemod to SDK CLI command strings in TypeScript and JavaScript source files. +Apply the v2 CLI rename codemods to SDK CLI command strings in TypeScript and JavaScript source files. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index 7a498fef4..d14e15b10 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -1,4 +1,6 @@ +import { parse, Lang } from "@ast-grep/napi"; import * as path from "pathe"; +import type { SgNode } from "@ast-grep/napi"; // Map of v1 multi-word command names to their v2 single-word replacements. const COMMAND_RENAMES: ReadonlyArray = [["crash-report", "crashreport"]]; @@ -7,8 +9,9 @@ const OPTION_RENAMES: ReadonlyArray = [ ]; const ARG_VALUE = `(?:[^\\s'"\`;&|]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`; -const BOOLEAN_GLOBAL_ARG = "(?:--verbose|--json|-j)"; -const VALUE_GLOBAL_ARG = "(?:--env-file|--env-file-if-exists|-e)"; +const BOOLEAN_GLOBAL_ARG = "(?:--verbose|--json|--yes|-j|-y)"; +const VALUE_GLOBAL_ARG = + "(?:--env-file|--env-file-if-exists|--profile|--config|--workspace-id|-e|-p|-c|-w)"; const GLOBAL_ARG_PATTERN = `(?:(?:\\s+${BOOLEAN_GLOBAL_ARG})|(?:\\s+${VALUE_GLOBAL_ARG}(?:=${ARG_VALUE}|\\s+${ARG_VALUE})))*`; const TAILOR_BINARY = `(?(CLI_VALUE_ARGS); +const PACKAGE_RUNNER_VALUE_ARGS = new Set([ + "--cache", + "--userconfig", + "--registry", + "--prefix", + "--dir", + "--filter", + "--cwd", + "-C", +]); +const CLI_ARGUMENT_CALLEE_RE = /(?:^|\.)(?:spawn|spawnSync|execFile|execFileSync|execa|execaSync)$/; interface TextRange { start: number; @@ -29,6 +74,32 @@ interface ActiveQuote { escaped: boolean; } +interface SourceStringToken { + value: string; + start: number; + end: number; +} + +interface TemplateToken { + value: string; + segments: SourceStringToken[]; + substitutions: SgNode[]; + quoted: boolean; +} + +interface TemplateTokenState { + quote: "'" | '"' | null; + token: TemplateToken | null; +} + +interface CliTemplateState { + afterTailorBinary: boolean; + commandMayBeNext: boolean; + skipNextValue: boolean; +} + +const TEMPLATE_SUBSTITUTION_PLACEHOLDER = "\u{E000}"; + function isOptionBoundaryChar(value: string | undefined): boolean { return value === undefined || !/[\w-]/.test(value); } @@ -144,16 +215,17 @@ function findMarkdownFencedCodeRanges(source: string): TextRange[] { for (const line of lines) { const body = line.replace(/\r?\n$/, ""); + const fenceBody = body.replace(/^ {0,3}\*\s?/, ""); if (open) { - const close = body.match(/^ {0,3}(`{3,}|~{3,})\s*$/); + const close = fenceBody.match(/^ {0,3}(`{3,}|~{3,})\s*$/); const marker = close?.[1]; if (marker && marker[0] === open.char && marker.length >= open.length) { ranges.push({ start: open.start, end: offset }); open = undefined; } } else { - const start = body.match(/^ {0,3}(`{3,}|~{3,}).*$/); + const start = fenceBody.match(/^ {0,3}(`{3,}|~{3,}).*$/); const marker = start?.[1]; if (marker) { open = { char: marker[0], length: marker.length, start: offset + line.length }; @@ -187,6 +259,10 @@ function startsCommandSubstitution(source: string, index: number): boolean { return source[index] === "$" && source[index + 1] === "(" && source[index - 1] !== "\\"; } +function startsTemplateSubstitution(source: string, index: number): boolean { + return source[index] === "$" && source[index + 1] === "{" && source[index - 1] !== "\\"; +} + function findCommandSubstitutionEnd(source: string, start: number): number | undefined { let depth = 1; let index = start + 2; @@ -250,6 +326,51 @@ function findCommandSubstitutionEnd(source: string, start: number): number | und return undefined; } +function findTemplateSubstitutionEnd(source: string, start: number): number | undefined { + let depth = 1; + let index = start + 2; + let quote: "'" | '"' | "`" | null = null; + + while (index < source.length) { + const ch = source[index]; + + if (quote !== null) { + if (ch === "\\" && index + 1 < source.length) { + index += 2; + continue; + } + if (quote === "`" && startsTemplateSubstitution(source, index)) { + const substitutionEnd = findTemplateSubstitutionEnd(source, index); + if (substitutionEnd !== undefined) { + index = substitutionEnd + 1; + continue; + } + } + if (ch === quote) { + quote = null; + } + index += 1; + continue; + } + + if (ch === "'" || ch === '"' || ch === "`") { + quote = ch; + index += 1; + continue; + } + + if (ch === "{") { + depth += 1; + } else if (ch === "}") { + depth -= 1; + if (depth === 0) return index; + } + index += 1; + } + + return undefined; +} + function findContainingRange( ranges: TextRange[] | undefined, index: number, @@ -354,11 +475,75 @@ function findOptionRename(command: string, index: number): readonly [string, str return OPTION_RENAMES.find( ([from]) => command.startsWith(from, index) && + command[index - 1] !== "=" && isOptionBoundaryChar(command[index - 1]) && isOptionBoundaryChar(command[index + from.length]), ); } +function findCliValueArg(command: string, index: number): string | undefined { + return CLI_VALUE_ARGS.find( + (arg) => + command.startsWith(arg, index) && + isOptionBoundaryChar(command[index - 1]) && + (command[index + arg.length] === "=" || isOptionBoundaryChar(command[index + arg.length])), + ); +} + +function findShellArgEnd(command: string, start: number): number { + let index = start; + let quote: ActiveQuote | null = null; + + while (index < command.length) { + const ch = command[index]; + if (quote !== null) { + if (quote.escaped) { + if (ch === "\\" && command[index + 1] === quote.char) { + quote = null; + index += 2; + continue; + } + } else if (ch === "\\" && quote.char === '"' && index + 1 < command.length) { + index += 2; + continue; + } else if (ch === quote.char) { + quote = null; + } + index += 1; + continue; + } + + if (ch === "'" || ch === '"') { + quote = { char: ch, escaped: false }; + index += 1; + continue; + } + if (ch === "\\" && (command[index + 1] === "'" || command[index + 1] === '"')) { + quote = { char: command[index + 1] as "'" | '"', escaped: true }; + index += 2; + continue; + } + if (startsCommandSubstitution(command, index)) { + const substitutionEnd = findCommandSubstitutionEnd(command, index); + if (substitutionEnd !== undefined) { + index = substitutionEnd + 1; + continue; + } + } + if (startsTemplateSubstitution(command, index)) { + const substitutionEnd = findTemplateSubstitutionEnd(command, index); + if (substitutionEnd !== undefined) { + index = substitutionEnd + 1; + continue; + } + } + if (/\s/.test(ch) || isCommandSeparator(command, index) || ch === ")") break; + index += 1; + } + + return index; +} + function replaceOptionsInCommand(command: string): string { let updated = ""; let index = 0; @@ -422,10 +607,37 @@ function replaceOptionsInCommand(command: string): string { } } + const valueArg = findCliValueArg(command, index); + if (valueArg) { + const afterName = index + valueArg.length; + updated += command.slice(index, afterName); + index = afterName; + if (command[index] === "=") { + const valueEnd = findShellArgEnd(command, index + 1); + updated += command.slice(index, valueEnd); + index = valueEnd; + continue; + } + const whitespace = command.slice(index).match(/^\s+/)?.[0]; + if (whitespace) { + updated += whitespace; + index += whitespace.length; + const valueEnd = findShellArgEnd(command, index); + updated += command.slice(index, valueEnd); + index = valueEnd; + } + continue; + } + const rename = findOptionRename(command, index); if (rename) { updated += rename[1]; index += rename[0].length; + if (command[index] === "=") { + const valueEnd = findShellArgEnd(command, index + 1); + updated += command.slice(index, valueEnd); + index = valueEnd; + } continue; } @@ -436,6 +648,902 @@ function replaceOptionsInCommand(command: string): string { return updated; } +function replaceCommandNameInCommand(command: string): string { + COMMAND_PATTERN.lastIndex = 0; + return command.replace( + COMMAND_PATTERN, + (match, _prefix: string, cmd: string) => + `${match.slice(0, -cmd.length)}${COMMAND_MAP.get(cmd) ?? cmd}`, + ); +} + +function replaceCliRenamesInCommand(command: string): string { + return replaceOptionsInCommand(replaceCommandNameInCommand(command)); +} + +function sourceLang(filePath: string): Lang { + const ext = path.extname(filePath).toLowerCase(); + return ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : Lang.TypeScript; +} + +function sourceStringToken(node: SgNode, source: string): SourceStringToken | undefined { + const kind = node.kind(); + if (kind !== "string" && kind !== "template_string") return undefined; + if ( + kind === "template_string" && + node.children().some((child: SgNode) => child.kind() === "template_substitution") + ) { + return undefined; + } + + const fragments = node.children().filter((child: SgNode) => child.kind() === "string_fragment"); + if (fragments.length !== 1) return undefined; + + const range = fragments[0]!.range(); + return { + value: source.slice(range.start.index, range.end.index), + start: range.start.index, + end: range.end.index, + }; +} + +function ensureTemplateToken(state: TemplateTokenState): TemplateToken { + state.token ??= { value: "", segments: [], substitutions: [], quoted: false }; + return state.token; +} + +function appendTemplateTokenChar(state: TemplateTokenState, ch: string, sourceIndex: number): void { + const token = ensureTemplateToken(state); + token.value += ch; + const previous = token.segments.at(-1); + if (previous && previous.end === sourceIndex) { + previous.value += ch; + previous.end += 1; + } else { + token.segments.push({ value: ch, start: sourceIndex, end: sourceIndex + 1 }); + } +} + +function pushTemplateToken(tokens: TemplateToken[], state: TemplateTokenState): void { + if (!state.token) return; + tokens.push(state.token); + state.token = null; +} + +function scanTemplateTextTokens( + state: TemplateTokenState, + text: string, + offset: number, + tokens: TemplateToken[], +): void { + for (let index = 0; index < text.length; index += 1) { + const ch = text[index]!; + + if (state.quote !== null) { + appendTemplateTokenChar(state, ch, offset + index); + if (ch === "\\" && state.quote === '"' && index + 1 < text.length) { + index += 1; + appendTemplateTokenChar(state, text[index]!, offset + index); + continue; + } + if (ch === state.quote) { + state.quote = null; + } + continue; + } + + if (/\s/.test(ch)) { + pushTemplateToken(tokens, state); + continue; + } + + if (isCommandSeparator(text, index)) { + pushTemplateToken(tokens, state); + appendTemplateTokenChar(state, ch, offset + index); + pushTemplateToken(tokens, state); + continue; + } + + appendTemplateTokenChar(state, ch, offset + index); + if (ch === "'" || ch === '"') { + state.quote = ch; + ensureTemplateToken(state).quoted = true; + } + } +} + +function appendTemplateSubstitution(state: TemplateTokenState, substitution: SgNode): void { + const token = ensureTemplateToken(state); + token.value += TEMPLATE_SUBSTITUTION_PLACEHOLDER; + token.substitutions.push(substitution); +} + +function collectTemplateTokens(node: SgNode, source: string): TemplateToken[] { + const tokens: TemplateToken[] = []; + const state: TemplateTokenState = { quote: null, token: null }; + + for (const child of node.children()) { + if (child.kind() === "string_fragment") { + const range = child.range(); + scanTemplateTextTokens( + state, + source.slice(range.start.index, range.end.index), + range.start.index, + tokens, + ); + continue; + } + if (child.kind() === "template_substitution") { + appendTemplateSubstitution(state, child); + } + } + + pushTemplateToken(tokens, state); + return tokens; +} + +function staticTemplateTokenValue(token: TemplateToken): string | undefined { + return token.substitutions.length === 0 ? token.value : undefined; +} + +function pushTemplateTokenReplacement( + edits: Array<[number, number, string]>, + token: TemplateToken, + replacement: string, +): void { + if (token.segments.length !== 1) return; + const [{ start, end }] = token.segments; + edits.push([start, end, replacement]); +} + +function optionName(value: string): string { + const equalsIndex = value.indexOf("="); + return equalsIndex === -1 ? value : value.slice(0, equalsIndex); +} + +function isGlobalSeparateValueArg(value: string): boolean { + return GLOBAL_VALUE_ARGS.has(value); +} + +function isInlineGlobalValueArg(value: string): boolean { + return isGlobalSeparateValueArg(optionName(value)) && value.includes("=") && !value.endsWith("="); +} + +function isOpenGlobalValueArg(value: string): boolean { + return ( + isGlobalSeparateValueArg(optionName(value)) && (!value.includes("=") || value.endsWith("=")) + ); +} + +function isAnySeparateValueArg(value: string): boolean { + return CLI_VALUE_ARG_SET.has(value); +} + +function isOpenCliValueArg(value: string): boolean { + return isAnySeparateValueArg(optionName(value)) && (!value.includes("=") || value.endsWith("=")); +} + +function isInlineCliValueArg(value: string): boolean { + return isAnySeparateValueArg(optionName(value)) && value.includes("=") && !value.endsWith("="); +} + +function isPackageRunnerSeparatePackageArg(value: string | undefined): boolean { + return value === "--package" || value === "-p"; +} + +function isPackageRunnerSeparateValueArg(value: string | undefined): boolean { + return value != null && PACKAGE_RUNNER_VALUE_ARGS.has(value); +} + +function replaceOptionsInToken(value: string): string { + const equalsIndex = value.indexOf("="); + if (equalsIndex !== -1 && value.startsWith("-")) { + const name = value.slice(0, equalsIndex); + return `${OPTION_MAP.get(name) ?? name}${value.slice(equalsIndex)}`; + } + return replaceOptionsInCommand(value); +} + +function resetCliTemplateState(state: CliTemplateState): void { + state.afterTailorBinary = false; + state.commandMayBeNext = false; + state.skipNextValue = false; +} + +function tokenStart(token: TemplateToken): number | undefined { + return token.segments[0]?.start; +} + +function tokenEnd(token: TemplateToken): number | undefined { + return token.segments.at(-1)?.end; +} + +function hasUnescapedLineBreak(value: string): boolean { + for (let index = 0; index < value.length; index += 1) { + if (value[index] === "\n" && value[index - 1] !== "\\") return true; + } + return false; +} + +function hasTemplateCommandBoundaryBetween( + previous: TemplateToken, + next: TemplateToken, + source: string, +): boolean { + const previousEnd = tokenEnd(previous); + const nextStart = tokenStart(next); + if (previousEnd == null || nextStart == null) return false; + return hasUnescapedLineBreak(source.slice(previousEnd, nextStart)); +} + +function hasCommandSeparatorToken(value: string): boolean { + for (let index = 0; index < value.length; index += 1) { + if (isCommandSeparator(value, index)) return true; + } + return false; +} + +function isCommandSeparatorOnlyToken(value: string): boolean { + return value !== "" && /^[;&|]+$/.test(value) && hasCommandSeparatorToken(value); +} + +function rewriteTokenizedCliArg(value: string, renameCommand: boolean): string { + const commandRenamed = renameCommand ? (COMMAND_MAP.get(value) ?? value) : value; + return replaceOptionsInToken(commandRenamed); +} + +function rewriteCommandToken(value: string): string { + for (let index = 0; index < value.length; index += 1) { + if (!isCommandSeparator(value, index)) continue; + const command = value.slice(0, index); + return `${COMMAND_MAP.get(command) ?? command}${value.slice(index)}`; + } + return COMMAND_MAP.get(value) ?? value; +} + +function isLikelySourceCommandString(value: string): boolean { + const trimmed = value.trimStart(); + if (TAILOR_BINARY_TOKEN.test(trimmed.split(/\s+/, 1)[0] ?? "")) return true; + if (/^(?:npx|bunx|pnpm|yarn)\b/.test(trimmed) && TAILOR_BINARY_PATTERN.test(trimmed)) { + TAILOR_BINARY_PATTERN.lastIndex = 0; + return true; + } + TAILOR_BINARY_PATTERN.lastIndex = 0; + return ( + SHELL_ASSIGNMENT_PREFIX_PATTERN.test(trimmed) || SHELL_SEPARATOR_TAILOR_PATTERN.test(trimmed) + ); +} + +function replaceSourceCommandString(value: string): string { + TAILOR_BINARY_PATTERN.lastIndex = 0; + if (!isLikelySourceCommandString(value)) return value; + TAILOR_BINARY_PATTERN.lastIndex = 0; + return replaceAll(value); +} + +function isTokenSequenceNode(node: SgNode): boolean { + return node.kind() === "array" || isCliArgumentsNode(node); +} + +function isSyntaxOnlyNode(node: SgNode): boolean { + const kind = node.kind(); + return ( + kind === "[" || + kind === "]" || + kind === "(" || + kind === ")" || + kind === "," || + kind === "comment" + ); +} + +function nodeRangeKey(node: SgNode): string { + const range = node.range(); + return `${range.start.index}:${range.end.index}`; +} + +function isCliArgumentsNode(node: SgNode): boolean { + if (node.kind() !== "arguments") return false; + const parent = node.parent(); + if (parent?.kind() !== "call_expression") return false; + const argumentRange = nodeRangeKey(node); + const callee = parent.children().find((child: SgNode) => nodeRangeKey(child) !== argumentRange); + const calleeText = callee?.text(); + return calleeText === "$" || (calleeText != null && CLI_ARGUMENT_CALLEE_RE.test(calleeText)); +} + +function collectInterpolatedOptionEdits( + node: SgNode, + source: string, + renameCommand = false, +): Array<[number, number, string]> { + if (node.kind() !== "template_string") return []; + if (!node.children().some((child: SgNode) => child.kind() === "template_substitution")) { + return []; + } + + const edits: Array<[number, number, string]> = []; + for (const fragment of node + .children() + .filter((child: SgNode) => child.kind() === "string_fragment")) { + const range = fragment.range(); + const text = source.slice(range.start.index, range.end.index); + const replacement = replaceOptionsInCommand(text); + if (replacement !== text) { + edits.push([range.start.index, range.end.index, replacement]); + } + } + if (templateStartsWithInlineOptionValue(node, source)) return edits; + for (const substitution of node + .children() + .filter((child: SgNode) => child.kind() === "template_substitution")) { + edits.push(...collectCliExpressionEdits(substitution, source, renameCommand)); + } + return edits; +} + +function cliArgExpressionChildren(node: SgNode): SgNode[] { + if (node.kind() === "parenthesized_expression") return node.children(); + if (node.kind() === "ternary_expression") return node.children().slice(1); + if (node.kind() === "binary_expression" && /&&|\|\|/.test(node.text())) { + return node.children().slice(1); + } + return []; +} + +function collectCliExpressionEdits( + node: SgNode, + source: string, + renameCommand: boolean, +): Array<[number, number, string]> { + const token = sourceStringToken(node, source); + if (token) { + const replacement = rewriteTokenizedCliArg(token.value, renameCommand); + return replacement === token.value ? [] : [[token.start, token.end, replacement]]; + } + + const edits: Array<[number, number, string]> = []; + const cliArgChildren = cliArgExpressionChildren(node); + const children = cliArgChildren.length > 0 ? cliArgChildren : node.children(); + for (const child of children) { + edits.push(...collectCliExpressionEdits(child, source, renameCommand)); + } + return edits; +} + +function sourceStringValues(node: SgNode, source: string): string[] { + const token = sourceStringToken(node, source); + if (token) return [token.value]; + + const values: string[] = []; + if (node.kind() === "template_string") { + for (const child of node.children()) { + if (child.kind() !== "string_fragment") continue; + const range = child.range(); + values.push(source.slice(range.start.index, range.end.index)); + } + } + const cliArgChildren = cliArgExpressionChildren(node); + const children = cliArgChildren.length > 0 ? cliArgChildren : node.children(); + for (const child of children) { + values.push(...sourceStringValues(child, source)); + } + return values; +} + +function templateStartsWithInlineOptionValue(node: SgNode, source: string): boolean { + if (node.kind() !== "template_string") return false; + const firstFragment = node.children().find((child: SgNode) => child.kind() === "string_fragment"); + if (!firstFragment) return false; + const range = firstFragment.range(); + const text = source.slice(range.start.index, range.end.index).trimStart(); + const equalsIndex = text.indexOf("="); + if (equalsIndex === -1) return false; + return text.startsWith("-") && !/\s/.test(text.slice(0, equalsIndex)); +} + +function dynamicCliArgState(node: SgNode, source: string): "keep-command" | "skip-value" | null { + if (templateStartsWithInlineOptionValue(node, source)) { + const values = sourceStringValues(node, source); + const first = values.find((value) => value.trim() !== "")?.trimStart() ?? ""; + const name = optionName(first); + return isGlobalSeparateValueArg(name) ? "keep-command" : null; + } + + const values = sourceStringValues(node, source) + .map((value) => value.trim()) + .filter((value) => value !== ""); + if (values.length === 0) return null; + if ( + values.every( + (value) => + GLOBAL_BOOLEAN_ARGS.has(optionName(value)) || + isInlineGlobalValueArg(value) || + isOpenGlobalValueArg(value), + ) + ) { + return values.some((value) => isOpenGlobalValueArg(value)) ? "skip-value" : "keep-command"; + } + return null; +} + +function dynamicCliValueArgState(node: SgNode, source: string): "skip-value" | null { + const values = sourceStringValues(node, source) + .map((value) => value.trim()) + .filter((value) => value !== ""); + if (values.length === 0) return null; + return values.every((value) => isOpenCliValueArg(value)) ? "skip-value" : null; +} + +function advanceCliTemplateState(state: CliTemplateState, value: string): void { + if (!state.afterTailorBinary) { + if (TAILOR_BINARY_TOKEN.test(value)) { + state.afterTailorBinary = true; + state.commandMayBeNext = true; + state.skipNextValue = false; + } + return; + } + + if (state.skipNextValue) { + state.skipNextValue = false; + return; + } + + const name = optionName(value); + if (state.commandMayBeNext) { + if (GLOBAL_BOOLEAN_ARGS.has(name)) return; + if (isGlobalSeparateValueArg(name)) { + state.skipNextValue = !value.includes("=") || value.endsWith("="); + return; + } + if (!value.startsWith("-")) { + state.commandMayBeNext = false; + } + return; + } + + if (isAnySeparateValueArg(name) && (!value.includes("=") || value.endsWith("="))) { + state.skipNextValue = true; + } +} + +function collectCliTemplateEdits(node: SgNode, source: string): Array<[number, number, string]> { + if (node.kind() !== "template_string") return []; + + const edits: Array<[number, number, string]> = []; + const state: CliTemplateState = { + afterTailorBinary: false, + commandMayBeNext: false, + skipNextValue: false, + }; + let skipNextPackageRunnerValue = false; + + const tokens = collectTemplateTokens(node, source); + for (const [index, token] of tokens.entries()) { + const previous = tokens[index - 1]; + const hasCommandBoundaryBefore = + previous != null && hasTemplateCommandBoundaryBetween(previous, token, source); + if (hasCommandBoundaryBefore) { + resetCliTemplateState(state); + } + + const value = staticTemplateTokenValue(token); + const tokenHasCommandSeparator = value !== undefined && hasCommandSeparatorToken(value); + if (value !== undefined && isCommandSeparatorOnlyToken(value)) { + resetCliTemplateState(state); + continue; + } + + if (!state.afterTailorBinary) { + if (value !== undefined) { + if (skipNextPackageRunnerValue) { + skipNextPackageRunnerValue = false; + continue; + } + if (isPackageRunnerSeparatePackageArg(value) || isPackageRunnerSeparateValueArg(value)) { + skipNextPackageRunnerValue = true; + continue; + } + if ( + TAILOR_BINARY_TOKEN.test(value) && + !isLikelyTemplateCommandToken(tokens, index, hasCommandBoundaryBefore) + ) { + continue; + } + advanceCliTemplateState(state, value); + } + continue; + } + + if (state.skipNextValue) { + state.skipNextValue = false; + continue; + } + + if (token.quoted) { + if (state.commandMayBeNext) { + state.commandMayBeNext = false; + } + continue; + } + + if (value !== undefined) { + const name = optionName(value); + if (state.commandMayBeNext) { + if (GLOBAL_BOOLEAN_ARGS.has(name)) { + if (tokenHasCommandSeparator) { + resetCliTemplateState(state); + } + continue; + } + if (isGlobalSeparateValueArg(name)) { + state.skipNextValue = !value.includes("=") || value.endsWith("="); + if (tokenHasCommandSeparator) { + resetCliTemplateState(state); + } + continue; + } + if (!value.startsWith("-")) { + const replacement = rewriteCommandToken(value); + if (replacement !== value) { + pushTemplateTokenReplacement(edits, token, replacement); + } + state.commandMayBeNext = false; + if (tokenHasCommandSeparator) { + resetCliTemplateState(state); + } + continue; + } + if (tokenHasCommandSeparator) { + resetCliTemplateState(state); + } + continue; + } + + if (isOpenCliValueArg(value)) { + state.skipNextValue = true; + continue; + } + + const replacement = replaceOptionsInToken(value); + if (replacement !== value) { + pushTemplateTokenReplacement(edits, token, replacement); + } + if (tokenHasCommandSeparator) { + resetCliTemplateState(state); + } + continue; + } + + if (state.commandMayBeNext) { + if (isInlineGlobalValueArg(token.value)) { + continue; + } + for (const substitution of token.substitutions) { + edits.push(...collectCliExpressionEdits(substitution, source, true)); + } + const dynamicState = + token.value === TEMPLATE_SUBSTITUTION_PLACEHOLDER && token.substitutions.length === 1 + ? dynamicCliArgState(token.substitutions[0]!, source) + : null; + if (dynamicState === "keep-command") { + continue; + } + if (dynamicState === "skip-value") { + state.skipNextValue = true; + continue; + } + state.commandMayBeNext = false; + continue; + } + + if (isInlineCliValueArg(token.value)) { + continue; + } + for (const substitution of token.substitutions) { + edits.push(...collectCliExpressionEdits(substitution, source, false)); + } + const dynamicState = + token.value === TEMPLATE_SUBSTITUTION_PLACEHOLDER && token.substitutions.length === 1 + ? dynamicCliValueArgState(token.substitutions[0]!, source) + : null; + if (dynamicState === "skip-value") { + state.skipNextValue = true; + } + } + + return edits; +} + +function isLikelyTemplateCommandToken( + tokens: TemplateToken[], + tokenIndex: number, + hasCommandBoundaryBefore = false, +): boolean { + if (tokenIndex === 0 || hasCommandBoundaryBefore) return true; + + const previous = tokens[tokenIndex - 1]; + if (previous && staticTemplateTokenValue(previous) !== undefined) { + const value = staticTemplateTokenValue(previous)!; + if (isCommandSeparatorOnlyToken(value)) return true; + } + + const staticPrefixValues = tokens + .slice(0, tokenIndex) + .map((token) => staticTemplateTokenValue(token)) + .filter((value): value is string => value !== undefined); + const [first] = staticPrefixValues; + return first === "npx" || first === "bunx" || first === "pnpm" || first === "yarn"; +} + +function collectCliTemplateSourceEdits( + root: SgNode, + source: string, +): Array<[number, number, string]> { + const edits: Array<[number, number, string]> = []; + const visit = (node: SgNode): void => { + if (node.kind() === "template_string") { + edits.push(...collectCliTemplateEdits(node, source)); + } + for (const child of node.children()) { + visit(child); + } + }; + visit(root); + return edits; +} + +function collectSourceLiteralCliCommandEdits( + root: SgNode, + source: string, +): Array<[number, number, string]> { + const edits: Array<[number, number, string]> = []; + + const visit = (node: SgNode): void => { + if (node.kind() === "comment") { + const range = node.range(); + const text = source.slice(range.start.index, range.end.index); + const replacement = replaceAll(text, false, true, true); + if (replacement !== text) { + edits.push([range.start.index, range.end.index, replacement]); + } + return; + } + + if (node.kind() === "string") { + const token = sourceStringToken(node, source); + if (token) { + const replacement = replaceSourceCommandString(token.value); + if (replacement !== token.value) { + edits.push([token.start, token.end, replacement]); + } + } + return; + } + + if (node.kind() === "jsx_text") { + const range = node.range(); + const text = source.slice(range.start.index, range.end.index); + const replacement = replaceSourceCommandString(text); + if (replacement !== text) { + edits.push([range.start.index, range.end.index, replacement]); + } + return; + } + + for (const child of node.children()) { + visit(child); + } + }; + + visit(root); + return edits; +} + +function collectTokenizedSourceEdits( + root: SgNode, + source: string, +): Array<[number, number, string]> { + const edits: Array<[number, number, string]> = []; + + const visit = (node: SgNode, inheritedAfterTailorBinary = false): void => { + if (inheritedAfterTailorBinary) { + const token = sourceStringToken(node, source); + if (token) { + const replacement = rewriteTokenizedCliArg(token.value, true); + if (replacement !== token.value) { + edits.push([token.start, token.end, replacement]); + } + return; + } + edits.push(...collectInterpolatedOptionEdits(node, source)); + } + + if (isTokenSequenceNode(node)) { + let afterTailorBinary = inheritedAfterTailorBinary; + let commandMayBeNext = inheritedAfterTailorBinary; + let skipNextValue = false; + let skipNextPackageRunnerValue = false; + let previousTokenValue: string | undefined; + for (const child of node.children()) { + const token = sourceStringToken(child, source); + if (token) { + const previous = previousTokenValue; + previousTokenValue = token.value; + + if (!afterTailorBinary) { + if (skipNextPackageRunnerValue) { + skipNextPackageRunnerValue = false; + continue; + } + if ( + isPackageRunnerSeparatePackageArg(token.value) || + isPackageRunnerSeparateValueArg(token.value) + ) { + skipNextPackageRunnerValue = true; + continue; + } + afterTailorBinary = TAILOR_BINARY_TOKEN.test(token.value); + commandMayBeNext = afterTailorBinary; + continue; + } + + if ( + skipNextValue || + (previous != null && + (commandMayBeNext ? isOpenGlobalValueArg(previous) : isOpenCliValueArg(previous))) + ) { + skipNextValue = false; + continue; + } + + const name = optionName(token.value); + const renameCommand = commandMayBeNext && !token.value.startsWith("-"); + const replacement = rewriteTokenizedCliArg(token.value, renameCommand); + if (replacement !== token.value) { + edits.push([token.start, token.end, replacement]); + } + if (commandMayBeNext) { + if (GLOBAL_BOOLEAN_ARGS.has(name)) { + continue; + } + if (isOpenGlobalValueArg(token.value)) { + skipNextValue = true; + continue; + } + if (!token.value.startsWith("-")) { + commandMayBeNext = false; + } + } else if (isOpenCliValueArg(token.value)) { + skipNextValue = true; + } + continue; + } + + if (isSyntaxOnlyNode(child)) { + visit(child); + continue; + } + + if (!afterTailorBinary && skipNextPackageRunnerValue) { + visit(child); + skipNextPackageRunnerValue = false; + previousTokenValue = undefined; + continue; + } + + if (skipNextValue) { + visit(child); + skipNextValue = false; + previousTokenValue = undefined; + continue; + } + + const childCanHoldCommand = + isTokenSequenceNode(child) || cliArgExpressionChildren(child).length > 0; + + if (afterTailorBinary && commandMayBeNext) { + edits.push(...collectInterpolatedOptionEdits(child, source, commandMayBeNext)); + } else if (afterTailorBinary) { + edits.push(...collectInterpolatedOptionEdits(child, source, false)); + if (!templateStartsWithInlineOptionValue(child, source)) { + edits.push(...collectCliExpressionEdits(child, source, false)); + } + } + + visit( + child, + afterTailorBinary && commandMayBeNext && !skipNextValue && childCanHoldCommand, + ); + if (commandMayBeNext && afterTailorBinary) { + const dynamicState = dynamicCliArgState(child, source); + if (dynamicState === "keep-command") { + continue; + } + if (dynamicState === "skip-value") { + skipNextValue = true; + continue; + } + commandMayBeNext = false; + } else if (afterTailorBinary) { + const dynamicState = dynamicCliValueArgState(child, source); + if (dynamicState === "skip-value") { + skipNextValue = true; + } + } + } + return; + } + + const cliArgChildren = inheritedAfterTailorBinary ? cliArgExpressionChildren(node) : []; + if (cliArgChildren.length > 0) { + for (const child of cliArgChildren) { + visit(child, true); + } + return; + } + + for (const child of node.children()) { + visit(child); + } + }; + + visit(root); + return edits; +} + +function replaceTokenizedSourceCliCommands(source: string, filePath: string): string { + let root: SgNode; + try { + root = parse(sourceLang(filePath), source).root(); + } catch { + return source; + } + + let updated = source; + const editByRange = new Map(); + for (const edit of [ + ...collectTokenizedSourceEdits(root, source), + ...collectCliTemplateSourceEdits(root, source), + ...collectSourceLiteralCliCommandEdits(root, source), + ]) { + editByRange.set(`${edit[0]}:${edit[1]}`, edit); + } + const edits = Array.from(editByRange.values()).toSorted(([a], [b]) => b - a); + for (const [start, end, replacement] of edits) { + updated = `${updated.slice(0, start)}${replacement}${updated.slice(end)}`; + } + return updated; +} + +function protectCliValueStrings(source: string): { source: string; protectedValues: string[] } { + const protectedValues: string[] = []; + const updated = source.replace( + /(["'`])(?:--env-file-if-exists|--env-file|--arg|--query|--file|-e|-a|-q|-f)\1\s*,\s*(["'`])([^"'`]*?(?:crash-report|--machineuser)[^"'`]*)\2/g, + (match, _optionQuote: string, valueQuote: string, value: string) => { + const placeholder = `__TAILOR_CLI_RENAME_PROTECTED_${protectedValues.length}__`; + protectedValues.push(value); + return match.replace( + `${valueQuote}${value}${valueQuote}`, + `${valueQuote}${placeholder}${valueQuote}`, + ); + }, + ); + return { source: updated, protectedValues }; +} + +function restoreCliValueStrings(source: string, protectedValues: string[]): string { + let restored = source; + for (const [index, value] of protectedValues.entries()) { + restored = restored.replaceAll(`__TAILOR_CLI_RENAME_PROTECTED_${index}__`, value); + } + return restored.replace( + /((["'`])tailor-sdk\2\s*,\s*(["'`])(?:--env-file|--env-file-if-exists|-e)\3\s*,\s*(["'`])[^"'`]*\4\s*,\s*)(["'`])crash-report\5/g, + "$1$5crashreport$5", + ); +} + function replaceOptionsInTailorCommands( source: string, foldedYamlRanges?: TextRange[], @@ -463,7 +1571,7 @@ function replaceOptionsInTailorCommands( const end = findTailorCommandEnd(source, start, foldedYamlRanges, markdownFencedCodeRanges); updated += source.slice(cursor, start); - updated += replaceOptionsInCommand(source.slice(start, end)); + updated += replaceCliRenamesInCommand(source.slice(start, end)); cursor = end; TAILOR_BINARY_PATTERN.lastIndex = end; } @@ -477,17 +1585,12 @@ function replaceAll( requireDelimitedContext = false, parseMarkdownFencedCode = false, ): string { - const updated = value.replace( - COMMAND_PATTERN, - (match, _prefix: string, cmd: string) => - `${match.slice(0, -cmd.length)}${COMMAND_MAP.get(cmd) ?? cmd}`, - ); - const foldedYamlRanges = parseFoldedYaml ? findFoldedYamlRanges(updated) : undefined; + const foldedYamlRanges = parseFoldedYaml ? findFoldedYamlRanges(value) : undefined; const markdownFencedCodeRanges = parseMarkdownFencedCode - ? findMarkdownFencedCodeRanges(updated) + ? findMarkdownFencedCodeRanges(value) : undefined; return replaceOptionsInTailorCommands( - updated, + value, foldedYamlRanges, requireDelimitedContext, markdownFencedCodeRanges, @@ -498,6 +1601,12 @@ function transformText(source: string, filePath: string): string | null { const ext = path.extname(filePath).toLowerCase(); const isYaml = ext === ".yml" || ext === ".yaml"; const isMarkdown = ext === ".md"; + if (SOURCE_EXTENSIONS.has(ext)) { + const protectedSource = protectCliValueStrings(source); + let updated = replaceTokenizedSourceCliCommands(protectedSource.source, filePath); + updated = restoreCliValueStrings(updated, protectedSource.protectedValues); + return updated === source ? null : updated; + } const updated = replaceAll(source, isYaml, isMarkdown, isMarkdown); return updated === source ? null : updated; } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh index faba9713c..0eef8a76d 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh @@ -7,6 +7,7 @@ pnpm exec tailor-sdk workflow start approval --machine-user ci tailor-sdk query --query 'select 1' --machine-user ci tailor-sdk query 2>&1 --machine-user ci tailor-sdk query $(build-query --machineuser=ci) --machine-user ci +tailor-sdk login ${CI:+--machine-user ci} tailor-sdk query --query 'select 1;' --machine-user ci tailor-sdk query --query "select 1 | 2" --machine-user ci tailor-sdk workflow start approval --arg '{"ok":true}' \ diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh index 5b1573060..d4d5ba382 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh @@ -7,6 +7,7 @@ pnpm exec tailor-sdk workflow start approval --machineuser ci tailor-sdk query --query 'select 1' --machineuser ci tailor-sdk query 2>&1 --machineuser ci tailor-sdk query $(build-query --machineuser=ci) --machineuser ci +tailor-sdk login ${CI:+--machineuser ci} tailor-sdk query --query 'select 1;' --machineuser ci tailor-sdk query --query "select 1 | 2" --machineuser ci tailor-sdk workflow start approval --arg '{"ok":true}' \ diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/expected.d.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/expected.d.ts index 7d6b77937..42fc9cc99 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/expected.d.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/expected.d.ts @@ -1,4 +1,9 @@ /** + * Plain prose mentions tailor-sdk crash-report --machineuser. * Inspect crash reports with `tailor-sdk crashreport list --machine-user ci`. + * Example: + * ```sh + * tailor-sdk crashreport list --machine-user ci + * ``` */ export {}; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/input.d.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/input.d.ts index c0364eca7..a97139391 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/input.d.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/input.d.ts @@ -1,4 +1,9 @@ /** + * Plain prose mentions tailor-sdk crash-report --machineuser. * Inspect crash reports with `tailor-sdk crash-report list --machineuser ci`. + * Example: + * ```sh + * tailor-sdk crash-report list --machineuser ci + * ``` */ export {}; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts index 856d2b12d..e4cdd3550 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts @@ -1,4 +1,40 @@ const machineUser = "ci"; const report = `tailor-sdk crashreport list --machine-user ${machineUser}`; +const conditionalArg = `tailor-sdk login ${isCI ? "--machine-user" : ""}`; +const conditionalCommand = `tailor-sdk ${showReports ? "crashreport" : "login"} list`; +const dynamicGlobalTemplate = `tailor-sdk ${verbose ? "--verbose" : ""} ${showReports ? "crashreport" : "login"} list`; +const staticCommandAfterDynamicGlobal = `tailor-sdk ${verbose ? "--verbose" : ""} crashreport list`; +const dynamicInlineGlobalTemplate = `tailor-sdk ${useEnv ? "--env-file=.env" : "--json"} ${showReports ? "crashreport" : "login"} list`; +const dynamicProfileTemplate = `tailor-sdk --profile ${profile} crashreport --machine-user`; +const separatedTemplate = `tailor-sdk crashreport; ${next}`; +const packageRunnerTemplate = `npx --package tailor-sdk tailor-sdk crashreport --machine-user=ci`; +const shortPackageRunnerTemplate = `npx -p tailor-sdk tailor-sdk crashreport --machine-user=ci`; +const valueFlagPackageRunnerTemplate = `npx --cache tailor-sdk tailor-sdk crashreport --machine-user=ci`; +const repeatedTemplate = `tailor-sdk crashreport --machine-user +tailor-sdk crashreport --machine-user`; +const repeatedSeparatedTemplate = `tailor-sdk crashreport; tailor-sdk crashreport --machine-user`; +const joinedRepeatedSeparatedTemplate = `tailor-sdk crashreport;tailor-sdk crashreport`; +const conditionalGlobalComparison = `tailor-sdk ${mode === "prod" ? "--verbose" : "--json"} crashreport list`; +const conditionalOptionComparison = `tailor-sdk login ${flag === "--machineuser" ? "--json" : "--machine-user"}`; +const conditionalCommandComparison = `tailor-sdk ${cmd === "crash-report" ? "login" : "crashreport"} list`; +const inlineEnvTemplate = `tailor-sdk --env-file=${envFile} ${showReports ? "crashreport" : "login"} list`; const login = "tailor-sdk login --machine-user ci"; +const envFilePath = "tailor-sdk --env-file=/tmp/--machineuser crashreport"; +const envFileCommandSubstitutionPath = "tailor-sdk --env-file=$(pwd)/--machineuser crashreport --machine-user"; +const argPayload = "tailor-sdk function test-run --arg=--machineuser"; +const argCommandSubstitutionPayload = "tailor-sdk function test-run --arg=$(printf --machineuser) --machine-user"; +const profileCommand = "tailor-sdk --profile prod crashreport --machine-user"; +const workspaceCommand = "tailor-sdk -w workspace-1 crashreport"; +const chainedShellCommand = "cd app && tailor-sdk crashreport --machine-user"; +const envShellCommand = "env FOO=bar tailor-sdk crashreport --machine-user"; +const assignmentShellCommand = "TAILOR=1 tailor-sdk crashreport --machine-user"; +const unknownInlineValue = "tailor-sdk login --name=--machineuser"; +const argTemplatePayload = `tailor-sdk function test-run --arg=${payload ? "--machineuser" : ""}`; +const dynamicArgTemplatePayload = `tailor-sdk function test-run ${includeArg ? "--arg" : ""} ${payload ? "--machineuser" : ""} --machine-user`; +const quotedQueryTemplate = `tailor-sdk query --query "select --machineuser ${where}" --machine-user`; +const inlineQueryTemplate = `tailor-sdk query --query=${payload ? "--machineuser" : ""} --machine-user`; +const chainedCommandTemplate = `tailor-sdk login --machine-user && other-cli --machineuser`; +const joinedSeparatorTemplate = `tailor-sdk login --machine-user;other-cli --machineuser`; +const newlineCommandTemplate = `tailor-sdk login --machine-user +other-cli --machineuser`; const other = "other-cli --machineuser ci"; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts index 1f0d0527a..f8bed9cfe 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts @@ -1,4 +1,40 @@ const machineUser = "ci"; const report = `tailor-sdk crash-report list --machineuser ${machineUser}`; +const conditionalArg = `tailor-sdk login ${isCI ? "--machineuser" : ""}`; +const conditionalCommand = `tailor-sdk ${showReports ? "crash-report" : "login"} list`; +const dynamicGlobalTemplate = `tailor-sdk ${verbose ? "--verbose" : ""} ${showReports ? "crash-report" : "login"} list`; +const staticCommandAfterDynamicGlobal = `tailor-sdk ${verbose ? "--verbose" : ""} crash-report list`; +const dynamicInlineGlobalTemplate = `tailor-sdk ${useEnv ? "--env-file=.env" : "--json"} ${showReports ? "crash-report" : "login"} list`; +const dynamicProfileTemplate = `tailor-sdk --profile ${profile} crash-report --machineuser`; +const separatedTemplate = `tailor-sdk crash-report; ${next}`; +const packageRunnerTemplate = `npx --package tailor-sdk tailor-sdk crash-report --machineuser=ci`; +const shortPackageRunnerTemplate = `npx -p tailor-sdk tailor-sdk crash-report --machineuser=ci`; +const valueFlagPackageRunnerTemplate = `npx --cache tailor-sdk tailor-sdk crash-report --machineuser=ci`; +const repeatedTemplate = `tailor-sdk crash-report --machineuser +tailor-sdk crash-report --machineuser`; +const repeatedSeparatedTemplate = `tailor-sdk crash-report; tailor-sdk crash-report --machineuser`; +const joinedRepeatedSeparatedTemplate = `tailor-sdk crash-report;tailor-sdk crash-report`; +const conditionalGlobalComparison = `tailor-sdk ${mode === "prod" ? "--verbose" : "--json"} crash-report list`; +const conditionalOptionComparison = `tailor-sdk login ${flag === "--machineuser" ? "--json" : "--machineuser"}`; +const conditionalCommandComparison = `tailor-sdk ${cmd === "crash-report" ? "login" : "crash-report"} list`; +const inlineEnvTemplate = `tailor-sdk --env-file=${envFile} ${showReports ? "crash-report" : "login"} list`; const login = "tailor-sdk login --machineuser ci"; +const envFilePath = "tailor-sdk --env-file=/tmp/--machineuser crash-report"; +const envFileCommandSubstitutionPath = "tailor-sdk --env-file=$(pwd)/--machineuser crash-report --machineuser"; +const argPayload = "tailor-sdk function test-run --arg=--machineuser"; +const argCommandSubstitutionPayload = "tailor-sdk function test-run --arg=$(printf --machineuser) --machineuser"; +const profileCommand = "tailor-sdk --profile prod crash-report --machineuser"; +const workspaceCommand = "tailor-sdk -w workspace-1 crash-report"; +const chainedShellCommand = "cd app && tailor-sdk crash-report --machineuser"; +const envShellCommand = "env FOO=bar tailor-sdk crash-report --machineuser"; +const assignmentShellCommand = "TAILOR=1 tailor-sdk crash-report --machineuser"; +const unknownInlineValue = "tailor-sdk login --name=--machineuser"; +const argTemplatePayload = `tailor-sdk function test-run --arg=${payload ? "--machineuser" : ""}`; +const dynamicArgTemplatePayload = `tailor-sdk function test-run ${includeArg ? "--arg" : ""} ${payload ? "--machineuser" : ""} --machineuser`; +const quotedQueryTemplate = `tailor-sdk query --query "select --machineuser ${where}" --machineuser`; +const inlineQueryTemplate = `tailor-sdk query --query=${payload ? "--machineuser" : ""} --machineuser`; +const chainedCommandTemplate = `tailor-sdk login --machineuser && other-cli --machineuser`; +const joinedSeparatorTemplate = `tailor-sdk login --machineuser;other-cli --machineuser`; +const newlineCommandTemplate = `tailor-sdk login --machineuser +other-cli --machineuser`; const other = "other-cli --machineuser ci"; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts new file mode 100644 index 000000000..c7eedcb1e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts @@ -0,0 +1,48 @@ +const args = ["tailor-sdk", "crashreport", "list", "--machine-user", "ci"]; +const withRunner = ["npx", "tailor-sdk@latest", "crashreport", "--machine-user=ci"]; +const packageRunnerValueArg = ["npx", "--cache", "tailor-sdk", "tailor-sdk", "crashreport", "--machine-user"]; +const packageRunner = ["npx", "--package", "tailor-sdk", "tailor-sdk", "crashreport"]; +const shortPackageRunner = ["npx", "-p", "tailor-sdk", "tailor-sdk", "crashreport"]; +const nested = spawn("tailor-sdk", ["crashreport", "list", "--machine-user", "ci"]); +const forkedModule = child_process.fork("tailor-sdk/register", ["crash-report", "--machineuser"]); +const dynamic = ["tailor-sdk", "login", `--machine-user=${machineUser}`]; +const conditional = ["tailor-sdk", "login", isCI ? "--machine-user" : "--json"]; +const optional = ["tailor-sdk", "login", includeMachineUser && "--machine-user"]; +const compactOptional = ["tailor-sdk", "login", includeMachineUser&&"--machine-user"]; +const dynamicGlobalOption = ["tailor-sdk", verbose && "--verbose", "crashreport", "list"]; +const dynamicInlineGlobalOption = ["tailor-sdk", useEnv ? "--env-file=.env" : "--json", "crashreport", "list"]; +const profileGlobalOption = ["tailor-sdk", "--profile", "prod", "crashreport", "--machine-user"]; +const workspaceGlobalOption = ["tailor-sdk", "-w", "workspace-1", "crashreport"]; +const inlineEnvTemplate = ["tailor-sdk", `--env-file=${envFile}`, "crashreport", "list"]; +const openEnvFileValue = ["tailor-sdk", "--env-file=", ".env", "crashreport", "--machine-user"]; +const argValue = ["tailor-sdk", "function", "test-run", "--arg", "crash-report"]; +const openArgValue = ["tailor-sdk", "function", "test-run", "--arg=", "--machineuser", "--machine-user"]; +const shortArgValue = ["tailor-sdk", "function", "test-run", "-a", "--machineuser", "--machine-user"]; +const queryValue = ["tailor-sdk", "query", "--query", "select --machineuser", "--machine-user", "ci"]; +const queryValueWithComment = [ + "tailor-sdk", + "query", + "--query", + /* sql */ "select --machineuser", + "--machine-user", +]; +const inlineQueryValue = ["tailor-sdk", "query", `--query=${payload ? "--machineuser" : ""}`, "--machine-user"]; +const fileValue = ["tailor-sdk", "query", "--file", "queries/--machineuser.graphql", "--machine-user"]; +const envFileValue = ["tailor-sdk", "--env-file", "crash-report", "crashreport"]; +const envFileBackticks = [`tailor-sdk`, `--env-file`, `crash-report`, `crashreport`]; +const envFileExpression = ["tailor-sdk", "--env-file", envFile, "crashreport", "list"]; +const nestedProfile = spawn("tailor-sdk", ["--config", "tailor.config.ts", "crashreport"]); +const argExpression = ["tailor-sdk", "function", "test-run", "--arg", payload, "--machine-user"]; +const dynamicArgExpression = ["tailor-sdk", "function", "test-run", includeArg ? "--arg" : "", "--machineuser"]; +const inlineArgValue = ["tailor-sdk", "function", "test-run", "--arg=--machineuser"]; +const inlineEnvFileValue = ["tailor-sdk", "--env-file=/tmp/--machineuser", "crashreport"]; +const dynamicCommand = ["tailor-sdk", command, "crash-report", "--machine-user"]; +const otherCli = ["other-cli", "crash-report", "--machineuser"]; +const diagnostic = formatDiagnostic("tailor-sdk", "crash-report", "--machineuser"); +const postCommandConditionalArg = ["tailor-sdk", "secret", "set", cond ? "crash-report" : "x", cond ? "--machine-user" : "--json"]; +const postCommandLogicalArg = ["tailor-sdk", "secret", "set", cond && "crash-report", cond && "--machine-user"]; +const dynamicLabel = `--machineuser=${machineUser}`; +const label = "crash-report"; +const regexPattern = /tailor-sdk crash-report --machineuser/; +const prose = "package tailor-sdk crash-report --machineuser"; +const proseTemplate = `package tailor-sdk crash-report --machineuser`; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts new file mode 100644 index 000000000..e381ad7b9 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts @@ -0,0 +1,48 @@ +const args = ["tailor-sdk", "crash-report", "list", "--machineuser", "ci"]; +const withRunner = ["npx", "tailor-sdk@latest", "crash-report", "--machineuser=ci"]; +const packageRunnerValueArg = ["npx", "--cache", "tailor-sdk", "tailor-sdk", "crash-report", "--machineuser"]; +const packageRunner = ["npx", "--package", "tailor-sdk", "tailor-sdk", "crash-report"]; +const shortPackageRunner = ["npx", "-p", "tailor-sdk", "tailor-sdk", "crash-report"]; +const nested = spawn("tailor-sdk", ["crash-report", "list", "--machineuser", "ci"]); +const forkedModule = child_process.fork("tailor-sdk/register", ["crash-report", "--machineuser"]); +const dynamic = ["tailor-sdk", "login", `--machineuser=${machineUser}`]; +const conditional = ["tailor-sdk", "login", isCI ? "--machineuser" : "--json"]; +const optional = ["tailor-sdk", "login", includeMachineUser && "--machineuser"]; +const compactOptional = ["tailor-sdk", "login", includeMachineUser&&"--machineuser"]; +const dynamicGlobalOption = ["tailor-sdk", verbose && "--verbose", "crash-report", "list"]; +const dynamicInlineGlobalOption = ["tailor-sdk", useEnv ? "--env-file=.env" : "--json", "crash-report", "list"]; +const profileGlobalOption = ["tailor-sdk", "--profile", "prod", "crash-report", "--machineuser"]; +const workspaceGlobalOption = ["tailor-sdk", "-w", "workspace-1", "crash-report"]; +const inlineEnvTemplate = ["tailor-sdk", `--env-file=${envFile}`, "crash-report", "list"]; +const openEnvFileValue = ["tailor-sdk", "--env-file=", ".env", "crash-report", "--machineuser"]; +const argValue = ["tailor-sdk", "function", "test-run", "--arg", "crash-report"]; +const openArgValue = ["tailor-sdk", "function", "test-run", "--arg=", "--machineuser", "--machineuser"]; +const shortArgValue = ["tailor-sdk", "function", "test-run", "-a", "--machineuser", "--machineuser"]; +const queryValue = ["tailor-sdk", "query", "--query", "select --machineuser", "--machineuser", "ci"]; +const queryValueWithComment = [ + "tailor-sdk", + "query", + "--query", + /* sql */ "select --machineuser", + "--machineuser", +]; +const inlineQueryValue = ["tailor-sdk", "query", `--query=${payload ? "--machineuser" : ""}`, "--machineuser"]; +const fileValue = ["tailor-sdk", "query", "--file", "queries/--machineuser.graphql", "--machineuser"]; +const envFileValue = ["tailor-sdk", "--env-file", "crash-report", "crash-report"]; +const envFileBackticks = [`tailor-sdk`, `--env-file`, `crash-report`, `crash-report`]; +const envFileExpression = ["tailor-sdk", "--env-file", envFile, "crash-report", "list"]; +const nestedProfile = spawn("tailor-sdk", ["--config", "tailor.config.ts", "crash-report"]); +const argExpression = ["tailor-sdk", "function", "test-run", "--arg", payload, "--machineuser"]; +const dynamicArgExpression = ["tailor-sdk", "function", "test-run", includeArg ? "--arg" : "", "--machineuser"]; +const inlineArgValue = ["tailor-sdk", "function", "test-run", "--arg=--machineuser"]; +const inlineEnvFileValue = ["tailor-sdk", "--env-file=/tmp/--machineuser", "crash-report"]; +const dynamicCommand = ["tailor-sdk", command, "crash-report", "--machineuser"]; +const otherCli = ["other-cli", "crash-report", "--machineuser"]; +const diagnostic = formatDiagnostic("tailor-sdk", "crash-report", "--machineuser"); +const postCommandConditionalArg = ["tailor-sdk", "secret", "set", cond ? "crash-report" : "x", cond ? "--machineuser" : "--json"]; +const postCommandLogicalArg = ["tailor-sdk", "secret", "set", cond && "crash-report", cond && "--machineuser"]; +const dynamicLabel = `--machineuser=${machineUser}`; +const label = "crash-report"; +const regexPattern = /tailor-sdk crash-report --machineuser/; +const prose = "package tailor-sdk crash-report --machineuser"; +const proseTemplate = `package tailor-sdk crash-report --machineuser`; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/expected.tsx b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/expected.tsx new file mode 100644 index 000000000..361fec5c6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/expected.tsx @@ -0,0 +1,6 @@ +const docs = ( + <> +

package tailor-sdk crash-report --machineuser

+ tailor-sdk crashreport list --machine-user ci + +); diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/input.tsx b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/input.tsx new file mode 100644 index 000000000..b789716d1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/input.tsx @@ -0,0 +1,6 @@ +const docs = ( + <> +

package tailor-sdk crash-report --machineuser

+ tailor-sdk crash-report list --machineuser ci + +); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 0512d48f7..95bcb5668 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -1,26 +1,1299 @@ +import { parse, Lang } from "@ast-grep/napi"; import * as path from "pathe"; +import type { SgNode } from "@ast-grep/napi"; -// Package-runner forms (`npx`, `pnpm dlx`, `yarn dlx`, `bunx`) resolve npm package -// names, so `tailor-sdk@...` must become `@tailor-platform/sdk@...` — rewriting -// to `tailor@...` would download the unrelated CSS Sprites Generator instead. -// Optional flags (e.g. `-y`, `--yes`) between the runner and the package name are -// captured as part of the runner group so the replacement preserves them. -const PKG_RUNNER_RE = - /\b((?:npx|pnpm\s+dlx|yarn\s+dlx|bunx)(?:\s+(?:-\w+|--\w[\w-]*))*)\s+tailor-sdk(?![\w-])(@[^\s'"`;|&)]+)?/g; +const SHELL_ARG_VALUE = `(?:[^\\s'"\`;|&]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`; +const RUNNER_VALUE_FLAG = "(?:--cache|--userconfig|--registry|--prefix|--dir|--filter|--cwd|-C)"; +const RUNNER_VALUE_ARG = `${RUNNER_VALUE_FLAG}(?:=${SHELL_ARG_VALUE}|\\s+${SHELL_ARG_VALUE})`; +const RUNNER_BOOLEAN_ARG = `(?:(?!-p(?:\\s|$))-\\w+|--(?!package(?:=|\\s|$))\\w[\\w-]*(?:=${SHELL_ARG_VALUE})?)`; +const RUNNER_OPTION = `(?:${RUNNER_VALUE_ARG}|${RUNNER_BOOLEAN_ARG})`; +const DIRECT_PKG_RUNNER = `(?:npx|bunx)(?:\\s+${RUNNER_OPTION})*`; +const DLX_PKG_RUNNER = `(?:pnpm|yarn)(?:\\s+${RUNNER_OPTION})*\\s+dlx(?:\\s+${RUNNER_OPTION})*`; + +// Package-runner forms resolve npm package names, so `tailor-sdk@...` must +// become `@tailor-platform/sdk@...`; rewriting to `tailor@...` would download +// the unrelated CSS Sprites Generator instead. Runner options, including +// options with values, are captured as part of the runner group. +const PKG_RUNNER_RE = new RegExp( + `\\b((?:${DIRECT_PKG_RUNNER}|${DLX_PKG_RUNNER}))\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?`, + "g", +); +const RUNNER_VALUE_REFERENCE_RE = new RegExp( + `(${RUNNER_VALUE_FLAG}(?:=|\\s+))(${SHELL_ARG_VALUE})`, + "g", +); +const RUNNER_PACKAGE_VALUE_REFERENCE_RE = new RegExp( + `\\b((?:${DIRECT_PKG_RUNNER}|${DLX_PKG_RUNNER})\\s+(?:--package|-p)(?:=|\\s+))(${SHELL_ARG_VALUE})`, + "g", +); +const TAILOR_CLI_VALUE_FLAG = + "(?:--env-file-if-exists|--env-file|--profile|--config|--workspace-id|--arg|--query|--file|-e|-p|-c|-w|-a|-q|-f)"; +const TAILOR_CLI_VALUE_REFERENCE_RE = new RegExp( + `(${TAILOR_CLI_VALUE_FLAG}(?:=|\\s+))(${SHELL_ARG_VALUE})`, + "g", +); // Match the `tailor-sdk` binary, optionally with a version pin (`@latest`, // `@2.0.0`, etc.). Lookbehind excludes `.tailor-sdk` (preceded by `.`) and // `create-tailor-sdk` (preceded by `-`). Lookahead excludes trailing `-word` // (e.g. `tailor-sdk-skills`) to avoid partial-match rewrites. const TAILOR_SDK_RE = /(? - version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, + const packageValueUpdated = rewriteRunnerPackageValueReferences(value); + const protectedRunnerValue = protectShellRunnerValueReferences(packageValueUpdated); + const protectedCliValue = protectTailorCliValueReferences(protectedRunnerValue.source); + const withRunners = protectedCliValue.source.replace( + PKG_RUNNER_RE, + (_, runner: string, version?: string) => + version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, ); - return withRunners.replace(TAILOR_SDK_RE, (_match, version?: string) => + const updated = withRunners.replace(TAILOR_SDK_RE, (_match, version?: string) => version ? `@tailor-platform/sdk${version}` : "tailor", ); + const cliValueRestored = restoreProtectedValues( + updated, + protectedCliValue.protectedValues, + "__TAILOR_SDK_CODEMOD_CLI_VALUE_", + ); + return restoreProtectedValues( + cliValueRestored, + protectedRunnerValue.protectedValues, + "__TAILOR_SDK_CODEMOD_SHELL_VALUE_", + ); +} + +function sourceLang(filePath: string): Lang { + const ext = path.extname(filePath).toLowerCase(); + return ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : Lang.TypeScript; +} + +function isTokenSequenceNode(node: SgNode): boolean { + return node.kind() === "array" || isCliArgumentsNode(node); +} + +function isSyntaxOnlyNode(node: SgNode): boolean { + const kind = node.kind(); + return ( + kind === "[" || + kind === "]" || + kind === "(" || + kind === ")" || + kind === "," || + kind === "comment" + ); +} + +function isProtectedRunnerValuePlaceholder(node: SgNode): boolean { + return /^__TAILOR_SDK_CODEMOD_VALUE_PROTECTED_\d+__$/.test(node.text()); +} + +function nodeRangeKey(node: SgNode): string { + const range = node.range(); + return `${range.start.index}:${range.end.index}`; +} + +function isCliArgumentsNode(node: SgNode): boolean { + if (node.kind() !== "arguments") return false; + const parent = node.parent(); + if (parent?.kind() !== "call_expression") return false; + const argumentRange = nodeRangeKey(node); + const callee = parent.children().find((child: SgNode) => nodeRangeKey(child) !== argumentRange); + const calleeText = callee?.text(); + return calleeText === "$" || (calleeText != null && CLI_ARGUMENT_CALLEE_RE.test(calleeText)); +} + +function sourceStringToken(node: SgNode, source: string): SourceStringToken | undefined { + const kind = node.kind(); + if (kind !== "string" && kind !== "template_string") return undefined; + if ( + kind === "template_string" && + node.children().some((child: SgNode) => child.kind() === "template_substitution") + ) { + return undefined; + } + + const fragments = node.children().filter((child: SgNode) => child.kind() === "string_fragment"); + if (fragments.length !== 1) return undefined; + + const range = fragments[0]!.range(); + return { + value: source.slice(range.start.index, range.end.index), + start: range.start.index, + end: range.end.index, + }; +} + +function ensureTemplateToken(state: TemplateTokenState): TemplateToken { + state.token ??= { value: "", segments: [], substitutions: [], quoted: false }; + return state.token; +} + +function appendTemplateTokenChar(state: TemplateTokenState, ch: string, sourceIndex: number): void { + const token = ensureTemplateToken(state); + token.value += ch; + const previous = token.segments.at(-1); + if (previous && previous.end === sourceIndex) { + previous.value += ch; + previous.end += 1; + } else { + token.segments.push({ value: ch, start: sourceIndex, end: sourceIndex + 1 }); + } +} + +function pushTemplateToken(tokens: TemplateToken[], state: TemplateTokenState): void { + if (!state.token) return; + tokens.push(state.token); + state.token = null; +} + +function scanTemplateTextTokens( + state: TemplateTokenState, + text: string, + offset: number, + tokens: TemplateToken[], +): void { + for (let index = 0; index < text.length; index += 1) { + const ch = text[index]!; + + if (state.quote !== null) { + appendTemplateTokenChar(state, ch, offset + index); + if (ch === "\\" && state.quote === '"' && index + 1 < text.length) { + index += 1; + appendTemplateTokenChar(state, text[index]!, offset + index); + continue; + } + if (ch === state.quote) { + state.quote = null; + } + continue; + } + + if (/\s/.test(ch)) { + pushTemplateToken(tokens, state); + continue; + } + + appendTemplateTokenChar(state, ch, offset + index); + if (ch === "'" || ch === '"') { + state.quote = ch; + ensureTemplateToken(state).quoted = true; + } + } +} + +function appendTemplateSubstitution(state: TemplateTokenState, substitution: SgNode): void { + const token = ensureTemplateToken(state); + token.value += TEMPLATE_SUBSTITUTION_PLACEHOLDER; + token.substitutions.push(substitution); +} + +function collectTemplateTokens(node: SgNode, source: string): TemplateToken[] { + const tokens: TemplateToken[] = []; + const state: TemplateTokenState = { quote: null, token: null }; + + for (const child of node.children()) { + if (child.kind() === "string_fragment") { + const range = child.range(); + scanTemplateTextTokens( + state, + source.slice(range.start.index, range.end.index), + range.start.index, + tokens, + ); + continue; + } + if (child.kind() === "template_substitution") { + appendTemplateSubstitution(state, child); + } + } + + pushTemplateToken(tokens, state); + return tokens; +} + +function staticTemplateTokenValue(token: TemplateToken): string | undefined { + return token.substitutions.length === 0 ? token.value : undefined; +} + +function isRunnerFlag(value: string): boolean { + if (isRunnerPackageFlag(value)) return false; + return /^-\w+$|^--\w[\w-]*(?:=.*)?$/.test(value); +} + +function isRunnerValueFlag(value: string): boolean { + return /^(?:--cache|--userconfig|--registry|--prefix|--dir|--filter|--cwd|-C)(?:=|$)/.test(value); +} + +function isRunnerOpenValueFlag(value: string): boolean { + return isRunnerValueFlag(value) && (!value.includes("=") || value.endsWith("=")); +} + +function isRunnerSeparateValueFlag(value: string | undefined): boolean { + return ( + value === "--cache" || + value === "--userconfig" || + value === "--registry" || + value === "--prefix" || + value === "--dir" || + value === "--filter" || + value === "--cwd" || + value === "-C" + ); +} + +function isRunnerPackageFlag(value: string): boolean { + return /^(?:--package|-p)(?:=|$)/.test(value); +} + +function isRunnerOpenPackageFlag(value: string): boolean { + return isRunnerPackageFlag(value) && (!value.includes("=") || value.endsWith("=")); +} + +function isRunnerSeparatePackageFlag(value: string | undefined): boolean { + return value === "--package" || value === "-p"; +} + +function rewriteRunnerPackageOptionValue( + token: SourceStringToken, +): Array<[number, number, string]> { + const equalsIndex = token.value.indexOf("="); + if (equalsIndex === -1) return []; + const value = token.value.slice(equalsIndex + 1); + const replacement = rewriteRunnerPackageValue(value); + return replacement ? [[token.start + equalsIndex + 1, token.end, replacement]] : []; +} + +function restoreProtectedValues(source: string, protectedValues: string[], prefix: string): string { + let restored = source; + for (const [index, value] of protectedValues.entries()) { + restored = restored.replaceAll(`${prefix}${index}__`, value); + } + return restored; +} + +function protectShellRunnerValueReferences(source: string): TokenizedRunnerRewrite { + const protectedValues: string[] = []; + const updated = source.replace( + RUNNER_VALUE_REFERENCE_RE, + (match, prefix: string, value: string) => { + if (!value.includes("tailor-sdk")) return match; + const placeholder = `__TAILOR_SDK_CODEMOD_SHELL_VALUE_${protectedValues.length}__`; + protectedValues.push(value); + return `${prefix}${placeholder}`; + }, + ); + return { source: updated, protectedValues }; +} + +function protectTailorCliValueReferences(source: string): TokenizedRunnerRewrite { + const protectedValues: string[] = []; + const updated = source.replace( + TAILOR_CLI_VALUE_REFERENCE_RE, + (match, prefix: string, value: string) => { + if (!value.includes("tailor-sdk")) return match; + const placeholder = `__TAILOR_SDK_CODEMOD_CLI_VALUE_${protectedValues.length}__`; + protectedValues.push(value); + return `${prefix}${placeholder}`; + }, + ); + return { source: updated, protectedValues }; +} + +function rewriteRunnerPackageValue(value: string): string | undefined { + const quote = value[0] === "'" || value[0] === '"' ? value[0] : ""; + const inner = quote ? value.slice(1, -1) : value; + const replacement = runnerPackageReplacement(inner); + if (!replacement) return undefined; + return quote ? `${quote}${replacement}${quote}` : replacement; +} + +function rewriteRunnerPackageValueReferences(source: string): string { + return source.replace( + RUNNER_PACKAGE_VALUE_REFERENCE_RE, + (match, prefix: string, value: string) => { + const replacement = rewriteRunnerPackageValue(value); + return replacement ? `${prefix}${replacement}` : match; + }, + ); +} + +function runnerStateAfterToken(value: string): RunnerState { + if (value === "npx" || value === "bunx") return "await-package"; + if (value === "pnpm" || value === "yarn") return "await-dlx"; + return "none"; +} + +function runnerPackageReplacement(value: string): string | undefined { + const match = TAILOR_SDK_TOKEN.exec(value); + if (!match) return undefined; + return `@tailor-platform/sdk${match[1] ?? ""}`; +} + +function isPathLikeRunnerFlagValue(value: string): boolean { + return value.startsWith(".") || value.startsWith("/") || value.startsWith("~"); +} + +function isTailorCliCommandToken(value: string): boolean { + const token = value.replace(/[),.:!?]+$/, ""); + return token.startsWith("-") || TAILOR_CLI_COMMANDS.has(token); +} + +function isTailorCliSeparateValueFlag(value: string | undefined): boolean { + return value != null && TAILOR_CLI_SEPARATE_VALUE_FLAGS.has(value); +} + +function collectPackageRunnerTokenRanges(value: string): TextRange[] { + const ranges: TextRange[] = []; + PKG_RUNNER_RE.lastIndex = 0; + for (;;) { + const match = PKG_RUNNER_RE.exec(value); + if (!match) break; + const tokenStartInMatch = match[0].lastIndexOf("tailor-sdk"); + if (tokenStartInMatch === -1) continue; + const start = match.index + tokenStartInMatch; + ranges.push({ start, end: start + "tailor-sdk".length + (match[2]?.length ?? 0) }); + } + PKG_RUNNER_RE.lastIndex = 0; + return ranges; +} + +function collectPackageRunnerOptionValueRanges(value: string): TextRange[] { + const ranges: TextRange[] = []; + RUNNER_PACKAGE_VALUE_REFERENCE_RE.lastIndex = 0; + for (;;) { + const match = RUNNER_PACKAGE_VALUE_REFERENCE_RE.exec(value); + if (!match) break; + const optionValue = match[2]!; + if (!rewriteRunnerPackageValue(optionValue)) continue; + const quoteOffset = optionValue[0] === "'" || optionValue[0] === '"' ? 1 : 0; + const start = match.index + match[1]!.length + quoteOffset; + ranges.push({ start, end: start + optionValue.length - quoteOffset * 2 }); + } + RUNNER_PACKAGE_VALUE_REFERENCE_RE.lastIndex = 0; + return ranges; +} + +function collectTailorCommandTokenRanges(value: string): TextRange[] { + const ranges: TextRange[] = []; + TAILOR_SDK_COMMAND_TOKEN_RE.lastIndex = 0; + for (;;) { + const match = TAILOR_SDK_COMMAND_TOKEN_RE.exec(value); + if (!match) break; + if (!isTailorCliCommandToken(match[1]!)) { + TAILOR_SDK_COMMAND_TOKEN_RE.lastIndex = match.index + 1; + continue; + } + const token = /^tailor-sdk(@[^\s'"`;|&)]+)?/.exec(match[0]); + if (!token) continue; + ranges.push({ start: match.index, end: match.index + token[0].length }); + } + TAILOR_SDK_COMMAND_TOKEN_RE.lastIndex = 0; + return ranges; +} + +function collectDynamicTemplateTailorCommandRanges(value: string): TextRange[] { + const ranges: TextRange[] = []; + const pattern = /(^|[;&|\n])(\s*)tailor-sdk(@[^\s'"`;|&)]+)?(?=\s|$)/gu; + for (;;) { + const match = pattern.exec(value); + if (!match) break; + const start = match.index + match[1]!.length + match[2]!.length; + ranges.push({ start, end: start + "tailor-sdk".length + (match[3]?.length ?? 0) }); + } + return ranges; +} + +function collectRewriteableTailorSdkRanges(value: string): TextRange[] { + return [ + ...collectPackageRunnerTokenRanges(value), + ...collectPackageRunnerOptionValueRanges(value), + ...collectTailorCommandTokenRanges(value), + ]; +} + +function containsRange(ranges: TextRange[], start: number, end: number): boolean { + return ranges.some((range) => range.start <= start && end <= range.end); +} + +function collectNonCommandTailorSdkRanges( + value: string, + offset: number, + rewriteableRanges = collectRewriteableTailorSdkRanges(value), + rewriteableOffset = 0, +): Array<[number, number, string]> { + const protectedRanges: Array<[number, number, string]> = []; + TAILOR_SDK_RE.lastIndex = 0; + for (;;) { + const match = TAILOR_SDK_RE.exec(value); + if (!match) break; + const start = match.index; + const end = start + match[0].length; + if (!containsRange(rewriteableRanges, rewriteableOffset + start, rewriteableOffset + end)) { + protectedRanges.push([offset + start, offset + end, match[0]]); + } + } + TAILOR_SDK_RE.lastIndex = 0; + return protectedRanges; +} + +function runnerPackageExpressionChildren(node: SgNode): SgNode[] { + if (node.kind() === "parenthesized_expression") return node.children(); + if (node.kind() === "ternary_expression") return node.children().slice(1); + if (node.kind() === "binary_expression" && /&&|\|\|/.test(node.text())) { + return node.children().slice(1); + } + return []; +} + +function collectTokenizedRunnerEdits( + root: SgNode, + source: string, +): { edits: Array<[number, number, string]>; protectedRanges: Array<[number, number, string]> } { + const edits: Array<[number, number, string]> = []; + const protectedRanges: Array<[number, number, string]> = []; + + const protectToken = (token: SourceStringToken): void => { + if (token.value.includes("tailor-sdk")) { + protectedRanges.push([token.start, token.end, token.value]); + } + }; + + const protectTemplateToken = (token: TemplateToken): void => { + for (const segment of token.segments) { + protectToken(segment); + } + for (const substitution of token.substitutions) { + protectTailorSdkStrings(substitution); + } + }; + + const pushTemplateTokenReplacement = (token: TemplateToken, replacement: string): void => { + if (token.segments.length !== 1) return; + const [{ start, end }] = token.segments; + edits.push([start, end, replacement]); + }; + + const collectTemplatePackageExpressionEdits = (token: TemplateToken): boolean => { + let changed = false; + for (const substitution of token.substitutions) { + changed = collectPackageExpressionEdits(substitution) || changed; + } + return changed; + }; + + const protectTailorSdkStrings = (node: SgNode): void => { + const token = sourceStringToken(node, source); + if (token) { + protectToken(token); + return; + } + for (const child of node.children()) { + protectTailorSdkStrings(child); + } + }; + + const collectPackageExpressionEdits = (node: SgNode): boolean => { + const token = sourceStringToken(node, source); + if (token) { + const replacement = runnerPackageReplacement(token.value); + if (replacement) { + edits.push([token.start, token.end, replacement]); + return true; + } + return false; + } + let changed = false; + const packageChildren = runnerPackageExpressionChildren(node); + if (packageChildren.length > 0) { + const packageChildRanges = new Set(packageChildren.map((child) => nodeRangeKey(child))); + for (const child of node.children()) { + if (!packageChildRanges.has(nodeRangeKey(child))) { + protectTailorSdkStrings(child); + } + } + } + const children = packageChildren.length > 0 ? packageChildren : node.children(); + for (const child of children) { + changed = collectPackageExpressionEdits(child) || changed; + } + return changed; + }; + + const expressionStringValues = (node: SgNode): string[] => { + const token = sourceStringToken(node, source); + if (token) return [token.value]; + const values: string[] = []; + const packageChildren = runnerPackageExpressionChildren(node); + const children = packageChildren.length > 0 ? packageChildren : node.children(); + for (const child of children) { + values.push(...expressionStringValues(child)); + } + return values; + }; + + const dynamicRunnerOptionState = (node: SgNode): RunnerState | null => { + const values = expressionStringValues(node) + .map((value) => value.trim()) + .filter((value) => value !== ""); + if (values.length === 0) return null; + if (values.every((value) => isRunnerOpenPackageFlag(value))) { + return "await-package-option-value"; + } + if (!values.every((value) => isRunnerFlag(value) || isRunnerValueFlag(value))) return null; + const hasOpenValueFlag = values.some((value) => isRunnerOpenValueFlag(value)); + if (hasOpenValueFlag) { + return "await-flag-value"; + } + return "await-package"; + }; + + const dynamicRunnerOptionStateFor = ( + runnerState: RunnerState, + node: SgNode, + ): RunnerState | null => { + const dynamicState = dynamicRunnerOptionState(node); + if (runnerState !== "await-dlx") return dynamicState; + if (dynamicState === "await-flag-value") return "await-dlx-flag-value"; + if (dynamicState === "await-package") return "await-dlx"; + return dynamicState; + }; + + const advanceRunnerState = (runnerState: RunnerState, value: string): RunnerState => { + if (runnerState === "await-flag-value") return "await-package"; + if (runnerState === "await-dlx-flag-value") return "await-dlx"; + if (runnerState === "await-package-option-value") return "none"; + + if (runnerState === "await-dlx") { + if (value === "dlx") return "await-package"; + if (isRunnerSeparateValueFlag(value)) return "await-dlx-flag-value"; + if (isRunnerValueFlag(value)) { + return value.includes("=") && !value.endsWith("=") ? "await-dlx" : "await-dlx-flag-value"; + } + if (isRunnerFlag(value)) return "await-dlx"; + return runnerStateAfterToken(value); + } + + if (runnerState === "await-package") { + if (isRunnerSeparatePackageFlag(value)) return "await-package-option-value"; + if (isRunnerPackageFlag(value)) { + return value.endsWith("=") ? "await-package-option-value" : "none"; + } + if (isRunnerSeparateValueFlag(value)) return "await-flag-value"; + if (isRunnerValueFlag(value)) { + return value.includes("=") && !value.endsWith("=") ? "await-package" : "await-flag-value"; + } + if (isRunnerFlag(value)) return "await-package"; + return "none"; + } + + return runnerStateAfterToken(value); + }; + + const processRunnerTemplateToken = ( + runnerState: RunnerState, + token: TemplateToken, + ): RunnerState => { + const nextState = runnerState; + const value = staticTemplateTokenValue(token); + + if (nextState === "await-flag-value") { + protectTemplateToken(token); + return "await-package"; + } + if (nextState === "await-dlx-flag-value") { + protectTemplateToken(token); + return "await-dlx"; + } + if (nextState === "await-package-option-value") { + if (value !== undefined) { + const replacement = rewriteRunnerPackageValue(value); + if (replacement) { + pushTemplateTokenReplacement(token, replacement); + } + } else { + collectTemplatePackageExpressionEdits(token); + } + return "none"; + } + if (nextState === "await-package") { + if (value !== undefined) { + if (isRunnerPackageFlag(value)) { + for (const segment of token.segments) { + edits.push(...rewriteRunnerPackageOptionValue(segment)); + } + return value.endsWith("=") ? "await-package-option-value" : "none"; + } + if (isRunnerFlag(value) || isRunnerValueFlag(value)) { + if (isRunnerValueFlag(value) && value.includes("=")) { + protectTemplateToken(token); + } + return advanceRunnerState(nextState, value); + } + const replacement = rewriteRunnerPackageValue(value); + if (replacement) { + pushTemplateTokenReplacement(token, replacement); + } + return "none"; + } + + if (isRunnerPackageFlag(token.value)) { + collectTemplatePackageExpressionEdits(token); + return token.value.endsWith("=") ? "await-package-option-value" : "none"; + } + if (isRunnerValueFlag(token.value)) { + protectTemplateToken(token); + return token.value.includes("=") && !token.value.endsWith("=") + ? "await-package" + : "await-flag-value"; + } + if (isRunnerFlag(token.value)) { + return "await-package"; + } + const changed = collectTemplatePackageExpressionEdits(token); + return changed + ? "none" + : (dynamicRunnerOptionState(token.substitutions[0]!, source) ?? "none"); + } + + if (nextState === "await-dlx" && value === undefined) { + return dynamicRunnerOptionStateFor(nextState, token.substitutions[0]!) ?? "none"; + } + + if (value !== undefined) { + return advanceRunnerState(nextState, value); + } + return dynamicRunnerOptionStateFor(nextState, token.substitutions[0]!) ?? "none"; + }; + + const visitTemplate = (node: SgNode, inheritedRunnerState: RunnerState): RunnerState => { + let runnerState = inheritedRunnerState; + for (const token of collectTemplateTokens(node, source)) { + if (token.substitutions.length === 0) { + runnerState = processRunnerTemplateToken(runnerState, token); + continue; + } + + if ( + runnerState === "await-package" || + runnerState === "await-package-option-value" || + runnerState === "await-flag-value" || + runnerState === "await-dlx" || + runnerState === "await-dlx-flag-value" + ) { + runnerState = processRunnerTemplateToken(runnerState, token); + continue; + } + + for (const substitution of token.substitutions) { + visit(substitution); + } + const dynamicState = dynamicRunnerOptionStateFor(runnerState, token.substitutions[0]!); + if (dynamicState) { + runnerState = dynamicState; + } else { + const value = staticTemplateTokenValue(token); + if (value !== undefined) { + runnerState = advanceRunnerState(runnerState, value); + } + } + } + return runnerState; + }; + + const visit = (node: SgNode, inheritedRunnerState: RunnerState = "none"): void => { + if (inheritedRunnerState === "await-package") { + const token = sourceStringToken(node, source); + if (token) { + const replacement = runnerPackageReplacement(token.value); + if (replacement) { + edits.push([token.start, token.end, replacement]); + } + return; + } + + const packageChildren = runnerPackageExpressionChildren(node); + if (packageChildren.length > 0) { + for (const child of packageChildren) { + visit(child, "await-package"); + } + return; + } + } + + if (node.kind() === "template_string") { + visitTemplate(node, inheritedRunnerState); + return; + } + + if (isTokenSequenceNode(node)) { + let runnerState = inheritedRunnerState; + let previousTokenValue: string | undefined; + for (const child of node.children()) { + const token = sourceStringToken(child, source); + if (token) { + const previous = previousTokenValue; + previousTokenValue = token.value; + + if (runnerState === "await-flag-value") { + protectToken(token); + runnerState = "await-package"; + continue; + } + + if (runnerState === "await-dlx-flag-value") { + protectToken(token); + runnerState = "await-dlx"; + continue; + } + + if (runnerState === "await-package-option-value") { + const replacement = rewriteRunnerPackageValue(token.value); + if (replacement) { + edits.push([token.start, token.end, replacement]); + } + runnerState = "none"; + previousTokenValue = undefined; + continue; + } + + if (runnerState === "await-dlx") { + if (token.value === "dlx") { + runnerState = "await-package"; + continue; + } + if (isRunnerSeparateValueFlag(token.value)) { + runnerState = "await-dlx-flag-value"; + continue; + } + if (isRunnerValueFlag(token.value)) { + if (token.value.includes("=")) { + protectToken(token); + } + continue; + } + if (isRunnerFlag(token.value)) continue; + runnerState = runnerStateAfterToken(token.value); + continue; + } + + if (runnerState === "await-package") { + if (isRunnerSeparatePackageFlag(token.value)) { + runnerState = "await-package-option-value"; + continue; + } + if (isRunnerPackageFlag(token.value)) { + edits.push(...rewriteRunnerPackageOptionValue(token)); + runnerState = token.value.endsWith("=") ? "await-package-option-value" : "none"; + continue; + } + if (isRunnerSeparateValueFlag(previous)) { + protectToken(token); + continue; + } + if (isRunnerSeparateValueFlag(token.value)) { + runnerState = "await-flag-value"; + continue; + } + if (isRunnerValueFlag(token.value)) { + if (token.value.includes("=")) { + protectToken(token); + } + runnerState = token.value.includes("=") ? "await-package" : "await-flag-value"; + continue; + } + if (isRunnerFlag(token.value)) continue; + const replacement = runnerPackageReplacement(token.value); + if (replacement) { + edits.push([token.start, token.end, replacement]); + } else if (isPathLikeRunnerFlagValue(token.value)) { + protectToken(token); + runnerState = "none"; + continue; + } + runnerState = "none"; + continue; + } + + runnerState = runnerStateAfterToken(token.value); + continue; + } + + if (isSyntaxOnlyNode(child)) { + visit(child); + continue; + } + + if ( + (runnerState === "await-package" || runnerState === "await-dlx") && + runnerPackageExpressionChildren(child).length > 0 + ) { + const changed = collectPackageExpressionEdits(child); + runnerState = changed + ? "none" + : (dynamicRunnerOptionStateFor(runnerState, child) ?? "none"); + continue; + } + + if ( + (runnerState === "await-package" || runnerState === "await-dlx") && + child.kind() === "spread_element" + ) { + runnerState = dynamicRunnerOptionStateFor(runnerState, child) ?? runnerState; + previousTokenValue = undefined; + continue; + } + + if (runnerState === "await-flag-value") { + protectTailorSdkStrings(child); + runnerState = "await-package"; + previousTokenValue = undefined; + continue; + } + + if (runnerState === "await-package-option-value") { + collectPackageExpressionEdits(child); + runnerState = "none"; + previousTokenValue = undefined; + continue; + } + + if (child.kind() === "template_string") { + runnerState = visitTemplate(child, runnerState); + previousTokenValue = undefined; + continue; + } + + if (runnerState === "await-dlx-flag-value") { + protectTailorSdkStrings(child); + runnerState = "await-dlx"; + previousTokenValue = undefined; + continue; + } + + if (runnerState === "await-package" && isProtectedRunnerValuePlaceholder(child)) { + continue; + } + + if (runnerState === "await-package") { + visit(child, isTokenSequenceNode(child) ? runnerState : "none"); + runnerState = "none"; + previousTokenValue = undefined; + continue; + } + + visit( + child, + isTokenSequenceNode(child) || child.kind() === "template_string" ? runnerState : "none", + ); + } + return; + } + + for (const child of node.children()) { + visit(child); + } + }; + + visit(root); + return { edits, protectedRanges }; +} + +function rewriteTokenizedPackageRunners(source: string, filePath: string): TokenizedRunnerRewrite { + let root: SgNode; + try { + root = parse(sourceLang(filePath), source).root(); + } catch { + return { source, protectedValues: [] }; + } + + let updated = source; + const { edits, protectedRanges } = collectTokenizedRunnerEdits(root, source); + const protectedValues: string[] = []; + const protectionEdits = protectedRanges.map(([start, end, value], index) => { + protectedValues.push(value); + return [start, end, `__TAILOR_SDK_CODEMOD_PROTECTED_${index}__`] as [number, number, string]; + }); + const editByRange = new Map(); + for (const edit of [...edits, ...protectionEdits]) { + editByRange.set(`${edit[0]}:${edit[1]}`, edit); + } + const allEdits = Array.from(editByRange.values()).toSorted(([a], [b]) => b - a); + for (const [start, end, replacement] of allEdits) { + updated = `${updated.slice(0, start)}${replacement}${updated.slice(end)}`; + } + return { source: updated, protectedValues }; +} + +function protectRunnerValueStrings(source: string): TokenizedRunnerRewrite { + const protectedValues: string[] = []; + const protectValueToken = (match: string, valueQuote: string, value: string): string => { + const placeholder = `__TAILOR_SDK_CODEMOD_VALUE_PROTECTED_${protectedValues.length}__`; + protectedValues.push(value); + return match.replace( + `${valueQuote}${value}${valueQuote}`, + `${valueQuote}${placeholder}${valueQuote}`, + ); + }; + const inlineProtected = source.replace( + /(["'`])((?:--cache|--userconfig|--registry|--prefix|--dir|--filter|--cwd|-C)=)([^"'`]*tailor-sdk[^"'`]*)\1/g, + (match, quote: string, prefix: string, value: string) => { + const placeholder = `__TAILOR_SDK_CODEMOD_VALUE_PROTECTED_${protectedValues.length}__`; + protectedValues.push(value); + return `${quote}${prefix}${placeholder}${quote}`; + }, + ); + const updated = inlineProtected.replace( + /(["'])(?:--cache|--userconfig|--registry|--prefix|--dir|--filter|--cwd|-C)\1\s*,\s*(["'`])([^"'`]*tailor-sdk[^"'`]*)\2/g, + (match, _flagQuote: string, valueQuote: string, value: string) => { + return protectValueToken(match, valueQuote, value); + }, + ); + return { source: updated, protectedValues }; +} + +function protectStandaloneTailorSdkSourceStrings( + source: string, + filePath: string, +): TokenizedRunnerRewrite { + let root: SgNode; + try { + root = parse(sourceLang(filePath), source).root(); + } catch { + return { source, protectedValues: [] }; + } + + const ranges: Array<[number, number, string]> = []; + const isSingleElementArrayToken = (node: SgNode): boolean => { + const parent = node.parent(); + if (parent?.kind() !== "array") return false; + return parent.children().filter((child: SgNode) => !isSyntaxOnlyNode(child)).length === 1; + }; + const arrayElementNodes = (node: SgNode): SgNode[] => { + return node.children().filter((child: SgNode) => !isSyntaxOnlyNode(child)); + }; + const arrayElementTokens = (node: SgNode): SourceStringToken[] => { + return arrayElementNodes(node) + .map((child: SgNode) => sourceStringToken(child, source)) + .filter((token): token is SourceStringToken => token != null); + }; + const hasTailorCommandTokenPair = (tokens: SourceStringToken[]): boolean => { + return tokens.some((token, index) => { + const next = tokens[index + 1]; + return ( + TAILOR_SDK_TOKEN.test(token.value) && next != null && isTailorCliCommandToken(next.value) + ); + }); + }; + const hasTailorPackageRunnerToken = (tokens: SourceStringToken[]): boolean => { + let runnerState: RunnerState = "none"; + for (const token of tokens) { + if (runnerState === "await-flag-value") { + runnerState = "await-package"; + continue; + } + if (runnerState === "await-dlx-flag-value") { + runnerState = "await-dlx"; + continue; + } + if (runnerState === "await-package-option-value") { + if (runnerPackageReplacement(token.value)) return true; + runnerState = "none"; + continue; + } + if (runnerState === "await-dlx") { + if (token.value === "dlx") { + runnerState = "await-package"; + continue; + } + if (isRunnerSeparateValueFlag(token.value)) { + runnerState = "await-dlx-flag-value"; + continue; + } + if (isRunnerValueFlag(token.value) || isRunnerFlag(token.value)) continue; + runnerState = runnerStateAfterToken(token.value); + continue; + } + if (runnerState === "await-package") { + if (isRunnerSeparatePackageFlag(token.value)) { + runnerState = "await-package-option-value"; + continue; + } + if (isRunnerPackageFlag(token.value)) { + if (rewriteRunnerPackageOptionValue(token).length > 0) return true; + runnerState = token.value.endsWith("=") ? "await-package-option-value" : "none"; + continue; + } + if (isRunnerSeparateValueFlag(token.value)) { + runnerState = "await-flag-value"; + continue; + } + if (isRunnerValueFlag(token.value) || isRunnerFlag(token.value)) continue; + if (runnerPackageReplacement(token.value)) return true; + runnerState = "none"; + continue; + } + runnerState = runnerStateAfterToken(token.value); + } + return false; + }; + const isTailorExecRunnerToken = (tokens: SourceStringToken[], tokenIndex: number): boolean => { + const execIndex = tokens.findIndex((token) => token.value === "exec"); + if (execIndex === -1 || tokenIndex <= execIndex) return false; + let expectFlagValue = false; + for (let index = execIndex + 1; index < tokens.length; index += 1) { + const token = tokens[index]!; + if (expectFlagValue) { + expectFlagValue = false; + continue; + } + if (isRunnerSeparateValueFlag(token.value)) { + expectFlagValue = true; + continue; + } + if (isRunnerValueFlag(token.value) || isRunnerFlag(token.value)) continue; + return index === tokenIndex && TAILOR_SDK_TOKEN.test(token.value); + } + return false; + }; + const hasTailorExecRunnerToken = (tokens: SourceStringToken[]): boolean => { + return tokens.some((_, index) => isTailorExecRunnerToken(tokens, index)); + }; + const hasDynamicTailorArgvRemainder = (node: SgNode, tokens: SourceStringToken[]): boolean => { + const [first] = tokens; + if (!first || !TAILOR_SDK_TOKEN.test(first.value)) return false; + if (node.parent()?.kind() === "array") return false; + const firstElementIndex = arrayElementNodes(node).findIndex((child) => { + const token = sourceStringToken(child, source); + return token?.start === first.start && token.end === first.end; + }); + if (firstElementIndex === -1) return false; + return arrayElementNodes(node) + .slice(firstElementIndex + 1) + .some((child) => sourceStringToken(child, source) == null); + }; + const isRewriteableTailorArgvToken = (node: SgNode, token: SourceStringToken): boolean => { + const parent = node.parent(); + if (parent?.kind() !== "array") return false; + const tokens = arrayElementTokens(parent); + const tokenIndex = tokens.findIndex( + (candidate) => candidate.start === token.start && candidate.end === token.end, + ); + if (tokenIndex === -1 || !TAILOR_SDK_TOKEN.test(token.value)) return false; + if (isTailorCliSeparateValueFlag(tokens[tokenIndex - 1]?.value)) return false; + const next = tokens[tokenIndex + 1]; + if (next != null && isTailorCliCommandToken(next.value)) return true; + if (tokenIndex === 0 && hasDynamicTailorArgvRemainder(parent, tokens)) return true; + return isTailorExecRunnerToken(tokens, tokenIndex); + }; + const isLikelyTailorArgvArray = (node: SgNode): boolean => { + if (node.kind() !== "array") return false; + const tokens = arrayElementTokens(node); + const [first, second] = tokens; + if (!first) return false; + if (hasTailorPackageRunnerToken(tokens) || hasTailorCommandTokenPair(tokens)) return true; + if (hasDynamicTailorArgvRemainder(node, tokens)) return true; + if (first.value === "pnpm" || first.value === "yarn") { + return hasTailorExecRunnerToken(tokens); + } + return ( + TAILOR_SDK_TOKEN.test(first.value) && second != null && isTailorCliCommandToken(second.value) + ); + }; + const isProtectedArrayDataToken = (node: SgNode): boolean => { + const parent = node.parent(); + return parent?.kind() === "array" && !isLikelyTailorArgvArray(parent); + }; + const sourceStringFragmentTokens = (node: SgNode): SourceStringToken[] => { + return node + .children() + .filter((child: SgNode) => child.kind() === "string_fragment") + .map((fragment: SgNode) => { + const range = fragment.range(); + return { + value: source.slice(range.start.index, range.end.index), + start: range.start.index, + end: range.end.index, + }; + }); + }; + const protectNodeText = (node: SgNode): void => { + const range = node.range(); + ranges.push([ + range.start.index, + range.end.index, + source.slice(range.start.index, range.end.index), + ]); + }; + const visit = (node: SgNode): void => { + const kind = node.kind(); + if (kind === "regex" && node.text().includes("tailor-sdk")) { + protectNodeText(node); + return; + } + if (kind === "comment" && node.text().includes("tailor-sdk")) { + const range = node.range(); + ranges.push(...collectNonCommandTailorSdkRanges(node.text(), range.start.index)); + return; + } + if (kind === "jsx_text" && node.text().includes("tailor-sdk")) { + const range = node.range(); + ranges.push( + ...collectNonCommandTailorSdkRanges( + source.slice(range.start.index, range.end.index), + range.start.index, + ), + ); + return; + } + if ( + kind === "template_string" && + node.children().some((child: SgNode) => child.kind() === "template_substitution") + ) { + const fragments = sourceStringFragmentTokens(node); + const staticText = fragments + .map((fragment) => fragment.value) + .join(TEMPLATE_SUBSTITUTION_PLACEHOLDER); + if (fragments.some((fragment) => fragment.value.includes("tailor-sdk"))) { + const rewriteableRanges = [ + ...collectRewriteableTailorSdkRanges(staticText), + ...collectDynamicTemplateTailorCommandRanges(staticText), + ]; + let fragmentOffset = 0; + for (const fragment of fragments) { + ranges.push( + ...collectNonCommandTailorSdkRanges( + fragment.value, + fragment.start, + rewriteableRanges, + fragmentOffset, + ), + ); + fragmentOffset += fragment.value.length + TEMPLATE_SUBSTITUTION_PLACEHOLDER.length; + } + } + } + const token = sourceStringToken(node, source); + if (token) { + const parent = node.parent(); + if ( + token.value.includes("tailor-sdk") && + (parent == null || + !isTokenSequenceNode(parent) || + isSingleElementArrayToken(node) || + isProtectedArrayDataToken(node) || + (parent.kind() === "array" && !isRewriteableTailorArgvToken(node, token))) + ) { + ranges.push(...collectNonCommandTailorSdkRanges(token.value, token.start)); + } + return; + } + for (const child of node.children()) { + visit(child); + } + }; + visit(root); + + let updated = source; + const protectedValues: string[] = []; + for (const [start, end, value] of ranges.toSorted(([a], [b]) => b - a)) { + const placeholder = `__TAILOR_SDK_CODEMOD_STANDALONE_${protectedValues.length}__`; + protectedValues.push(value); + updated = `${updated.slice(0, start)}${placeholder}${updated.slice(end)}`; + } + return { source: updated, protectedValues }; } function transformPackageJson(source: string): string | null { @@ -69,6 +1342,30 @@ export default function transform(source: string, filePath: string): string | nu const ext = path.extname(filePath).toLowerCase(); if (ext === ".json") return transformPackageJson(source); - const updated = renameBinary(source); + const valueProtected = SOURCE_EXTENSIONS.has(ext) + ? protectRunnerValueStrings(source) + : { source, protectedValues: [] }; + const tokenizedRunnerRewrite = SOURCE_EXTENSIONS.has(ext) + ? rewriteTokenizedPackageRunners(valueProtected.source, filePath) + : { source: valueProtected.source, protectedValues: [] }; + const standaloneProtected = SOURCE_EXTENSIONS.has(ext) + ? protectStandaloneTailorSdkSourceStrings(tokenizedRunnerRewrite.source, filePath) + : { source: tokenizedRunnerRewrite.source, protectedValues: [] }; + let updated = renameBinary(standaloneProtected.source); + updated = restoreProtectedValues( + updated, + standaloneProtected.protectedValues, + "__TAILOR_SDK_CODEMOD_STANDALONE_", + ); + updated = restoreProtectedValues( + updated, + tokenizedRunnerRewrite.protectedValues, + "__TAILOR_SDK_CODEMOD_PROTECTED_", + ); + updated = restoreProtectedValues( + updated, + valueProtected.protectedValues, + "__TAILOR_SDK_CODEMOD_VALUE_PROTECTED_", + ); return updated === source ? null : updated; } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh index 1752dc093..27f3e64fe 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh @@ -3,6 +3,7 @@ set -euo pipefail pnpm exec tailor deploy tailor login +tailor -p tailor-sdk deploy @tailor-platform/sdk@latest deploy npx @tailor-platform/sdk@2.0.0 workspace list npx -y @tailor-platform/sdk login diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh index e7743b6b7..3fb148e2f 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh @@ -3,6 +3,7 @@ set -euo pipefail pnpm exec tailor-sdk deploy tailor-sdk login +tailor-sdk -p tailor-sdk deploy tailor-sdk@latest deploy npx tailor-sdk@2.0.0 workspace list npx -y tailor-sdk login diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/expected.d.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/expected.d.ts index fe27b2eb3..9bc34fa5f 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/expected.d.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/expected.d.ts @@ -1,6 +1,7 @@ /** * Run `tailor deploy` after editing the app config. * Regenerate types with `tailor generate`. + * Install a pinned CLI with `npx --package @tailor-platform/sdk tailor login`. * Keep the generated output directory `.tailor-sdk/` in place until v2 setup runs. */ export {}; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/input.d.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/input.d.ts index 61fd5bfaf..1835c8a56 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/input.d.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/input.d.ts @@ -1,6 +1,7 @@ /** * Run `tailor-sdk deploy` after editing the app config. * Regenerate types with `tailor-sdk generate`. + * Install a pinned CLI with `npx --package tailor-sdk tailor-sdk login`. * Keep the generated output directory `.tailor-sdk/` in place until v2 setup runs. */ export {}; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index d4657966e..3e6afea12 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -1,3 +1,17 @@ +const packageName = "tailor-sdk"; +const packageList = ["tailor-sdk"]; +const resolvedPackage = require.resolve("tailor-sdk/package.json"); +const resolvedTemplatePackage = require.resolve(`tailor-sdk/${subpath}`); +const packageTemplate = `tailor-sdk/${version}`; +const packageRegex = /tailor-sdk/; +// package tailor-sdk +// package tailor-sdk is installed +const packageMessage = "package tailor-sdk is installed"; +// Install tailor-sdk before running tailor deploy +const mixedPackageAndCommand = "Install tailor-sdk before running tailor deploy"; +const dynamicImport = import("tailor-sdk"); +const installedPackage = installPackage("tailor-sdk"); +const forkedModule = child_process.fork("tailor-sdk/register", ["crash-report"]); const migrate = "bunx @tailor-platform/sdk@2.0.0-next.2 generate"; const script = ["tailor", "deploy"].join(" "); const skills = "tailor-sdk-skills"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index c215d6e06..3c9d0f2f2 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -1,3 +1,17 @@ +const packageName = "tailor-sdk"; +const packageList = ["tailor-sdk"]; +const resolvedPackage = require.resolve("tailor-sdk/package.json"); +const resolvedTemplatePackage = require.resolve(`tailor-sdk/${subpath}`); +const packageTemplate = `tailor-sdk/${version}`; +const packageRegex = /tailor-sdk/; +// package tailor-sdk +// package tailor-sdk is installed +const packageMessage = "package tailor-sdk is installed"; +// Install tailor-sdk before running tailor-sdk deploy +const mixedPackageAndCommand = "Install tailor-sdk before running tailor-sdk deploy"; +const dynamicImport = import("tailor-sdk"); +const installedPackage = installPackage("tailor-sdk"); +const forkedModule = child_process.fork("tailor-sdk/register", ["crash-report"]); const migrate = "bunx tailor-sdk@2.0.0-next.2 generate"; const script = ["tailor-sdk", "deploy"].join(" "); const skills = "tailor-sdk-skills"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 6c8b38639..f3ec360a1 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -1,7 +1,32 @@ const siteName = "portal"; const setup = `pnpm tailor staticwebsite deploy --name ${siteName}`; const deploy = "tailor deploy"; +const dynamicCommand = `tailor ${subcommand}`; +const dynamicCommandAfterSeparator = `cd app && tailor ${subcommand}`; +const dynamicCommandWithFlags = `tailor ${flags} deploy`; +const dynamicCommandWithConditionalFlags = `tailor ${useJson ? "--json" : ""} deploy`; +const profileValue = "tailor --profile tailor-sdk deploy"; +const shortProfileValue = "tailor -p tailor-sdk deploy"; const latest = "npx @tailor-platform/sdk@latest login"; +const cacheRunner = "npx --cache .npm @tailor-platform/sdk login"; +const registryRunner = "npx --registry=https://npm.example/tailor-sdk @tailor-platform/sdk login"; +const pnpmOptionRunner = "pnpm --dir . dlx @tailor-platform/sdk login"; +const inlineBooleanFlagRunner = "npx --yes=true @tailor-platform/sdk login"; +const packageOptionRunner = "npx --package=@tailor-platform/sdk tailor login"; +const separatePackageOptionRunner = "npx --package @tailor-platform/sdk tailor login"; +const separatePackageOptionTemplate = `npx --package @tailor-platform/sdk tailor login`; +const quotedPackageOptionRunner = 'npx --package="@tailor-platform/sdk" tailor login'; +const dynamicPackageOptionRunner = `npx --package=${useLocal ? "@tailor-platform/sdk" : pkg} tailor login`; +const dynamicSeparatePackageOptionRunner = `npx ${useLocal ? "--package" : ""} ${useLocal ? "@tailor-platform/sdk" : pkg} tailor login`; +const dynamicFlagStaticTemplate = `npx ${yes ? "-y" : ""} @tailor-platform/sdk login`; +const dynamicFlagConditionTemplate = `npx ${mode === "prod" ? "-y" : "--yes"} @tailor-platform/sdk login`; +const dynamicDlxValueFlagTemplate = `pnpm ${use ? "--filter" : ""} app dlx @tailor-platform/sdk login`; +const dynamicDlxInlineValueFlagTemplate = `pnpm ${use ? "--filter=tailor-sdk" : ""} dlx @tailor-platform/sdk login`; +const packageConditionTemplate = `npx ${pkg === "tailor-sdk" ? pkg : "@tailor-platform/sdk"} login`; +const quotedCacheTemplate = `npx --cache 'tailor-sdk/${cache}' @tailor-platform/sdk login`; +const quotedRegistryTemplate = `npx --registry "https://npm.example/tailor-sdk/${scope}" @tailor-platform/sdk login`; +const cacheValue = "npx --cache tailor-sdk some-package"; const outputDir = ".tailor-sdk/"; const scaffold = "create-tailor-sdk"; const skills = "tailor-sdk-skills install"; +const proseTemplate = `package tailor-sdk ${version}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 5b39513b6..b8f82086f 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -1,7 +1,32 @@ const siteName = "portal"; const setup = `pnpm tailor-sdk staticwebsite deploy --name ${siteName}`; const deploy = "tailor-sdk deploy"; +const dynamicCommand = `tailor-sdk ${subcommand}`; +const dynamicCommandAfterSeparator = `cd app && tailor-sdk ${subcommand}`; +const dynamicCommandWithFlags = `tailor-sdk ${flags} deploy`; +const dynamicCommandWithConditionalFlags = `tailor-sdk ${useJson ? "--json" : ""} deploy`; +const profileValue = "tailor-sdk --profile tailor-sdk deploy"; +const shortProfileValue = "tailor-sdk -p tailor-sdk deploy"; const latest = "npx tailor-sdk@latest login"; +const cacheRunner = "npx --cache .npm tailor-sdk login"; +const registryRunner = "npx --registry=https://npm.example/tailor-sdk tailor-sdk login"; +const pnpmOptionRunner = "pnpm --dir . dlx tailor-sdk login"; +const inlineBooleanFlagRunner = "npx --yes=true tailor-sdk login"; +const packageOptionRunner = "npx --package=tailor-sdk tailor-sdk login"; +const separatePackageOptionRunner = "npx --package tailor-sdk tailor-sdk login"; +const separatePackageOptionTemplate = `npx --package tailor-sdk tailor-sdk login`; +const quotedPackageOptionRunner = 'npx --package="tailor-sdk" tailor-sdk login'; +const dynamicPackageOptionRunner = `npx --package=${useLocal ? "tailor-sdk" : pkg} tailor-sdk login`; +const dynamicSeparatePackageOptionRunner = `npx ${useLocal ? "--package" : ""} ${useLocal ? "tailor-sdk" : pkg} tailor-sdk login`; +const dynamicFlagStaticTemplate = `npx ${yes ? "-y" : ""} tailor-sdk login`; +const dynamicFlagConditionTemplate = `npx ${mode === "prod" ? "-y" : "--yes"} tailor-sdk login`; +const dynamicDlxValueFlagTemplate = `pnpm ${use ? "--filter" : ""} app dlx tailor-sdk login`; +const dynamicDlxInlineValueFlagTemplate = `pnpm ${use ? "--filter=tailor-sdk" : ""} dlx tailor-sdk login`; +const packageConditionTemplate = `npx ${pkg === "tailor-sdk" ? pkg : "tailor-sdk"} login`; +const quotedCacheTemplate = `npx --cache 'tailor-sdk/${cache}' tailor-sdk login`; +const quotedRegistryTemplate = `npx --registry "https://npm.example/tailor-sdk/${scope}" tailor-sdk login`; +const cacheValue = "npx --cache tailor-sdk some-package"; const outputDir = ".tailor-sdk/"; const scaffold = "create-tailor-sdk"; const skills = "tailor-sdk-skills install"; +const proseTemplate = `package tailor-sdk ${version}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts new file mode 100644 index 000000000..859ae3052 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts @@ -0,0 +1,54 @@ +const packageName = "tailor-sdk"; +const runnerArgs = ["npx", "@tailor-platform/sdk@latest", "login"]; +const dynamicPackageVariableRunner = ["npx", pkg, "tailor", "login"]; +const dynamicDlxPackageVariableRunner = ["pnpm", "dlx", pkg, "tailor", "login"]; +const dynamicFlagRunner = ["npx", yes && "-y", "@tailor-platform/sdk", "login"]; +const dynamicAmbiguousValueFlagRunner = ["npx", cond ? "--cache" : "--yes", "tailor-sdk", "login"]; +const localPackageRunner = ["npx", "./tailor-sdk", "tailor", "login"]; +const dynamicInlineValueFlagRunner = ["npx", useRegistry ? "--registry=https://npm.example" : "-y", "@tailor-platform/sdk", "login"]; +const dynamicFlagConditionRunner = ["npx", mode === "prod" ? "-y" : "--yes", "@tailor-platform/sdk", "login"]; +const packageConditionRunner = ["npx", pkg === "tailor-sdk" ? pkg : "@tailor-platform/sdk", "login"]; +const inlineBooleanFlagRunner = ["npx", "--yes=true", "@tailor-platform/sdk", "login"]; +const nestedRunner = spawn("npx", ["-y", "@tailor-platform/sdk", "login"]); +const pnpmRunner = spawn("pnpm", ["dlx", "@tailor-platform/sdk@2.0.0", "login"]); +const pnpmExecRunner = spawn("pnpm", ["exec", "tailor", "login"]); +const yarnExecRunner = ["yarn", "exec", "tailor", "login"]; +const pnpmExecOtherCommand = ["pnpm", "exec", "other-cli", "tailor-sdk"]; +const valuedFlagRunner = ["npx", "--cache", ".npm", "@tailor-platform/sdk", "login"]; +const namedValueFlagRunner = ["npx", "--cache", "npm-cache", "@tailor-platform/sdk", "login"]; +const registryValueFlagRunner = ["npx", "--registry", "https://npm.example", "@tailor-platform/sdk", "login"]; +const prefixValueFlagRunner = ["npx", "--prefix", "npm-cache", "@tailor-platform/sdk", "login"]; +const protectedValueFlagRunner = ["npx", "--cache", "tailor-sdk", "some-package"]; +const expressionValueFlagRunner = ["npx", "--cache", cacheDir, "@tailor-platform/sdk", "login"]; +const protectedExpressionValueFlagRunner = ["npx", "--cache", cacheDir || "tailor-sdk", "@tailor-platform/sdk", "login"]; +const inlineValueFlagRunner = ["npx", "--registry=https://npm.example/tailor-sdk", "@tailor-platform/sdk"]; +const inlinePackageFlagRunner = ["npx", "--package=@tailor-platform/sdk", "tailor", "login"]; +const dynamicInlinePackageFlagRunner = ["npx", `--package=${useLocal ? "@tailor-platform/sdk" : pkg}`, "tailor", "login"]; +const dynamicSeparatePackageFlagRunner = ["npx", useLocal ? "--package" : "", useLocal ? "@tailor-platform/sdk" : pkg, "tailor", "login"]; +const templateConditionalRunner = `npx ${useLatest ? "@tailor-platform/sdk@latest" : "@tailor-platform/sdk"} login`; +const templateDynamicInlineValueFlagRunner = `npx ${useRegistry ? "--registry=https://npm.example" : "-y"} @tailor-platform/sdk login`; +const pnpmOptionRunner = spawn("pnpm", ["--dir", ".", "dlx", "@tailor-platform/sdk", "login"]); +const pnpmFilterRunner = ["pnpm", "--filter", "app", "dlx", "@tailor-platform/sdk", "login"]; +const pnpmRegistryRunner = ["pnpm", "--registry", "https://npm.example", "dlx", "@tailor-platform/sdk", "login"]; +const dynamicPnpmFilterRunner = ["pnpm", use ? "--filter" : "", "app", "dlx", "@tailor-platform/sdk", "login"]; +const dynamicInlinePnpmFilterRunner = ["pnpm", use ? "--filter=tailor-sdk" : "", "dlx", "@tailor-platform/sdk", "login"]; +const dynamicInlineRegistryRunner = ["npx", use ? "--registry=https://npm.example/tailor-sdk" : "-y", "@tailor-platform/sdk", "login"]; +const spreadFlagRunner = ["npx", ...["-y"], "@tailor-platform/sdk", "login"]; +const unknownSpreadFlagRunner = ["npx", ...flags, "@tailor-platform/sdk", "login"]; +const commentedRunner = ["npx", /* install CLI */ "@tailor-platform/sdk", "login"]; +const commentedPackageFlagRunner = ["npx", "--package", /* package */ "@tailor-platform/sdk", "tailor", "login"]; +const commentedPnpmDlxRunner = ["pnpm", /* comment */ "dlx", "@tailor-platform/sdk", "login"]; +const commentedValueFlagRunner = ["npx", "--cache", /* cache dir */ ".npm", "@tailor-platform/sdk", "login"]; +const unrelatedTailorPackageRunner = ["npx", "--cache", ".npm", "tailor", "sprite"]; +const conditionalRunner = ["npx", useLatest ? "@tailor-platform/sdk@latest" : "@tailor-platform/sdk", "login"]; +const binaryArgs = ["tailor", "login"]; +const profileArgValue = ["tailor", "--profile", "tailor-sdk", "deploy"]; +const shortProfileArgValue = ["tailor", "-p", "tailor-sdk", "deploy"]; +const dynamicBinaryArgs = ["tailor", cmd]; +const spreadBinaryArgs = ["tailor", ...args]; +const commandArgPackageName = ["tailor", "login", "tailor-sdk"]; +const packageManagerNames = ["pnpm", "tailor-sdk"]; +const unrelatedNpxValues = ["npx", "create-tailor-sdk", "tailor-sdk-skills", "tailor-sdk"]; +const packageNames = ["tailor-sdk", "react"]; +const packageTuple = [["tailor-sdk", version]]; +const outputDir = ".tailor-sdk"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts new file mode 100644 index 000000000..79179b447 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts @@ -0,0 +1,54 @@ +const packageName = "tailor-sdk"; +const runnerArgs = ["npx", "tailor-sdk@latest", "login"]; +const dynamicPackageVariableRunner = ["npx", pkg, "tailor-sdk", "login"]; +const dynamicDlxPackageVariableRunner = ["pnpm", "dlx", pkg, "tailor-sdk", "login"]; +const dynamicFlagRunner = ["npx", yes && "-y", "tailor-sdk", "login"]; +const dynamicAmbiguousValueFlagRunner = ["npx", cond ? "--cache" : "--yes", "tailor-sdk", "login"]; +const localPackageRunner = ["npx", "./tailor-sdk", "tailor-sdk", "login"]; +const dynamicInlineValueFlagRunner = ["npx", useRegistry ? "--registry=https://npm.example" : "-y", "tailor-sdk", "login"]; +const dynamicFlagConditionRunner = ["npx", mode === "prod" ? "-y" : "--yes", "tailor-sdk", "login"]; +const packageConditionRunner = ["npx", pkg === "tailor-sdk" ? pkg : "tailor-sdk", "login"]; +const inlineBooleanFlagRunner = ["npx", "--yes=true", "tailor-sdk", "login"]; +const nestedRunner = spawn("npx", ["-y", "tailor-sdk", "login"]); +const pnpmRunner = spawn("pnpm", ["dlx", "tailor-sdk@2.0.0", "login"]); +const pnpmExecRunner = spawn("pnpm", ["exec", "tailor-sdk", "login"]); +const yarnExecRunner = ["yarn", "exec", "tailor-sdk", "login"]; +const pnpmExecOtherCommand = ["pnpm", "exec", "other-cli", "tailor-sdk"]; +const valuedFlagRunner = ["npx", "--cache", ".npm", "tailor-sdk", "login"]; +const namedValueFlagRunner = ["npx", "--cache", "npm-cache", "tailor-sdk", "login"]; +const registryValueFlagRunner = ["npx", "--registry", "https://npm.example", "tailor-sdk", "login"]; +const prefixValueFlagRunner = ["npx", "--prefix", "npm-cache", "tailor-sdk", "login"]; +const protectedValueFlagRunner = ["npx", "--cache", "tailor-sdk", "some-package"]; +const expressionValueFlagRunner = ["npx", "--cache", cacheDir, "tailor-sdk", "login"]; +const protectedExpressionValueFlagRunner = ["npx", "--cache", cacheDir || "tailor-sdk", "tailor-sdk", "login"]; +const inlineValueFlagRunner = ["npx", "--registry=https://npm.example/tailor-sdk", "tailor-sdk"]; +const inlinePackageFlagRunner = ["npx", "--package=tailor-sdk", "tailor-sdk", "login"]; +const dynamicInlinePackageFlagRunner = ["npx", `--package=${useLocal ? "tailor-sdk" : pkg}`, "tailor-sdk", "login"]; +const dynamicSeparatePackageFlagRunner = ["npx", useLocal ? "--package" : "", useLocal ? "tailor-sdk" : pkg, "tailor-sdk", "login"]; +const templateConditionalRunner = `npx ${useLatest ? "tailor-sdk@latest" : "tailor-sdk"} login`; +const templateDynamicInlineValueFlagRunner = `npx ${useRegistry ? "--registry=https://npm.example" : "-y"} tailor-sdk login`; +const pnpmOptionRunner = spawn("pnpm", ["--dir", ".", "dlx", "tailor-sdk", "login"]); +const pnpmFilterRunner = ["pnpm", "--filter", "app", "dlx", "tailor-sdk", "login"]; +const pnpmRegistryRunner = ["pnpm", "--registry", "https://npm.example", "dlx", "tailor-sdk", "login"]; +const dynamicPnpmFilterRunner = ["pnpm", use ? "--filter" : "", "app", "dlx", "tailor-sdk", "login"]; +const dynamicInlinePnpmFilterRunner = ["pnpm", use ? "--filter=tailor-sdk" : "", "dlx", "tailor-sdk", "login"]; +const dynamicInlineRegistryRunner = ["npx", use ? "--registry=https://npm.example/tailor-sdk" : "-y", "tailor-sdk", "login"]; +const spreadFlagRunner = ["npx", ...["-y"], "tailor-sdk", "login"]; +const unknownSpreadFlagRunner = ["npx", ...flags, "tailor-sdk", "login"]; +const commentedRunner = ["npx", /* install CLI */ "tailor-sdk", "login"]; +const commentedPackageFlagRunner = ["npx", "--package", /* package */ "tailor-sdk", "tailor-sdk", "login"]; +const commentedPnpmDlxRunner = ["pnpm", /* comment */ "dlx", "tailor-sdk", "login"]; +const commentedValueFlagRunner = ["npx", "--cache", /* cache dir */ ".npm", "tailor-sdk", "login"]; +const unrelatedTailorPackageRunner = ["npx", "--cache", ".npm", "tailor", "sprite"]; +const conditionalRunner = ["npx", useLatest ? "tailor-sdk@latest" : "tailor-sdk", "login"]; +const binaryArgs = ["tailor-sdk", "login"]; +const profileArgValue = ["tailor-sdk", "--profile", "tailor-sdk", "deploy"]; +const shortProfileArgValue = ["tailor-sdk", "-p", "tailor-sdk", "deploy"]; +const dynamicBinaryArgs = ["tailor-sdk", cmd]; +const spreadBinaryArgs = ["tailor-sdk", ...args]; +const commandArgPackageName = ["tailor-sdk", "login", "tailor-sdk"]; +const packageManagerNames = ["pnpm", "tailor-sdk"]; +const unrelatedNpxValues = ["npx", "create-tailor-sdk", "tailor-sdk-skills", "tailor-sdk"]; +const packageNames = ["tailor-sdk", "react"]; +const packageTuple = [["tailor-sdk", version]]; +const outputDir = ".tailor-sdk"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/expected.tsx b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/expected.tsx new file mode 100644 index 000000000..edf3495b7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/expected.tsx @@ -0,0 +1,7 @@ +const docs = ( + <> +

package tailor-sdk is installed

+ tailor deploy + npx --package @tailor-platform/sdk tailor login + +); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/input.tsx b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/input.tsx new file mode 100644 index 000000000..da53e41a1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/input.tsx @@ -0,0 +1,7 @@ +const docs = ( + <> +

package tailor-sdk is installed

+ tailor-sdk deploy + npx --package tailor-sdk tailor-sdk login + +); diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 7c13ec50c..668f00205 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -71,6 +71,13 @@ describe("getApplicableCodemods", () => { expect(matches("packages/app/frontend/e2e/global-setup.ts")).toBe(true); expect(matches("tailor.d.ts")).toBe(true); } + expect(cliRename?.sourceStringLegacyPatterns).toEqual( + expect.arrayContaining([ + ["tailor-sdk", "crash-report"], + ["tailor", "crash-report"], + "--machineuser", + ]), + ); }); test("flags CommonJS TypeScript files for runtime globals review", () => { diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 972e652d9..2ef7544c5 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -215,6 +215,11 @@ export const allCodemods: CodemodPackage[] = [ "**/*.md", ], legacyPatterns: ["tailor-sdk crash-report", "--machineuser"], + sourceStringLegacyPatterns: [ + ["tailor-sdk", "crash-report"], + ["tailor", "crash-report"], + "--machineuser", + ], examples: [ { lang: "sh", diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 080ae1173..57a8fcb7f 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -315,6 +315,7 @@ describe("runCodemods", () => { describe("legacy pattern warnings", () => { const partialTransformPath = path.join(os.tmpdir(), "transform-partial.ts"); + const renameBinaryTransformPath = path.join(os.tmpdir(), "transform-rename-binary.ts"); beforeEach(async () => { await fs.promises.writeFile( @@ -324,10 +325,18 @@ describe("runCodemods", () => { }`, "utf-8", ); + await fs.promises.writeFile( + renameBinaryTransformPath, + `export default function transform(source) { + return source.replaceAll("tailor-sdk", "tailor"); + }`, + "utf-8", + ); }); afterEach(async () => { await fs.promises.rm(partialTransformPath, { force: true }); + await fs.promises.rm(renameBinaryTransformPath, { force: true }); }); test("warns when legacy patterns remain after a partial migration", async () => { @@ -493,6 +502,36 @@ describe("runCodemods", () => { ]); }); + test("checks source string warnings after chained transforms", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "cli.ts"), + 'const command = ["tailor-sdk", mode, "crash-report"];', + "utf-8", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", renameBinaryTransformPath, ["**/*.ts"]), + scriptPath: renameBinaryTransformPath, + }, + { + codemod: makeCodemod("test/cli-rename", undefined, ["**/*.ts"], [], { + sourceStringLegacyPatterns: [["tailor", "crash-report"]], + }), + }, + ], + dir, + false, + ); + + expect(result.warnings).toEqual([ + "cli.ts: contains tailor + crash-report but was not migrated automatically (rule: test/cli-rename). Manual migration may be needed.", + ]); + }); + test("flags files matching a suspicious pattern for LLM review", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-test-")); tmpDir = dir; From f6dbd626def9b4e4c7b2c2c3fb097addd6cf9fef Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 13:29:08 +0900 Subject: [PATCH 269/618] fix(sdk-codemod): preserve source cli payloads --- .../v2/cli-rename/scripts/transform.ts | 62 ++- .../tests/source-template/expected.ts | 2 + .../cli-rename/tests/source-template/input.ts | 2 + .../tests/source-token-array/expected.ts | 10 + .../tests/source-token-array/input.ts | 10 + .../v2/rename-bin/scripts/transform.ts | 354 ++++++++++++++++-- .../tests/source-template/expected.ts | 6 + .../rename-bin/tests/source-template/input.ts | 6 + .../tests/source-token-runner/expected.ts | 9 + .../tests/source-token-runner/input.ts | 9 + 10 files changed, 433 insertions(+), 37 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index d14e15b10..6a15b75e0 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -887,6 +887,20 @@ function isCommandSeparatorOnlyToken(value: string): boolean { return value !== "" && /^[;&|]+$/.test(value) && hasCommandSeparatorToken(value); } +function isShellAssignmentToken(value: string): boolean { + return /^[A-Za-z_]\w*=/.test(value); +} + +function shellCommandPrefixEndIndex(values: string[]): number { + let index = values[0] === "env" ? 1 : 0; + const assignmentStart = index; + while (index < values.length && isShellAssignmentToken(values[index]!)) { + index += 1; + } + if (index > assignmentStart) return index; + return values[0] === "env" ? 1 : 0; +} + function rewriteTokenizedCliArg(value: string, renameCommand: boolean): string { const commandRenamed = renameCommand ? (COMMAND_MAP.get(value) ?? value) : value; return replaceOptionsInToken(commandRenamed); @@ -984,6 +998,9 @@ function collectInterpolatedOptionEdits( function cliArgExpressionChildren(node: SgNode): SgNode[] { if (node.kind() === "parenthesized_expression") return node.children(); + if (node.kind() === "as_expression" || node.kind() === "satisfies_expression") { + return node.children().slice(0, 1); + } if (node.kind() === "ternary_expression") return node.children().slice(1); if (node.kind() === "binary_expression" && /&&|\|\|/.test(node.text())) { return node.children().slice(1); @@ -1273,16 +1290,28 @@ function isLikelyTemplateCommandToken( .slice(0, tokenIndex) .map((token) => staticTemplateTokenValue(token)) .filter((value): value is string => value !== undefined); - const [first] = staticPrefixValues; + const shellPrefixEndIndex = shellCommandPrefixEndIndex(staticPrefixValues); + if (shellPrefixEndIndex > 0 && shellPrefixEndIndex === staticPrefixValues.length) return true; + const [first] = staticPrefixValues.slice(shellPrefixEndIndex); return first === "npx" || first === "bunx" || first === "pnpm" || first === "yarn"; } function collectCliTemplateSourceEdits( root: SgNode, source: string, + protectedRanges: TextRange[] = [], ): Array<[number, number, string]> { const edits: Array<[number, number, string]> = []; + const isProtected = (node: SgNode): boolean => { + const range = node.range(); + return protectedRanges.some( + (protectedRange) => + protectedRange.start <= range.start.index && range.end.index <= protectedRange.end, + ); + }; + const visit = (node: SgNode): void => { + if (isProtected(node)) return; if (node.kind() === "template_string") { edits.push(...collectCliTemplateEdits(node, source)); } @@ -1297,10 +1326,21 @@ function collectCliTemplateSourceEdits( function collectSourceLiteralCliCommandEdits( root: SgNode, source: string, + protectedRanges: TextRange[] = [], ): Array<[number, number, string]> { const edits: Array<[number, number, string]> = []; + const isProtected = (node: SgNode): boolean => { + const range = node.range(); + return protectedRanges.some( + (protectedRange) => + protectedRange.start <= range.start.index && range.end.index <= protectedRange.end, + ); + }; + const visit = (node: SgNode): void => { + if (isProtected(node)) return; + if (node.kind() === "comment") { const range = node.range(); const text = source.slice(range.start.index, range.end.index); @@ -1344,8 +1384,14 @@ function collectSourceLiteralCliCommandEdits( function collectTokenizedSourceEdits( root: SgNode, source: string, -): Array<[number, number, string]> { +): { edits: Array<[number, number, string]>; protectedRanges: TextRange[] } { const edits: Array<[number, number, string]> = []; + const protectedRanges: TextRange[] = []; + + const protectNode = (node: SgNode): void => { + const range = node.range(); + protectedRanges.push({ start: range.start.index, end: range.end.index }); + }; const visit = (node: SgNode, inheritedAfterTailorBinary = false): void => { if (inheritedAfterTailorBinary) { @@ -1394,6 +1440,7 @@ function collectTokenizedSourceEdits( (previous != null && (commandMayBeNext ? isOpenGlobalValueArg(previous) : isOpenCliValueArg(previous))) ) { + protectNode(child); skipNextValue = false; continue; } @@ -1434,7 +1481,7 @@ function collectTokenizedSourceEdits( } if (skipNextValue) { - visit(child); + protectNode(child); skipNextValue = false; previousTokenValue = undefined; continue; @@ -1490,7 +1537,7 @@ function collectTokenizedSourceEdits( }; visit(root); - return edits; + return { edits, protectedRanges }; } function replaceTokenizedSourceCliCommands(source: string, filePath: string): string { @@ -1502,11 +1549,12 @@ function replaceTokenizedSourceCliCommands(source: string, filePath: string): st } let updated = source; + const tokenized = collectTokenizedSourceEdits(root, source); const editByRange = new Map(); for (const edit of [ - ...collectTokenizedSourceEdits(root, source), - ...collectCliTemplateSourceEdits(root, source), - ...collectSourceLiteralCliCommandEdits(root, source), + ...tokenized.edits, + ...collectCliTemplateSourceEdits(root, source, tokenized.protectedRanges), + ...collectSourceLiteralCliCommandEdits(root, source, tokenized.protectedRanges), ]) { editByRange.set(`${edit[0]}:${edit[1]}`, edit); } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts index e4cdd3550..c23defcdf 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts @@ -28,6 +28,8 @@ const workspaceCommand = "tailor-sdk -w workspace-1 crashreport"; const chainedShellCommand = "cd app && tailor-sdk crashreport --machine-user"; const envShellCommand = "env FOO=bar tailor-sdk crashreport --machine-user"; const assignmentShellCommand = "TAILOR=1 tailor-sdk crashreport --machine-user"; +const envDynamicCommandTemplate = `env FOO=bar tailor-sdk ${showReports ? "crashreport" : "login"} --machine-user`; +const assignmentDynamicCommandTemplate = `TAILOR=1 tailor-sdk ${showReports ? "crashreport" : "login"} --machine-user`; const unknownInlineValue = "tailor-sdk login --name=--machineuser"; const argTemplatePayload = `tailor-sdk function test-run --arg=${payload ? "--machineuser" : ""}`; const dynamicArgTemplatePayload = `tailor-sdk function test-run ${includeArg ? "--arg" : ""} ${payload ? "--machineuser" : ""} --machine-user`; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts index f8bed9cfe..915668c9a 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts @@ -28,6 +28,8 @@ const workspaceCommand = "tailor-sdk -w workspace-1 crash-report"; const chainedShellCommand = "cd app && tailor-sdk crash-report --machineuser"; const envShellCommand = "env FOO=bar tailor-sdk crash-report --machineuser"; const assignmentShellCommand = "TAILOR=1 tailor-sdk crash-report --machineuser"; +const envDynamicCommandTemplate = `env FOO=bar tailor-sdk ${showReports ? "crash-report" : "login"} --machineuser`; +const assignmentDynamicCommandTemplate = `TAILOR=1 tailor-sdk ${showReports ? "crash-report" : "login"} --machineuser`; const unknownInlineValue = "tailor-sdk login --name=--machineuser"; const argTemplatePayload = `tailor-sdk function test-run --arg=${payload ? "--machineuser" : ""}`; const dynamicArgTemplatePayload = `tailor-sdk function test-run ${includeArg ? "--arg" : ""} ${payload ? "--machineuser" : ""} --machineuser`; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts index c7eedcb1e..7b8a91ed7 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts @@ -16,6 +16,16 @@ const workspaceGlobalOption = ["tailor-sdk", "-w", "workspace-1", "crashreport"] const inlineEnvTemplate = ["tailor-sdk", `--env-file=${envFile}`, "crashreport", "list"]; const openEnvFileValue = ["tailor-sdk", "--env-file=", ".env", "crashreport", "--machine-user"]; const argValue = ["tailor-sdk", "function", "test-run", "--arg", "crash-report"]; +const argCommandPayload = ["tailor-sdk", "function", "test-run", "--arg", "tailor-sdk crash-report --machineuser"]; +const argCommandPayloadWithComment = [ + "tailor-sdk", + "function", + "test-run", + "--arg", + /* payload */ "tailor-sdk crash-report --machineuser", +]; +const argCommandPayloadExpression = ["tailor-sdk", "function", "test-run", "--arg", cond ? "tailor-sdk crash-report --machineuser" : "{}"]; +const argCommandPayloadTemplate = ["tailor-sdk", "function", "test-run", "--arg", `tailor-sdk crash-report --machineuser`]; const openArgValue = ["tailor-sdk", "function", "test-run", "--arg=", "--machineuser", "--machine-user"]; const shortArgValue = ["tailor-sdk", "function", "test-run", "-a", "--machineuser", "--machine-user"]; const queryValue = ["tailor-sdk", "query", "--query", "select --machineuser", "--machine-user", "ci"]; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts index e381ad7b9..6ac23ba8b 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts @@ -16,6 +16,16 @@ const workspaceGlobalOption = ["tailor-sdk", "-w", "workspace-1", "crash-report" const inlineEnvTemplate = ["tailor-sdk", `--env-file=${envFile}`, "crash-report", "list"]; const openEnvFileValue = ["tailor-sdk", "--env-file=", ".env", "crash-report", "--machineuser"]; const argValue = ["tailor-sdk", "function", "test-run", "--arg", "crash-report"]; +const argCommandPayload = ["tailor-sdk", "function", "test-run", "--arg", "tailor-sdk crash-report --machineuser"]; +const argCommandPayloadWithComment = [ + "tailor-sdk", + "function", + "test-run", + "--arg", + /* payload */ "tailor-sdk crash-report --machineuser", +]; +const argCommandPayloadExpression = ["tailor-sdk", "function", "test-run", "--arg", cond ? "tailor-sdk crash-report --machineuser" : "{}"]; +const argCommandPayloadTemplate = ["tailor-sdk", "function", "test-run", "--arg", `tailor-sdk crash-report --machineuser`]; const openArgValue = ["tailor-sdk", "function", "test-run", "--arg=", "--machineuser", "--machineuser"]; const shortArgValue = ["tailor-sdk", "function", "test-run", "-a", "--machineuser", "--machineuser"]; const queryValue = ["tailor-sdk", "query", "--query", "select --machineuser", "--machineuser", "ci"]; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 95bcb5668..4f5a55f3b 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -91,12 +91,21 @@ const TAILOR_CLI_SEPARATE_VALUE_FLAGS = new Set([ "-q", "-f", ]); +const SOURCE_STRING_WRAPPER_NODE_KINDS = new Set([ + "as_expression", + "non_null_expression", + "parenthesized_expression", + "satisfies_expression", + "type_assertion", +]); const CLI_ARGUMENT_CALLEE_RE = /(?:^|\.)(?:spawn|spawnSync|execFile|execFileSync|execa|execaSync)$/; type RunnerState = | "none" | "await-dlx" | "await-dlx-flag-value" + | "await-executable" + | "await-executable-flag-value" | "await-package" | "await-package-option-value" | "await-flag-value"; @@ -431,8 +440,28 @@ function isTailorCliCommandToken(value: string): boolean { return token.startsWith("-") || TAILOR_CLI_COMMANDS.has(token); } -function isTailorCliSeparateValueFlag(value: string | undefined): boolean { - return value != null && TAILOR_CLI_SEPARATE_VALUE_FLAGS.has(value); +function tailorCliValueFlagName(value: string | undefined): string | undefined { + if (value == null) return undefined; + return Array.from(TAILOR_CLI_SEPARATE_VALUE_FLAGS).find( + (flag) => value === flag || value.startsWith(`${flag}=`), + ); +} + +function isOpenTailorCliValueFlag(value: string | undefined): boolean { + const flag = tailorCliValueFlagName(value); + return flag != null && (value === flag || value === `${flag}=`); +} + +function hasInlineTailorCliValue(value: string | undefined): boolean { + const flag = tailorCliValueFlagName(value); + return ( + flag != null && value != null && value.startsWith(`${flag}=`) && value.length > flag.length + 1 + ); +} + +function isLikelyDynamicTailorArgvRemainder(node: SgNode): boolean { + if (node.kind() === "spread_element") return true; + return /\b(?:argv|args?|cmd|command|subcommand)\b/i.test(node.text()); } function collectPackageRunnerTokenRanges(value: string): TextRange[] { @@ -486,12 +515,34 @@ function collectTailorCommandTokenRanges(value: string): TextRange[] { function collectDynamicTemplateTailorCommandRanges(value: string): TextRange[] { const ranges: TextRange[] = []; - const pattern = /(^|[;&|\n])(\s*)tailor-sdk(@[^\s'"`;|&)]+)?(?=\s|$)/gu; - for (;;) { - const match = pattern.exec(value); - if (!match) break; - const start = match.index + match[1]!.length + match[2]!.length; - ranges.push({ start, end: start + "tailor-sdk".length + (match[3]?.length ?? 0) }); + const commandBoundary = String.raw`(?:^|[;&|\n])\s*`; + const shellPrefix = String.raw`(?:(?:env\s+)?(?:[A-Za-z_]\w*=${SHELL_ARG_VALUE}\s+)*)`; + const tailorToken = "tailor-sdk(?:@[^\\s'\"`;|&)]+)?"; + const patterns = [ + new RegExp(`${commandBoundary}${shellPrefix}${tailorToken}(?=\\s|$)`, "gu"), + new RegExp( + `${commandBoundary}${shellPrefix}(?:pnpm|yarn)(?:\\s+${RUNNER_OPTION})*\\s+exec(?:\\s+${RUNNER_OPTION})*\\s+${tailorToken}(?=\\s|$)`, + "gu", + ), + new RegExp( + `${commandBoundary}${shellPrefix}(?:pnpm|yarn)(?:\\s+${RUNNER_OPTION})*\\s+${tailorToken}(?=\\s|$)`, + "gu", + ), + new RegExp( + `${commandBoundary}${shellPrefix}(?:npx|bunx)(?:\\s+${RUNNER_OPTION})*\\s+(?:--package|-p)(?:=${SHELL_ARG_VALUE}|\\s+${SHELL_ARG_VALUE})(?:\\s+${RUNNER_OPTION})*\\s+${tailorToken}(?=\\s|$)`, + "gu", + ), + ]; + for (const pattern of patterns) { + for (;;) { + const match = pattern.exec(value); + if (!match) break; + const tokenStartInMatch = match[0].lastIndexOf("tailor-sdk"); + if (tokenStartInMatch === -1) continue; + const version = /^tailor-sdk(@[^\s'"`;|&)]+)?/.exec(match[0].slice(tokenStartInMatch))?.[1]; + const start = match.index + tokenStartInMatch; + ranges.push({ start, end: start + "tailor-sdk".length + (version?.length ?? 0) }); + } } return ranges; } @@ -645,6 +696,11 @@ function collectTokenizedRunnerEdits( node: SgNode, ): RunnerState | null => { const dynamicState = dynamicRunnerOptionState(node); + if (runnerState === "await-executable") { + if (dynamicState === "await-flag-value") return "await-executable-flag-value"; + if (dynamicState === "await-package") return "await-executable"; + return dynamicState; + } if (runnerState !== "await-dlx") return dynamicState; if (dynamicState === "await-flag-value") return "await-dlx-flag-value"; if (dynamicState === "await-package") return "await-dlx"; @@ -654,7 +710,23 @@ function collectTokenizedRunnerEdits( const advanceRunnerState = (runnerState: RunnerState, value: string): RunnerState => { if (runnerState === "await-flag-value") return "await-package"; if (runnerState === "await-dlx-flag-value") return "await-dlx"; - if (runnerState === "await-package-option-value") return "none"; + if (runnerState === "await-executable-flag-value") return "await-executable"; + if (runnerState === "await-package-option-value") return "await-executable"; + + if (runnerState === "await-executable") { + if (isRunnerSeparatePackageFlag(value)) return "await-package-option-value"; + if (isRunnerPackageFlag(value)) { + return isRunnerOpenPackageFlag(value) ? "await-package-option-value" : "await-executable"; + } + if (isRunnerSeparateValueFlag(value)) return "await-executable-flag-value"; + if (isRunnerValueFlag(value)) { + return value.includes("=") && !value.endsWith("=") + ? "await-executable" + : "await-executable-flag-value"; + } + if (isRunnerFlag(value)) return "await-executable"; + return "none"; + } if (runnerState === "await-dlx") { if (value === "dlx") return "await-package"; @@ -669,7 +741,7 @@ function collectTokenizedRunnerEdits( if (runnerState === "await-package") { if (isRunnerSeparatePackageFlag(value)) return "await-package-option-value"; if (isRunnerPackageFlag(value)) { - return value.endsWith("=") ? "await-package-option-value" : "none"; + return isRunnerOpenPackageFlag(value) ? "await-package-option-value" : "await-executable"; } if (isRunnerSeparateValueFlag(value)) return "await-flag-value"; if (isRunnerValueFlag(value)) { @@ -706,7 +778,7 @@ function collectTokenizedRunnerEdits( } else { collectTemplatePackageExpressionEdits(token); } - return "none"; + return "await-executable"; } if (nextState === "await-package") { if (value !== undefined) { @@ -714,7 +786,7 @@ function collectTokenizedRunnerEdits( for (const segment of token.segments) { edits.push(...rewriteRunnerPackageOptionValue(segment)); } - return value.endsWith("=") ? "await-package-option-value" : "none"; + return isRunnerOpenPackageFlag(value) ? "await-package-option-value" : "await-executable"; } if (isRunnerFlag(value) || isRunnerValueFlag(value)) { if (isRunnerValueFlag(value) && value.includes("=")) { @@ -731,7 +803,9 @@ function collectTokenizedRunnerEdits( if (isRunnerPackageFlag(token.value)) { collectTemplatePackageExpressionEdits(token); - return token.value.endsWith("=") ? "await-package-option-value" : "none"; + return isRunnerOpenPackageFlag(token.value) + ? "await-package-option-value" + : "await-executable"; } if (isRunnerValueFlag(token.value)) { protectTemplateToken(token); @@ -743,9 +817,51 @@ function collectTokenizedRunnerEdits( return "await-package"; } const changed = collectTemplatePackageExpressionEdits(token); - return changed - ? "none" - : (dynamicRunnerOptionState(token.substitutions[0]!, source) ?? "none"); + return changed ? "none" : (dynamicRunnerOptionState(token.substitutions[0]!) ?? "none"); + } + + if (nextState === "await-executable") { + if (value !== undefined) { + if (isRunnerPackageFlag(value)) { + for (const segment of token.segments) { + edits.push(...rewriteRunnerPackageOptionValue(segment)); + } + return isRunnerOpenPackageFlag(value) ? "await-package-option-value" : "await-executable"; + } + if (isRunnerValueFlag(value)) { + if (value.includes("=")) { + protectTemplateToken(token); + } + return value.includes("=") && !value.endsWith("=") + ? "await-executable" + : "await-executable-flag-value"; + } + if (isRunnerFlag(value)) return "await-executable"; + if (TAILOR_SDK_TOKEN.test(value)) { + pushTemplateTokenReplacement(token, "tailor"); + } + return "none"; + } + + if (isRunnerPackageFlag(token.value)) { + collectTemplatePackageExpressionEdits(token); + return isRunnerOpenPackageFlag(token.value) + ? "await-package-option-value" + : "await-executable"; + } + if (isRunnerValueFlag(token.value)) { + protectTemplateToken(token); + return token.value.includes("=") && !token.value.endsWith("=") + ? "await-executable" + : "await-executable-flag-value"; + } + if (isRunnerFlag(token.value)) return "await-executable"; + return dynamicRunnerOptionStateFor("await-executable", token.substitutions[0]!) ?? "none"; + } + + if (nextState === "await-executable-flag-value") { + protectTemplateToken(token); + return "await-executable"; } if (nextState === "await-dlx" && value === undefined) { @@ -770,6 +886,8 @@ function collectTokenizedRunnerEdits( runnerState === "await-package" || runnerState === "await-package-option-value" || runnerState === "await-flag-value" || + runnerState === "await-executable" || + runnerState === "await-executable-flag-value" || runnerState === "await-dlx" || runnerState === "await-dlx-flag-value" ) { @@ -839,16 +957,61 @@ function collectTokenizedRunnerEdits( continue; } + if (runnerState === "await-executable-flag-value") { + protectToken(token); + runnerState = "await-executable"; + continue; + } + if (runnerState === "await-package-option-value") { const replacement = rewriteRunnerPackageValue(token.value); if (replacement) { edits.push([token.start, token.end, replacement]); } - runnerState = "none"; + runnerState = "await-executable"; previousTokenValue = undefined; continue; } + if (runnerState === "await-executable") { + if (isRunnerSeparatePackageFlag(token.value)) { + runnerState = "await-package-option-value"; + continue; + } + if (isRunnerPackageFlag(token.value)) { + edits.push(...rewriteRunnerPackageOptionValue(token)); + runnerState = isRunnerOpenPackageFlag(token.value) + ? "await-package-option-value" + : "await-executable"; + continue; + } + if (isRunnerSeparateValueFlag(previous)) { + protectToken(token); + continue; + } + if (isRunnerSeparateValueFlag(token.value)) { + runnerState = "await-executable-flag-value"; + continue; + } + if (isRunnerValueFlag(token.value)) { + if (token.value.includes("=")) { + protectToken(token); + } + runnerState = token.value.includes("=") + ? "await-executable" + : "await-executable-flag-value"; + continue; + } + if (isRunnerFlag(token.value)) continue; + if (TAILOR_SDK_TOKEN.test(token.value)) { + edits.push([token.start, token.end, "tailor"]); + } else if (isPathLikeRunnerFlagValue(token.value)) { + protectToken(token); + } + runnerState = "none"; + continue; + } + if (runnerState === "await-dlx") { if (token.value === "dlx") { runnerState = "await-package"; @@ -876,7 +1039,9 @@ function collectTokenizedRunnerEdits( } if (isRunnerPackageFlag(token.value)) { edits.push(...rewriteRunnerPackageOptionValue(token)); - runnerState = token.value.endsWith("=") ? "await-package-option-value" : "none"; + runnerState = isRunnerOpenPackageFlag(token.value) + ? "await-package-option-value" + : "await-executable"; continue; } if (isRunnerSeparateValueFlag(previous)) { @@ -945,7 +1110,14 @@ function collectTokenizedRunnerEdits( if (runnerState === "await-package-option-value") { collectPackageExpressionEdits(child); - runnerState = "none"; + runnerState = "await-executable"; + previousTokenValue = undefined; + continue; + } + + if (runnerState === "await-executable-flag-value") { + protectTailorSdkStrings(child); + runnerState = "await-executable"; previousTokenValue = undefined; continue; } @@ -967,6 +1139,16 @@ function collectTokenizedRunnerEdits( continue; } + if (runnerState === "await-executable") { + visit( + child, + isTokenSequenceNode(child) || child.kind() === "template_string" ? runnerState : "none", + ); + runnerState = "none"; + previousTokenValue = undefined; + continue; + } + if (runnerState === "await-package") { visit(child, isTokenSequenceNode(child) ? runnerState : "none"); runnerState = "none"; @@ -1056,8 +1238,37 @@ function protectStandaloneTailorSdkSourceStrings( } const ranges: Array<[number, number, string]> = []; + const isSourceStringWrapperNode = (node: SgNode): boolean => { + return SOURCE_STRING_WRAPPER_NODE_KINDS.has(node.kind()); + }; + const sourceStringExpressionToken = ( + node: SgNode, + visited = new Set(), + ): SourceStringToken | undefined => { + const direct = sourceStringToken(node, source); + if (direct) return direct; + if (!isSourceStringWrapperNode(node)) return undefined; + const key = nodeRangeKey(node); + if (visited.has(key)) return undefined; + visited.add(key); + for (const child of node.children()) { + const token = sourceStringExpressionToken(child, visited); + if (token) return token; + } + return undefined; + }; + const arrayElementForSourceString = (node: SgNode): SgNode | undefined => { + let element = node; + let parent = element.parent(); + while (parent && isSourceStringWrapperNode(parent)) { + element = parent; + parent = element.parent(); + } + return parent?.kind() === "array" ? element : undefined; + }; const isSingleElementArrayToken = (node: SgNode): boolean => { - const parent = node.parent(); + const element = arrayElementForSourceString(node) ?? node; + const parent = element.parent(); if (parent?.kind() !== "array") return false; return parent.children().filter((child: SgNode) => !isSyntaxOnlyNode(child)).length === 1; }; @@ -1066,7 +1277,7 @@ function protectStandaloneTailorSdkSourceStrings( }; const arrayElementTokens = (node: SgNode): SourceStringToken[] => { return arrayElementNodes(node) - .map((child: SgNode) => sourceStringToken(child, source)) + .map((child: SgNode) => sourceStringExpressionToken(child)) .filter((token): token is SourceStringToken => token != null); }; const hasTailorCommandTokenPair = (tokens: SourceStringToken[]): boolean => { @@ -1090,9 +1301,34 @@ function protectStandaloneTailorSdkSourceStrings( } if (runnerState === "await-package-option-value") { if (runnerPackageReplacement(token.value)) return true; + runnerState = "await-executable"; + continue; + } + if (runnerState === "await-executable") { + if (isRunnerSeparatePackageFlag(token.value)) { + runnerState = "await-package-option-value"; + continue; + } + if (isRunnerPackageFlag(token.value)) { + if (rewriteRunnerPackageOptionValue(token).length > 0) return true; + runnerState = isRunnerOpenPackageFlag(token.value) + ? "await-package-option-value" + : "await-executable"; + continue; + } + if (isRunnerSeparateValueFlag(token.value)) { + runnerState = "await-executable-flag-value"; + continue; + } + if (isRunnerValueFlag(token.value) || isRunnerFlag(token.value)) continue; + if (TAILOR_SDK_TOKEN.test(token.value)) return true; runnerState = "none"; continue; } + if (runnerState === "await-executable-flag-value") { + runnerState = "await-executable"; + continue; + } if (runnerState === "await-dlx") { if (token.value === "dlx") { runnerState = "await-package"; @@ -1113,7 +1349,9 @@ function protectStandaloneTailorSdkSourceStrings( } if (isRunnerPackageFlag(token.value)) { if (rewriteRunnerPackageOptionValue(token).length > 0) return true; - runnerState = token.value.endsWith("=") ? "await-package-option-value" : "none"; + runnerState = isRunnerOpenPackageFlag(token.value) + ? "await-package-option-value" + : "await-executable"; continue; } if (isRunnerSeparateValueFlag(token.value)) { @@ -1156,23 +1394,27 @@ function protectStandaloneTailorSdkSourceStrings( if (!first || !TAILOR_SDK_TOKEN.test(first.value)) return false; if (node.parent()?.kind() === "array") return false; const firstElementIndex = arrayElementNodes(node).findIndex((child) => { - const token = sourceStringToken(child, source); + const token = sourceStringExpressionToken(child); return token?.start === first.start && token.end === first.end; }); if (firstElementIndex === -1) return false; return arrayElementNodes(node) .slice(firstElementIndex + 1) - .some((child) => sourceStringToken(child, source) == null); + .some( + (child) => + sourceStringToken(child, source) == null && isLikelyDynamicTailorArgvRemainder(child), + ); }; const isRewriteableTailorArgvToken = (node: SgNode, token: SourceStringToken): boolean => { - const parent = node.parent(); - if (parent?.kind() !== "array") return false; + const element = arrayElementForSourceString(node); + const parent = element?.parent(); + if (!element || parent?.kind() !== "array") return false; const tokens = arrayElementTokens(parent); const tokenIndex = tokens.findIndex( (candidate) => candidate.start === token.start && candidate.end === token.end, ); if (tokenIndex === -1 || !TAILOR_SDK_TOKEN.test(token.value)) return false; - if (isTailorCliSeparateValueFlag(tokens[tokenIndex - 1]?.value)) return false; + if (isOpenTailorCliValueFlag(tokens[tokenIndex - 1]?.value)) return false; const next = tokens[tokenIndex + 1]; if (next != null && isTailorCliCommandToken(next.value)) return true; if (tokenIndex === 0 && hasDynamicTailorArgvRemainder(parent, tokens)) return true; @@ -1193,7 +1435,8 @@ function protectStandaloneTailorSdkSourceStrings( ); }; const isProtectedArrayDataToken = (node: SgNode): boolean => { - const parent = node.parent(); + const element = arrayElementForSourceString(node) ?? node; + const parent = element.parent(); return parent?.kind() === "array" && !isLikelyTailorArgvArray(parent); }; const sourceStringFragmentTokens = (node: SgNode): SourceStringToken[] => { @@ -1217,8 +1460,57 @@ function protectStandaloneTailorSdkSourceStrings( source.slice(range.start.index, range.end.index), ]); }; + const visitTokenSequence = (node: SgNode): void => { + let afterTailorBinary = false; + let skipNextTailorValue = false; + + for (const child of node.children()) { + if (isSyntaxOnlyNode(child)) { + visit(child); + continue; + } + + if (skipNextTailorValue) { + if (child.text().includes("tailor-sdk")) { + protectNodeText(child); + } + skipNextTailorValue = false; + continue; + } + + const token = sourceStringExpressionToken(child); + if (token && !afterTailorBinary) { + if (isRewriteableTailorArgvToken(child, token)) { + afterTailorBinary = true; + } + visit(child); + continue; + } + + if (token && afterTailorBinary) { + if (hasInlineTailorCliValue(token.value)) { + if (token.value.includes("tailor-sdk")) { + ranges.push([token.start, token.end, token.value]); + } + visit(child); + continue; + } + if (isOpenTailorCliValueFlag(token.value)) { + skipNextTailorValue = true; + visit(child); + continue; + } + } + + visit(child); + } + }; const visit = (node: SgNode): void => { const kind = node.kind(); + if (isTokenSequenceNode(node)) { + visitTokenSequence(node); + return; + } if (kind === "regex" && node.text().includes("tailor-sdk")) { protectNodeText(node); return; @@ -1268,13 +1560,15 @@ function protectStandaloneTailorSdkSourceStrings( const token = sourceStringToken(node, source); if (token) { const parent = node.parent(); + const arrayElement = arrayElementForSourceString(node); + const arrayParent = arrayElement?.parent(); if ( token.value.includes("tailor-sdk") && (parent == null || - !isTokenSequenceNode(parent) || + (!isTokenSequenceNode(parent) && arrayParent?.kind() !== "array") || isSingleElementArrayToken(node) || isProtectedArrayDataToken(node) || - (parent.kind() === "array" && !isRewriteableTailorArgvToken(node, token))) + (arrayParent?.kind() === "array" && !isRewriteableTailorArgvToken(node, token))) ) { ranges.push(...collectNonCommandTailorSdkRanges(token.value, token.start)); } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index f3ec360a1..f88e51388 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -3,6 +3,10 @@ const setup = `pnpm tailor staticwebsite deploy --name ${siteName}`; const deploy = "tailor deploy"; const dynamicCommand = `tailor ${subcommand}`; const dynamicCommandAfterSeparator = `cd app && tailor ${subcommand}`; +const dynamicCommandAfterEnv = `env FOO=bar tailor ${subcommand}`; +const dynamicCommandAfterAssignment = `TAILOR=1 tailor ${subcommand}`; +const dynamicPnpmCommand = `pnpm tailor ${subcommand}`; +const dynamicPnpmExecCommand = `pnpm exec tailor ${subcommand}`; const dynamicCommandWithFlags = `tailor ${flags} deploy`; const dynamicCommandWithConditionalFlags = `tailor ${useJson ? "--json" : ""} deploy`; const profileValue = "tailor --profile tailor-sdk deploy"; @@ -18,6 +22,8 @@ const separatePackageOptionTemplate = `npx --package @tailor-platform/sdk tailor const quotedPackageOptionRunner = 'npx --package="@tailor-platform/sdk" tailor login'; const dynamicPackageOptionRunner = `npx --package=${useLocal ? "@tailor-platform/sdk" : pkg} tailor login`; const dynamicSeparatePackageOptionRunner = `npx ${useLocal ? "--package" : ""} ${useLocal ? "@tailor-platform/sdk" : pkg} tailor login`; +const dynamicPackageOptionCommand = `npx --package @tailor-platform/sdk tailor ${subcommand}`; +const dynamicInlinePackageOptionCommand = `npx --package=${pkg} tailor ${subcommand}`; const dynamicFlagStaticTemplate = `npx ${yes ? "-y" : ""} @tailor-platform/sdk login`; const dynamicFlagConditionTemplate = `npx ${mode === "prod" ? "-y" : "--yes"} @tailor-platform/sdk login`; const dynamicDlxValueFlagTemplate = `pnpm ${use ? "--filter" : ""} app dlx @tailor-platform/sdk login`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index b8f82086f..86c58e150 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -3,6 +3,10 @@ const setup = `pnpm tailor-sdk staticwebsite deploy --name ${siteName}`; const deploy = "tailor-sdk deploy"; const dynamicCommand = `tailor-sdk ${subcommand}`; const dynamicCommandAfterSeparator = `cd app && tailor-sdk ${subcommand}`; +const dynamicCommandAfterEnv = `env FOO=bar tailor-sdk ${subcommand}`; +const dynamicCommandAfterAssignment = `TAILOR=1 tailor-sdk ${subcommand}`; +const dynamicPnpmCommand = `pnpm tailor-sdk ${subcommand}`; +const dynamicPnpmExecCommand = `pnpm exec tailor-sdk ${subcommand}`; const dynamicCommandWithFlags = `tailor-sdk ${flags} deploy`; const dynamicCommandWithConditionalFlags = `tailor-sdk ${useJson ? "--json" : ""} deploy`; const profileValue = "tailor-sdk --profile tailor-sdk deploy"; @@ -18,6 +22,8 @@ const separatePackageOptionTemplate = `npx --package tailor-sdk tailor-sdk login const quotedPackageOptionRunner = 'npx --package="tailor-sdk" tailor-sdk login'; const dynamicPackageOptionRunner = `npx --package=${useLocal ? "tailor-sdk" : pkg} tailor-sdk login`; const dynamicSeparatePackageOptionRunner = `npx ${useLocal ? "--package" : ""} ${useLocal ? "tailor-sdk" : pkg} tailor-sdk login`; +const dynamicPackageOptionCommand = `npx --package tailor-sdk tailor-sdk ${subcommand}`; +const dynamicInlinePackageOptionCommand = `npx --package=${pkg} tailor-sdk ${subcommand}`; const dynamicFlagStaticTemplate = `npx ${yes ? "-y" : ""} tailor-sdk login`; const dynamicFlagConditionTemplate = `npx ${mode === "prod" ? "-y" : "--yes"} tailor-sdk login`; const dynamicDlxValueFlagTemplate = `pnpm ${use ? "--filter" : ""} app dlx tailor-sdk login`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts index 859ae3052..ecd6f1a81 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts @@ -25,6 +25,8 @@ const inlineValueFlagRunner = ["npx", "--registry=https://npm.example/tailor-sdk const inlinePackageFlagRunner = ["npx", "--package=@tailor-platform/sdk", "tailor", "login"]; const dynamicInlinePackageFlagRunner = ["npx", `--package=${useLocal ? "@tailor-platform/sdk" : pkg}`, "tailor", "login"]; const dynamicSeparatePackageFlagRunner = ["npx", useLocal ? "--package" : "", useLocal ? "@tailor-platform/sdk" : pkg, "tailor", "login"]; +const dynamicPackageOptionCommandRunner = ["npx", "--package", "@tailor-platform/sdk", "tailor", cmd]; +const dynamicInlinePackageOptionCommandRunner = ["npx", `--package=${pkg}`, "tailor", cmd]; const templateConditionalRunner = `npx ${useLatest ? "@tailor-platform/sdk@latest" : "@tailor-platform/sdk"} login`; const templateDynamicInlineValueFlagRunner = `npx ${useRegistry ? "--registry=https://npm.example" : "-y"} @tailor-platform/sdk login`; const pnpmOptionRunner = spawn("pnpm", ["--dir", ".", "dlx", "@tailor-platform/sdk", "login"]); @@ -44,11 +46,18 @@ const conditionalRunner = ["npx", useLatest ? "@tailor-platform/sdk@latest" : "@ const binaryArgs = ["tailor", "login"]; const profileArgValue = ["tailor", "--profile", "tailor-sdk", "deploy"]; const shortProfileArgValue = ["tailor", "-p", "tailor-sdk", "deploy"]; +const profileCommandPayload = ["tailor", "--profile", "tailor-sdk deploy", "deploy"]; +const argCommandPayload = ["tailor", "function", "test-run", "--arg", "{\"cmd\":\"tailor-sdk deploy\"}"]; +const argCommandPayloadExpression = ["tailor", "function", "test-run", "--arg", cond ? "tailor-sdk deploy" : "{}"]; +const argCommandPayloadTemplate = ["tailor", "function", "test-run", "--arg", `tailor-sdk deploy`]; const dynamicBinaryArgs = ["tailor", cmd]; const spreadBinaryArgs = ["tailor", ...args]; +const wrappedCommandArgs = ["tailor", "login" as const]; +const parenthesizedCommandArgs = ["tailor", ("deploy")]; const commandArgPackageName = ["tailor", "login", "tailor-sdk"]; const packageManagerNames = ["pnpm", "tailor-sdk"]; const unrelatedNpxValues = ["npx", "create-tailor-sdk", "tailor-sdk-skills", "tailor-sdk"]; const packageNames = ["tailor-sdk", "react"]; const packageTuple = [["tailor-sdk", version]]; +const packageTupleValue = ["tailor-sdk", version]; const outputDir = ".tailor-sdk"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts index 79179b447..4cc2be120 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts @@ -25,6 +25,8 @@ const inlineValueFlagRunner = ["npx", "--registry=https://npm.example/tailor-sdk const inlinePackageFlagRunner = ["npx", "--package=tailor-sdk", "tailor-sdk", "login"]; const dynamicInlinePackageFlagRunner = ["npx", `--package=${useLocal ? "tailor-sdk" : pkg}`, "tailor-sdk", "login"]; const dynamicSeparatePackageFlagRunner = ["npx", useLocal ? "--package" : "", useLocal ? "tailor-sdk" : pkg, "tailor-sdk", "login"]; +const dynamicPackageOptionCommandRunner = ["npx", "--package", "tailor-sdk", "tailor-sdk", cmd]; +const dynamicInlinePackageOptionCommandRunner = ["npx", `--package=${pkg}`, "tailor-sdk", cmd]; const templateConditionalRunner = `npx ${useLatest ? "tailor-sdk@latest" : "tailor-sdk"} login`; const templateDynamicInlineValueFlagRunner = `npx ${useRegistry ? "--registry=https://npm.example" : "-y"} tailor-sdk login`; const pnpmOptionRunner = spawn("pnpm", ["--dir", ".", "dlx", "tailor-sdk", "login"]); @@ -44,11 +46,18 @@ const conditionalRunner = ["npx", useLatest ? "tailor-sdk@latest" : "tailor-sdk" const binaryArgs = ["tailor-sdk", "login"]; const profileArgValue = ["tailor-sdk", "--profile", "tailor-sdk", "deploy"]; const shortProfileArgValue = ["tailor-sdk", "-p", "tailor-sdk", "deploy"]; +const profileCommandPayload = ["tailor-sdk", "--profile", "tailor-sdk deploy", "deploy"]; +const argCommandPayload = ["tailor-sdk", "function", "test-run", "--arg", "{\"cmd\":\"tailor-sdk deploy\"}"]; +const argCommandPayloadExpression = ["tailor-sdk", "function", "test-run", "--arg", cond ? "tailor-sdk deploy" : "{}"]; +const argCommandPayloadTemplate = ["tailor-sdk", "function", "test-run", "--arg", `tailor-sdk deploy`]; const dynamicBinaryArgs = ["tailor-sdk", cmd]; const spreadBinaryArgs = ["tailor-sdk", ...args]; +const wrappedCommandArgs = ["tailor-sdk", "login" as const]; +const parenthesizedCommandArgs = ["tailor-sdk", ("deploy")]; const commandArgPackageName = ["tailor-sdk", "login", "tailor-sdk"]; const packageManagerNames = ["pnpm", "tailor-sdk"]; const unrelatedNpxValues = ["npx", "create-tailor-sdk", "tailor-sdk-skills", "tailor-sdk"]; const packageNames = ["tailor-sdk", "react"]; const packageTuple = [["tailor-sdk", version]]; +const packageTupleValue = ["tailor-sdk", version]; const outputDir = ".tailor-sdk"; From 8a08e71cc9c0b42c3b1a951f4c5307bcf7b2a4d3 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 13:35:51 +0900 Subject: [PATCH 270/618] fix(sdk-codemod): preserve cli global option values --- .../v2/cli-rename/scripts/transform.ts | 19 ++++++++++++++++++- .../cli-rename/tests/basic-shell/expected.sh | 1 + .../v2/cli-rename/tests/basic-shell/input.sh | 1 + .../tests/source-template/expected.ts | 1 + .../cli-rename/tests/source-template/input.ts | 1 + 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index 6a15b75e0..799724ba2 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -51,6 +51,23 @@ const CLI_VALUE_ARGS = [ "-q", "-f", ] as const; +const COMMAND_VALUE_ARGS = [ + "--env-file-if-exists", + "--env-file", + "--profile", + "--config", + "--workspace-id", + "--arg", + "--query", + "--file", + "-e", + "-p", + "-c", + "-w", + "-a", + "-q", + "-f", +] as const; const CLI_VALUE_ARG_SET = new Set(CLI_VALUE_ARGS); const PACKAGE_RUNNER_VALUE_ARGS = new Set([ "--cache", @@ -482,7 +499,7 @@ function findOptionRename(command: string, index: number): readonly [string, str } function findCliValueArg(command: string, index: number): string | undefined { - return CLI_VALUE_ARGS.find( + return COMMAND_VALUE_ARGS.find( (arg) => command.startsWith(arg, index) && isOptionBoundaryChar(command[index - 1]) && diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh index 0eef8a76d..aefa05483 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh @@ -13,4 +13,5 @@ tailor-sdk query --query "select 1 | 2" --machine-user ci tailor-sdk workflow start approval --arg '{"ok":true}' \ --machine-user ci tailor-sdk --json crashreport list +tailor-sdk --profile --machineuser crashreport --machine-user ci TOKEN=$(tailor-sdk query --machine-user ci) other-cli --machineuser=ci diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh index d4d5ba382..ca8d11fd1 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh @@ -13,4 +13,5 @@ tailor-sdk query --query "select 1 | 2" --machineuser ci tailor-sdk workflow start approval --arg '{"ok":true}' \ --machineuser ci tailor-sdk --json crash-report list +tailor-sdk --profile --machineuser crash-report --machineuser ci TOKEN=$(tailor-sdk query --machineuser ci) other-cli --machineuser=ci diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts index c23defcdf..f1ec69aa3 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts @@ -24,6 +24,7 @@ const envFileCommandSubstitutionPath = "tailor-sdk --env-file=$(pwd)/--machineus const argPayload = "tailor-sdk function test-run --arg=--machineuser"; const argCommandSubstitutionPayload = "tailor-sdk function test-run --arg=$(printf --machineuser) --machine-user"; const profileCommand = "tailor-sdk --profile prod crashreport --machine-user"; +const profileMachineUserValue = "tailor-sdk --profile --machineuser crashreport --machine-user"; const workspaceCommand = "tailor-sdk -w workspace-1 crashreport"; const chainedShellCommand = "cd app && tailor-sdk crashreport --machine-user"; const envShellCommand = "env FOO=bar tailor-sdk crashreport --machine-user"; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts index 915668c9a..74e5055e9 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts @@ -24,6 +24,7 @@ const envFileCommandSubstitutionPath = "tailor-sdk --env-file=$(pwd)/--machineus const argPayload = "tailor-sdk function test-run --arg=--machineuser"; const argCommandSubstitutionPayload = "tailor-sdk function test-run --arg=$(printf --machineuser) --machineuser"; const profileCommand = "tailor-sdk --profile prod crash-report --machineuser"; +const profileMachineUserValue = "tailor-sdk --profile --machineuser crash-report --machineuser"; const workspaceCommand = "tailor-sdk -w workspace-1 crash-report"; const chainedShellCommand = "cd app && tailor-sdk crash-report --machineuser"; const envShellCommand = "env FOO=bar tailor-sdk crash-report --machineuser"; From b8b48a379a73314c26fbf53c74c2181e77f0565b Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 13:38:11 +0900 Subject: [PATCH 271/618] fix: detect runtime globals in JavaScript codemod targets --- .changeset/runtime-global-source-strings.md | 5 ++ packages/sdk-codemod/src/migration-doc.ts | 3 +- packages/sdk-codemod/src/registry.test.ts | 5 +- packages/sdk-codemod/src/registry.ts | 17 ++++++- packages/sdk-codemod/src/runner.test.ts | 53 ++++++++++++++++++++- packages/sdk-codemod/src/runner.ts | 19 ++++++-- packages/sdk-codemod/src/types.ts | 9 +++- packages/sdk/docs/migration/v2.md | 4 +- 8 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 .changeset/runtime-global-source-strings.md diff --git a/.changeset/runtime-global-source-strings.md b/.changeset/runtime-global-source-strings.md new file mode 100644 index 000000000..2b3d617b6 --- /dev/null +++ b/.changeset/runtime-global-source-strings.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Flag JavaScript files and embedded code strings that still reference ambient Tailor runtime globals during v2 migration review. diff --git a/packages/sdk-codemod/src/migration-doc.ts b/packages/sdk-codemod/src/migration-doc.ts index 8c3cd7d9a..6ae09266e 100644 --- a/packages/sdk-codemod/src/migration-doc.ts +++ b/packages/sdk-codemod/src/migration-doc.ts @@ -7,7 +7,7 @@ export type AutomationLevel = "Automatic" | "Partially automatic" | "Manual"; * - `Automatic`: a transform fully covers it, with no residual to flag. * - `Partially automatic`: a transform covers the common cases but flags * residuals (via `legacyPatterns`/`sourceStringLegacyPatterns`/ - * `suspiciousPatterns`/`prompt`) to finish. + * `suspiciousPatterns`/`sourceStringSuspiciousPatterns`/`prompt`) to finish. * - `Manual`: no transform; the change is migrated by hand (optionally guided * by a `prompt`). Whether a person or an LLM does it does not matter here. * @param codemod - The codemod registry entry @@ -19,6 +19,7 @@ export function automationLevel(codemod: CodemodPackage): AutomationLevel { (codemod.legacyPatterns?.length ?? 0) > 0 || (codemod.sourceStringLegacyPatterns?.length ?? 0) > 0 || (codemod.suspiciousPatterns?.length ?? 0) > 0 || + (codemod.sourceStringSuspiciousPatterns?.length ?? 0) > 0 || codemod.prompt != null; return flagsResidual ? "Partially automatic" : "Automatic"; } diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 301045f83..481c4ed07 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -55,11 +55,12 @@ describe("getApplicableCodemods", () => { ); }); - test("flags CommonJS TypeScript files for runtime globals review", () => { + test("flags source files for runtime globals review", () => { const codemod = allCodemods.find((entry) => entry.id === "v2/runtime-globals-opt-in"); - expect(codemod?.filePatterns).toContain("**/*.{ts,tsx,mts,cts}"); + expect(codemod?.filePatterns).toContain("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"); expect(codemod?.suspiciousPatterns).toContain("tailor.idp"); + expect(codemod?.sourceStringSuspiciousPatterns).toContain("new tailor.idp.Client"); expect(codemod?.prompt).toContain("@tailor-platform/sdk/runtime/globals"); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index fd4640a7c..f9ec35c3a 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -435,7 +435,7 @@ export const allCodemods: CodemodPackage[] = [ 'Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Normal SDK development does not need them — use the SDK APIs and the typed wrappers from `@tailor-platform/sdk/runtime`. Only if you relied on the ambient globals directly, add `import "@tailor-platform/sdk/runtime/globals"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.)', since: "1.0.0", until: "2.0.0", - filePatterns: ["**/*.{ts,tsx,mts,cts}"], + filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], suspiciousPatterns: [ "tailor.context", "tailor.iconv", @@ -452,6 +452,17 @@ export const allCodemods: CodemodPackage[] = [ "TailorErrorMessage", "TailorErrors", ], + sourceStringSuspiciousPatterns: [ + "new tailor.idp.Client", + /\btailor\.(?:context|iconv|workflow)\s*\./, + /\btailordb\.(?:Client|CommandType|QueryResult|file)\b/, + "tailor[", + "tailordb[", + "TailorDBFileError", + "TailorErrorItem", + "TailorErrorMessage", + "TailorErrors", + ], examples: [ { caption: @@ -484,7 +495,9 @@ export const allCodemods: CodemodPackage[] = [ " the relevant tsconfig compilerOptions", "", "Leave files unchanged when the matching name is local, imported from another", - "module, or appears only in comments or strings.", + "module, or appears only in comments or prose strings. Embedded code strings", + "that use runtime globals are review-only findings; do not insert imports inside", + "string literals.", ].join("\n"), }, { diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 080ae1173..d1b45de2e 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -5,6 +5,11 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { runCodemods } from "./runner"; import type { CodemodPackage } from "./types"; +type TestCodemodExtra = Pick< + CodemodPackage, + "sourceStringLegacyPatterns" | "suspiciousPatterns" | "sourceStringSuspiciousPatterns" | "prompt" +>; + /** * Create a temporary directory with a test file for codemod testing. * @param fileName - Name of the test file @@ -26,7 +31,7 @@ function makeCodemod( scriptPath?: string, filePatterns?: string[], legacyPatterns?: Array, - extra?: Pick, + extra?: TestCodemodExtra, ): CodemodPackage { return { id, @@ -646,6 +651,52 @@ describe("runCodemods", () => { expect(result.llmReviews).toEqual([]); }); + test("flags source strings matching source-string suspicious patterns for LLM review", async () => { + const dir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), "runner-llm-source-string-test-"), + ); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "seed.mjs"), + [ + 'const code = `const client = new tailor.idp.Client({ namespace: "default" });`;', + 'const note = "tailor.idp.Client is mentioned in prose";', + ].join("\n"), + ); + await fs.promises.writeFile( + path.join(dir, "prose.mjs"), + 'const note = "tailor.idp.Client is mentioned in prose";\n', + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod( + "test/llm-source-string", + partialTransformPath, + ["**/*.{ts,js,mjs,cjs}"], + undefined, + { + sourceStringSuspiciousPatterns: ["new tailor.idp.Client"], + prompt: "Review embedded runtime global usage by hand.", + }, + ), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.llmReviews).toEqual([ + { + codemodId: "test/llm-source-string", + prompt: "Review embedded runtime global usage by hand.", + files: ["seed.mjs"], + }, + ]); + }); + test("keeps LLM review patterns inside template substitutions", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-source-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 4bf94a544..9cf06e097 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -138,6 +138,7 @@ interface LoadedTransform { legacyPatterns: CodemodPatternGroup[]; sourceStringLegacyPatterns: CodemodPatternGroup[]; suspiciousPatterns: CodemodPatternGroup[]; + sourceStringSuspiciousPatterns: CodemodPatternGroup[]; prompt?: string; } @@ -312,6 +313,7 @@ export async function runCodemods( legacyPatterns: codemod.legacyPatterns ?? [], sourceStringLegacyPatterns: codemod.sourceStringLegacyPatterns ?? [], suspiciousPatterns: codemod.suspiciousPatterns ?? [], + sourceStringSuspiciousPatterns: codemod.sourceStringSuspiciousPatterns ?? [], prompt: codemod.prompt, }); } @@ -364,8 +366,19 @@ export async function runCodemods( ); for (const lt of matchedTransforms) { - if (!lt.prompt || lt.suspiciousPatterns.length === 0) continue; - if (lt.suspiciousPatterns.some((p) => matchResidualPattern(residualContent, p) !== null)) { + if ( + !lt.prompt || + (lt.suspiciousPatterns.length === 0 && lt.sourceStringSuspiciousPatterns.length === 0) + ) { + continue; + } + const matchesSource = + lt.suspiciousPatterns.some((p) => matchResidualPattern(residualContent, p) !== null) || + (sourceStringContent != null && + lt.sourceStringSuspiciousPatterns.some( + (p) => matchResidualPattern(sourceStringContent, p) !== null, + )); + if (matchesSource) { const files = suspiciousByCodemod.get(lt.id) ?? []; files.push(relative); suspiciousByCodemod.set(lt.id, files); @@ -376,7 +389,7 @@ export async function runCodemods( const llmReviews: LlmReview[] = []; for (const lt of loaded) { if (!lt.prompt) continue; - if (lt.suspiciousPatterns.length > 0) { + if (lt.suspiciousPatterns.length > 0 || lt.sourceStringSuspiciousPatterns.length > 0) { // File-scoped: only surface when a suspicious pattern actually matched. const files = suspiciousByCodemod.get(lt.id); // Sort for deterministic output regardless of filesystem traversal order. diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index 34965ebd6..a5ac19042 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -67,9 +67,16 @@ export interface CodemodPackage { * `legacyPatterns`. Has no effect unless `prompt` is also set. */ suspiciousPatterns?: CodemodPatternGroup[]; + /** + * Patterns to detect only inside string/template fragments of source files + * for LLM-assisted review. Use this when source strings normally remain + * masked for `suspiciousPatterns`, but embedded code snippets may still need + * manual migration. Has no effect unless `prompt` is also set. + */ + sourceStringSuspiciousPatterns?: CodemodPatternGroup[]; /** * Prompt that instructs an LLM how to finish the migration for files matched - * by `suspiciousPatterns`. + * by `suspiciousPatterns` or `sourceStringSuspiciousPatterns`. */ prompt?: string; /** Before/after examples shown in the generated migration doc. */ diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index d8954d6fc..9336274d0 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -531,7 +531,9 @@ opt into the global declarations instead by adding one of these: the relevant tsconfig compilerOptions Leave files unchanged when the matching name is local, imported from another -module, or appears only in comments or strings. +module, or appears only in comments or prose strings. Embedded code strings +that use runtime globals are review-only findings; do not insert imports inside +string literals. ``` From 79780bce19864a238602f4dd7a82fcc84e9f8501 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 13:38:13 +0900 Subject: [PATCH 272/618] fix(codemod): migrate generated output ignore entries --- .changeset/tailor-output-ignore-dir.md | 5 +++ .../v2/tailor-output-ignore-dir/codemod.yaml | 7 ++++ .../scripts/transform.ts | 17 +++++++++ .../tests/basic-gitignore/expected.gitignore | 7 ++++ .../tests/basic-gitignore/input.gitignore | 7 ++++ .../tests/no-match/input.gitignore | 7 ++++ packages/sdk-codemod/src/registry.ts | 35 ++++++++++++++++++- packages/sdk-codemod/src/transform.test.ts | 4 +++ packages/sdk-codemod/tsdown.config.ts | 2 ++ packages/sdk/docs/migration/v2.md | 20 ++++++++++- 10 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 .changeset/tailor-output-ignore-dir.md create mode 100644 packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/tests/basic-gitignore/expected.gitignore create mode 100644 packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/tests/basic-gitignore/input.gitignore create mode 100644 packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/tests/no-match/input.gitignore diff --git a/.changeset/tailor-output-ignore-dir.md b/.changeset/tailor-output-ignore-dir.md new file mode 100644 index 000000000..b46ae7149 --- /dev/null +++ b/.changeset/tailor-output-ignore-dir.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Add the `v2/tailor-output-ignore-dir` codemod so SDK upgrades rewrite exact `.tailor-sdk/` ignore-file entries to `.tailor/` while leaving other `.tailor-sdk` paths unchanged. diff --git a/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/codemod.yaml b/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/codemod.yaml new file mode 100644 index 000000000..ededfbd3c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/tailor-output-ignore-dir" +version: "1.0.0" +description: "Rename exact ignore-file entries for the generated output directory from .tailor-sdk to .tailor" +engine: jssg +language: text +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/scripts/transform.ts new file mode 100644 index 000000000..9414bfb03 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/scripts/transform.ts @@ -0,0 +1,17 @@ +const GENERATED_DIR_IGNORE_ENTRY_RE = /^(!?\/?)\.tailor-sdk(\/?)([ \t]*)$/gm; + +/** + * Rewrite exact ignore-file entries for the generated SDK output directory. + * @param source - File contents + * @returns Transformed source or null when nothing matched. + */ +export default function transform(source: string): string | null { + if (!source.includes(".tailor-sdk")) return null; + + const updated = source.replace( + GENERATED_DIR_IGNORE_ENTRY_RE, + (_match, prefix: string, slash: string, trailingWhitespace: string) => + `${prefix}.tailor${slash}${trailingWhitespace}`, + ); + return updated === source ? null : updated; +} diff --git a/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/tests/basic-gitignore/expected.gitignore b/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/tests/basic-gitignore/expected.gitignore new file mode 100644 index 000000000..90d6c4767 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/tests/basic-gitignore/expected.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.tailor/ +/.tailor/ +.tailor +/.tailor +!.tailor/ +dist/ diff --git a/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/tests/basic-gitignore/input.gitignore b/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/tests/basic-gitignore/input.gitignore new file mode 100644 index 000000000..ddcc66899 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/tests/basic-gitignore/input.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.tailor-sdk/ +/.tailor-sdk/ +.tailor-sdk +/.tailor-sdk +!.tailor-sdk/ +dist/ diff --git a/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/tests/no-match/input.gitignore b/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/tests/no-match/input.gitignore new file mode 100644 index 000000000..ba9115457 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailor-output-ignore-dir/tests/no-match/input.gitignore @@ -0,0 +1,7 @@ +# .tailor-sdk/ +.tailor-sdk/cache +docs: `.tailor-sdk/` was the old generated directory +prefix/.tailor-sdk/ + .tailor-sdk/ +.tailor-sdk/extra +.tailor-sdk-old/ diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index fd4640a7c..2441cc39c 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -543,7 +543,7 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/rename-bin", name: "tailor-sdk binary → tailor", description: - "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot).", + "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Exact ignore-file entries for `.tailor-sdk/` are handled by the generated-output ignore codemod.", since: "1.0.0", until: "2.0.0", scriptPath: "v2/rename-bin/scripts/transform.js", @@ -562,6 +562,39 @@ export const allCodemods: CodemodPackage[] = [ "package references unchanged.", ].join("\n"), }, + { + id: "v2/tailor-output-ignore-dir", + name: ".tailor-sdk ignore entries → .tailor", + description: + "Rewrite exact ignore-file entries for the v1 generated output directory from `.tailor-sdk` to the v2 `.tailor` directory. Other `.tailor-sdk` paths and prose are left unchanged.", + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/tailor-output-ignore-dir/scripts/transform.js", + filePatterns: [ + "**/.gitignore", + "**/.npmignore", + "**/.dockerignore", + "**/gitignore", + "**/npmignore", + "**/dockerignore", + "**/_gitignore", + "**/_npmignore", + "**/_dockerignore", + "**/__dot__gitignore", + "**/__dot__npmignore", + "**/__dot__dockerignore", + "**/*.gitignore", + "**/*.npmignore", + "**/*.dockerignore", + ], + examples: [ + { + lang: "gitignore", + before: ".tailor-sdk/", + after: ".tailor/", + }, + ], + }, { id: "v2/node-minimum-22-15-0", name: "Node.js minimum version raised to 22.15.0", diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index 9699f5fa8..c3e8ca6dc 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -107,6 +107,10 @@ describe("codemod transforms", () => { await expect(runFixtureCases("v2/execute-script-arg")).resolves.toBeUndefined(); }); + test("v2/tailor-output-ignore-dir transforms correctly", async () => { + await expect(runFixtureCases("v2/tailor-output-ignore-dir")).resolves.toBeUndefined(); + }); + test("v2/rename-bin transforms correctly", async () => { await expect(runFixtureCases("v2/rename-bin")).resolves.toBeUndefined(); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index 8c930336e..bfc9e7c56 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -34,6 +34,8 @@ export default defineConfig([ "codemods/v2/tailordb-namespace/scripts/transform.ts", "v2/execute-script-arg/scripts/transform": "codemods/v2/execute-script-arg/scripts/transform.ts", + "v2/tailor-output-ignore-dir/scripts/transform": + "codemods/v2/tailor-output-ignore-dir/scripts/transform.ts", "v2/rename-bin/scripts/transform": "codemods/v2/rename-bin/scripts/transform.ts", "v2/wait-point-rename/scripts/transform": "codemods/v2/wait-point-rename/scripts/transform.ts", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index d8954d6fc..4d042fd33 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -577,7 +577,7 @@ trigger result as the job output directly (no Promise wrapper to unwrap). **Migration:** Partially automatic -Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Update `.gitignore` entries manually (the codemod skips paths preceded by a dot). +Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Exact ignore-file entries for `.tailor-sdk/` are handled by the generated-output ignore codemod. Before: @@ -604,6 +604,24 @@ package references unchanged. +## .tailor-sdk ignore entries → .tailor + +**Migration:** Automatic + +Rewrite exact ignore-file entries for the v1 generated output directory from `.tailor-sdk` to the v2 `.tailor` directory. Other `.tailor-sdk` paths and prose are left unchanged. + +Before: + +```gitignore +.tailor-sdk/ +``` + +After: + +```gitignore +.tailor/ +``` + ## Behavioral changes (no migration required) These v2 changes alter runtime or CLI behavior; no source change is needed. From 7faff07982909b63b87185dc1186e2919a06d4bb Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 13:41:02 +0900 Subject: [PATCH 273/618] fix(sdk-codemod): report codemod runner identity --- .changeset/codemod-runner-metadata.md | 5 ++ docs/changeset.md | 8 +++ packages/sdk-codemod/src/index.ts | 18 ++++- .../sdk-codemod/src/runner-metadata.test.ts | 61 +++++++++++++++++ packages/sdk-codemod/src/runner-metadata.ts | 68 +++++++++++++++++++ packages/sdk-codemod/src/types.ts | 3 + 6 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 .changeset/codemod-runner-metadata.md create mode 100644 packages/sdk-codemod/src/runner-metadata.test.ts create mode 100644 packages/sdk-codemod/src/runner-metadata.ts diff --git a/.changeset/codemod-runner-metadata.md b/.changeset/codemod-runner-metadata.md new file mode 100644 index 000000000..e2e85700b --- /dev/null +++ b/.changeset/codemod-runner-metadata.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Report the codemod runner identity in the JSON summary, including the source checkout commit and local build command when run from a branch build, so prerelease migration validation can distinguish exact npm packages from branch-head behavior. diff --git a/docs/changeset.md b/docs/changeset.md index a1be7e182..d5995c03c 100644 --- a/docs/changeset.md +++ b/docs/changeset.md @@ -8,6 +8,14 @@ This project uses [Changesets](https://github.com/changesets/changesets) for ver pnpm changeset ``` +## Prerelease Codemod Validation + +When validating codemod behavior before a prerelease, record the exact runner that produced the result: + +- For package-based validation, cite an exact published npm version such as `@tailor-platform/sdk-codemod@0.3.0-next.3`. +- For branch-head validation, build the package locally, run `node packages/sdk-codemod/dist/index.js ...`, and cite the `runner.gitCommit` plus `runner.localBuildCommand` from the JSON summary. +- Do not cite an older npm prerelease for behavior that only exists on the branch head. Publish a new prerelease first, or keep the validation explicitly tied to the branch commit and local build command. + ## Version Bump Levels The key question for choosing a level: **How does this change affect SDK users?** diff --git a/packages/sdk-codemod/src/index.ts b/packages/sdk-codemod/src/index.ts index 183a06c5a..4ce1e3f05 100644 --- a/packages/sdk-codemod/src/index.ts +++ b/packages/sdk-codemod/src/index.ts @@ -8,9 +8,13 @@ import { z } from "zod"; import { automationLevel } from "./migration-doc"; import { allCodemods, getApplicableCodemods, resolveCodemodScript } from "./registry"; import { runCodemods } from "./runner"; +import { createRunnerMetadata } from "./runner-metadata"; import type { LlmReview, RunOutput } from "./types"; -const packageJson = await readPackageJSON(path.dirname(fileURLToPath(import.meta.url)) + "/.."); +const packageRoot = path.dirname(fileURLToPath(import.meta.url)) + "/.."; +const packageJson = await readPackageJSON(packageRoot); +const packageName = packageJson.name ?? "sdk-codemod"; +const packageVersion = packageJson.version ?? "0.0.0"; /** One rule in the `list` command output. */ interface RuleSummary { @@ -61,7 +65,7 @@ function printLlmReview(review: LlmReview): void { } const main = defineCommand({ - name: packageJson.name ?? "sdk-codemod", + name: packageName, description: packageJson.description ?? "Codemod runner for Tailor Platform SDK upgrades", subCommands: { list: listCommand }, notes: `Applies the codemods matching the \`--from\`/\`--to\` version range to the @@ -72,6 +76,8 @@ const main = defineCommand({ - \`llmReviews\`: changes the codemods could not fully migrate on their own. Each entry has the affected \`files\` and a \`prompt\` — hand the prompt and files to an LLM (or follow it yourself) to finish those cases. +- \`runner\`: exact codemod runner identity. Local source builds include the + repository commit and the build command used to produce \`dist/index.js\`. Progress, warnings, and the LLM-review prompts are also printed to \`stderr\` in human-readable form, so \`stdout\` stays pure JSON for piping.`, @@ -105,10 +111,16 @@ human-readable form, so \`stdout\` stays pure JSON for piping.`, run: async (args) => { const targetPath = path.resolve(args.target); const dryRun = args["dry-run"]; + const runner = createRunnerMetadata({ + packageName, + packageVersion, + packageRoot, + }); const codemods = getApplicableCodemods(args.from, args.to); const output: RunOutput = { + runner, codemodsApplied: 0, codemodsSkipped: 0, filesModified: [], @@ -165,4 +177,4 @@ human-readable form, so \`stdout\` stays pure JSON for piping.`, }, }); -void runMain(main, { version: packageJson.version }); +void runMain(main, { version: packageVersion }); diff --git a/packages/sdk-codemod/src/runner-metadata.test.ts b/packages/sdk-codemod/src/runner-metadata.test.ts new file mode 100644 index 000000000..83df0d32c --- /dev/null +++ b/packages/sdk-codemod/src/runner-metadata.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, test } from "vitest"; +import { createRunnerMetadata } from "./runner-metadata"; + +const packageInfo = { + packageName: "@tailor-platform/sdk-codemod", + packageVersion: "0.3.0-next.2", +}; + +describe("createRunnerMetadata", () => { + test("includes the exact package identity", () => { + const metadata = createRunnerMetadata({ + ...packageInfo, + packageRoot: "/repo/packages/sdk-codemod", + readGit: () => undefined, + realpath: (value) => value, + }); + + expect(metadata).toEqual({ + packageName: "@tailor-platform/sdk-codemod", + packageVersion: "0.3.0-next.2", + }); + }); + + test("includes the branch commit and local build command for a source checkout", () => { + const metadata = createRunnerMetadata({ + ...packageInfo, + packageRoot: "/repo/packages/sdk-codemod", + readGit: (_cwd, args) => { + if (args.join(" ") === "rev-parse --show-toplevel") return "/repo"; + if (args.join(" ") === "rev-parse --verify HEAD") return "abc123"; + return undefined; + }, + realpath: (value) => value, + }); + + expect(metadata).toEqual({ + packageName: "@tailor-platform/sdk-codemod", + packageVersion: "0.3.0-next.2", + gitCommit: "abc123", + localBuildCommand: "pnpm --dir packages/sdk-codemod build", + }); + }); + + test("does not report the consuming project's commit for an installed package", () => { + const metadata = createRunnerMetadata({ + ...packageInfo, + packageRoot: "/project/node_modules/@tailor-platform/sdk-codemod", + readGit: (_cwd, args) => { + if (args.join(" ") === "rev-parse --show-toplevel") return "/project"; + if (args.join(" ") === "rev-parse --verify HEAD") return "project-commit"; + return undefined; + }, + realpath: (value) => value, + }); + + expect(metadata).toEqual({ + packageName: "@tailor-platform/sdk-codemod", + packageVersion: "0.3.0-next.2", + }); + }); +}); diff --git a/packages/sdk-codemod/src/runner-metadata.ts b/packages/sdk-codemod/src/runner-metadata.ts new file mode 100644 index 000000000..d77901ccb --- /dev/null +++ b/packages/sdk-codemod/src/runner-metadata.ts @@ -0,0 +1,68 @@ +import { execFileSync } from "node:child_process"; +import { realpathSync } from "node:fs"; +import * as path from "pathe"; + +export interface RunnerMetadata { + packageName: string; + packageVersion: string; + gitCommit?: string; + localBuildCommand?: string; +} + +interface CreateRunnerMetadataOptions { + packageName: string; + packageVersion: string; + packageRoot: string; + readGit?: (cwd: string, args: string[]) => string | undefined; + realpath?: (value: string) => string; +} + +const SOURCE_PACKAGE_PATH = "packages/sdk-codemod"; +const LOCAL_BUILD_COMMAND = "pnpm --dir packages/sdk-codemod build"; + +export function createRunnerMetadata({ + packageName, + packageVersion, + packageRoot, + readGit = readGitOutput, + realpath = safeRealpath, +}: CreateRunnerMetadataOptions): RunnerMetadata { + const metadata: RunnerMetadata = { packageName, packageVersion }; + const gitRoot = readGit(packageRoot, ["rev-parse", "--show-toplevel"]); + if (!gitRoot) return metadata; + + const packagePathFromRoot = path + .normalize(path.relative(realpath(gitRoot), realpath(packageRoot))) + .replaceAll("\\", "/"); + + if (packagePathFromRoot !== SOURCE_PACKAGE_PATH) return metadata; + + const gitCommit = readGit(packageRoot, ["rev-parse", "--verify", "HEAD"]); + if (!gitCommit) return metadata; + + return { + ...metadata, + gitCommit, + localBuildCommand: LOCAL_BUILD_COMMAND, + }; +} + +function readGitOutput(cwd: string, args: string[]): string | undefined { + try { + const output = execFileSync("git", ["-C", cwd, ...args], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + return output || undefined; + } catch { + return undefined; + } +} + +function safeRealpath(value: string): string { + try { + return realpathSync(value); + } catch { + return path.resolve(value); + } +} diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index 34965ebd6..6cd50238f 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -1,3 +1,5 @@ +import type { RunnerMetadata } from "./runner-metadata"; + /** A before/after code pair shown in the generated migration doc. */ export interface CodemodExample { /** Code as written before the migration. */ @@ -96,6 +98,7 @@ export interface LlmReview { * JSON output written to stdout by the sdk-codemod CLI. */ export interface RunOutput { + runner: RunnerMetadata; codemodsApplied: number; codemodsSkipped: number; filesModified: string[]; From 006a5884583f23fc6852714c41e58a7ab6d65a5a Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 13:43:47 +0900 Subject: [PATCH 274/618] feat: add runtime idp wrapper codemod --- .changeset/runtime-idp-wrapper.md | 5 + .../v2/runtime-globals-opt-in/codemod.yaml | 7 + .../scripts/transform.ts | 260 ++++++++++++++++++ .../tests/basic/expected.ts | 6 + .../tests/basic/input.ts | 4 + .../tests/existing-idp-import/expected.ts | 6 + .../tests/existing-idp-import/input.ts | 6 + .../tests/existing-runtime-import/expected.ts | 7 + .../tests/existing-runtime-import/input.ts | 7 + .../tests/local-idp/input.ts | 6 + .../tests/local-tailor/input.ts | 6 + .../tests/string-literal/input.ts | 1 + .../tests/type-only-idp/input.ts | 6 + packages/sdk-codemod/src/registry.test.ts | 3 +- packages/sdk-codemod/src/registry.ts | 12 +- packages/sdk-codemod/src/transform.test.ts | 4 + packages/sdk-codemod/tsdown.config.ts | 2 + packages/sdk/docs/migration/v2.md | 15 +- 18 files changed, 350 insertions(+), 13 deletions(-) create mode 100644 .changeset/runtime-idp-wrapper.md create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/basic/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/basic/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-idp-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-idp-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-runtime-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-runtime-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-idp/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-tailor/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/string-literal/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp/input.ts diff --git a/.changeset/runtime-idp-wrapper.md b/.changeset/runtime-idp-wrapper.md new file mode 100644 index 000000000..58c7001a3 --- /dev/null +++ b/.changeset/runtime-idp-wrapper.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Automatically migrate simple direct `tailor.idp.Client` runtime global usage to the typed `idp.Client` wrapper during v2 upgrades. diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/codemod.yaml b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/codemod.yaml new file mode 100644 index 000000000..a00bf5058 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/runtime-globals-opt-in" +version: "1.0.0" +description: "Rewrite simple direct tailor.idp.Client runtime global usage to the typed idp wrapper and flag broader runtime globals for review" +engine: jssg +language: typescript +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts new file mode 100644 index 000000000..3874b54d4 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts @@ -0,0 +1,260 @@ +import { parse, Lang } from "@ast-grep/napi"; +import type { Edit, SgNode } from "@ast-grep/napi"; + +const RUNTIME_MODULE = "@tailor-platform/sdk/runtime"; +const TAILOR_IDP_CLIENT = "tailor.idp.Client"; + +interface ImportBinding { + localName: string; + importedName?: string; + source: string; + typeOnly: boolean; +} + +function quickFilter(source: string): boolean { + return source.includes(TAILOR_IDP_CLIENT); +} + +function sourceLang(filePath: string, source: string): Lang { + return filePath.endsWith(".tsx") || filePath.endsWith(".jsx") || source.includes(" child.kind() === "type"); +} + +function importSource(stmt: SgNode): string | null { + const source = stmt.children().find((child) => child.kind() === "string"); + return stringValue(source ?? null); +} + +function namedImportsNode(importStmt: SgNode): SgNode | null { + return importStmt.find({ rule: { kind: "named_imports" } }) ?? null; +} + +function importSpecNames( + spec: SgNode, +): { importedName: string; localName: string; typeOnly: boolean } | null { + const ids = spec.children().filter((child) => child.kind() === "identifier"); + if (ids.length === 0) return null; + return { + importedName: ids[0]!.text(), + localName: ids[1]?.text() ?? ids[0]!.text(), + typeOnly: spec.children().some((child) => child.kind() === "type"), + }; +} + +function importBindings(importStmt: SgNode): ImportBinding[] { + const source = importSource(importStmt); + if (!source) return []; + + const typeOnly = isTypeOnlyImport(importStmt); + const clause = importStmt.children().find((child) => child.kind() === "import_clause"); + if (!clause) return []; + + const bindings: ImportBinding[] = []; + for (const child of clause.children()) { + if (child.kind() === "identifier") { + bindings.push({ localName: child.text(), source, typeOnly }); + continue; + } + + if (child.kind() === "namespace_import") { + const local = child.children().find((c) => c.kind() === "identifier"); + if (local) bindings.push({ localName: local.text(), source, typeOnly }); + continue; + } + + if (child.kind() !== "named_imports") continue; + for (const spec of child.findAll({ rule: { kind: "import_specifier" } })) { + const names = importSpecNames(spec); + if (!names) continue; + bindings.push({ ...names, source, typeOnly: typeOnly || names.typeOnly }); + } + } + + return bindings; +} + +function collectBindingNames(node: SgNode, out: Set): void { + const kind = node.kind(); + if ( + kind === "identifier" || + kind === "type_identifier" || + kind === "shorthand_property_identifier_pattern" + ) { + out.add(node.text()); + return; + } + + for (const child of node.children()) { + if (child.kind() === "property_identifier") continue; + collectBindingNames(child, out); + } +} + +function firstDeclaratorChild(node: SgNode): SgNode | null { + return node.children().find((child) => child.kind() !== "=") ?? null; +} + +function localDeclarationNames(root: SgNode): Set { + const names = new Set(); + + for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { + const binding = firstDeclaratorChild(decl); + if (binding) collectBindingNames(binding, names); + } + + for (const param of root.findAll({ + rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] }, + })) { + const binding = param + .children() + .find((child) => + ["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind()), + ); + if (binding) collectBindingNames(binding, names); + } + + for (const decl of root.findAll({ + rule: { + any: [ + { kind: "function_declaration" }, + { kind: "class_declaration" }, + { kind: "interface_declaration" }, + { kind: "type_alias_declaration" }, + { kind: "enum_declaration" }, + ], + }, + })) { + const name = decl + .children() + .find((child) => child.kind() === "identifier" || child.kind() === "type_identifier"); + if (name) names.add(name.text()); + } + + return names; +} + +function findImportStatements(root: SgNode): SgNode[] { + return root + .findAll({ rule: { kind: "import_statement" } }) + .toSorted((a, b) => a.range().start.index - b.range().start.index); +} + +function runtimeIdpLocalName(imports: SgNode[]): string | null { + for (const importStmt of imports) { + for (const binding of importBindings(importStmt)) { + if ( + binding.source === RUNTIME_MODULE && + binding.importedName === "idp" && + !binding.typeOnly + ) { + return binding.localName; + } + } + } + return null; +} + +function hasCollision(imports: SgNode[], localNames: Set): boolean { + if (localNames.has("tailor") || localNames.has("idp")) return true; + + for (const importStmt of imports) { + for (const binding of importBindings(importStmt)) { + if (binding.localName === "tailor") return true; + if ( + binding.localName === "idp" && + !(binding.source === RUNTIME_MODULE && binding.importedName === "idp" && !binding.typeOnly) + ) { + return true; + } + } + } + + return false; +} + +function runtimeNamedValueImport(imports: SgNode[]): SgNode | null { + return ( + imports.find( + (stmt) => + importSource(stmt) === RUNTIME_MODULE && !isTypeOnlyImport(stmt) && namedImportsNode(stmt), + ) ?? null + ); +} + +function importInsertionIndex(imports: SgNode[], source: string): number { + const lastImport = imports.at(-1); + if (lastImport) return lastImport.range().end.index; + + if (source.startsWith("#!")) { + const newlineIndex = source.indexOf("\n"); + return newlineIndex === -1 ? source.length : newlineIndex + 1; + } + + return 0; +} + +function buildAddRuntimeImportEdit(source: string, imports: SgNode[]): Edit { + const existingRuntimeImport = runtimeNamedValueImport(imports); + const namedImports = existingRuntimeImport ? namedImportsNode(existingRuntimeImport) : null; + if (namedImports) { + const specTexts = namedImports + .findAll({ rule: { kind: "import_specifier" } }) + .map((spec) => spec.text()); + return namedImports.replace(`{ ${[...specTexts, "idp"].join(", ")} }`); + } + + const pos = importInsertionIndex(imports, source); + const insertedText = + pos === 0 || (pos > 0 && source[pos - 1] === "\n") + ? `import { idp } from "${RUNTIME_MODULE}";\n\n` + : `\nimport { idp } from "${RUNTIME_MODULE}";`; + return { startPos: pos, endPos: pos, insertedText }; +} + +function findTailorIdpClientConstructors(root: SgNode): SgNode[] { + return root + .findAll({ rule: { kind: "new_expression" } }) + .map((node) => node.field("constructor")) + .filter((node): node is SgNode => node?.text() === TAILOR_IDP_CLIENT); +} + +/** + * Rewrite direct `new tailor.idp.Client(...)` calls to the typed runtime + * wrapper. Files with local `tailor` or conflicting `idp` bindings are left + * unchanged for the runtime-globals review prompt. + * @param source - File contents + * @param filePath - Absolute path to the file + * @returns Transformed source or null when nothing matched. + */ +export default function transform(source: string, filePath: string): string | null { + if (!quickFilter(source)) return null; + + const root = parse(sourceLang(filePath, source), source).root(); + const constructors = findTailorIdpClientConstructors(root); + if (constructors.length === 0) return null; + + const imports = findImportStatements(root); + if (hasCollision(imports, localDeclarationNames(root))) return null; + + const existingIdpLocal = runtimeIdpLocalName(imports); + const idpLocal = existingIdpLocal ?? "idp"; + const edits: Edit[] = constructors.map((constructor) => + constructor.replace(`${idpLocal}.Client`), + ); + + if (!existingIdpLocal) { + edits.push(buildAddRuntimeImportEdit(source, imports)); + } + + const result = root.commitEdits(edits); + return result === source ? null : result; +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/basic/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/basic/expected.ts new file mode 100644 index 000000000..2f61259b2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/basic/expected.ts @@ -0,0 +1,6 @@ +import { idp } from "@tailor-platform/sdk/runtime"; + +export async function run() { + const client = new idp.Client({ namespace: "default" }); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/basic/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/basic/input.ts new file mode 100644 index 000000000..e21a2970c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/basic/input.ts @@ -0,0 +1,4 @@ +export async function run() { + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-idp-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-idp-import/expected.ts new file mode 100644 index 000000000..2f61259b2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-idp-import/expected.ts @@ -0,0 +1,6 @@ +import { idp } from "@tailor-platform/sdk/runtime"; + +export async function run() { + const client = new idp.Client({ namespace: "default" }); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-idp-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-idp-import/input.ts new file mode 100644 index 000000000..a516d8d50 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-idp-import/input.ts @@ -0,0 +1,6 @@ +import { idp } from "@tailor-platform/sdk/runtime"; + +export async function run() { + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-runtime-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-runtime-import/expected.ts new file mode 100644 index 000000000..025c22600 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-runtime-import/expected.ts @@ -0,0 +1,7 @@ +import { workflow, idp } from "@tailor-platform/sdk/runtime"; + +export async function run() { + await workflow.wait("ready"); + const client = new idp.Client({ namespace: "default" }); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-runtime-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-runtime-import/input.ts new file mode 100644 index 000000000..dfd6409fb --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/existing-runtime-import/input.ts @@ -0,0 +1,7 @@ +import { workflow } from "@tailor-platform/sdk/runtime"; + +export async function run() { + await workflow.wait("ready"); + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-idp/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-idp/input.ts new file mode 100644 index 000000000..61dd769f6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-idp/input.ts @@ -0,0 +1,6 @@ +const idp = createLocalIdp(); + +export async function run() { + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-tailor/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-tailor/input.ts new file mode 100644 index 000000000..81d7567de --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/local-tailor/input.ts @@ -0,0 +1,6 @@ +const tailor = createLocalTailor(); + +export async function run() { + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/string-literal/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/string-literal/input.ts new file mode 100644 index 000000000..f95c1c608 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/string-literal/input.ts @@ -0,0 +1 @@ +export const source = 'const client = new tailor.idp.Client({ namespace: "default" });'; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp/input.ts new file mode 100644 index 000000000..7ab633df1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp/input.ts @@ -0,0 +1,6 @@ +import { type idp } from "@tailor-platform/sdk/runtime"; + +export async function run() { + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 301045f83..71b4dd2a3 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -58,6 +58,7 @@ describe("getApplicableCodemods", () => { test("flags CommonJS TypeScript files for runtime globals review", () => { const codemod = allCodemods.find((entry) => entry.id === "v2/runtime-globals-opt-in"); + expect(codemod?.scriptPath).toBe("v2/runtime-globals-opt-in/scripts/transform.js"); expect(codemod?.filePatterns).toContain("**/*.{ts,tsx,mts,cts}"); expect(codemod?.suspiciousPatterns).toContain("tailor.idp"); expect(codemod?.prompt).toContain("@tailor-platform/sdk/runtime/globals"); @@ -66,7 +67,7 @@ describe("getApplicableCodemods", () => { test("leads runtime globals migration with the typed wrappers", () => { const codemod = allCodemods.find((entry) => entry.id === "v2/runtime-globals-opt-in"); - expect(codemod?.prompt).toContain('import { idp } from "@tailor-platform/sdk/runtime"'); + expect(codemod?.prompt).toContain("new idp.Client(...)"); expect(codemod?.examples?.[0]?.after).toContain( 'import { idp } from "@tailor-platform/sdk/runtime"', ); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index fd4640a7c..d89a9f7bb 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -432,9 +432,10 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/runtime-globals-opt-in", name: "Ambient runtime globals are opt-in", description: - 'Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Normal SDK development does not need them — use the SDK APIs and the typed wrappers from `@tailor-platform/sdk/runtime`. Only if you relied on the ambient globals directly, add `import "@tailor-platform/sdk/runtime/globals"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.)', + 'Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. The codemod rewrites simple direct `new tailor.idp.Client(...)` calls to the typed `idp.Client` wrapper from `@tailor-platform/sdk/runtime`; broader runtime global usage remains review-only. Only if you relied on the ambient globals directly, add `import "@tailor-platform/sdk/runtime/globals"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.)', since: "1.0.0", until: "2.0.0", + scriptPath: "v2/runtime-globals-opt-in/scripts/transform.js", filePatterns: ["**/*.{ts,tsx,mts,cts}"], suspiciousPatterns: [ "tailor.context", @@ -472,10 +473,11 @@ export const allCodemods: CodemodPackage[] = [ "The v2 SDK no longer enables ambient Tailor runtime globals from", "`@tailor-platform/sdk`. For each flagged file that uses `tailor.*`,", "`tailordb.*`, or Tailor runtime error globals, prefer migrating to the", - "typed wrappers from `@tailor-platform/sdk/runtime` (e.g. replace", - '`new tailor.idp.Client()` with `import { idp } from "@tailor-platform/sdk/runtime"`', - "and `new idp.Client({ namespace })`). The wrappers are self-contained, so the", - "ambient globals are no longer needed.", + "typed wrappers from `@tailor-platform/sdk/runtime`. The codemod already", + "rewrites direct `new tailor.idp.Client(...)` calls to `new idp.Client(...)`", + "when the file has no conflicting `tailor` or `idp` binding. For any remaining", + "`tailor.idp.Client` references, either resolve the binding collision and use", + "`idp.Client`, or keep the ambient global deliberately.", "", "Only when the file must keep referencing the bare `tailor.*` names directly,", "opt into the global declarations instead by adding one of these:", diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index 9699f5fa8..99fdcc58e 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -103,6 +103,10 @@ describe("codemod transforms", () => { await expect(runFixtureCases("v2/tailordb-namespace")).resolves.toBeUndefined(); }); + test("v2/runtime-globals-opt-in transforms correctly", async () => { + await expect(runFixtureCases("v2/runtime-globals-opt-in")).resolves.toBeUndefined(); + }); + test("v2/execute-script-arg transforms correctly", async () => { await expect(runFixtureCases("v2/execute-script-arg")).resolves.toBeUndefined(); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index 8c930336e..ae8bb3597 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -32,6 +32,8 @@ export default defineConfig([ "codemods/v2/auth-invoker-unwrap/scripts/transform.ts", "v2/tailordb-namespace/scripts/transform": "codemods/v2/tailordb-namespace/scripts/transform.ts", + "v2/runtime-globals-opt-in/scripts/transform": + "codemods/v2/runtime-globals-opt-in/scripts/transform.ts", "v2/execute-script-arg/scripts/transform": "codemods/v2/execute-script-arg/scripts/transform.ts", "v2/rename-bin/scripts/transform": "codemods/v2/rename-bin/scripts/transform.ts", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index d8954d6fc..ed97ecea3 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -478,9 +478,9 @@ downloadStream and returns FileDownloadStreamResponse. ## Ambient runtime globals are opt-in -**Migration:** Manual +**Migration:** Partially automatic -Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Normal SDK development does not need them — use the SDK APIs and the typed wrappers from `@tailor-platform/sdk/runtime`. Only if you relied on the ambient globals directly, add `import "@tailor-platform/sdk/runtime/globals"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.) +Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. The codemod rewrites simple direct `new tailor.idp.Client(...)` calls to the typed `idp.Client` wrapper from `@tailor-platform/sdk/runtime`; broader runtime global usage remains review-only. Only if you relied on the ambient globals directly, add `import "@tailor-platform/sdk/runtime/globals"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.) Preferred: switch to the typed wrappers from `@tailor-platform/sdk/runtime` and drop the ambient globals: @@ -513,16 +513,17 @@ const client = new tailor.idp.Client(); ```
-Prompt for an AI agent (to perform this migration) +Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text The v2 SDK no longer enables ambient Tailor runtime globals from `@tailor-platform/sdk`. For each flagged file that uses `tailor.*`, `tailordb.*`, or Tailor runtime error globals, prefer migrating to the -typed wrappers from `@tailor-platform/sdk/runtime` (e.g. replace -`new tailor.idp.Client()` with `import { idp } from "@tailor-platform/sdk/runtime"` -and `new idp.Client({ namespace })`). The wrappers are self-contained, so the -ambient globals are no longer needed. +typed wrappers from `@tailor-platform/sdk/runtime`. The codemod already +rewrites direct `new tailor.idp.Client(...)` calls to `new idp.Client(...)` +when the file has no conflicting `tailor` or `idp` binding. For any remaining +`tailor.idp.Client` references, either resolve the binding collision and use +`idp.Client`, or keep the ambient global deliberately. Only when the file must keep referencing the bare `tailor.*` names directly, opt into the global declarations instead by adding one of these: From 1c1ca499b4fd55a616b1531ec7ab280ceed531d3 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 13:46:07 +0900 Subject: [PATCH 275/618] fix: report precise principal-unify review findings --- .changeset/principal-unify-review-findings.md | 5 + .../v2/principal-unify/scripts/transform.ts | 298 ++++++++++++++++++ packages/sdk-codemod/src/index.ts | 15 +- .../src/principal-unify-review.test.ts | 126 ++++++++ packages/sdk-codemod/src/runner.ts | 74 ++++- packages/sdk-codemod/src/types.ts | 27 ++ 6 files changed, 531 insertions(+), 14 deletions(-) create mode 100644 .changeset/principal-unify-review-findings.md create mode 100644 packages/sdk-codemod/src/principal-unify-review.test.ts diff --git a/.changeset/principal-unify-review-findings.md b/.changeset/principal-unify-review-findings.md new file mode 100644 index 000000000..c9bab5eff --- /dev/null +++ b/.changeset/principal-unify-review-findings.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Report precise file-local findings for `principal-unify` review follow-ups, including nullable caller call sites and `context.user` helper adapters. diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 83464d911..a27f531e1 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -1,4 +1,5 @@ import { parse, Lang } from "@ast-grep/napi"; +import type { LlmReviewFinding } from "../../../../src/types"; import type { Edit, SgNode } from "@ast-grep/napi"; const TYPE_RENAME_MAP: Record = { @@ -2162,6 +2163,303 @@ function transformParseArgsObject( } } +const KYSELY_PREDICATE_METHODS = new Set(["where", "having", "on"]); + +function excerptForLine(source: string, line: number): string { + const excerpt = (source.split(/\r?\n/)[line - 1] ?? "").trim(); + return excerpt.length > 160 ? `${excerpt.slice(0, 157)}...` : excerpt; +} + +function addReviewFinding( + findings: LlmReviewFinding[], + seen: Set, + source: string, + file: string, + node: SgNode, + message: string, +): void { + const line = node.range().start.line + 1; + const excerpt = excerptForLine(source, line); + const key = `${file}:${line}:${message}:${excerpt}`; + if (seen.has(key)) return; + seen.add(key); + findings.push({ file, line, message, excerpt }); +} + +function isCallerOptionalMemberExpression(node: SgNode): boolean { + if (node.kind() !== "member_expression") return false; + if (!node.children().some((child) => child.kind() === "optional_chain")) return false; + const object = node.field("object"); + if (!object) return false; + if (object.kind() === "identifier") return object.text() === "caller"; + if (object.kind() !== "member_expression") return false; + return object.field("property")?.text() === "caller"; +} + +function nodeContainsArgumentCallerOptionalAccess(node: SgNode): boolean { + if (isFunctionNode(node)) return false; + if (isCallerOptionalMemberExpression(node)) return true; + return node.children().some((child) => nodeContainsArgumentCallerOptionalAccess(child)); +} + +function reviewCallName(call: SgNode): string { + const fn = call.field("function"); + if (!fn) return "a call"; + if (fn.kind() === "identifier") return `${fn.text()}()`; + if (fn.kind() === "member_expression") { + const property = fn.field("property"); + if (property) return `${property.text()}()`; + } + return "a call"; +} + +function collectNullableCallerReviewFindings( + root: SgNode, + source: string, + file: string, + findings: LlmReviewFinding[], + seen: Set, +): void { + const calls = root.findAll({ rule: { kind: "call_expression" } }); + for (const call of calls) { + const args = call.field("arguments"); + const nullableArg = args + ?.children() + .find((child) => nodeContainsArgumentCallerOptionalAccess(child)); + if (!nullableArg) continue; + + const memberName = findMemberCallName(call); + if (memberName && KYSELY_PREDICATE_METHODS.has(memberName)) { + addReviewFinding( + findings, + seen, + source, + file, + nullableArg, + "Nullable caller value is used as a Kysely predicate value.", + ); + continue; + } + + addReviewFinding( + findings, + seen, + source, + file, + nullableArg, + `Nullable caller value is passed as a non-null argument to ${reviewCallName(call)}.`, + ); + } +} + +function functionIdentifierParamName(fn: SgNode): string | null { + const param = getFirstFunctionParam(fn); + const pattern = param ? getFunctionParamPattern(param) : null; + return pattern?.kind() === "identifier" ? pattern.text() : null; +} + +function functionReadsContextUser(fn: SgNode, contextName: string): boolean { + const body = fn.field("body"); + if (!body) return false; + const shadowRanges = collectCtxShadowRanges(body, contextName, fn); + const userProperties = body.findAll({ + rule: { kind: "property_identifier", regex: "^user$" }, + }); + for (const propId of userProperties) { + const parent = propId.parent(); + if (!parent || parent.kind() !== "member_expression") continue; + const object = parent.field("object"); + if (!object || object.kind() !== "identifier" || object.text() !== contextName) continue; + if (isInsideAnyRange(object.range().start.index, shadowRanges)) continue; + return true; + } + return false; +} + +type ContextUserHelperBinding = LocalCallbackBinding & { contextName: string }; + +function collectContextUserHelperBindings(root: SgNode): ContextUserHelperBinding[] { + return collectLocalCallbackBindings(root).flatMap((binding) => { + const contextName = functionIdentifierParamName(binding.fn); + if (!contextName || !functionReadsContextUser(binding.fn, contextName)) return []; + return [{ ...binding, contextName }]; + }); +} + +function resolveContextUserHelperBinding( + node: SgNode, + bindings: ContextUserHelperBinding[], + root: SgNode, +): ContextUserHelperBinding | null { + if (node.kind() !== "identifier") return null; + const pos = node.range().start.index; + return ( + bindings.find( + (binding) => + binding.name === node.text() && + rangeContains(binding.scope, pos) && + !isShadowedLocalReference(root, binding.name, pos, binding.bindingStart), + ) ?? null + ); +} + +interface ResolverContextBody { + arrow: SgNode; + body: SgNode; + contextName: string; +} + +function addResolverContextBody(arrow: SgNode, bodies: ResolverContextBody[]): void { + const paramName = functionIdentifierParamName(arrow); + const body = arrow.field("body"); + if (!paramName || !body) return; + bodies.push({ arrow, body, contextName: paramName }); +} + +function collectResolverContextBodies(root: SgNode): ResolverContextBody[] { + const sdkImports = root.findAll({ + rule: { + kind: "import_statement", + has: { kind: "string", regex: "^[\"']@tailor-platform/sdk(/test)?[\"']$" }, + }, + }); + const createResolverLocalNames = new Set(); + const sdkNamespaceNames = new Set(); + for (const importStmt of sdkImports) { + for (const namespaceName of iterateNamespaceImportLocalNames(importStmt)) { + sdkNamespaceNames.add(namespaceName); + } + for (const { importedName, localName } of iterateImportSpecs(importStmt)) { + if (importedName === "createResolver") createResolverLocalNames.add(localName); + } + } + + const bodies: ResolverContextBody[] = []; + for (const localName of createResolverLocalNames) { + const shadowRanges = collectAllShadowRanges(root, localName); + const calls = root.findAll({ + rule: { + kind: "call_expression", + has: { + field: "function", + kind: "identifier", + regex: `^${escapeRegex(localName)}$`, + }, + }, + }); + for (const call of calls) { + const callee = call.field("function"); + if (!callee || isInsideAnyRange(callee.range().start.index, shadowRanges)) continue; + const arrow = findResolverBodyArrow(call); + if (arrow) addResolverContextBody(arrow, bodies); + } + } + + for (const namespaceName of sdkNamespaceNames) { + const shadowRanges = collectAllShadowRanges(root, namespaceName); + const calls = root.findAll({ + rule: { + kind: "call_expression", + has: { + field: "function", + kind: "member_expression", + has: { + field: "property", + kind: "property_identifier", + regex: "^createResolver$", + }, + }, + }, + }); + for (const call of calls) { + const callee = call.field("function"); + const object = callee?.field("object"); + if (!object || object.kind() !== "identifier" || object.text() !== namespaceName) continue; + if (isInsideAnyRange(object.range().start.index, shadowRanges)) continue; + const arrow = findResolverBodyArrow(call); + if (arrow) addResolverContextBody(arrow, bodies); + } + } + return bodies; +} + +function firstIdentifierArgument(call: SgNode): SgNode | null { + const args = call.field("arguments"); + if (!args) return null; + for (const child of args.children()) { + if (child.kind() === "(" || child.kind() === ")" || child.kind() === ",") continue; + return child.kind() === "identifier" ? child : null; + } + return null; +} + +function collectContextUserHelperReviewFindings( + root: SgNode, + source: string, + file: string, + findings: LlmReviewFinding[], + seen: Set, +): void { + const helperBindings = collectContextUserHelperBindings(root); + if (helperBindings.length === 0) return; + + const reportedDefinitions = new Set(); + for (const { arrow, body, contextName } of collectResolverContextBodies(root)) { + const shadowRanges = collectCtxShadowRanges(body, contextName, arrow); + const calls = body.findAll({ rule: { kind: "call_expression" } }); + for (const call of calls) { + if (isInsideAnyRange(call.range().start.index, shadowRanges)) continue; + const arg = firstIdentifierArgument(call); + if (!arg || arg.text() !== contextName) continue; + const callee = call.field("function"); + if (!callee) continue; + const helper = resolveContextUserHelperBinding(callee, helperBindings, root); + if (!helper) continue; + + if (!reportedDefinitions.has(helper.bindingStart)) { + reportedDefinitions.add(helper.bindingStart); + addReviewFinding( + findings, + seen, + source, + file, + helper.fn, + `Helper adapter ${helper.name} reads ${helper.contextName}.user and needs v2 caller/invoker semantics.`, + ); + } + addReviewFinding( + findings, + seen, + source, + file, + call, + `${helper.name}(${contextName}) passes an SDK resolver context into a helper that reads ${helper.contextName}.user.`, + ); + } + } +} + +export function reviewFindings( + source: string, + _filePath: string, + relativePath: string, +): LlmReviewFinding[] { + if (!source.includes("caller?.") && !source.includes(".user")) return []; + + let root: SgNode; + try { + root = parse(Lang.TypeScript, source).root(); + } catch { + return []; + } + + const findings: LlmReviewFinding[] = []; + const seen = new Set(); + collectNullableCallerReviewFindings(root, source, relativePath, findings, seen); + collectContextUserHelperReviewFindings(root, source, relativePath, findings, seen); + return findings; +} + /** * Migrate user/actor/invoker types and identifiers to the unified TailorPrincipal. * diff --git a/packages/sdk-codemod/src/index.ts b/packages/sdk-codemod/src/index.ts index 183a06c5a..6ab692e36 100644 --- a/packages/sdk-codemod/src/index.ts +++ b/packages/sdk-codemod/src/index.ts @@ -54,8 +54,18 @@ function printLlmReview(review: LlmReview): void { ? "the codemod cannot safely migrate these automatically" : "review the project for this manual change"; process.stderr.write(`\n🤖 LLM-assisted review suggested (${review.codemodId}) — ${scope}:\n`); + const findingsByFile = new Map>(); + for (const finding of review.findings ?? []) { + const findings = findingsByFile.get(finding.file) ?? []; + findings.push(finding); + findingsByFile.set(finding.file, findings); + } for (const file of review.files) { process.stderr.write(` - ${file}\n`); + for (const finding of findingsByFile.get(file) ?? []) { + process.stderr.write(` - line ${finding.line}: ${finding.message}\n`); + process.stderr.write(` ${finding.excerpt}\n`); + } } process.stderr.write(`\nPrompt for an LLM:\n${review.prompt.trim()}\n`); } @@ -70,8 +80,9 @@ const main = defineCommand({ - \`filesModified\`: files a codemod changed - \`warnings\`: files that may still need manual migration - \`llmReviews\`: changes the codemods could not fully migrate on their own. Each - entry has the affected \`files\` and a \`prompt\` — hand the prompt and files to - an LLM (or follow it yourself) to finish those cases. + entry has the affected \`files\`, optional file-local \`findings\`, and a + \`prompt\` — hand the prompt and files to an LLM (or follow it yourself) to + finish those cases. Progress, warnings, and the LLM-review prompts are also printed to \`stderr\` in human-readable form, so \`stdout\` stays pure JSON for piping.`, diff --git a/packages/sdk-codemod/src/principal-unify-review.test.ts b/packages/sdk-codemod/src/principal-unify-review.test.ts new file mode 100644 index 000000000..710877e9d --- /dev/null +++ b/packages/sdk-codemod/src/principal-unify-review.test.ts @@ -0,0 +1,126 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "pathe"; +import { afterEach, describe, expect, test } from "vitest"; +import { allCodemods } from "./registry"; +import { runCodemods } from "./runner"; + +const CODEMODS_DIR = path.resolve(__dirname, "../codemods"); + +const principalUnify = allCodemods.find((codemod) => codemod.id === "v2/principal-unify"); + +if (!principalUnify?.scriptPath) { + throw new Error("v2/principal-unify codemod is not registered with a script"); +} + +const principalUnifyEntry = { + codemod: principalUnify, + scriptPath: path.join(CODEMODS_DIR, principalUnify.scriptPath.replace(/\.js$/, ".ts")), +}; + +describe("principal-unify review findings", () => { + let tmpDir: string | undefined; + + afterEach(async () => { + if (tmpDir) { + await fs.promises.rm(tmpDir, { recursive: true, force: true }); + tmpDir = undefined; + } + }); + + async function writeProjectFile(relative: string, source: string): Promise { + tmpDir ??= await fs.promises.mkdtemp(path.join(os.tmpdir(), "principal-review-test-")); + const file = path.join(tmpDir, relative); + await fs.promises.mkdir(path.dirname(file), { recursive: true }); + await fs.promises.writeFile(file, source, "utf-8"); + } + + test("reports nullable caller values passed to non-null-looking calls", async () => { + await writeProjectFile( + "resolvers/order.ts", + [ + 'import { createResolver } from "@tailor-platform/sdk";', + "", + "declare const db: any;", + "declare function publishAudit(userId: string): Promise;", + "", + "export const resolver = createResolver({", + " body: async (context) => {", + ' await db.selectFrom("orders").where("createdBy", "=", context.user.id).execute();', + " await publishAudit(context.user.id);", + " const maybeId = context.user.id;", + " return maybeId;", + " },", + "});", + "", + ].join("\n"), + ); + + const result = await runCodemods([principalUnifyEntry], tmpDir!, false); + + expect(result.llmReviews).toEqual([ + expect.objectContaining({ + codemodId: "v2/principal-unify", + files: ["resolvers/order.ts"], + findings: [ + expect.objectContaining({ + file: "resolvers/order.ts", + line: 8, + message: expect.stringContaining("Kysely predicate"), + excerpt: expect.stringContaining("context.caller?.id"), + }), + expect.objectContaining({ + file: "resolvers/order.ts", + line: 9, + message: expect.stringContaining("non-null argument"), + excerpt: expect.stringContaining("publishAudit(context.caller?.id)"), + }), + ], + }), + ]); + }); + + test("reports context.user helper adapters called with resolver contexts", async () => { + await writeProjectFile( + "resolvers/customer.ts", + [ + 'import { createResolver } from "@tailor-platform/sdk";', + "", + "function createContext(context: any) {", + " return {", + " userId: context.user.id,", + " userType: context.user.type,", + " };", + "}", + "", + "export const resolver = createResolver({", + " body: async (context) => createContext(context),", + "});", + "", + ].join("\n"), + ); + + const result = await runCodemods([principalUnifyEntry], tmpDir!, false); + + expect(result.llmReviews).toEqual([ + expect.objectContaining({ + codemodId: "v2/principal-unify", + files: ["resolvers/customer.ts"], + findings: [ + expect.objectContaining({ + file: "resolvers/customer.ts", + line: 3, + message: expect.stringContaining("createContext"), + excerpt: expect.stringContaining("function createContext"), + }), + expect.objectContaining({ + file: "resolvers/customer.ts", + line: 11, + message: expect.stringContaining("createContext"), + excerpt: expect.stringContaining("createContext(context)"), + }), + ], + }), + ]); + }); +}); diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 4bf94a544..3bc036443 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -5,7 +5,14 @@ import chalk from "chalk"; import { structuredPatch } from "diff"; import * as path from "pathe"; import picomatch from "picomatch"; -import type { CodemodPackage, CodemodPattern, CodemodPatternGroup, LlmReview } from "./types"; +import type { + CodemodPackage, + CodemodPattern, + CodemodPatternGroup, + LlmReview, + LlmReviewFinding, + ReviewFindingsFn, +} from "./types"; import type { SgNode } from "@ast-grep/napi"; /** @@ -119,14 +126,22 @@ function printDiff(filePath: string, before: string, after: string): void { * Load a transform module from a TypeScript file path. * Expects the module to have a default export that is a TransformFn. * @param scriptPath - Absolute path to the transform script - * @returns The transform function + * @returns The transform function and optional review detector */ -async function loadTransform(scriptPath: string): Promise { +async function loadTransformModule( + scriptPath: string, +): Promise<{ transform: TransformFn; reviewFindings?: ReviewFindingsFn }> { const mod = await import(url.pathToFileURL(scriptPath).href); if (typeof mod.default !== "function") { throw new Error(`Transform at ${scriptPath} does not have a default export function`); } - return mod.default as TransformFn; + return { + transform: mod.default as TransformFn, + reviewFindings: + typeof mod.reviewFindings === "function" + ? (mod.reviewFindings as ReviewFindingsFn) + : undefined, + }; } /** A loaded transform with its file matcher. */ @@ -134,6 +149,7 @@ interface LoadedTransform { id: string; /** Undefined for codemod-less ("manual") entries that ship only guidance. */ transform?: TransformFn; + reviewFindings?: ReviewFindingsFn; matches: (relativePath: string) => boolean; legacyPatterns: CodemodPatternGroup[]; sourceStringLegacyPatterns: CodemodPatternGroup[]; @@ -285,6 +301,15 @@ function legacyPatternWarnings( }); } +function compareReviewFindings(a: LlmReviewFinding, b: LlmReviewFinding): number { + return ( + a.file.localeCompare(b.file) || + a.line - b.line || + a.message.localeCompare(b.message) || + a.excerpt.localeCompare(b.excerpt) + ); +} + /** * Run multiple codemods on a project directory using in-memory chaining. * Each file is processed through all transforms whose filePatterns match it. @@ -305,9 +330,11 @@ export async function runCodemods( const loaded: LoadedTransform[] = []; for (const { codemod, scriptPath } of codemods) { const patterns = codemod.filePatterns ?? DEFAULT_FILE_PATTERNS; + const loadedModule = scriptPath ? await loadTransformModule(scriptPath) : undefined; loaded.push({ id: codemod.id, - transform: scriptPath ? await loadTransform(scriptPath) : undefined, + transform: loadedModule?.transform, + reviewFindings: loadedModule?.reviewFindings ?? codemod.reviewFindings, matches: picomatch(patterns, { dot: true }), legacyPatterns: codemod.legacyPatterns ?? [], sourceStringLegacyPatterns: codemod.sourceStringLegacyPatterns ?? [], @@ -320,8 +347,8 @@ export async function runCodemods( const warnings: string[] = []; const appliedCodemodIds = new Set(); const seen = new Set(); - // codemod id -> files flagged for LLM-assisted review - const suspiciousByCodemod = new Map(); + const suspiciousByCodemod = new Map>(); + const findingsByCodemod = new Map(); for await (const relative of walkFiles(targetPath)) { const absolute = path.resolve(targetPath, relative); @@ -364,10 +391,25 @@ export async function runCodemods( ); for (const lt of matchedTransforms) { - if (!lt.prompt || lt.suspiciousPatterns.length === 0) continue; + if (!lt.prompt) continue; + if (lt.reviewFindings) { + const findings = await lt.reviewFindings(current, absolute, relative); + if (findings.length > 0) { + const files = suspiciousByCodemod.get(lt.id) ?? new Set(); + for (const finding of findings) { + files.add(finding.file); + } + suspiciousByCodemod.set(lt.id, files); + const existing = findingsByCodemod.get(lt.id) ?? []; + existing.push(...findings); + findingsByCodemod.set(lt.id, existing); + } + continue; + } + if (lt.suspiciousPatterns.length === 0) continue; if (lt.suspiciousPatterns.some((p) => matchResidualPattern(residualContent, p) !== null)) { - const files = suspiciousByCodemod.get(lt.id) ?? []; - files.push(relative); + const files = suspiciousByCodemod.get(lt.id) ?? new Set(); + files.add(relative); suspiciousByCodemod.set(lt.id, files); } } @@ -376,11 +418,19 @@ export async function runCodemods( const llmReviews: LlmReview[] = []; for (const lt of loaded) { if (!lt.prompt) continue; - if (lt.suspiciousPatterns.length > 0) { + if (lt.suspiciousPatterns.length > 0 || lt.reviewFindings) { // File-scoped: only surface when a suspicious pattern actually matched. const files = suspiciousByCodemod.get(lt.id); // Sort for deterministic output regardless of filesystem traversal order. - if (files) llmReviews.push({ codemodId: lt.id, prompt: lt.prompt, files: files.toSorted() }); + if (files) { + const findings = findingsByCodemod.get(lt.id)?.toSorted(compareReviewFindings); + llmReviews.push({ + codemodId: lt.id, + prompt: lt.prompt, + files: Array.from(files).toSorted(), + ...(findings && findings.length > 0 ? { findings } : {}), + }); + } } else if (lt.legacyPatterns.length === 0) { // Codemod-less manual change with no pattern to scope by: surface as // project-wide guidance (legacyPattern-only entries warn instead). diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index 34965ebd6..285786bf3 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -67,6 +67,12 @@ export interface CodemodPackage { * `legacyPatterns`. Has no effect unless `prompt` is also set. */ suspiciousPatterns?: CodemodPatternGroup[]; + /** + * Optional script-level detector for file-local review findings. A codemod + * transform module may export this when pattern matching is too broad for + * actionable review output. + */ + reviewFindings?: ReviewFindingsFn; /** * Prompt that instructs an LLM how to finish the migration for files matched * by `suspiciousPatterns`. @@ -82,6 +88,25 @@ export interface CodemodPackage { notice?: boolean; } +/** A specific location that needs manual or LLM-assisted migration review. */ +export interface LlmReviewFinding { + /** File path relative to the transformed project root. */ + file: string; + /** One-based line number in the post-transform file content. */ + line: number; + /** Short reason this location needs review. */ + message: string; + /** Trimmed source line or nearby expression for local context. */ + excerpt: string; +} + +/** Detector exported by a transform module for precise review locations. */ +export type ReviewFindingsFn = ( + source: string, + filePath: string, + relativePath: string, +) => Promise | LlmReviewFinding[]; + /** A batch of files an LLM should review for one codemod, with its prompt. */ export interface LlmReview { /** Codemod id that flagged these files. */ @@ -90,6 +115,8 @@ export interface LlmReview { prompt: string; /** Files (relative to the target) that matched a suspicious pattern. */ files: string[]; + /** Optional file-local findings produced by the codemod script. */ + findings?: LlmReviewFinding[]; } /** From 3628e3aa30b7276b87e358224b554611971ef967 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 13:48:21 +0900 Subject: [PATCH 276/618] refactor: keep codemod review detectors script-local --- packages/sdk-codemod/src/runner.ts | 2 +- packages/sdk-codemod/src/types.ts | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 3bc036443..6080a9f50 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -334,7 +334,7 @@ export async function runCodemods( loaded.push({ id: codemod.id, transform: loadedModule?.transform, - reviewFindings: loadedModule?.reviewFindings ?? codemod.reviewFindings, + reviewFindings: loadedModule?.reviewFindings, matches: picomatch(patterns, { dot: true }), legacyPatterns: codemod.legacyPatterns ?? [], sourceStringLegacyPatterns: codemod.sourceStringLegacyPatterns ?? [], diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index 285786bf3..8e8d0962f 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -67,12 +67,6 @@ export interface CodemodPackage { * `legacyPatterns`. Has no effect unless `prompt` is also set. */ suspiciousPatterns?: CodemodPatternGroup[]; - /** - * Optional script-level detector for file-local review findings. A codemod - * transform module may export this when pattern matching is too broad for - * actionable review output. - */ - reviewFindings?: ReviewFindingsFn; /** * Prompt that instructs an LLM how to finish the migration for files matched * by `suspiciousPatterns`. From 7d2e3cb31f4d377703b089aa1bf272382b491275 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 13:49:11 +0900 Subject: [PATCH 277/618] fix: cover runtime globals in codemod string scans --- packages/sdk-codemod/src/registry.test.ts | 13 +++++++++++++ packages/sdk-codemod/src/registry.ts | 14 ++++++++------ packages/sdk-codemod/src/runner.test.ts | 23 +++++++++++++++-------- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 481c4ed07..896155c5d 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -60,7 +60,20 @@ describe("getApplicableCodemods", () => { expect(codemod?.filePatterns).toContain("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"); expect(codemod?.suspiciousPatterns).toContain("tailor.idp"); + expect(codemod?.suspiciousPatterns).toContain("tailor.secretmanager"); + expect(codemod?.suspiciousPatterns).toContain("tailor.authconnection"); expect(codemod?.sourceStringSuspiciousPatterns).toContain("new tailor.idp.Client"); + expect( + codemod?.sourceStringSuspiciousPatterns?.some( + (pattern) => pattern instanceof RegExp && pattern.test("const C = tailor.idp.Client;"), + ), + ).toBe(true); + expect( + codemod?.sourceStringSuspiciousPatterns?.some( + (pattern) => + pattern instanceof RegExp && pattern.test("await tailor.secretmanager.getSecret();"), + ), + ).toBe(true); expect(codemod?.prompt).toContain("@tailor-platform/sdk/runtime/globals"); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index f9ec35c3a..e7987d4ec 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -440,6 +440,8 @@ export const allCodemods: CodemodPackage[] = [ "tailor.context", "tailor.iconv", "tailor.idp", + "tailor.secretmanager", + "tailor.authconnection", "tailor.workflow", "tailor[", "tailordb.Client", @@ -454,14 +456,14 @@ export const allCodemods: CodemodPackage[] = [ ], sourceStringSuspiciousPatterns: [ "new tailor.idp.Client", - /\btailor\.(?:context|iconv|workflow)\s*\./, - /\btailordb\.(?:Client|CommandType|QueryResult|file)\b/, + /[=(:,[]\s*tailor\.idp\.Client\b/, + /\btailor\.(?:authconnection|context|iconv|idp|secretmanager|workflow)\.[A-Za-z_$][\w$]*\s*\(/, "tailor[", + /\btailordb\.file\.[A-Za-z_$][\w$]*\s*\(/, + /(?:\bnew\s+|[:=]\s*)tailordb\.(?:Client|CommandType|QueryResult)\b/, "tailordb[", - "TailorDBFileError", - "TailorErrorItem", - "TailorErrorMessage", - "TailorErrors", + /(?:\bnew\s+|\bthrow\s+|\binstanceof\s+)Tailor(?:DBFileError|Errors|ErrorMessage)\b/, + /[:<]\s*TailorErrorItem\b/, ], examples: [ { diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index d1b45de2e..52c683d17 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -656,13 +656,16 @@ describe("runCodemods", () => { path.join(os.tmpdir(), "runner-llm-source-string-test-"), ); tmpDir = dir; - await fs.promises.writeFile( - path.join(dir, "seed.mjs"), - [ - 'const code = `const client = new tailor.idp.Client({ namespace: "default" });`;', - 'const note = "tailor.idp.Client is mentioned in prose";', - ].join("\n"), - ); + const embeddedCode = [ + 'const client = new tailor.idp.Client({ namespace: "default" });', + "const C = tailor.idp.Client;", + 'await tailor.secretmanager.getSecret("vault", "key");', + ].join("\\n"); + const seedSource = [ + `const code = \`${embeddedCode}\`;`, + 'const note = "tailor.idp.Client is mentioned in prose";', + ].join("\n"); + await fs.promises.writeFile(path.join(dir, "seed.mjs"), seedSource); await fs.promises.writeFile( path.join(dir, "prose.mjs"), 'const note = "tailor.idp.Client is mentioned in prose";\n', @@ -677,7 +680,11 @@ describe("runCodemods", () => { ["**/*.{ts,js,mjs,cjs}"], undefined, { - sourceStringSuspiciousPatterns: ["new tailor.idp.Client"], + sourceStringSuspiciousPatterns: [ + "new tailor.idp.Client", + /[=(:,[]\s*tailor\.idp\.Client\b/, + /\btailor\.(?:idp|secretmanager)\.[A-Za-z_$][\w$]*\s*\(/, + ], prompt: "Review embedded runtime global usage by hand.", }, ), From bed65f3e52b038a885618643ee1e719a153db778 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 13:49:58 +0900 Subject: [PATCH 278/618] fix(sdk-codemod): handle escaped cli source strings --- .../v2/cli-rename/scripts/transform.ts | 59 ++++++++++++------- .../tests/source-template/expected.ts | 2 + .../cli-rename/tests/source-template/input.ts | 2 + .../v2/rename-bin/scripts/transform.ts | 11 ++-- .../tests/source-js-string/expected.js | 2 + .../tests/source-js-string/input.js | 2 + 6 files changed, 50 insertions(+), 28 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index 799724ba2..be52dc1dc 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -9,16 +9,9 @@ const OPTION_RENAMES: ReadonlyArray = [ ]; const ARG_VALUE = `(?:[^\\s'"\`;&|]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`; -const BOOLEAN_GLOBAL_ARG = "(?:--verbose|--json|--yes|-j|-y)"; -const VALUE_GLOBAL_ARG = - "(?:--env-file|--env-file-if-exists|--profile|--config|--workspace-id|-e|-p|-c|-w)"; -const GLOBAL_ARG_PATTERN = `(?:(?:\\s+${BOOLEAN_GLOBAL_ARG})|(?:\\s+${VALUE_GLOBAL_ARG}(?:=${ARG_VALUE}|\\s+${ARG_VALUE})))*`; const TAILOR_BINARY = `(? from).join("|")})\\b`, - "g", -); const TAILOR_BINARY_PATTERN = new RegExp(TAILOR_BINARY, "g"); +const TAILOR_BINARY_START_PATTERN = new RegExp(`^${TAILOR_BINARY}`); const SHELL_ASSIGNMENT_PREFIX_PATTERN = new RegExp( `^(?:env\\s+)?(?:[A-Za-z_]\\w*=${ARG_VALUE}\\s+)+${TAILOR_BINARY}(?=\\s|$)`, ); @@ -666,12 +659,39 @@ function replaceOptionsInCommand(command: string): string { } function replaceCommandNameInCommand(command: string): string { - COMMAND_PATTERN.lastIndex = 0; - return command.replace( - COMMAND_PATTERN, - (match, _prefix: string, cmd: string) => - `${match.slice(0, -cmd.length)}${COMMAND_MAP.get(cmd) ?? cmd}`, - ); + const binary = command.match(TAILOR_BINARY_START_PATTERN)?.[0]; + if (!binary) return command; + + let index = binary.length; + for (;;) { + const whitespace = command.slice(index).match(/^\s+/)?.[0]; + if (whitespace) index += whitespace.length; + if (index >= command.length) return command; + + const tokenEnd = findShellArgEnd(command, index); + const token = command.slice(index, tokenEnd); + const name = optionName(token); + if (GLOBAL_BOOLEAN_ARGS.has(name)) { + index = tokenEnd; + continue; + } + if (isGlobalSeparateValueArg(name)) { + index = tokenEnd; + if (!token.includes("=") || token.endsWith("=")) { + const valueWhitespace = command.slice(index).match(/^\s+/)?.[0]; + if (!valueWhitespace) return command; + index += valueWhitespace.length; + index = findShellArgEnd(command, index); + } + continue; + } + if (token.startsWith("-")) return command; + + const replacement = COMMAND_MAP.get(token); + return replacement + ? `${command.slice(0, index)}${replacement}${command.slice(tokenEnd)}` + : command; + } } function replaceCliRenamesInCommand(command: string): string { @@ -693,14 +713,11 @@ function sourceStringToken(node: SgNode, source: string): SourceStringToken | un return undefined; } - const fragments = node.children().filter((child: SgNode) => child.kind() === "string_fragment"); - if (fragments.length !== 1) return undefined; - - const range = fragments[0]!.range(); + const range = node.range(); return { - value: source.slice(range.start.index, range.end.index), - start: range.start.index, - end: range.end.index, + value: source.slice(range.start.index + 1, range.end.index - 1), + start: range.start.index + 1, + end: range.end.index - 1, }; } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts index f1ec69aa3..27129c0e7 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts @@ -23,6 +23,8 @@ const envFilePath = "tailor-sdk --env-file=/tmp/--machineuser crashreport"; const envFileCommandSubstitutionPath = "tailor-sdk --env-file=$(pwd)/--machineuser crashreport --machine-user"; const argPayload = "tailor-sdk function test-run --arg=--machineuser"; const argCommandSubstitutionPayload = "tailor-sdk function test-run --arg=$(printf --machineuser) --machine-user"; +const quotedArgCommandPayload = "tailor-sdk function test-run --arg='tailor-sdk crash-report --machineuser' --machine-user"; +const escapedQueryCommand = "tailor-sdk query --query \"select 1\" --machine-user"; const profileCommand = "tailor-sdk --profile prod crashreport --machine-user"; const profileMachineUserValue = "tailor-sdk --profile --machineuser crashreport --machine-user"; const workspaceCommand = "tailor-sdk -w workspace-1 crashreport"; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts index 74e5055e9..b3a5210e2 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts @@ -23,6 +23,8 @@ const envFilePath = "tailor-sdk --env-file=/tmp/--machineuser crash-report"; const envFileCommandSubstitutionPath = "tailor-sdk --env-file=$(pwd)/--machineuser crash-report --machineuser"; const argPayload = "tailor-sdk function test-run --arg=--machineuser"; const argCommandSubstitutionPayload = "tailor-sdk function test-run --arg=$(printf --machineuser) --machineuser"; +const quotedArgCommandPayload = "tailor-sdk function test-run --arg='tailor-sdk crash-report --machineuser' --machineuser"; +const escapedQueryCommand = "tailor-sdk query --query \"select 1\" --machineuser"; const profileCommand = "tailor-sdk --profile prod crash-report --machineuser"; const profileMachineUserValue = "tailor-sdk --profile --machineuser crash-report --machineuser"; const workspaceCommand = "tailor-sdk -w workspace-1 crash-report"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 4f5a55f3b..c8c1cc868 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -214,14 +214,11 @@ function sourceStringToken(node: SgNode, source: string): SourceStringToken | un return undefined; } - const fragments = node.children().filter((child: SgNode) => child.kind() === "string_fragment"); - if (fragments.length !== 1) return undefined; - - const range = fragments[0]!.range(); + const range = node.range(); return { - value: source.slice(range.start.index, range.end.index), - start: range.start.index, - end: range.end.index, + value: source.slice(range.start.index + 1, range.end.index - 1), + start: range.start.index + 1, + end: range.end.index - 1, }; } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 3e6afea12..20b04ce30 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -7,11 +7,13 @@ const packageRegex = /tailor-sdk/; // package tailor-sdk // package tailor-sdk is installed const packageMessage = "package tailor-sdk is installed"; +const escapedPackageMessage = "package \"tailor-sdk\" is installed"; // Install tailor-sdk before running tailor deploy const mixedPackageAndCommand = "Install tailor-sdk before running tailor deploy"; const dynamicImport = import("tailor-sdk"); const installedPackage = installPackage("tailor-sdk"); const forkedModule = child_process.fork("tailor-sdk/register", ["crash-report"]); const migrate = "bunx @tailor-platform/sdk@2.0.0-next.2 generate"; +const escapedQuery = "tailor query --query \"select 1\""; const script = ["tailor", "deploy"].join(" "); const skills = "tailor-sdk-skills"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 3c9d0f2f2..87d8a89ef 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -7,11 +7,13 @@ const packageRegex = /tailor-sdk/; // package tailor-sdk // package tailor-sdk is installed const packageMessage = "package tailor-sdk is installed"; +const escapedPackageMessage = "package \"tailor-sdk\" is installed"; // Install tailor-sdk before running tailor-sdk deploy const mixedPackageAndCommand = "Install tailor-sdk before running tailor-sdk deploy"; const dynamicImport = import("tailor-sdk"); const installedPackage = installPackage("tailor-sdk"); const forkedModule = child_process.fork("tailor-sdk/register", ["crash-report"]); const migrate = "bunx tailor-sdk@2.0.0-next.2 generate"; +const escapedQuery = "tailor-sdk query --query \"select 1\""; const script = ["tailor-sdk", "deploy"].join(" "); const skills = "tailor-sdk-skills"; From 6de71e9a86bef58b2a5b8bf5b6bfc73a97a7eb12 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 13:52:12 +0900 Subject: [PATCH 279/618] fix: avoid ambiguous runtime idp rewrites --- .../v2/runtime-globals-opt-in/scripts/transform.ts | 11 ++++++++++- .../tests/import-equals-tailor/input.cts | 6 ++++++ .../tests/nested-ambient-import/expected.ts | 11 +++++++++++ .../tests/nested-ambient-import/input.ts | 9 +++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-equals-tailor/input.cts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-ambient-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-ambient-import/input.ts diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts index 3874b54d4..436b1a083 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts @@ -30,7 +30,7 @@ function isTypeOnlyImport(stmt: SgNode): boolean { } function importSource(stmt: SgNode): string | null { - const source = stmt.children().find((child) => child.kind() === "string"); + const source = stmt.find({ rule: { kind: "string" } }); return stringValue(source ?? null); } @@ -54,6 +54,14 @@ function importBindings(importStmt: SgNode): ImportBinding[] { const source = importSource(importStmt); if (!source) return []; + const requireClause = importStmt + .children() + .find((child) => child.kind() === "import_require_clause"); + if (requireClause) { + const local = requireClause.children().find((child) => child.kind() === "identifier"); + return local ? [{ localName: local.text(), source, typeOnly: false }] : []; + } + const typeOnly = isTypeOnlyImport(importStmt); const clause = importStmt.children().find((child) => child.kind() === "import_clause"); if (!clause) return []; @@ -145,6 +153,7 @@ function localDeclarationNames(root: SgNode): Set { function findImportStatements(root: SgNode): SgNode[] { return root .findAll({ rule: { kind: "import_statement" } }) + .filter((stmt) => stmt.parent()?.kind() === "program") .toSorted((a, b) => a.range().start.index - b.range().start.index); } diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-equals-tailor/input.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-equals-tailor/input.cts new file mode 100644 index 000000000..a96026f64 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-equals-tailor/input.cts @@ -0,0 +1,6 @@ +import tailor = require("./local-tailor"); + +export async function run() { + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-ambient-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-ambient-import/expected.ts new file mode 100644 index 000000000..38d7ec6fd --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-ambient-import/expected.ts @@ -0,0 +1,11 @@ +import { idp } from "@tailor-platform/sdk/runtime"; + +declare module "pkg" { + import { Existing } from "other"; + + export interface Thing { + value: Existing; + } +} + +export const client = new idp.Client({ namespace: "default" }); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-ambient-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-ambient-import/input.ts new file mode 100644 index 000000000..6d3fb547b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/nested-ambient-import/input.ts @@ -0,0 +1,9 @@ +declare module "pkg" { + import { Existing } from "other"; + + export interface Thing { + value: Existing; + } +} + +export const client = new tailor.idp.Client({ namespace: "default" }); From 15a179e08e32bb6afddc529d181de4b220df0bbc Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 13:56:54 +0900 Subject: [PATCH 280/618] fix: preserve codemod review fallback patterns --- .../src/principal-unify-review.test.ts | 22 +++++++++++++++++++ packages/sdk-codemod/src/runner.ts | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/src/principal-unify-review.test.ts b/packages/sdk-codemod/src/principal-unify-review.test.ts index 710877e9d..39183821c 100644 --- a/packages/sdk-codemod/src/principal-unify-review.test.ts +++ b/packages/sdk-codemod/src/principal-unify-review.test.ts @@ -123,4 +123,26 @@ describe("principal-unify review findings", () => { }), ]); }); + + test("keeps file-level suspicious-pattern fallback without precise findings", async () => { + await writeProjectFile( + "resolvers/context-type.ts", + [ + 'import type { ResolverContext } from "@tailor-platform/sdk";', + "", + "export type AdapterContext = ResolverContext;", + "", + ].join("\n"), + ); + + const result = await runCodemods([principalUnifyEntry], tmpDir!, false); + + expect(result.llmReviews).toEqual([ + expect.objectContaining({ + codemodId: "v2/principal-unify", + files: ["resolvers/context-type.ts"], + }), + ]); + expect(result.llmReviews[0]).not.toHaveProperty("findings"); + }); }); diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 6080a9f50..5572d2b18 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -404,7 +404,6 @@ export async function runCodemods( existing.push(...findings); findingsByCodemod.set(lt.id, existing); } - continue; } if (lt.suspiciousPatterns.length === 0) continue; if (lt.suspiciousPatterns.some((p) => matchResidualPattern(residualContent, p) !== null)) { From 23571dad105ccecc5eee580440f7b5d7effd22f6 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 13:57:42 +0900 Subject: [PATCH 281/618] fix: flag aliased runtime globals in codemod strings --- packages/sdk-codemod/src/registry.test.ts | 13 +++++++++++++ packages/sdk-codemod/src/registry.ts | 1 + packages/sdk-codemod/src/runner.test.ts | 3 +++ 3 files changed, 17 insertions(+) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 896155c5d..1ff79d1d2 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -74,6 +74,19 @@ describe("getApplicableCodemods", () => { pattern instanceof RegExp && pattern.test("await tailor.secretmanager.getSecret();"), ), ).toBe(true); + expect( + codemod?.sourceStringSuspiciousPatterns?.some( + (pattern) => + pattern instanceof RegExp && pattern.test("const { getSecret } = tailor.secretmanager;"), + ), + ).toBe(true); + expect( + codemod?.sourceStringSuspiciousPatterns?.some( + (pattern) => + pattern instanceof RegExp && + pattern.test("const getInvoker = tailor.context.getInvoker;"), + ), + ).toBe(true); expect(codemod?.prompt).toContain("@tailor-platform/sdk/runtime/globals"); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index e7987d4ec..c5662ac2a 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -457,6 +457,7 @@ export const allCodemods: CodemodPackage[] = [ sourceStringSuspiciousPatterns: [ "new tailor.idp.Client", /[=(:,[]\s*tailor\.idp\.Client\b/, + /(?:(?:[=(:,{]|\[)\s*|\b(?:return|await)\s+)tailor\.(?:authconnection|context|iconv|idp|secretmanager|workflow)(?:\.[A-Za-z_$][\w$]*)?\b/, /\btailor\.(?:authconnection|context|iconv|idp|secretmanager|workflow)\.[A-Za-z_$][\w$]*\s*\(/, "tailor[", /\btailordb\.file\.[A-Za-z_$][\w$]*\s*\(/, diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 52c683d17..a436d5cc9 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -660,6 +660,8 @@ describe("runCodemods", () => { 'const client = new tailor.idp.Client({ namespace: "default" });', "const C = tailor.idp.Client;", 'await tailor.secretmanager.getSecret("vault", "key");', + "const { getSecret } = tailor.secretmanager;", + "const getInvoker = tailor.context.getInvoker;", ].join("\\n"); const seedSource = [ `const code = \`${embeddedCode}\`;`, @@ -683,6 +685,7 @@ describe("runCodemods", () => { sourceStringSuspiciousPatterns: [ "new tailor.idp.Client", /[=(:,[]\s*tailor\.idp\.Client\b/, + /(?:(?:[=(:,{]|\[)\s*|\b(?:return|await)\s+)tailor\.(?:context|idp|secretmanager)(?:\.[A-Za-z_$][\w$]*)?\b/, /\btailor\.(?:idp|secretmanager)\.[A-Za-z_$][\w$]*\s*\(/, ], prompt: "Review embedded runtime global usage by hand.", From 5001c03c0794a0885902e9a8a83114122aa3e8f0 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:05:05 +0900 Subject: [PATCH 282/618] fix: guard runtime idp codemod cases --- .../scripts/transform.ts | 20 +++++++++++++++++++ .../tests/catch-idp/input.ts | 8 ++++++++ .../tests/cts-auto-import/input.cts | 4 ++++ .../tests/function-expression-tailor/input.ts | 4 ++++ .../tests/no-args/input.ts | 4 ++++ 5 files changed, 40 insertions(+) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-idp/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-auto-import/input.cts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-tailor/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/no-args/input.ts diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts index 436b1a083..a690b76d9 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts @@ -3,6 +3,7 @@ import type { Edit, SgNode } from "@ast-grep/napi"; const RUNTIME_MODULE = "@tailor-platform/sdk/runtime"; const TAILOR_IDP_CLIENT = "tailor.idp.Client"; +const ARGUMENT_PUNCTUATION = new Set(["(", ")", ","]); interface ImportBinding { localName: string; @@ -134,10 +135,13 @@ function localDeclarationNames(root: SgNode): Set { rule: { any: [ { kind: "function_declaration" }, + { kind: "function_expression" }, { kind: "class_declaration" }, + { kind: "class" }, { kind: "interface_declaration" }, { kind: "type_alias_declaration" }, { kind: "enum_declaration" }, + { kind: "internal_module" }, ], }, })) { @@ -147,6 +151,11 @@ function localDeclarationNames(root: SgNode): Set { if (name) names.add(name.text()); } + for (const catchClause of root.findAll({ rule: { kind: "catch_clause" } })) { + const binding = catchClause.children().find((child) => child.kind() === "identifier"); + if (binding) names.add(binding.text()); + } + return names; } @@ -229,9 +238,19 @@ function buildAddRuntimeImportEdit(source: string, imports: SgNode[]): Edit { return { startPos: pos, endPos: pos, insertedText }; } +function argumentExpressions(args: SgNode): SgNode[] { + return args.children().filter((child) => !ARGUMENT_PUNCTUATION.has(child.kind())); +} + +function hasConstructorArguments(newExpression: SgNode): boolean { + const args = newExpression.field("arguments"); + return args ? argumentExpressions(args).length > 0 : false; +} + function findTailorIdpClientConstructors(root: SgNode): SgNode[] { return root .findAll({ rule: { kind: "new_expression" } }) + .filter(hasConstructorArguments) .map((node) => node.field("constructor")) .filter((node): node is SgNode => node?.text() === TAILOR_IDP_CLIENT); } @@ -261,6 +280,7 @@ export default function transform(source: string, filePath: string): string | nu ); if (!existingIdpLocal) { + if (filePath.endsWith(".cts")) return null; edits.push(buildAddRuntimeImportEdit(source, imports)); } diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-idp/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-idp/input.ts new file mode 100644 index 000000000..f3c34f04d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-idp/input.ts @@ -0,0 +1,8 @@ +export async function run() { + try { + return await load(); + } catch (idp) { + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); + } +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-auto-import/input.cts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-auto-import/input.cts new file mode 100644 index 000000000..e21a2970c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/cts-auto-import/input.cts @@ -0,0 +1,4 @@ +export async function run() { + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-tailor/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-tailor/input.ts new file mode 100644 index 000000000..408cb5785 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/function-expression-tailor/input.ts @@ -0,0 +1,4 @@ +export const run = function tailor() { + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); +}; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/no-args/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/no-args/input.ts new file mode 100644 index 000000000..4f3a82a55 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/no-args/input.ts @@ -0,0 +1,4 @@ +export async function run() { + const client = new tailor.idp.Client(); + return client.listUsers(); +} From ae2cb91a815163bdff832c630050e2590d10e624 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:06:01 +0900 Subject: [PATCH 283/618] fix: isolate codemod source-string matches --- packages/sdk-codemod/src/registry.test.ts | 5 +++++ packages/sdk-codemod/src/registry.ts | 1 + packages/sdk-codemod/src/runner.test.ts | 6 +++++- packages/sdk-codemod/src/runner.ts | 3 ++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 1ff79d1d2..6ee1408fc 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -87,6 +87,11 @@ describe("getApplicableCodemods", () => { pattern.test("const getInvoker = tailor.context.getInvoker;"), ), ).toBe(true); + expect( + codemod?.sourceStringSuspiciousPatterns?.some( + (pattern) => pattern instanceof RegExp && pattern.test("const { upload } = tailordb.file;"), + ), + ).toBe(true); expect(codemod?.prompt).toContain("@tailor-platform/sdk/runtime/globals"); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index c5662ac2a..a7017ecbf 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -461,6 +461,7 @@ export const allCodemods: CodemodPackage[] = [ /\btailor\.(?:authconnection|context|iconv|idp|secretmanager|workflow)\.[A-Za-z_$][\w$]*\s*\(/, "tailor[", /\btailordb\.file\.[A-Za-z_$][\w$]*\s*\(/, + /(?:(?:[=(:,{]|\[)\s*|\b(?:return|await)\s+)tailordb\.file\b/, /(?:\bnew\s+|[:=]\s*)tailordb\.(?:Client|CommandType|QueryResult)\b/, "tailordb[", /(?:\bnew\s+|\bthrow\s+|\binstanceof\s+)Tailor(?:DBFileError|Errors|ErrorMessage)\b/, diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index a436d5cc9..80ec00fe0 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -662,6 +662,7 @@ describe("runCodemods", () => { 'await tailor.secretmanager.getSecret("vault", "key");', "const { getSecret } = tailor.secretmanager;", "const getInvoker = tailor.context.getInvoker;", + "const { upload } = tailordb.file;", ].join("\\n"); const seedSource = [ `const code = \`${embeddedCode}\`;`, @@ -670,7 +671,9 @@ describe("runCodemods", () => { await fs.promises.writeFile(path.join(dir, "seed.mjs"), seedSource); await fs.promises.writeFile( path.join(dir, "prose.mjs"), - 'const note = "tailor.idp.Client is mentioned in prose";\n', + ['const separator = "=";', 'const note = "tailor.idp.Client is mentioned in prose";'].join( + "\n", + ), ); const result = await runCodemods( @@ -687,6 +690,7 @@ describe("runCodemods", () => { /[=(:,[]\s*tailor\.idp\.Client\b/, /(?:(?:[=(:,{]|\[)\s*|\b(?:return|await)\s+)tailor\.(?:context|idp|secretmanager)(?:\.[A-Za-z_$][\w$]*)?\b/, /\btailor\.(?:idp|secretmanager)\.[A-Za-z_$][\w$]*\s*\(/, + /(?:(?:[=(:,{]|\[)\s*|\b(?:return|await)\s+)tailordb\.file\b/, ], prompt: "Review embedded runtime global usage by hand.", }, diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 9cf06e097..a0c2c8daa 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -53,6 +53,7 @@ const DEFAULT_FILE_PATTERNS = ["**/*.{ts,tsx,mts,cts}"]; const EXCLUDE_DIRS = new Set(["node_modules", "dist", ".git"]); const ALLOWED_DOT_DIRS = new Set([".github", ".circleci"]); const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]); +const SOURCE_STRING_FRAGMENT_SEPARATOR = "\0"; const MASKED_SOURCE_NODE_KINDS: ReadonlySet> = new Set([ "comment", "string", @@ -169,7 +170,7 @@ function sourceStringContentForResidualMatching(relative: string, content: strin } }; visit(root); - return fragments.join("\n"); + return fragments.join(SOURCE_STRING_FRAGMENT_SEPARATOR); } function sourceLang(relative: string): Lang { From dab15886ced506a8800f4a79955b5cac145449bc Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:07:50 +0900 Subject: [PATCH 284/618] fix: detect aliased nullable principal review sites --- .../v2/principal-unify/scripts/transform.ts | 107 ++++++++++++++++-- .../src/principal-unify-review.test.ts | 35 ++++++ 2 files changed, 130 insertions(+), 12 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index a27f531e1..94a96dc0d 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -2186,20 +2186,30 @@ function addReviewFinding( findings.push({ file, line, message, excerpt }); } -function isCallerOptionalMemberExpression(node: SgNode): boolean { +function isPrincipalOptionalMemberExpression( + node: SgNode, + principalLocalNames: Set, +): boolean { if (node.kind() !== "member_expression") return false; if (!node.children().some((child) => child.kind() === "optional_chain")) return false; const object = node.field("object"); if (!object) return false; - if (object.kind() === "identifier") return object.text() === "caller"; + if (object.kind() === "identifier") { + return object.text() === "caller" || principalLocalNames.has(object.text()); + } if (object.kind() !== "member_expression") return false; return object.field("property")?.text() === "caller"; } -function nodeContainsArgumentCallerOptionalAccess(node: SgNode): boolean { +function nodeContainsArgumentPrincipalOptionalAccess( + node: SgNode, + principalLocalNames: Set, +): boolean { if (isFunctionNode(node)) return false; - if (isCallerOptionalMemberExpression(node)) return true; - return node.children().some((child) => nodeContainsArgumentCallerOptionalAccess(child)); + if (isPrincipalOptionalMemberExpression(node, principalLocalNames)) return true; + return node + .children() + .some((child) => nodeContainsArgumentPrincipalOptionalAccess(child, principalLocalNames)); } function reviewCallName(call: SgNode): string { @@ -2217,6 +2227,7 @@ function collectNullableCallerReviewFindings( root: SgNode, source: string, file: string, + principalLocalNames: Set, findings: LlmReviewFinding[], seen: Set, ): void { @@ -2225,7 +2236,7 @@ function collectNullableCallerReviewFindings( const args = call.field("arguments"); const nullableArg = args ?.children() - .find((child) => nodeContainsArgumentCallerOptionalAccess(child)); + .find((child) => nodeContainsArgumentPrincipalOptionalAccess(child, principalLocalNames)); if (!nullableArg) continue; const memberName = findMemberCallName(call); @@ -2316,7 +2327,7 @@ function addResolverContextBody(arrow: SgNode, bodies: ResolverContextBody[]): v bodies.push({ arrow, body, contextName: paramName }); } -function collectResolverContextBodies(root: SgNode): ResolverContextBody[] { +function collectResolverBodyArrows(root: SgNode): SgNode[] { const sdkImports = root.findAll({ rule: { kind: "import_statement", @@ -2334,7 +2345,7 @@ function collectResolverContextBodies(root: SgNode): ResolverContextBody[] { } } - const bodies: ResolverContextBody[] = []; + const arrows: SgNode[] = []; for (const localName of createResolverLocalNames) { const shadowRanges = collectAllShadowRanges(root, localName); const calls = root.findAll({ @@ -2351,7 +2362,7 @@ function collectResolverContextBodies(root: SgNode): ResolverContextBody[] { const callee = call.field("function"); if (!callee || isInsideAnyRange(callee.range().start.index, shadowRanges)) continue; const arrow = findResolverBodyArrow(call); - if (arrow) addResolverContextBody(arrow, bodies); + if (arrow) arrows.push(arrow); } } @@ -2377,12 +2388,76 @@ function collectResolverContextBodies(root: SgNode): ResolverContextBody[] { if (!object || object.kind() !== "identifier" || object.text() !== namespaceName) continue; if (isInsideAnyRange(object.range().start.index, shadowRanges)) continue; const arrow = findResolverBodyArrow(call); - if (arrow) addResolverContextBody(arrow, bodies); + if (arrow) arrows.push(arrow); } } + return arrows; +} + +function collectResolverContextBodies(root: SgNode): ResolverContextBody[] { + const bodies: ResolverContextBody[] = []; + for (const arrow of collectResolverBodyArrows(root)) { + addResolverContextBody(arrow, bodies); + } return bodies; } +function collectCallerPatternBindings(pattern: SgNode, bindings: Set): void { + if (pattern.kind() !== "object_pattern") return; + for (const child of pattern.children()) { + const kind = child.kind(); + if (kind === "shorthand_property_identifier_pattern" && child.text() === "caller") { + bindings.add("caller"); + } else if (kind === "pair_pattern") { + const key = child.field("key"); + const value = child.field("value"); + if (key?.text() === "caller" && value?.kind() === "identifier") { + bindings.add(value.text()); + } + } else if (kind === "object_assignment_pattern") { + const inner = child + .children() + .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); + if (inner?.text() === "caller") bindings.add("caller"); + } + } +} + +function collectResolverPrincipalLocalNames(root: SgNode): Set { + const bindings = new Set(); + for (const arrow of collectResolverBodyArrows(root)) { + const param = getFirstFunctionParam(arrow); + const pattern = param ? getFunctionParamPattern(param) : null; + const body = arrow.field("body"); + if (!pattern || !body) continue; + + if (pattern.kind() === "object_pattern") { + collectCallerPatternBindings(pattern, bindings); + continue; + } + + if (pattern.kind() !== "identifier") continue; + const contextName = pattern.text(); + const shadowRanges = collectCtxShadowRanges(body, contextName, arrow); + const contextDestructures = body.findAll({ + rule: { + kind: "variable_declarator", + has: { + field: "value", + kind: "identifier", + regex: `^${escapeRegex(contextName)}$`, + }, + }, + }); + for (const decl of contextDestructures) { + if (isInsideAnyRange(decl.range().start.index, shadowRanges)) continue; + const name = decl.field("name"); + if (name) collectCallerPatternBindings(name, bindings); + } + } + return bindings; +} + function firstIdentifierArgument(call: SgNode): SgNode | null { const args = call.field("arguments"); if (!args) return null; @@ -2444,7 +2519,7 @@ export function reviewFindings( _filePath: string, relativePath: string, ): LlmReviewFinding[] { - if (!source.includes("caller?.") && !source.includes(".user")) return []; + if (!source.includes("?.") && !source.includes(".user")) return []; let root: SgNode; try { @@ -2455,7 +2530,15 @@ export function reviewFindings( const findings: LlmReviewFinding[] = []; const seen = new Set(); - collectNullableCallerReviewFindings(root, source, relativePath, findings, seen); + const principalLocalNames = collectResolverPrincipalLocalNames(root); + collectNullableCallerReviewFindings( + root, + source, + relativePath, + principalLocalNames, + findings, + seen, + ); collectContextUserHelperReviewFindings(root, source, relativePath, findings, seen); return findings; } diff --git a/packages/sdk-codemod/src/principal-unify-review.test.ts b/packages/sdk-codemod/src/principal-unify-review.test.ts index 39183821c..688714968 100644 --- a/packages/sdk-codemod/src/principal-unify-review.test.ts +++ b/packages/sdk-codemod/src/principal-unify-review.test.ts @@ -145,4 +145,39 @@ describe("principal-unify review findings", () => { ]); expect(result.llmReviews[0]).not.toHaveProperty("findings"); }); + + test("reports nullable aliased caller values passed to non-null-looking calls", async () => { + await writeProjectFile( + "resolvers/aliased.ts", + [ + 'import { createResolver } from "@tailor-platform/sdk";', + "", + "declare function publishAudit(userId: string): Promise;", + "", + "export const resolver = createResolver({", + " body: async ({ user: currentUser }) => {", + " await publishAudit(currentUser.id);", + " },", + "});", + "", + ].join("\n"), + ); + + const result = await runCodemods([principalUnifyEntry], tmpDir!, false); + + expect(result.llmReviews).toEqual([ + expect.objectContaining({ + codemodId: "v2/principal-unify", + files: ["resolvers/aliased.ts"], + findings: [ + expect.objectContaining({ + file: "resolvers/aliased.ts", + line: 7, + message: expect.stringContaining("non-null argument"), + excerpt: expect.stringContaining("publishAudit(currentUser?.id)"), + }), + ], + }), + ]); + }); }); From 613967d058220c4db36d09a71f29f858df504086 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:08:38 +0900 Subject: [PATCH 285/618] fix(sdk-codemod): handle package runner cli strings --- .../codemods/v2/cli-rename/scripts/transform.ts | 12 ++++++++++++ .../v2/cli-rename/tests/basic-shell/expected.sh | 2 ++ .../v2/cli-rename/tests/basic-shell/input.sh | 2 ++ .../codemods/v2/rename-bin/scripts/transform.ts | 10 +++++++++- .../v2/rename-bin/tests/basic-shell/expected.sh | 2 ++ .../v2/rename-bin/tests/basic-shell/input.sh | 2 ++ 6 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index be52dc1dc..1fe587d63 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -12,6 +12,9 @@ const ARG_VALUE = `(?:[^\\s'"\`;&|]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`; const TAILOR_BINARY = `(? { + (match, prefix: string, value: string, offset: number) => { if (!value.includes("tailor-sdk")) return match; + if (isPackageRunnerExecutableReference(source, offset + prefix.length)) return match; const placeholder = `__TAILOR_SDK_CODEMOD_CLI_VALUE_${protectedValues.length}__`; protectedValues.push(value); return `${prefix}${placeholder}`; @@ -398,6 +402,10 @@ function protectTailorCliValueReferences(source: string): TokenizedRunnerRewrite return { source: updated, protectedValues }; } +function isPackageRunnerExecutableReference(source: string, start: number): boolean { + return PACKAGE_RUNNER_EXECUTABLE_PREFIX_RE.test(source.slice(0, start)); +} + function rewriteRunnerPackageValue(value: string): string | undefined { const quote = value[0] === "'" || value[0] === '"' ? value[0] : ""; const inner = quote ? value.slice(1, -1) : value; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh index 27f3e64fe..1db6d922a 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh @@ -7,3 +7,5 @@ tailor -p tailor-sdk deploy @tailor-platform/sdk@latest deploy npx @tailor-platform/sdk@2.0.0 workspace list npx -y @tailor-platform/sdk login +npx -f @tailor-platform/sdk login +npx -q @tailor-platform/sdk query diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh index 3fb148e2f..1f515c34f 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh @@ -7,3 +7,5 @@ tailor-sdk -p tailor-sdk deploy tailor-sdk@latest deploy npx tailor-sdk@2.0.0 workspace list npx -y tailor-sdk login +npx -f tailor-sdk login +npx -q tailor-sdk query From 52c10776b553b6318a22d3015eaac9dc521b87a0 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:18:59 +0900 Subject: [PATCH 286/618] fix: preserve runtime idp review signals --- .../scripts/transform.ts | 38 +++++++++++++++---- .../tests/comment-only-args/input.ts | 4 ++ .../tests/directive-prologue/expected.ts | 4 ++ .../tests/directive-prologue/input.ts | 3 ++ .../tests/for-of-idp/input.ts | 6 +++ 5 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/comment-only-args/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-prologue/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-prologue/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-of-idp/input.ts diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts index a690b76d9..e949fb60b 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts @@ -3,7 +3,7 @@ import type { Edit, SgNode } from "@ast-grep/napi"; const RUNTIME_MODULE = "@tailor-platform/sdk/runtime"; const TAILOR_IDP_CLIENT = "tailor.idp.Client"; -const ARGUMENT_PUNCTUATION = new Set(["(", ")", ","]); +const NON_ARGUMENT_KINDS = new Set(["(", ")", ",", "comment"]); interface ImportBinding { localName: string; @@ -156,6 +156,17 @@ function localDeclarationNames(root: SgNode): Set { if (binding) names.add(binding.text()); } + for (const loop of root.findAll({ rule: { kind: "for_in_statement" } })) { + const children = loop.children(); + const keywordIndex = children.findIndex( + (child) => child.kind() === "in" || child.kind() === "of", + ); + if (keywordIndex === -1) continue; + for (const child of children.slice(0, keywordIndex)) { + collectBindingNames(child, names); + } + } + return names; } @@ -208,19 +219,30 @@ function runtimeNamedValueImport(imports: SgNode[]): SgNode | null { ); } -function importInsertionIndex(imports: SgNode[], source: string): number { +function isDirectiveStatement(node: SgNode): boolean { + return node.kind() === "expression_statement" && node.children()[0]?.kind() === "string"; +} + +function importInsertionIndex(root: SgNode, imports: SgNode[], source: string): number { const lastImport = imports.at(-1); if (lastImport) return lastImport.range().end.index; + let pos = 0; if (source.startsWith("#!")) { const newlineIndex = source.indexOf("\n"); - return newlineIndex === -1 ? source.length : newlineIndex + 1; + pos = newlineIndex === -1 ? source.length : newlineIndex + 1; + } + + for (const child of root.children()) { + if (child.range().start.index < pos) continue; + if (!isDirectiveStatement(child)) break; + pos = child.range().end.index; } - return 0; + return pos; } -function buildAddRuntimeImportEdit(source: string, imports: SgNode[]): Edit { +function buildAddRuntimeImportEdit(root: SgNode, source: string, imports: SgNode[]): Edit { const existingRuntimeImport = runtimeNamedValueImport(imports); const namedImports = existingRuntimeImport ? namedImportsNode(existingRuntimeImport) : null; if (namedImports) { @@ -230,7 +252,7 @@ function buildAddRuntimeImportEdit(source: string, imports: SgNode[]): Edit { return namedImports.replace(`{ ${[...specTexts, "idp"].join(", ")} }`); } - const pos = importInsertionIndex(imports, source); + const pos = importInsertionIndex(root, imports, source); const insertedText = pos === 0 || (pos > 0 && source[pos - 1] === "\n") ? `import { idp } from "${RUNTIME_MODULE}";\n\n` @@ -239,7 +261,7 @@ function buildAddRuntimeImportEdit(source: string, imports: SgNode[]): Edit { } function argumentExpressions(args: SgNode): SgNode[] { - return args.children().filter((child) => !ARGUMENT_PUNCTUATION.has(child.kind())); + return args.children().filter((child) => !NON_ARGUMENT_KINDS.has(child.kind())); } function hasConstructorArguments(newExpression: SgNode): boolean { @@ -281,7 +303,7 @@ export default function transform(source: string, filePath: string): string | nu if (!existingIdpLocal) { if (filePath.endsWith(".cts")) return null; - edits.push(buildAddRuntimeImportEdit(source, imports)); + edits.push(buildAddRuntimeImportEdit(root, source, imports)); } const result = root.commitEdits(edits); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/comment-only-args/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/comment-only-args/input.ts new file mode 100644 index 000000000..2fcb13ef2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/comment-only-args/input.ts @@ -0,0 +1,4 @@ +export async function run() { + const client = new tailor.idp.Client(/* namespace required */); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-prologue/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-prologue/expected.ts new file mode 100644 index 000000000..60671aa7a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-prologue/expected.ts @@ -0,0 +1,4 @@ +"use server"; +import { idp } from "@tailor-platform/sdk/runtime"; + +export const client = new idp.Client({ namespace: "default" }); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-prologue/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-prologue/input.ts new file mode 100644 index 000000000..4ce7d57f7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/directive-prologue/input.ts @@ -0,0 +1,3 @@ +"use server"; + +export const client = new tailor.idp.Client({ namespace: "default" }); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-of-idp/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-of-idp/input.ts new file mode 100644 index 000000000..b6b5dc761 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/for-of-idp/input.ts @@ -0,0 +1,6 @@ +export async function run(users: unknown[]) { + for (const idp of users) { + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); + } +} From 06e312c082be6ee5d829b8d522cea1ac44660e52 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:21:40 +0900 Subject: [PATCH 287/618] fix: scope nullable principal review aliases --- .../v2/principal-unify/scripts/transform.ts | 118 +++++++++++++++--- .../src/principal-unify-review.test.ts | 64 ++++++++++ 2 files changed, 164 insertions(+), 18 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 94a96dc0d..cd3f6a235 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -2186,16 +2186,40 @@ function addReviewFinding( findings.push({ file, line, message, excerpt }); } +interface ReviewPrincipalBinding { + name: string; + bindingStart: number; + scope: [number, number]; + shadowRoot?: SgNode; +} + +function resolvesToReviewPrincipalBinding( + node: SgNode, + bindings: ReviewPrincipalBinding[], + root: SgNode, +): boolean { + if (node.kind() !== "identifier") return false; + const pos = node.range().start.index; + return bindings.some((binding) => { + if (binding.name !== node.text() || !rangeContains(binding.scope, pos)) return false; + if (binding.shadowRoot) { + return !isInsideAnyRange(pos, collectAllShadowRanges(binding.shadowRoot, binding.name)); + } + return !isShadowedLocalReference(root, binding.name, pos, binding.bindingStart); + }); +} + function isPrincipalOptionalMemberExpression( node: SgNode, - principalLocalNames: Set, + principalBindings: ReviewPrincipalBinding[], + root: SgNode, ): boolean { if (node.kind() !== "member_expression") return false; if (!node.children().some((child) => child.kind() === "optional_chain")) return false; const object = node.field("object"); if (!object) return false; if (object.kind() === "identifier") { - return object.text() === "caller" || principalLocalNames.has(object.text()); + return resolvesToReviewPrincipalBinding(object, principalBindings, root); } if (object.kind() !== "member_expression") return false; return object.field("property")?.text() === "caller"; @@ -2203,13 +2227,14 @@ function isPrincipalOptionalMemberExpression( function nodeContainsArgumentPrincipalOptionalAccess( node: SgNode, - principalLocalNames: Set, + principalBindings: ReviewPrincipalBinding[], + root: SgNode, ): boolean { if (isFunctionNode(node)) return false; - if (isPrincipalOptionalMemberExpression(node, principalLocalNames)) return true; + if (isPrincipalOptionalMemberExpression(node, principalBindings, root)) return true; return node .children() - .some((child) => nodeContainsArgumentPrincipalOptionalAccess(child, principalLocalNames)); + .some((child) => nodeContainsArgumentPrincipalOptionalAccess(child, principalBindings, root)); } function reviewCallName(call: SgNode): string { @@ -2227,7 +2252,7 @@ function collectNullableCallerReviewFindings( root: SgNode, source: string, file: string, - principalLocalNames: Set, + principalBindings: ReviewPrincipalBinding[], findings: LlmReviewFinding[], seen: Set, ): void { @@ -2236,7 +2261,7 @@ function collectNullableCallerReviewFindings( const args = call.field("arguments"); const nullableArg = args ?.children() - .find((child) => nodeContainsArgumentPrincipalOptionalAccess(child, principalLocalNames)); + .find((child) => nodeContainsArgumentPrincipalOptionalAccess(child, principalBindings, root)); if (!nullableArg) continue; const memberName = findMemberCallName(call); @@ -2269,6 +2294,26 @@ function functionIdentifierParamName(fn: SgNode): string | null { return pattern?.kind() === "identifier" ? pattern.text() : null; } +function objectPatternHasTopLevelProperty(pattern: SgNode, propertyName: string): boolean { + if (pattern.kind() !== "object_pattern") return false; + for (const child of pattern.children()) { + const kind = child.kind(); + if (kind === "shorthand_property_identifier_pattern" && child.text() === propertyName) { + return true; + } + if (kind === "pair_pattern" && child.field("key")?.text() === propertyName) { + return true; + } + if (kind === "object_assignment_pattern") { + const inner = child + .children() + .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); + if (inner?.text() === propertyName) return true; + } + } + return false; +} + function functionReadsContextUser(fn: SgNode, contextName: string): boolean { const body = fn.field("body"); if (!body) return false; @@ -2284,6 +2329,23 @@ function functionReadsContextUser(fn: SgNode, contextName: string): boolean { if (isInsideAnyRange(object.range().start.index, shadowRanges)) continue; return true; } + + const destructures = body.findAll({ + rule: { + kind: "variable_declarator", + has: { + field: "value", + kind: "identifier", + regex: `^${escapeRegex(contextName)}$`, + }, + }, + }); + for (const decl of destructures) { + const value = decl.field("value"); + if (!value || isInsideAnyRange(value.range().start.index, shadowRanges)) continue; + const name = decl.field("name"); + if (name && objectPatternHasTopLevelProperty(name, "user")) return true; + } return false; } @@ -2402,37 +2464,56 @@ function collectResolverContextBodies(root: SgNode): ResolverContextBody[] { return bodies; } -function collectCallerPatternBindings(pattern: SgNode, bindings: Set): void { +function collectCallerPatternBindings( + pattern: SgNode, + scope: [number, number], + bindings: ReviewPrincipalBinding[], + shadowRoot?: SgNode, +): void { if (pattern.kind() !== "object_pattern") return; for (const child of pattern.children()) { const kind = child.kind(); if (kind === "shorthand_property_identifier_pattern" && child.text() === "caller") { - bindings.add("caller"); + bindings.push({ name: "caller", bindingStart: child.range().start.index, scope, shadowRoot }); } else if (kind === "pair_pattern") { const key = child.field("key"); const value = child.field("value"); if (key?.text() === "caller" && value?.kind() === "identifier") { - bindings.add(value.text()); + bindings.push({ + name: value.text(), + bindingStart: value.range().start.index, + scope, + shadowRoot, + }); } } else if (kind === "object_assignment_pattern") { const inner = child .children() .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); - if (inner?.text() === "caller") bindings.add("caller"); + if (inner?.text() === "caller") { + bindings.push({ + name: "caller", + bindingStart: inner.range().start.index, + scope, + shadowRoot, + }); + } } } } -function collectResolverPrincipalLocalNames(root: SgNode): Set { - const bindings = new Set(); +function collectResolverPrincipalBindings(root: SgNode): ReviewPrincipalBinding[] { + const bindings: ReviewPrincipalBinding[] = []; for (const arrow of collectResolverBodyArrows(root)) { const param = getFirstFunctionParam(arrow); const pattern = param ? getFunctionParamPattern(param) : null; const body = arrow.field("body"); if (!pattern || !body) continue; + const bodyRange = body.range(); + const bodyScope: [number, number] = [bodyRange.start.index, bodyRange.end.index]; if (pattern.kind() === "object_pattern") { - collectCallerPatternBindings(pattern, bindings); + collectCallerPatternBindings(pattern, bodyScope, bindings, body); continue; } @@ -2452,7 +2533,8 @@ function collectResolverPrincipalLocalNames(root: SgNode): Set { for (const decl of contextDestructures) { if (isInsideAnyRange(decl.range().start.index, shadowRanges)) continue; const name = decl.field("name"); - if (name) collectCallerPatternBindings(name, bindings); + if (name) + collectCallerPatternBindings(name, enclosingScopeRange(decl) ?? bodyScope, bindings); } } return bindings; @@ -2519,7 +2601,7 @@ export function reviewFindings( _filePath: string, relativePath: string, ): LlmReviewFinding[] { - if (!source.includes("?.") && !source.includes(".user")) return []; + if (!source.includes("?.") && !source.includes(".user") && !source.includes("user")) return []; let root: SgNode; try { @@ -2530,12 +2612,12 @@ export function reviewFindings( const findings: LlmReviewFinding[] = []; const seen = new Set(); - const principalLocalNames = collectResolverPrincipalLocalNames(root); + const principalBindings = collectResolverPrincipalBindings(root); collectNullableCallerReviewFindings( root, source, relativePath, - principalLocalNames, + principalBindings, findings, seen, ); diff --git a/packages/sdk-codemod/src/principal-unify-review.test.ts b/packages/sdk-codemod/src/principal-unify-review.test.ts index 688714968..e44de509e 100644 --- a/packages/sdk-codemod/src/principal-unify-review.test.ts +++ b/packages/sdk-codemod/src/principal-unify-review.test.ts @@ -124,6 +124,46 @@ describe("principal-unify review findings", () => { ]); }); + test("reports helper adapters that destructure context.user", async () => { + await writeProjectFile( + "resolvers/destructured-helper.ts", + [ + 'import { createResolver } from "@tailor-platform/sdk";', + "", + "function createContext(context: any) {", + " const { user } = context;", + " return { userId: user.id };", + "}", + "", + "export const resolver = createResolver({", + " body: async (context) => createContext(context),", + "});", + "", + ].join("\n"), + ); + + const result = await runCodemods([principalUnifyEntry], tmpDir!, false); + + expect(result.llmReviews).toEqual([ + expect.objectContaining({ + codemodId: "v2/principal-unify", + files: ["resolvers/destructured-helper.ts"], + findings: [ + expect.objectContaining({ + file: "resolvers/destructured-helper.ts", + line: 3, + message: expect.stringContaining("createContext"), + }), + expect.objectContaining({ + file: "resolvers/destructured-helper.ts", + line: 9, + message: expect.stringContaining("createContext"), + }), + ], + }), + ]); + }); + test("keeps file-level suspicious-pattern fallback without precise findings", async () => { await writeProjectFile( "resolvers/context-type.ts", @@ -180,4 +220,28 @@ describe("principal-unify review findings", () => { }), ]); }); + + test("does not report matching alias names outside the resolver scope", async () => { + await writeProjectFile( + "resolvers/scoped-alias.ts", + [ + 'import { createResolver } from "@tailor-platform/sdk";', + "", + "declare function publishAudit(userId: string | undefined): Promise;", + "", + "export const resolver = createResolver({", + " body: async ({ user: currentUser }) => currentUser.id,", + "});", + "", + "async function audit(currentUser?: { id: string }) {", + " await publishAudit(currentUser?.id);", + "}", + "", + ].join("\n"), + ); + + const result = await runCodemods([principalUnifyEntry], tmpDir!, false); + + expect(result.llmReviews).toEqual([]); + }); }); From 3749ecf14df778f6515d12874aec46133ceba9e9 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:29:12 +0900 Subject: [PATCH 288/618] fix: detect destructured principal helper adapters --- .../v2/principal-unify/scripts/transform.ts | 14 ++++++- .../src/principal-unify-review.test.ts | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index cd3f6a235..155b090e1 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -2349,12 +2349,22 @@ function functionReadsContextUser(fn: SgNode, contextName: string): boolean { return false; } +function functionContextUserSourceName(fn: SgNode): string | null { + const param = getFirstFunctionParam(fn); + const pattern = param ? getFunctionParamPattern(param) : null; + if (!pattern) return null; + if (pattern.kind() === "identifier") { + return functionReadsContextUser(fn, pattern.text()) ? pattern.text() : null; + } + return objectPatternHasTopLevelProperty(pattern, "user") ? "context" : null; +} + type ContextUserHelperBinding = LocalCallbackBinding & { contextName: string }; function collectContextUserHelperBindings(root: SgNode): ContextUserHelperBinding[] { return collectLocalCallbackBindings(root).flatMap((binding) => { - const contextName = functionIdentifierParamName(binding.fn); - if (!contextName || !functionReadsContextUser(binding.fn, contextName)) return []; + const contextName = functionContextUserSourceName(binding.fn); + if (!contextName) return []; return [{ ...binding, contextName }]; }); } diff --git a/packages/sdk-codemod/src/principal-unify-review.test.ts b/packages/sdk-codemod/src/principal-unify-review.test.ts index e44de509e..955243703 100644 --- a/packages/sdk-codemod/src/principal-unify-review.test.ts +++ b/packages/sdk-codemod/src/principal-unify-review.test.ts @@ -164,6 +164,45 @@ describe("principal-unify review findings", () => { ]); }); + test("reports helper adapters with destructured context parameters", async () => { + await writeProjectFile( + "resolvers/destructured-param-helper.ts", + [ + 'import { createResolver } from "@tailor-platform/sdk";', + "", + "function createContext({ user }: any) {", + " return { userId: user.id };", + "}", + "", + "export const resolver = createResolver({", + " body: async (context) => createContext(context),", + "});", + "", + ].join("\n"), + ); + + const result = await runCodemods([principalUnifyEntry], tmpDir!, false); + + expect(result.llmReviews).toEqual([ + expect.objectContaining({ + codemodId: "v2/principal-unify", + files: ["resolvers/destructured-param-helper.ts"], + findings: [ + expect.objectContaining({ + file: "resolvers/destructured-param-helper.ts", + line: 3, + message: expect.stringContaining("createContext"), + }), + expect.objectContaining({ + file: "resolvers/destructured-param-helper.ts", + line: 8, + message: expect.stringContaining("createContext"), + }), + ], + }), + ]); + }); + test("keeps file-level suspicious-pattern fallback without precise findings", async () => { await writeProjectFile( "resolvers/context-type.ts", From bd95409eecceeeec5ef9598462e2e3218ef235b5 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:30:58 +0900 Subject: [PATCH 289/618] fix: guard runtime idp local bindings --- .../scripts/transform.ts | 28 +++++++++++++++---- .../tests/aliased-idp-local/input.ts | 6 ++++ .../tests/arrow-idp/input.ts | 4 +++ .../tests/catch-pattern-idp/input.ts | 8 ++++++ .../leading-comment-directive/expected.ts | 5 ++++ .../tests/leading-comment-directive/input.ts | 4 +++ 6 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/aliased-idp-local/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/arrow-idp/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-pattern-idp/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/leading-comment-directive/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/leading-comment-directive/input.ts diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts index e949fb60b..9949ab2aa 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts @@ -152,8 +152,20 @@ function localDeclarationNames(root: SgNode): Set { } for (const catchClause of root.findAll({ rule: { kind: "catch_clause" } })) { - const binding = catchClause.children().find((child) => child.kind() === "identifier"); - if (binding) names.add(binding.text()); + for (const child of catchClause.children()) { + if (["identifier", "object_pattern", "array_pattern"].includes(child.kind())) { + collectBindingNames(child, names); + } + } + } + + for (const arrow of root.findAll({ rule: { kind: "arrow_function" } })) { + const children = arrow.children(); + const arrowIndex = children.findIndex((child) => child.kind() === "=>"); + if (arrowIndex === -1) continue; + for (const child of children.slice(0, arrowIndex)) { + collectBindingNames(child, names); + } } for (const loop of root.findAll({ rule: { kind: "for_in_statement" } })) { @@ -192,8 +204,8 @@ function runtimeIdpLocalName(imports: SgNode[]): string | null { return null; } -function hasCollision(imports: SgNode[], localNames: Set): boolean { - if (localNames.has("tailor") || localNames.has("idp")) return true; +function hasCollision(imports: SgNode[], localNames: Set, idpLocal: string): boolean { + if (localNames.has("tailor") || localNames.has("idp") || localNames.has(idpLocal)) return true; for (const importStmt of imports) { for (const binding of importBindings(importStmt)) { @@ -235,6 +247,10 @@ function importInsertionIndex(root: SgNode, imports: SgNode[], source: string): for (const child of root.children()) { if (child.range().start.index < pos) continue; + if (child.kind() === "comment") { + pos = child.range().end.index; + continue; + } if (!isDirectiveStatement(child)) break; pos = child.range().end.index; } @@ -293,10 +309,10 @@ export default function transform(source: string, filePath: string): string | nu if (constructors.length === 0) return null; const imports = findImportStatements(root); - if (hasCollision(imports, localDeclarationNames(root))) return null; - const existingIdpLocal = runtimeIdpLocalName(imports); const idpLocal = existingIdpLocal ?? "idp"; + if (hasCollision(imports, localDeclarationNames(root), idpLocal)) return null; + const edits: Edit[] = constructors.map((constructor) => constructor.replace(`${idpLocal}.Client`), ); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/aliased-idp-local/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/aliased-idp-local/input.ts new file mode 100644 index 000000000..41d31b8e4 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/aliased-idp-local/input.ts @@ -0,0 +1,6 @@ +import { idp as runtimeIdp } from "@tailor-platform/sdk/runtime"; + +export const run = (runtimeIdp: unknown) => { + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); +}; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/arrow-idp/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/arrow-idp/input.ts new file mode 100644 index 000000000..527d16470 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/arrow-idp/input.ts @@ -0,0 +1,4 @@ +export const run = idp => { + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); +}; diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-pattern-idp/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-pattern-idp/input.ts new file mode 100644 index 000000000..7b70c382d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/catch-pattern-idp/input.ts @@ -0,0 +1,8 @@ +export async function run() { + try { + return await load(); + } catch ({ idp }) { + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); + } +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/leading-comment-directive/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/leading-comment-directive/expected.ts new file mode 100644 index 000000000..f1ba1a4c7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/leading-comment-directive/expected.ts @@ -0,0 +1,5 @@ +// @ts-nocheck +"use client"; +import { idp } from "@tailor-platform/sdk/runtime"; + +export const client = new idp.Client({ namespace: "default" }); diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/leading-comment-directive/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/leading-comment-directive/input.ts new file mode 100644 index 000000000..b9c615209 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/leading-comment-directive/input.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +"use client"; + +export const client = new tailor.idp.Client({ namespace: "default" }); From 2d0976f6bd00b3c16f4a5a600807e2d4e3084035 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:32:54 +0900 Subject: [PATCH 290/618] fix(sdk-codemod): parse js cli sources as tsx --- .../sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts | 2 +- .../codemods/v2/cli-rename/tests/source-jsx-js/expected.js | 6 ++++++ .../codemods/v2/cli-rename/tests/source-jsx-js/input.js | 6 ++++++ .../sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts | 2 +- .../v2/rename-bin/tests/source-js-string/expected.js | 6 ++++++ .../codemods/v2/rename-bin/tests/source-js-string/input.js | 6 ++++++ packages/sdk-codemod/src/runner.ts | 2 +- 7 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/expected.js create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/input.js diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index 1fe587d63..df5ca977d 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -703,7 +703,7 @@ function replaceCliRenamesInCommand(command: string): string { function sourceLang(filePath: string): Lang { const ext = path.extname(filePath).toLowerCase(); - return ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : Lang.TypeScript; + return ext === ".tsx" || ext === ".jsx" || ext === ".js" ? Lang.Tsx : Lang.TypeScript; } function sourceStringToken(node: SgNode, source: string): SourceStringToken | undefined { diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/expected.js b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/expected.js new file mode 100644 index 000000000..361fec5c6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/expected.js @@ -0,0 +1,6 @@ +const docs = ( + <> +

package tailor-sdk crash-report --machineuser

+ tailor-sdk crashreport list --machine-user ci + +); diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/input.js b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/input.js new file mode 100644 index 000000000..b789716d1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/input.js @@ -0,0 +1,6 @@ +const docs = ( + <> +

package tailor-sdk crash-report --machineuser

+ tailor-sdk crash-report list --machineuser ci + +); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index d486c91d9..a0272abd2 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -169,7 +169,7 @@ function renameBinary(value: string): string { function sourceLang(filePath: string): Lang { const ext = path.extname(filePath).toLowerCase(); - return ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : Lang.TypeScript; + return ext === ".tsx" || ext === ".jsx" || ext === ".js" ? Lang.Tsx : Lang.TypeScript; } function isTokenSequenceNode(node: SgNode): boolean { diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 20b04ce30..34979c5e7 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -17,3 +17,9 @@ const migrate = "bunx @tailor-platform/sdk@2.0.0-next.2 generate"; const escapedQuery = "tailor query --query \"select 1\""; const script = ["tailor", "deploy"].join(" "); const skills = "tailor-sdk-skills"; +const docs = ( + <> +

package tailor-sdk is installed

+ tailor deploy + +); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 87d8a89ef..4786fa118 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -17,3 +17,9 @@ const migrate = "bunx tailor-sdk@2.0.0-next.2 generate"; const escapedQuery = "tailor-sdk query --query \"select 1\""; const script = ["tailor-sdk", "deploy"].join(" "); const skills = "tailor-sdk-skills"; +const docs = ( + <> +

package tailor-sdk is installed

+ tailor-sdk deploy + +); diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 4bf94a544..cbf54d2cd 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -173,7 +173,7 @@ function sourceStringContentForResidualMatching(relative: string, content: strin function sourceLang(relative: string): Lang { const ext = path.extname(relative).toLowerCase(); - return ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : Lang.TypeScript; + return ext === ".tsx" || ext === ".jsx" || ext === ".js" ? Lang.Tsx : Lang.TypeScript; } function isProcessEnvSubscriptKey(node: SgNode): boolean { From 67968903fe7930e64a6f4f5fc670435624f42376 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:15:40 +0900 Subject: [PATCH 291/618] fix: flag runtime globals in codemod type strings --- packages/sdk-codemod/src/registry.test.ts | 40 +++++++++++++++++++++++ packages/sdk-codemod/src/registry.ts | 8 +++-- packages/sdk-codemod/src/runner.test.ts | 34 ++++++++++++++----- 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 6ee1408fc..21b87022d 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -92,6 +92,46 @@ describe("getApplicableCodemods", () => { (pattern) => pattern instanceof RegExp && pattern.test("const { upload } = tailordb.file;"), ), ).toBe(true); + expect( + codemod?.sourceStringSuspiciousPatterns?.some( + (pattern) => pattern instanceof RegExp && pattern.test("const e: TailorErrors = err;"), + ), + ).toBe(true); + expect( + codemod?.sourceStringSuspiciousPatterns?.some( + (pattern) => + pattern instanceof RegExp && pattern.test("type U = Promise;"), + ), + ).toBe(true); + expect( + codemod?.sourceStringSuspiciousPatterns?.some( + (pattern) => + pattern instanceof RegExp && pattern.test("type Ctor = typeof tailordb.Client;"), + ), + ).toBe(true); + expect( + codemod?.sourceStringSuspiciousPatterns?.some( + (pattern) => pattern instanceof RegExp && pattern.test("return tailordb.Client;"), + ), + ).toBe(true); + expect( + codemod?.sourceStringSuspiciousPatterns?.some( + (pattern) => pattern instanceof RegExp && pattern.test("foo(tailordb.Client);"), + ), + ).toBe(true); + expect( + codemod?.sourceStringSuspiciousPatterns?.some( + (pattern) => + pattern instanceof RegExp && pattern.test("type F = () => tailordb.QueryResult;"), + ), + ).toBe(true); + expect( + codemod?.sourceStringSuspiciousPatterns?.some( + (pattern) => + pattern instanceof RegExp && + pattern.test("type R = Promise>;"), + ), + ).toBe(true); expect(codemod?.prompt).toContain("@tailor-platform/sdk/runtime/globals"); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index a7017ecbf..a3e995e02 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -457,14 +457,16 @@ export const allCodemods: CodemodPackage[] = [ sourceStringSuspiciousPatterns: [ "new tailor.idp.Client", /[=(:,[]\s*tailor\.idp\.Client\b/, - /(?:(?:[=(:,{]|\[)\s*|\b(?:return|await)\s+)tailor\.(?:authconnection|context|iconv|idp|secretmanager|workflow)(?:\.[A-Za-z_$][\w$]*)?\b/, + /(?:(?:=>|[=(:,<{]|\[)\s*|\b(?:return|await|typeof)\s+)tailor\.(?:authconnection|context|iconv|idp|secretmanager|workflow)(?:\.[A-Za-z_$][\w$]*)?\b/, /\btailor\.(?:authconnection|context|iconv|idp|secretmanager|workflow)\.[A-Za-z_$][\w$]*\s*\(/, "tailor[", /\btailordb\.file\.[A-Za-z_$][\w$]*\s*\(/, - /(?:(?:[=(:,{]|\[)\s*|\b(?:return|await)\s+)tailordb\.file\b/, - /(?:\bnew\s+|[:=]\s*)tailordb\.(?:Client|CommandType|QueryResult)\b/, + /(?:(?:=>|[=(:,<{]|\[)\s*|\b(?:return|await|typeof)\s+)tailordb\.file\b/, + /(?:\bnew\s+|(?:=>|[=(:,<{]|\[)\s*|\b(?:return|await|typeof)\s+)tailordb\.(?:Client|CommandType|QueryResult)\b/, + /<\s*tailordb\.(?:Client|CommandType|QueryResult)\b/, "tailordb[", /(?:\bnew\s+|\bthrow\s+|\binstanceof\s+)Tailor(?:DBFileError|Errors|ErrorMessage)\b/, + /(?:[:=<]\s*|\bas\s+)Tailor(?:DBFileError|Errors|ErrorMessage|ErrorItem)\b/, /[:<]\s*TailorErrorItem\b/, ], examples: [ diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 80ec00fe0..e1b566faf 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -663,12 +663,25 @@ describe("runCodemods", () => { "const { getSecret } = tailor.secretmanager;", "const getInvoker = tailor.context.getInvoker;", "const { upload } = tailordb.file;", + "const e: TailorErrors = err;", + "type R = Promise>;", + ].join("\\n"); + const typeOnlyEmbeddedCode = [ + "type U = Promise;", + "type Ctor = typeof tailordb.Client;", + "return tailordb.Client;", + "foo(tailordb.Client);", + "type F = () => tailordb.QueryResult;", ].join("\\n"); const seedSource = [ `const code = \`${embeddedCode}\`;`, 'const note = "tailor.idp.Client is mentioned in prose";', ].join("\n"); await fs.promises.writeFile(path.join(dir, "seed.mjs"), seedSource); + await fs.promises.writeFile( + path.join(dir, "types.mjs"), + `const code = \`${typeOnlyEmbeddedCode}\`;`, + ); await fs.promises.writeFile( path.join(dir, "prose.mjs"), ['const separator = "=";', 'const note = "tailor.idp.Client is mentioned in prose";'].join( @@ -688,9 +701,12 @@ describe("runCodemods", () => { sourceStringSuspiciousPatterns: [ "new tailor.idp.Client", /[=(:,[]\s*tailor\.idp\.Client\b/, - /(?:(?:[=(:,{]|\[)\s*|\b(?:return|await)\s+)tailor\.(?:context|idp|secretmanager)(?:\.[A-Za-z_$][\w$]*)?\b/, + /(?:(?:=>|[=(:,<{]|\[)\s*|\b(?:return|await|typeof)\s+)tailor\.(?:context|idp|secretmanager)(?:\.[A-Za-z_$][\w$]*)?\b/, /\btailor\.(?:idp|secretmanager)\.[A-Za-z_$][\w$]*\s*\(/, /(?:(?:[=(:,{]|\[)\s*|\b(?:return|await)\s+)tailordb\.file\b/, + /(?:\bnew\s+|(?:=>|[=(:,<{]|\[)\s*|\b(?:return|await|typeof)\s+)tailordb\.(?:Client|QueryResult)\b/, + /<\s*tailordb\.(?:QueryResult)\b/, + /(?:[:=<]\s*|\bas\s+)Tailor(?:Errors)\b/, ], prompt: "Review embedded runtime global usage by hand.", }, @@ -702,13 +718,15 @@ describe("runCodemods", () => { true, ); - expect(result.llmReviews).toEqual([ - { - codemodId: "test/llm-source-string", - prompt: "Review embedded runtime global usage by hand.", - files: ["seed.mjs"], - }, - ]); + expect(result.llmReviews).toHaveLength(1); + expect(result.llmReviews[0]).toMatchObject({ + codemodId: "test/llm-source-string", + prompt: "Review embedded runtime global usage by hand.", + }); + expect(result.llmReviews[0]?.files).toEqual( + expect.arrayContaining(["seed.mjs", "types.mjs"]), + ); + expect(result.llmReviews[0]?.files).toHaveLength(2); }); test("keeps LLM review patterns inside template substitutions", async () => { From 8cb0976e71dc9d8ef52f44a45e6f3c6cd163a5b4 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:40:39 +0900 Subject: [PATCH 292/618] fix: detect direct nullable principal arguments --- .../v2/principal-unify/scripts/transform.ts | 21 ++++++++- .../src/principal-unify-review.test.ts | 47 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 155b090e1..727caf9d6 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -2225,12 +2225,24 @@ function isPrincipalOptionalMemberExpression( return object.field("property")?.text() === "caller"; } +function isDirectPrincipalExpression( + node: SgNode, + principalBindings: ReviewPrincipalBinding[], + root: SgNode, +): boolean { + if (resolvesToReviewPrincipalBinding(node, principalBindings, root)) return true; + if (node.kind() !== "member_expression") return false; + if (node.children().some((child) => child.kind() === "optional_chain")) return false; + return node.field("property")?.text() === "caller"; +} + function nodeContainsArgumentPrincipalOptionalAccess( node: SgNode, principalBindings: ReviewPrincipalBinding[], root: SgNode, ): boolean { if (isFunctionNode(node)) return false; + if (isDirectPrincipalExpression(node, principalBindings, root)) return true; if (isPrincipalOptionalMemberExpression(node, principalBindings, root)) return true; return node .children() @@ -2611,7 +2623,14 @@ export function reviewFindings( _filePath: string, relativePath: string, ): LlmReviewFinding[] { - if (!source.includes("?.") && !source.includes(".user") && !source.includes("user")) return []; + if ( + !source.includes("?.") && + !source.includes(".user") && + !source.includes("user") && + !source.includes("caller") + ) { + return []; + } let root: SgNode; try { diff --git a/packages/sdk-codemod/src/principal-unify-review.test.ts b/packages/sdk-codemod/src/principal-unify-review.test.ts index 955243703..3efeef1bd 100644 --- a/packages/sdk-codemod/src/principal-unify-review.test.ts +++ b/packages/sdk-codemod/src/principal-unify-review.test.ts @@ -260,6 +260,53 @@ describe("principal-unify review findings", () => { ]); }); + test("reports nullable caller objects passed directly to non-null-looking calls", async () => { + await writeProjectFile( + "resolvers/direct-caller.ts", + [ + 'import { createResolver } from "@tailor-platform/sdk";', + "", + "declare function publishAudit(user: { id: string }): Promise;", + "", + "export const contextResolver = createResolver({", + " body: async (context) => {", + " await publishAudit(context.user);", + " },", + "});", + "", + "export const destructuredResolver = createResolver({", + " body: async ({ user }) => {", + " await publishAudit(user);", + " },", + "});", + "", + ].join("\n"), + ); + + const result = await runCodemods([principalUnifyEntry], tmpDir!, false); + + expect(result.llmReviews).toEqual([ + expect.objectContaining({ + codemodId: "v2/principal-unify", + files: ["resolvers/direct-caller.ts"], + findings: [ + expect.objectContaining({ + file: "resolvers/direct-caller.ts", + line: 7, + message: expect.stringContaining("non-null argument"), + excerpt: expect.stringContaining("publishAudit(context.caller)"), + }), + expect.objectContaining({ + file: "resolvers/direct-caller.ts", + line: 13, + message: expect.stringContaining("non-null argument"), + excerpt: expect.stringContaining("publishAudit(caller)"), + }), + ], + }), + ]); + }); + test("does not report matching alias names outside the resolver scope", async () => { await writeProjectFile( "resolvers/scoped-alias.ts", From 6199f9161c7d9d12324d630a65d225b1a3c61e15 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:45:25 +0900 Subject: [PATCH 293/618] fix: guard runtime idp import aliases --- .../codemods/v2/runtime-globals-opt-in/scripts/transform.ts | 1 + .../tests/import-alias-tailor/input.ts | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-alias-tailor/input.ts diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts index 9949ab2aa..902a8a765 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts @@ -142,6 +142,7 @@ function localDeclarationNames(root: SgNode): Set { { kind: "type_alias_declaration" }, { kind: "enum_declaration" }, { kind: "internal_module" }, + { kind: "import_alias" }, ], }, })) { diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-alias-tailor/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-alias-tailor/input.ts new file mode 100644 index 000000000..fb4d7e05e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/import-alias-tailor/input.ts @@ -0,0 +1,5 @@ +namespace X { + import tailor = localTailor; + + export const client = new tailor.idp.Client({ namespace: "default" }); +} From 869a9a2b5cb9b8973afdd4c2af506895470a3c19 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:46:47 +0900 Subject: [PATCH 294/618] fix: handle escaped codemod source strings --- packages/sdk-codemod/src/runner.test.ts | 8 ++++-- packages/sdk-codemod/src/runner.ts | 38 +++++++++++++++++++++---- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index e1b566faf..5574ac5ae 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -678,6 +678,10 @@ describe("runCodemods", () => { 'const note = "tailor.idp.Client is mentioned in prose";', ].join("\n"); await fs.promises.writeFile(path.join(dir, "seed.mjs"), seedSource); + await fs.promises.writeFile( + path.join(dir, "escaped.mjs"), + 'const code = "const C =\\n tailor.idp.Client;";', + ); await fs.promises.writeFile( path.join(dir, "types.mjs"), `const code = \`${typeOnlyEmbeddedCode}\`;`, @@ -724,9 +728,9 @@ describe("runCodemods", () => { prompt: "Review embedded runtime global usage by hand.", }); expect(result.llmReviews[0]?.files).toEqual( - expect.arrayContaining(["seed.mjs", "types.mjs"]), + expect.arrayContaining(["escaped.mjs", "seed.mjs", "types.mjs"]), ); - expect(result.llmReviews[0]?.files).toHaveLength(2); + expect(result.llmReviews[0]?.files).toHaveLength(3); }); test("keeps LLM review patterns inside template substitutions", async () => { diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index a0c2c8daa..4cf66e218 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -148,6 +148,32 @@ function contentForResidualMatching(relative: string, content: string): string { return SOURCE_EXTENSIONS.has(ext) ? maskSourceNonCode(relative, content) : content; } +function sourceStringFragmentGapForResidualMatching(gap: string): string { + return /^(?:\\(?:[nrtvf]|\r\n|\r|\n)|\s)+$/.test(gap) ? " " : SOURCE_STRING_FRAGMENT_SEPARATOR; +} + +function sourceStringNodeContentForResidualMatching(node: SgNode, content: string): string | null { + const parts: string[] = []; + let previousFragmentEnd: number | null = null; + + for (const child of node.children()) { + if (child.kind() !== "string_fragment") continue; + + const range = child.range(); + if (previousFragmentEnd != null && range.start.index > previousFragmentEnd) { + parts.push( + sourceStringFragmentGapForResidualMatching( + content.slice(previousFragmentEnd, range.start.index), + ), + ); + } + parts.push(child.text()); + previousFragmentEnd = range.end.index; + } + + return parts.length === 0 ? null : parts.join(""); +} + function sourceStringContentForResidualMatching(relative: string, content: string): string | null { const ext = path.extname(relative).toLowerCase(); if (!SOURCE_EXTENSIONS.has(ext)) return null; @@ -159,18 +185,20 @@ function sourceStringContentForResidualMatching(relative: string, content: strin return null; } - const fragments: string[] = []; + const sourceStrings: string[] = []; const visit = (node: SgNode): void => { - if (node.kind() === "string_fragment") { - fragments.push(node.text()); - return; + const kind = node.kind(); + if (kind === "string" || kind === "template_string") { + const sourceString = sourceStringNodeContentForResidualMatching(node, content); + if (sourceString != null) sourceStrings.push(sourceString); } for (const child of node.children()) { + if (child.kind() === "string_fragment") continue; visit(child); } }; visit(root); - return fragments.join(SOURCE_STRING_FRAGMENT_SEPARATOR); + return sourceStrings.join(SOURCE_STRING_FRAGMENT_SEPARATOR); } function sourceLang(relative: string): Lang { From ee2dc3d156c143ba0c94797d774a52b834783a33 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:50:33 +0900 Subject: [PATCH 295/618] fix(sdk-codemod): avoid source data cli false positives --- .../v2/cli-rename/scripts/transform.ts | 8 ++++- .../tests/source-token-array/expected.ts | 1 + .../tests/source-token-array/input.ts | 1 + .../v2/rename-bin/scripts/transform.ts | 31 ++++++++++++++++++- .../tests/source-token-runner/expected.ts | 1 + .../tests/source-token-runner/input.ts | 1 + packages/sdk-codemod/src/registry.test.ts | 2 +- packages/sdk-codemod/src/registry.ts | 3 +- packages/sdk-codemod/src/runner.test.ts | 28 +++++++++++++++-- 9 files changed, 70 insertions(+), 6 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index df5ca977d..81dc69c87 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -1488,7 +1488,13 @@ function collectTokenizedSourceEdits( const name = optionName(token.value); const renameCommand = commandMayBeNext && !token.value.startsWith("-"); - const replacement = rewriteTokenizedCliArg(token.value, renameCommand); + const replacement = + commandMayBeNext && + token.value.startsWith("-") && + !GLOBAL_BOOLEAN_ARGS.has(name) && + !isOpenGlobalValueArg(token.value) + ? token.value + : rewriteTokenizedCliArg(token.value, renameCommand); if (replacement !== token.value) { edits.push([token.start, token.end, replacement]); } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts index 7b8a91ed7..8bdb04710 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts @@ -56,3 +56,4 @@ const label = "crash-report"; const regexPattern = /tailor-sdk crash-report --machineuser/; const prose = "package tailor-sdk crash-report --machineuser"; const proseTemplate = `package tailor-sdk crash-report --machineuser`; +const packageOptionData = ["tailor-sdk", "--machineuser"]; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts index 6ac23ba8b..5a408f2ac 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts @@ -56,3 +56,4 @@ const label = "crash-report"; const regexPattern = /tailor-sdk crash-report --machineuser/; const prose = "package tailor-sdk crash-report --machineuser"; const proseTemplate = `package tailor-sdk crash-report --machineuser`; +const packageOptionData = ["tailor-sdk", "--machineuser"]; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index a0272abd2..e54d28abd 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -94,6 +94,26 @@ const TAILOR_CLI_SEPARATE_VALUE_FLAGS = new Set([ "-q", "-f", ]); +const TAILOR_CLI_GLOBAL_FLAGS = new Set([ + "--env-file-if-exists", + "--env-file", + "--profile", + "--config", + "--workspace-id", + "--verbose", + "--json", + "--yes", + "--help", + "--version", + "-e", + "-p", + "-c", + "-w", + "-j", + "-y", + "-h", + "-v", +]); const SOURCE_STRING_WRAPPER_NODE_KINDS = new Set([ "as_expression", "non_null_expression", @@ -442,7 +462,16 @@ function isPathLikeRunnerFlagValue(value: string): boolean { function isTailorCliCommandToken(value: string): boolean { const token = value.replace(/[),.:!?]+$/, ""); - return token.startsWith("-") || TAILOR_CLI_COMMANDS.has(token); + return TAILOR_CLI_COMMANDS.has(token) || isTailorCliGlobalFlag(token); +} + +function optionName(value: string): string { + const equalsIndex = value.indexOf("="); + return equalsIndex === -1 ? value : value.slice(0, equalsIndex); +} + +function isTailorCliGlobalFlag(value: string): boolean { + return TAILOR_CLI_GLOBAL_FLAGS.has(optionName(value)); } function tailorCliValueFlagName(value: string | undefined): string | undefined { diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts index ecd6f1a81..97331d3e8 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts @@ -60,4 +60,5 @@ const unrelatedNpxValues = ["npx", "create-tailor-sdk", "tailor-sdk-skills", "ta const packageNames = ["tailor-sdk", "react"]; const packageTuple = [["tailor-sdk", version]]; const packageTupleValue = ["tailor-sdk", version]; +const packageOptionData = ["tailor-sdk", "--not-cli"]; const outputDir = ".tailor-sdk"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts index 4cc2be120..d1719f680 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts @@ -60,4 +60,5 @@ const unrelatedNpxValues = ["npx", "create-tailor-sdk", "tailor-sdk-skills", "ta const packageNames = ["tailor-sdk", "react"]; const packageTuple = [["tailor-sdk", version]]; const packageTupleValue = ["tailor-sdk", version]; +const packageOptionData = ["tailor-sdk", "--not-cli"]; const outputDir = ".tailor-sdk"; diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 668f00205..995fea754 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -74,7 +74,7 @@ describe("getApplicableCodemods", () => { expect(cliRename?.sourceStringLegacyPatterns).toEqual( expect.arrayContaining([ ["tailor-sdk", "crash-report"], - ["tailor", "crash-report"], + [/(?:^|[\s;&|])tailor(?=[\s;&|]|$)/, "crash-report"], "--machineuser", ]), ); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 2ef7544c5..473240d5e 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -4,6 +4,7 @@ import { lt, gte, valid } from "semver"; import type { CodemodPackage } from "./types"; const CODEMODS_ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "codemods"); +const TAILOR_COMMAND_TOKEN_RE = /(?:^|[\s;&|])tailor(?=[\s;&|]|$)/; /** All registered codemods, in registration order. */ export const allCodemods: CodemodPackage[] = [ @@ -217,7 +218,7 @@ export const allCodemods: CodemodPackage[] = [ legacyPatterns: ["tailor-sdk crash-report", "--machineuser"], sourceStringLegacyPatterns: [ ["tailor-sdk", "crash-report"], - ["tailor", "crash-report"], + [TAILOR_COMMAND_TOKEN_RE, "crash-report"], "--machineuser", ], examples: [ diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 57a8fcb7f..8c794425d 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -519,7 +519,7 @@ describe("runCodemods", () => { }, { codemod: makeCodemod("test/cli-rename", undefined, ["**/*.ts"], [], { - sourceStringLegacyPatterns: [["tailor", "crash-report"]], + sourceStringLegacyPatterns: [[/(?:^|[\s;&|])tailor(?=[\s;&|]|$)/, "crash-report"]], }), }, ], @@ -528,10 +528,34 @@ describe("runCodemods", () => { ); expect(result.warnings).toEqual([ - "cli.ts: contains tailor + crash-report but was not migrated automatically (rule: test/cli-rename). Manual migration may be needed.", + "cli.ts: contains /(?:^|[\\s;&|])tailor(?=[\\s;&|]|$)/ + crash-report but was not migrated automatically (rule: test/cli-rename). Manual migration may be needed.", ]); }); + test("does not treat SDK package specifiers as tailor command tokens", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "cli.ts"), + ['import "@tailor-platform/sdk";', 'const commandName = "crash-report";'].join("\n"), + "utf-8", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/cli-rename", undefined, ["**/*.ts"], [], { + sourceStringLegacyPatterns: [[/(?:^|[\s;&|])tailor(?=[\s;&|]|$)/, "crash-report"]], + }), + }, + ], + dir, + false, + ); + + expect(result.warnings).toEqual([]); + }); + test("flags files matching a suspicious pattern for LLM review", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-test-")); tmpDir = dir; From 9e0b2bc410eef57bdec837b81a84456d8c75061f Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 14:55:53 +0900 Subject: [PATCH 296/618] fix: track principal caller aliases precisely --- .../v2/principal-unify/scripts/transform.ts | 144 ++++++++++++++---- .../src/principal-unify-review.test.ts | 59 +++++++ 2 files changed, 176 insertions(+), 27 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 727caf9d6..35bbe86ec 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -2193,7 +2193,7 @@ interface ReviewPrincipalBinding { shadowRoot?: SgNode; } -function resolvesToReviewPrincipalBinding( +function resolvesToReviewBinding( node: SgNode, bindings: ReviewPrincipalBinding[], root: SgNode, @@ -2209,9 +2209,29 @@ function resolvesToReviewPrincipalBinding( }); } +function resolvesToReviewPrincipalBinding( + node: SgNode, + bindings: ReviewPrincipalBinding[], + root: SgNode, +): boolean { + return resolvesToReviewBinding(node, bindings, root); +} + +function isReviewContextCallerMemberExpression( + node: SgNode, + contextBindings: ReviewPrincipalBinding[], + root: SgNode, +): boolean { + if (node.kind() !== "member_expression") return false; + if (node.field("property")?.text() !== "caller") return false; + const object = node.field("object"); + return object ? resolvesToReviewBinding(object, contextBindings, root) : false; +} + function isPrincipalOptionalMemberExpression( node: SgNode, principalBindings: ReviewPrincipalBinding[], + contextBindings: ReviewPrincipalBinding[], root: SgNode, ): boolean { if (node.kind() !== "member_expression") return false; @@ -2221,32 +2241,34 @@ function isPrincipalOptionalMemberExpression( if (object.kind() === "identifier") { return resolvesToReviewPrincipalBinding(object, principalBindings, root); } - if (object.kind() !== "member_expression") return false; - return object.field("property")?.text() === "caller"; + return isReviewContextCallerMemberExpression(object, contextBindings, root); } function isDirectPrincipalExpression( node: SgNode, principalBindings: ReviewPrincipalBinding[], + contextBindings: ReviewPrincipalBinding[], root: SgNode, ): boolean { if (resolvesToReviewPrincipalBinding(node, principalBindings, root)) return true; - if (node.kind() !== "member_expression") return false; - if (node.children().some((child) => child.kind() === "optional_chain")) return false; - return node.field("property")?.text() === "caller"; + return isReviewContextCallerMemberExpression(node, contextBindings, root); } function nodeContainsArgumentPrincipalOptionalAccess( node: SgNode, principalBindings: ReviewPrincipalBinding[], + contextBindings: ReviewPrincipalBinding[], root: SgNode, ): boolean { if (isFunctionNode(node)) return false; - if (isDirectPrincipalExpression(node, principalBindings, root)) return true; - if (isPrincipalOptionalMemberExpression(node, principalBindings, root)) return true; + if (isDirectPrincipalExpression(node, principalBindings, contextBindings, root)) return true; + if (isPrincipalOptionalMemberExpression(node, principalBindings, contextBindings, root)) + return true; return node .children() - .some((child) => nodeContainsArgumentPrincipalOptionalAccess(child, principalBindings, root)); + .some((child) => + nodeContainsArgumentPrincipalOptionalAccess(child, principalBindings, contextBindings, root), + ); } function reviewCallName(call: SgNode): string { @@ -2265,6 +2287,7 @@ function collectNullableCallerReviewFindings( source: string, file: string, principalBindings: ReviewPrincipalBinding[], + contextBindings: ReviewPrincipalBinding[], findings: LlmReviewFinding[], seen: Set, ): void { @@ -2273,7 +2296,14 @@ function collectNullableCallerReviewFindings( const args = call.field("arguments"); const nullableArg = args ?.children() - .find((child) => nodeContainsArgumentPrincipalOptionalAccess(child, principalBindings, root)); + .find((child) => + nodeContainsArgumentPrincipalOptionalAccess( + child, + principalBindings, + contextBindings, + root, + ), + ); if (!nullableArg) continue; const memberName = findMemberCallName(call); @@ -2486,6 +2516,51 @@ function collectResolverContextBodies(root: SgNode): ResolverContextBody[] { return bodies; } +function collectResolverContextBindings(root: SgNode): ReviewPrincipalBinding[] { + const bindings: ReviewPrincipalBinding[] = []; + const rootRange = root.range(); + const rootScope: [number, number] = [rootRange.start.index, rootRange.end.index]; + + for (const arrow of collectResolverBodyArrows(root)) { + const param = getFirstFunctionParam(arrow); + const pattern = param ? getFunctionParamPattern(param) : null; + const body = arrow.field("body"); + if (!pattern || pattern.kind() !== "identifier" || !body) continue; + + const bodyRange = body.range(); + bindings.push({ + name: pattern.text(), + bindingStart: pattern.range().start.index, + scope: [bodyRange.start.index, bodyRange.end.index], + shadowRoot: body, + }); + } + + let changed = true; + while (changed) { + changed = false; + const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarators) { + const name = decl.field("name"); + const value = decl.field("value"); + if (!name || name.kind() !== "identifier" || !value) continue; + + const bindingStart = name.range().start.index; + if (bindings.some((binding) => binding.bindingStart === bindingStart)) continue; + if (!resolvesToReviewBinding(value, bindings, root)) continue; + + bindings.push({ + name: name.text(), + bindingStart, + scope: enclosingScopeRange(decl) ?? rootScope, + }); + changed = true; + } + } + + return bindings; +} + function collectCallerPatternBindings( pattern: SgNode, scope: [number, number], @@ -2524,7 +2599,10 @@ function collectCallerPatternBindings( } } -function collectResolverPrincipalBindings(root: SgNode): ReviewPrincipalBinding[] { +function collectResolverPrincipalBindings( + root: SgNode, + contextBindings: ReviewPrincipalBinding[], +): ReviewPrincipalBinding[] { const bindings: ReviewPrincipalBinding[] = []; for (const arrow of collectResolverBodyArrows(root)) { const param = getFirstFunctionParam(arrow); @@ -2540,23 +2618,33 @@ function collectResolverPrincipalBindings(root: SgNode): ReviewPrincipalBinding[ } if (pattern.kind() !== "identifier") continue; - const contextName = pattern.text(); - const shadowRanges = collectCtxShadowRanges(body, contextName, arrow); - const contextDestructures = body.findAll({ - rule: { - kind: "variable_declarator", - has: { - field: "value", - kind: "identifier", - regex: `^${escapeRegex(contextName)}$`, - }, - }, - }); - for (const decl of contextDestructures) { - if (isInsideAnyRange(decl.range().start.index, shadowRanges)) continue; + + const declarators = body.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarators) { const name = decl.field("name"); - if (name) + const value = decl.field("value"); + if (!name || !value) continue; + + if (resolvesToReviewBinding(value, contextBindings, root)) { collectCallerPatternBindings(name, enclosingScopeRange(decl) ?? bodyScope, bindings); + continue; + } + + if (name.kind() !== "identifier") continue; + const bindingStart = name.range().start.index; + if (bindings.some((binding) => binding.bindingStart === bindingStart)) continue; + if ( + !isReviewContextCallerMemberExpression(value, contextBindings, root) && + !resolvesToReviewPrincipalBinding(value, bindings, root) + ) { + continue; + } + + bindings.push({ + name: name.text(), + bindingStart, + scope: enclosingScopeRange(decl) ?? bodyScope, + }); } } return bindings; @@ -2641,12 +2729,14 @@ export function reviewFindings( const findings: LlmReviewFinding[] = []; const seen = new Set(); - const principalBindings = collectResolverPrincipalBindings(root); + const contextBindings = collectResolverContextBindings(root); + const principalBindings = collectResolverPrincipalBindings(root, contextBindings); collectNullableCallerReviewFindings( root, source, relativePath, principalBindings, + contextBindings, findings, seen, ); diff --git a/packages/sdk-codemod/src/principal-unify-review.test.ts b/packages/sdk-codemod/src/principal-unify-review.test.ts index 3efeef1bd..6deb8e73b 100644 --- a/packages/sdk-codemod/src/principal-unify-review.test.ts +++ b/packages/sdk-codemod/src/principal-unify-review.test.ts @@ -307,6 +307,65 @@ describe("principal-unify review findings", () => { ]); }); + test("reports aliases assigned from nullable caller values", async () => { + await writeProjectFile( + "resolvers/assigned-alias.ts", + [ + 'import { createResolver } from "@tailor-platform/sdk";', + "", + "declare function publishAudit(userId: string): Promise;", + "", + "export const resolver = createResolver({", + " body: async (context) => {", + " const user = context.user;", + " await publishAudit(user.id);", + " },", + "});", + "", + ].join("\n"), + ); + + const result = await runCodemods([principalUnifyEntry], tmpDir!, false); + + expect(result.llmReviews).toEqual([ + expect.objectContaining({ + codemodId: "v2/principal-unify", + files: ["resolvers/assigned-alias.ts"], + findings: [ + expect.objectContaining({ + file: "resolvers/assigned-alias.ts", + line: 8, + message: expect.stringContaining("non-null argument"), + excerpt: expect.stringContaining("publishAudit(user.id)"), + }), + ], + }), + ]); + }); + + test("does not report unrelated caller properties as nullable principals", async () => { + await writeProjectFile( + "resolvers/domain-caller.ts", + [ + 'import { createResolver } from "@tailor-platform/sdk";', + "", + "declare function publishAudit(userId: string): Promise;", + "", + "export const resolver = createResolver({", + " body: async ({ user }) => {", + " const event = { caller: { id: user.id } };", + " await publishAudit(event.caller.id);", + " },", + "});", + "", + ].join("\n"), + ); + + const result = await runCodemods([principalUnifyEntry], tmpDir!, false); + + expect(result.llmReviews.flatMap((review) => review.findings ?? [])).toEqual([]); + }); + test("does not report matching alias names outside the resolver scope", async () => { await writeProjectFile( "resolvers/scoped-alias.ts", From bc938d88888c648a1aeaff8cbdbc452b4af65131 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 15:10:15 +0900 Subject: [PATCH 297/618] fix(sdk-codemod): protect template cli values --- .../v2/cli-rename/scripts/transform.ts | 49 +++++++++++-------- .../tests/source-template/expected.ts | 2 + .../cli-rename/tests/source-template/input.ts | 2 + .../v2/rename-bin/scripts/transform.ts | 41 ++++++++++++++++ .../tests/source-template/expected.ts | 1 + .../rename-bin/tests/source-template/input.ts | 1 + 6 files changed, 75 insertions(+), 21 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index 81dc69c87..34e2c269a 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -36,17 +36,6 @@ const GLOBAL_VALUE_ARGS = new Set([ "-c", "-w", ]); -const CLI_VALUE_ARGS = [ - "--env-file-if-exists", - "--env-file", - "--arg", - "--query", - "--file", - "-e", - "-a", - "-q", - "-f", -] as const; const COMMAND_VALUE_ARGS = [ "--env-file-if-exists", "--env-file", @@ -64,7 +53,7 @@ const COMMAND_VALUE_ARGS = [ "-q", "-f", ] as const; -const CLI_VALUE_ARG_SET = new Set(CLI_VALUE_ARGS); +const CLI_VALUE_ARG_SET = new Set(COMMAND_VALUE_ARGS); const PACKAGE_RUNNER_VALUE_ARGS = new Set([ "--cache", "--userconfig", @@ -111,6 +100,11 @@ interface CliTemplateState { skipNextValue: boolean; } +interface CliTemplateEditResult { + edits: Array<[number, number, string]>; + protectedRanges: TextRange[]; +} + const TEMPLATE_SUBSTITUTION_PLACEHOLDER = "\u{E000}"; function isOptionBoundaryChar(value: string | undefined): boolean { @@ -1166,16 +1160,23 @@ function advanceCliTemplateState(state: CliTemplateState, value: string): void { } } -function collectCliTemplateEdits(node: SgNode, source: string): Array<[number, number, string]> { - if (node.kind() !== "template_string") return []; +function collectCliTemplateEdits(node: SgNode, source: string): CliTemplateEditResult { + if (node.kind() !== "template_string") return { edits: [], protectedRanges: [] }; const edits: Array<[number, number, string]> = []; + const protectedRanges: TextRange[] = []; const state: CliTemplateState = { afterTailorBinary: false, commandMayBeNext: false, skipNextValue: false, }; let skipNextPackageRunnerValue = false; + const protectTemplateSubstitutions = (token: TemplateToken): void => { + for (const substitution of token.substitutions) { + const range = substitution.range(); + protectedRanges.push({ start: range.start.index, end: range.end.index }); + } + }; const tokens = collectTemplateTokens(node, source); for (const [index, token] of tokens.entries()) { @@ -1215,6 +1216,7 @@ function collectCliTemplateEdits(node: SgNode, source: string): Array<[number, n } if (state.skipNextValue) { + protectTemplateSubstitutions(token); state.skipNextValue = false; continue; } @@ -1311,7 +1313,7 @@ function collectCliTemplateEdits(node: SgNode, source: string): Array<[number, n } } - return edits; + return { edits, protectedRanges }; } function isLikelyTemplateCommandToken( @@ -1341,11 +1343,12 @@ function collectCliTemplateSourceEdits( root: SgNode, source: string, protectedRanges: TextRange[] = [], -): Array<[number, number, string]> { +): CliTemplateEditResult { const edits: Array<[number, number, string]> = []; + const templateProtectedRanges = [...protectedRanges]; const isProtected = (node: SgNode): boolean => { const range = node.range(); - return protectedRanges.some( + return templateProtectedRanges.some( (protectedRange) => protectedRange.start <= range.start.index && range.end.index <= protectedRange.end, ); @@ -1354,14 +1357,16 @@ function collectCliTemplateSourceEdits( const visit = (node: SgNode): void => { if (isProtected(node)) return; if (node.kind() === "template_string") { - edits.push(...collectCliTemplateEdits(node, source)); + const result = collectCliTemplateEdits(node, source); + edits.push(...result.edits); + templateProtectedRanges.push(...result.protectedRanges); } for (const child of node.children()) { visit(child); } }; visit(root); - return edits; + return { edits, protectedRanges: templateProtectedRanges }; } function collectSourceLiteralCliCommandEdits( @@ -1597,11 +1602,13 @@ function replaceTokenizedSourceCliCommands(source: string, filePath: string): st let updated = source; const tokenized = collectTokenizedSourceEdits(root, source); + const template = collectCliTemplateSourceEdits(root, source, tokenized.protectedRanges); + const protectedRanges = [...tokenized.protectedRanges, ...template.protectedRanges]; const editByRange = new Map(); for (const edit of [ ...tokenized.edits, - ...collectCliTemplateSourceEdits(root, source, tokenized.protectedRanges), - ...collectSourceLiteralCliCommandEdits(root, source, tokenized.protectedRanges), + ...template.edits, + ...collectSourceLiteralCliCommandEdits(root, source, protectedRanges), ]) { editByRange.set(`${edit[0]}:${edit[1]}`, edit); } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts index 27129c0e7..e8de96451 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts @@ -24,6 +24,7 @@ const envFileCommandSubstitutionPath = "tailor-sdk --env-file=$(pwd)/--machineus const argPayload = "tailor-sdk function test-run --arg=--machineuser"; const argCommandSubstitutionPayload = "tailor-sdk function test-run --arg=$(printf --machineuser) --machine-user"; const quotedArgCommandPayload = "tailor-sdk function test-run --arg='tailor-sdk crash-report --machineuser' --machine-user"; +const substitutedArgCommandPayload = `tailor-sdk function test-run --arg ${cond ? "tailor-sdk crash-report --machineuser" : "{}"} --machine-user`; const escapedQueryCommand = "tailor-sdk query --query \"select 1\" --machine-user"; const profileCommand = "tailor-sdk --profile prod crashreport --machine-user"; const profileMachineUserValue = "tailor-sdk --profile --machineuser crashreport --machine-user"; @@ -38,6 +39,7 @@ const argTemplatePayload = `tailor-sdk function test-run --arg=${payload ? "--ma const dynamicArgTemplatePayload = `tailor-sdk function test-run ${includeArg ? "--arg" : ""} ${payload ? "--machineuser" : ""} --machine-user`; const quotedQueryTemplate = `tailor-sdk query --query "select --machineuser ${where}" --machine-user`; const inlineQueryTemplate = `tailor-sdk query --query=${payload ? "--machineuser" : ""} --machine-user`; +const postCommandConfigValue = ["tailor-sdk", "deploy", "--config", "/tmp/--machineuser"]; const chainedCommandTemplate = `tailor-sdk login --machine-user && other-cli --machineuser`; const joinedSeparatorTemplate = `tailor-sdk login --machine-user;other-cli --machineuser`; const newlineCommandTemplate = `tailor-sdk login --machine-user diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts index b3a5210e2..9ee570e0c 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts @@ -24,6 +24,7 @@ const envFileCommandSubstitutionPath = "tailor-sdk --env-file=$(pwd)/--machineus const argPayload = "tailor-sdk function test-run --arg=--machineuser"; const argCommandSubstitutionPayload = "tailor-sdk function test-run --arg=$(printf --machineuser) --machineuser"; const quotedArgCommandPayload = "tailor-sdk function test-run --arg='tailor-sdk crash-report --machineuser' --machineuser"; +const substitutedArgCommandPayload = `tailor-sdk function test-run --arg ${cond ? "tailor-sdk crash-report --machineuser" : "{}"} --machineuser`; const escapedQueryCommand = "tailor-sdk query --query \"select 1\" --machineuser"; const profileCommand = "tailor-sdk --profile prod crash-report --machineuser"; const profileMachineUserValue = "tailor-sdk --profile --machineuser crash-report --machineuser"; @@ -38,6 +39,7 @@ const argTemplatePayload = `tailor-sdk function test-run --arg=${payload ? "--ma const dynamicArgTemplatePayload = `tailor-sdk function test-run ${includeArg ? "--arg" : ""} ${payload ? "--machineuser" : ""} --machineuser`; const quotedQueryTemplate = `tailor-sdk query --query "select --machineuser ${where}" --machineuser`; const inlineQueryTemplate = `tailor-sdk query --query=${payload ? "--machineuser" : ""} --machineuser`; +const postCommandConfigValue = ["tailor-sdk", "deploy", "--config", "/tmp/--machineuser"]; const chainedCommandTemplate = `tailor-sdk login --machineuser && other-cli --machineuser`; const joinedSeparatorTemplate = `tailor-sdk login --machineuser;other-cli --machineuser`; const newlineCommandTemplate = `tailor-sdk login --machineuser diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index e54d28abd..c26dae0ce 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -1486,6 +1486,46 @@ function protectStandaloneTailorSdkSourceStrings( }; }); }; + const protectTemplateSubstitutions = (token: TemplateToken): void => { + for (const substitution of token.substitutions) { + protectNodeText(substitution); + } + }; + const protectTailorCliTemplateValues = (node: SgNode): void => { + let afterTailorBinary = false; + let skipNextTailorValue = false; + + for (const token of collectTemplateTokens(node, source)) { + const value = staticTemplateTokenValue(token); + if (!afterTailorBinary) { + if (value !== undefined && TAILOR_SDK_TOKEN.test(value)) { + afterTailorBinary = true; + } + continue; + } + + if (skipNextTailorValue) { + protectTemplateSubstitutions(token); + skipNextTailorValue = false; + continue; + } + + if (value !== undefined) { + if (isOpenTailorCliValueFlag(value)) { + skipNextTailorValue = true; + } + continue; + } + + if (hasInlineTailorCliValue(token.value)) { + protectTemplateSubstitutions(token); + continue; + } + if (isOpenTailorCliValueFlag(token.value)) { + skipNextTailorValue = true; + } + } + }; const protectNodeText = (node: SgNode): void => { const range = node.range(); ranges.push([ @@ -1590,6 +1630,7 @@ function protectStandaloneTailorSdkSourceStrings( fragmentOffset += fragment.value.length + TEMPLATE_SUBSTITUTION_PLACEHOLDER.length; } } + protectTailorCliTemplateValues(node); } const token = sourceStringToken(node, source); if (token) { diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index f88e51388..a42e0a011 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -11,6 +11,7 @@ const dynamicCommandWithFlags = `tailor ${flags} deploy`; const dynamicCommandWithConditionalFlags = `tailor ${useJson ? "--json" : ""} deploy`; const profileValue = "tailor --profile tailor-sdk deploy"; const shortProfileValue = "tailor -p tailor-sdk deploy"; +const substitutedArgValue = `tailor function test-run --arg ${cond ? "tailor-sdk deploy" : "{}"}`; const latest = "npx @tailor-platform/sdk@latest login"; const cacheRunner = "npx --cache .npm @tailor-platform/sdk login"; const registryRunner = "npx --registry=https://npm.example/tailor-sdk @tailor-platform/sdk login"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 86c58e150..a9ce61dab 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -11,6 +11,7 @@ const dynamicCommandWithFlags = `tailor-sdk ${flags} deploy`; const dynamicCommandWithConditionalFlags = `tailor-sdk ${useJson ? "--json" : ""} deploy`; const profileValue = "tailor-sdk --profile tailor-sdk deploy"; const shortProfileValue = "tailor-sdk -p tailor-sdk deploy"; +const substitutedArgValue = `tailor-sdk function test-run --arg ${cond ? "tailor-sdk deploy" : "{}"}`; const latest = "npx tailor-sdk@latest login"; const cacheRunner = "npx --cache .npm tailor-sdk login"; const registryRunner = "npx --registry=https://npm.example/tailor-sdk tailor-sdk login"; From 1c0fbe81323ca73491f4a3eed744164646be47a3 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 15:10:43 +0900 Subject: [PATCH 298/618] fix: skip safe parse invoker review findings --- .../v2/principal-unify/scripts/transform.ts | 87 +++++++++++++++++-- .../src/principal-unify-review.test.ts | 22 +++++ 2 files changed, 101 insertions(+), 8 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 35bbe86ec..52d1cbddf 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -1809,6 +1809,27 @@ function isSdkFieldMemberCall( : false; } +function collectSdkFieldRootNames(root: SgNode): Set { + const sdkFieldRootNames = new Set(); + const sdkImports = root.findAll({ + rule: { + kind: "import_statement", + has: { kind: "string", regex: "^[\"']@tailor-platform/sdk(/test)?[\"']$" }, + }, + }); + for (const importStmt of sdkImports) { + for (const namespaceName of iterateNamespaceImportLocalNames(importStmt)) { + sdkFieldRootNames.add(namespaceName); + } + for (const { importedName, localName } of iterateImportSpecs(importStmt)) { + if (importedName === "t" || importedName === "db") { + sdkFieldRootNames.add(localName); + } + } + } + return sdkFieldRootNames; +} + interface LocalCallbackBinding { name: string; fn: SgNode; @@ -2258,8 +2279,10 @@ function nodeContainsArgumentPrincipalOptionalAccess( node: SgNode, principalBindings: ReviewPrincipalBinding[], contextBindings: ReviewPrincipalBinding[], + safePrincipalRanges: Array<[number, number]>, root: SgNode, ): boolean { + if (isInsideAnyRange(node.range().start.index, safePrincipalRanges)) return false; if (isFunctionNode(node)) return false; if (isDirectPrincipalExpression(node, principalBindings, contextBindings, root)) return true; if (isPrincipalOptionalMemberExpression(node, principalBindings, contextBindings, root)) @@ -2267,7 +2290,13 @@ function nodeContainsArgumentPrincipalOptionalAccess( return node .children() .some((child) => - nodeContainsArgumentPrincipalOptionalAccess(child, principalBindings, contextBindings, root), + nodeContainsArgumentPrincipalOptionalAccess( + child, + principalBindings, + contextBindings, + safePrincipalRanges, + root, + ), ); } @@ -2282,18 +2311,61 @@ function reviewCallName(call: SgNode): string { return "a call"; } +function collectParseInvokerValueRanges(call: SgNode): Array<[number, number]> { + const args = call.field("arguments"); + const objectArg = args?.children().find((child) => child.kind() === "object"); + if (!objectArg) return []; + + const ranges: Array<[number, number]> = []; + for (const child of objectArg.children()) { + if (child.kind() === "shorthand_property_identifier" && child.text() === "invoker") { + const range = child.range(); + ranges.push([range.start.index, range.end.index]); + continue; + } + + if (child.kind() !== "pair") continue; + const key = child.field("key"); + if (key?.text() !== "invoker") continue; + const value = child.field("value"); + if (!value) continue; + const range = value.range(); + ranges.push([range.start.index, range.end.index]); + } + return ranges; +} + +function collectSafeNullablePrincipalArgumentRanges( + call: SgNode, + sdkFieldRootNames: Set, + sdkFieldLocalBindings: SdkFieldLocalBinding[], + root: SgNode, +): Array<[number, number]> { + if (findMemberCallName(call) !== "parse") return []; + if (!isSdkFieldMemberCall(call, sdkFieldRootNames, sdkFieldLocalBindings, root)) return []; + return collectParseInvokerValueRanges(call); +} + function collectNullableCallerReviewFindings( root: SgNode, source: string, file: string, principalBindings: ReviewPrincipalBinding[], contextBindings: ReviewPrincipalBinding[], + sdkFieldRootNames: Set, + sdkFieldLocalBindings: SdkFieldLocalBinding[], findings: LlmReviewFinding[], seen: Set, ): void { const calls = root.findAll({ rule: { kind: "call_expression" } }); for (const call of calls) { const args = call.field("arguments"); + const safePrincipalRanges = collectSafeNullablePrincipalArgumentRanges( + call, + sdkFieldRootNames, + sdkFieldLocalBindings, + root, + ); const nullableArg = args ?.children() .find((child) => @@ -2301,6 +2373,7 @@ function collectNullableCallerReviewFindings( child, principalBindings, contextBindings, + safePrincipalRanges, root, ), ); @@ -2731,12 +2804,16 @@ export function reviewFindings( const seen = new Set(); const contextBindings = collectResolverContextBindings(root); const principalBindings = collectResolverPrincipalBindings(root, contextBindings); + const sdkFieldRootNames = collectSdkFieldRootNames(root); + const sdkFieldLocalBindings = collectSdkFieldLocalBindings(root, sdkFieldRootNames); collectNullableCallerReviewFindings( root, source, relativePath, principalBindings, contextBindings, + sdkFieldRootNames, + sdkFieldLocalBindings, findings, seen, ); @@ -2875,10 +2952,7 @@ export default function transform(source: string): string | null { // does not actually bring `createResolver` in) are not. const createResolverLocalNames = new Set(); const createExecutorLocalNames = new Set(); - const sdkFieldRootNames = new Set(); - for (const namespaceName of sdkNamespaceNames) { - sdkFieldRootNames.add(namespaceName); - } + const sdkFieldRootNames = collectSdkFieldRootNames(tree); for (const importStmt of sdkImports) { for (const { importedName, localName } of iterateImportSpecs(importStmt)) { if (importedName === "createResolver") { @@ -2887,9 +2961,6 @@ export default function transform(source: string): string | null { if (importedName === "createExecutor") { createExecutorLocalNames.add(localName); } - if (importedName === "t" || importedName === "db") { - sdkFieldRootNames.add(localName); - } } } const sdkFieldLocalBindings = collectSdkFieldLocalBindings(tree, sdkFieldRootNames); diff --git a/packages/sdk-codemod/src/principal-unify-review.test.ts b/packages/sdk-codemod/src/principal-unify-review.test.ts index 6deb8e73b..4682fd10e 100644 --- a/packages/sdk-codemod/src/principal-unify-review.test.ts +++ b/packages/sdk-codemod/src/principal-unify-review.test.ts @@ -366,6 +366,28 @@ describe("principal-unify review findings", () => { expect(result.llmReviews.flatMap((review) => review.findings ?? [])).toEqual([]); }); + test("does not report SDK field parse invoker arguments", async () => { + await writeProjectFile( + "resolvers/parse-invoker.ts", + [ + 'import { createResolver, t } from "@tailor-platform/sdk";', + "", + "const nameField = t.string();", + "", + "export const resolver = createResolver({", + " body: async ({ input, user }) => {", + " return nameField.parse({ value: input.name, user });", + " },", + "});", + "", + ].join("\n"), + ); + + const result = await runCodemods([principalUnifyEntry], tmpDir!, false); + + expect(result.llmReviews.flatMap((review) => review.findings ?? [])).toEqual([]); + }); + test("does not report matching alias names outside the resolver scope", async () => { await writeProjectFile( "resolvers/scoped-alias.ts", From cd28035515eab9ff909bebcdf8eb6070aabd0078 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 15:24:25 +0900 Subject: [PATCH 299/618] fix(sdk-codemod): preserve package runner payloads --- .../v2/rename-bin/scripts/transform.ts | 26 ++++++++++++++----- .../tests/source-template/expected.ts | 2 ++ .../rename-bin/tests/source-template/input.ts | 2 ++ .../tests/source-token-runner/expected.ts | 1 + .../tests/source-token-runner/input.ts | 1 + 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index c26dae0ce..bafe542b3 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -43,6 +43,7 @@ const TAILOR_CLI_VALUE_REFERENCE_RE = new RegExp( const TAILOR_SDK_RE = /(? { const next = tokens[index + 1]; return ( - TAILOR_SDK_TOKEN.test(token.value) && next != null && isTailorCliCommandToken(next.value) + TAILOR_CLI_CONTEXT_TOKEN.test(token.value) && + next != null && + isTailorCliCommandToken(next.value) ); }); }; @@ -1425,7 +1428,7 @@ function protectStandaloneTailorSdkSourceStrings( }; const hasDynamicTailorArgvRemainder = (node: SgNode, tokens: SourceStringToken[]): boolean => { const [first] = tokens; - if (!first || !TAILOR_SDK_TOKEN.test(first.value)) return false; + if (!first || !TAILOR_CLI_CONTEXT_TOKEN.test(first.value)) return false; if (node.parent()?.kind() === "array") return false; const firstElementIndex = arrayElementNodes(node).findIndex((child) => { const token = sourceStringExpressionToken(child); @@ -1447,7 +1450,7 @@ function protectStandaloneTailorSdkSourceStrings( const tokenIndex = tokens.findIndex( (candidate) => candidate.start === token.start && candidate.end === token.end, ); - if (tokenIndex === -1 || !TAILOR_SDK_TOKEN.test(token.value)) return false; + if (tokenIndex === -1 || !TAILOR_CLI_CONTEXT_TOKEN.test(token.value)) return false; if (isOpenTailorCliValueFlag(tokens[tokenIndex - 1]?.value)) return false; const next = tokens[tokenIndex + 1]; if (next != null && isTailorCliCommandToken(next.value)) return true; @@ -1465,7 +1468,9 @@ function protectStandaloneTailorSdkSourceStrings( return hasTailorExecRunnerToken(tokens); } return ( - TAILOR_SDK_TOKEN.test(first.value) && second != null && isTailorCliCommandToken(second.value) + TAILOR_CLI_CONTEXT_TOKEN.test(first.value) && + second != null && + isTailorCliCommandToken(second.value) ); }; const isProtectedArrayDataToken = (node: SgNode): boolean => { @@ -1498,7 +1503,7 @@ function protectStandaloneTailorSdkSourceStrings( for (const token of collectTemplateTokens(node, source)) { const value = staticTemplateTokenValue(token); if (!afterTailorBinary) { - if (value !== undefined && TAILOR_SDK_TOKEN.test(value)) { + if (value !== undefined && TAILOR_CLI_CONTEXT_TOKEN.test(value)) { afterTailorBinary = true; } continue; @@ -1657,7 +1662,16 @@ function protectStandaloneTailorSdkSourceStrings( let updated = source; const protectedValues: string[] = []; - for (const [start, end, value] of ranges.toSorted(([a], [b]) => b - a)) { + const nonOverlappingRanges: Array<[number, number, string]> = []; + let coveredEnd = -1; + for (const range of ranges.toSorted(([aStart, aEnd], [bStart, bEnd]) => { + return aStart - bStart || bEnd - aEnd; + })) { + if (range[0] < coveredEnd) continue; + nonOverlappingRanges.push(range); + coveredEnd = range[1]; + } + for (const [start, end, value] of nonOverlappingRanges.toSorted(([a], [b]) => b - a)) { const placeholder = `__TAILOR_SDK_CODEMOD_STANDALONE_${protectedValues.length}__`; protectedValues.push(value); updated = `${updated.slice(0, start)}${placeholder}${updated.slice(end)}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index a42e0a011..d6b395c28 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -12,6 +12,7 @@ const dynamicCommandWithConditionalFlags = `tailor ${useJson ? "--json" : ""} de const profileValue = "tailor --profile tailor-sdk deploy"; const shortProfileValue = "tailor -p tailor-sdk deploy"; const substitutedArgValue = `tailor function test-run --arg ${cond ? "tailor-sdk deploy" : "{}"}`; +const substitutedSingleArgValue = `tailor function test-run --arg ${"tailor-sdk"}`; const latest = "npx @tailor-platform/sdk@latest login"; const cacheRunner = "npx --cache .npm @tailor-platform/sdk login"; const registryRunner = "npx --registry=https://npm.example/tailor-sdk @tailor-platform/sdk login"; @@ -24,6 +25,7 @@ const quotedPackageOptionRunner = 'npx --package="@tailor-platform/sdk" tailor l const dynamicPackageOptionRunner = `npx --package=${useLocal ? "@tailor-platform/sdk" : pkg} tailor login`; const dynamicSeparatePackageOptionRunner = `npx ${useLocal ? "--package" : ""} ${useLocal ? "@tailor-platform/sdk" : pkg} tailor login`; const dynamicPackageOptionCommand = `npx --package @tailor-platform/sdk tailor ${subcommand}`; +const packageOptionArgValue = `npx --package @tailor-platform/sdk tailor function test-run --arg ${cond ? "tailor-sdk deploy" : "{}"}`; const dynamicInlinePackageOptionCommand = `npx --package=${pkg} tailor ${subcommand}`; const dynamicFlagStaticTemplate = `npx ${yes ? "-y" : ""} @tailor-platform/sdk login`; const dynamicFlagConditionTemplate = `npx ${mode === "prod" ? "-y" : "--yes"} @tailor-platform/sdk login`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index a9ce61dab..3eb5fe92a 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -12,6 +12,7 @@ const dynamicCommandWithConditionalFlags = `tailor-sdk ${useJson ? "--json" : "" const profileValue = "tailor-sdk --profile tailor-sdk deploy"; const shortProfileValue = "tailor-sdk -p tailor-sdk deploy"; const substitutedArgValue = `tailor-sdk function test-run --arg ${cond ? "tailor-sdk deploy" : "{}"}`; +const substitutedSingleArgValue = `tailor-sdk function test-run --arg ${"tailor-sdk"}`; const latest = "npx tailor-sdk@latest login"; const cacheRunner = "npx --cache .npm tailor-sdk login"; const registryRunner = "npx --registry=https://npm.example/tailor-sdk tailor-sdk login"; @@ -24,6 +25,7 @@ const quotedPackageOptionRunner = 'npx --package="tailor-sdk" tailor-sdk login'; const dynamicPackageOptionRunner = `npx --package=${useLocal ? "tailor-sdk" : pkg} tailor-sdk login`; const dynamicSeparatePackageOptionRunner = `npx ${useLocal ? "--package" : ""} ${useLocal ? "tailor-sdk" : pkg} tailor-sdk login`; const dynamicPackageOptionCommand = `npx --package tailor-sdk tailor-sdk ${subcommand}`; +const packageOptionArgValue = `npx --package tailor-sdk tailor-sdk function test-run --arg ${cond ? "tailor-sdk deploy" : "{}"}`; const dynamicInlinePackageOptionCommand = `npx --package=${pkg} tailor-sdk ${subcommand}`; const dynamicFlagStaticTemplate = `npx ${yes ? "-y" : ""} tailor-sdk login`; const dynamicFlagConditionTemplate = `npx ${mode === "prod" ? "-y" : "--yes"} tailor-sdk login`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts index 97331d3e8..47fa15e00 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts @@ -26,6 +26,7 @@ const inlinePackageFlagRunner = ["npx", "--package=@tailor-platform/sdk", "tailo const dynamicInlinePackageFlagRunner = ["npx", `--package=${useLocal ? "@tailor-platform/sdk" : pkg}`, "tailor", "login"]; const dynamicSeparatePackageFlagRunner = ["npx", useLocal ? "--package" : "", useLocal ? "@tailor-platform/sdk" : pkg, "tailor", "login"]; const dynamicPackageOptionCommandRunner = ["npx", "--package", "@tailor-platform/sdk", "tailor", cmd]; +const packageOptionArgPayloadRunner = ["npx", "--package", "@tailor-platform/sdk", "tailor", "function", "test-run", "--arg", cond ? "tailor-sdk deploy" : "{}"]; const dynamicInlinePackageOptionCommandRunner = ["npx", `--package=${pkg}`, "tailor", cmd]; const templateConditionalRunner = `npx ${useLatest ? "@tailor-platform/sdk@latest" : "@tailor-platform/sdk"} login`; const templateDynamicInlineValueFlagRunner = `npx ${useRegistry ? "--registry=https://npm.example" : "-y"} @tailor-platform/sdk login`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts index d1719f680..e1f0a8e77 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts @@ -26,6 +26,7 @@ const inlinePackageFlagRunner = ["npx", "--package=tailor-sdk", "tailor-sdk", "l const dynamicInlinePackageFlagRunner = ["npx", `--package=${useLocal ? "tailor-sdk" : pkg}`, "tailor-sdk", "login"]; const dynamicSeparatePackageFlagRunner = ["npx", useLocal ? "--package" : "", useLocal ? "tailor-sdk" : pkg, "tailor-sdk", "login"]; const dynamicPackageOptionCommandRunner = ["npx", "--package", "tailor-sdk", "tailor-sdk", cmd]; +const packageOptionArgPayloadRunner = ["npx", "--package", "tailor-sdk", "tailor-sdk", "function", "test-run", "--arg", cond ? "tailor-sdk deploy" : "{}"]; const dynamicInlinePackageOptionCommandRunner = ["npx", `--package=${pkg}`, "tailor-sdk", cmd]; const templateConditionalRunner = `npx ${useLatest ? "tailor-sdk@latest" : "tailor-sdk"} login`; const templateDynamicInlineValueFlagRunner = `npx ${useRegistry ? "--registry=https://npm.example" : "-y"} tailor-sdk login`; From b12d2aa7dd6eb04beb9eb5a359584c7bf1fe85ec Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 15:39:17 +0900 Subject: [PATCH 300/618] fix(sdk-codemod): keep template boundaries protected --- .../v2/rename-bin/scripts/transform.ts | 82 +++++++++++++++---- .../tests/source-template/expected.ts | 2 + .../rename-bin/tests/source-template/input.ts | 2 + .../tests/source-token-runner/expected.ts | 1 + .../tests/source-token-runner/input.ts | 1 + 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index bafe542b3..0fd4560b8 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -1478,18 +1478,60 @@ function protectStandaloneTailorSdkSourceStrings( const parent = element.parent(); return parent?.kind() === "array" && !isLikelyTailorArgvArray(parent); }; - const sourceStringFragmentTokens = (node: SgNode): SourceStringToken[] => { - return node - .children() - .filter((child: SgNode) => child.kind() === "string_fragment") - .map((fragment: SgNode) => { - const range = fragment.range(); - return { + const templateFragmentsWithOffsets = ( + node: SgNode, + ): Array<{ fragment: SourceStringToken; offset: number }> => { + const fragments: Array<{ fragment: SourceStringToken; offset: number }> = []; + let offset = 0; + for (const child of node.children()) { + if (child.kind() === "string_fragment") { + const range = child.range(); + const fragment = { value: source.slice(range.start.index, range.end.index), start: range.start.index, end: range.end.index, }; - }); + fragments.push({ fragment, offset }); + offset += fragment.value.length; + } else if (child.kind() === "template_substitution") { + offset += TEMPLATE_SUBSTITUTION_PLACEHOLDER.length; + } + } + return fragments; + }; + const templateStaticText = (node: SgNode): string => { + let text = ""; + for (const child of node.children()) { + if (child.kind() === "string_fragment") { + const range = child.range(); + text += source.slice(range.start.index, range.end.index); + } else if (child.kind() === "template_substitution") { + text += TEMPLATE_SUBSTITUTION_PLACEHOLDER; + } + } + return text; + }; + const expressionStringValues = (node: SgNode): string[] => { + const token = sourceStringExpressionToken(node); + if (token) return [token.value]; + const values: string[] = []; + const expressionChildren = runnerPackageExpressionChildren(node); + const children = expressionChildren.length > 0 ? expressionChildren : node.children(); + for (const child of children) { + values.push(...expressionStringValues(child)); + } + return values; + }; + const isDynamicOpenTailorCliValueFlag = (node: SgNode): boolean => { + const values = expressionStringValues(node) + .map((value) => value.trim()) + .filter((value) => value !== ""); + return values.length > 0 && values.every((value) => isOpenTailorCliValueFlag(value)); + }; + const isDynamicOpenTailorCliValueFlagToken = (token: TemplateToken): boolean => { + return token.substitutions.some((substitution) => + isDynamicOpenTailorCliValueFlag(substitution), + ); }; const protectTemplateSubstitutions = (token: TemplateToken): void => { for (const substitution of token.substitutions) { @@ -1528,6 +1570,10 @@ function protectStandaloneTailorSdkSourceStrings( } if (isOpenTailorCliValueFlag(token.value)) { skipNextTailorValue = true; + continue; + } + if (isDynamicOpenTailorCliValueFlagToken(token)) { + skipNextTailorValue = true; } } }; @@ -1581,6 +1627,12 @@ function protectStandaloneTailorSdkSourceStrings( } } + if (afterTailorBinary && isDynamicOpenTailorCliValueFlag(child)) { + skipNextTailorValue = true; + visit(child); + continue; + } + visit(child); } }; @@ -1613,26 +1665,22 @@ function protectStandaloneTailorSdkSourceStrings( kind === "template_string" && node.children().some((child: SgNode) => child.kind() === "template_substitution") ) { - const fragments = sourceStringFragmentTokens(node); - const staticText = fragments - .map((fragment) => fragment.value) - .join(TEMPLATE_SUBSTITUTION_PLACEHOLDER); - if (fragments.some((fragment) => fragment.value.includes("tailor-sdk"))) { + const fragments = templateFragmentsWithOffsets(node); + const staticText = templateStaticText(node); + if (fragments.some(({ fragment }) => fragment.value.includes("tailor-sdk"))) { const rewriteableRanges = [ ...collectRewriteableTailorSdkRanges(staticText), ...collectDynamicTemplateTailorCommandRanges(staticText), ]; - let fragmentOffset = 0; - for (const fragment of fragments) { + for (const { fragment, offset } of fragments) { ranges.push( ...collectNonCommandTailorSdkRanges( fragment.value, fragment.start, rewriteableRanges, - fragmentOffset, + offset, ), ); - fragmentOffset += fragment.value.length + TEMPLATE_SUBSTITUTION_PLACEHOLDER.length; } } protectTailorCliTemplateValues(node); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index d6b395c28..4b45b773e 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -13,6 +13,7 @@ const profileValue = "tailor --profile tailor-sdk deploy"; const shortProfileValue = "tailor -p tailor-sdk deploy"; const substitutedArgValue = `tailor function test-run --arg ${cond ? "tailor-sdk deploy" : "{}"}`; const substitutedSingleArgValue = `tailor function test-run --arg ${"tailor-sdk"}`; +const dynamicArgFlagValue = `tailor function test-run ${"--arg"} ${"tailor-sdk deploy"}`; const latest = "npx @tailor-platform/sdk@latest login"; const cacheRunner = "npx --cache .npm @tailor-platform/sdk login"; const registryRunner = "npx --registry=https://npm.example/tailor-sdk @tailor-platform/sdk login"; @@ -38,4 +39,5 @@ const cacheValue = "npx --cache tailor-sdk some-package"; const outputDir = ".tailor-sdk/"; const scaffold = "create-tailor-sdk"; const skills = "tailor-sdk-skills install"; +const packageTemplateSuffix = `tailor-sdk${suffix}`; const proseTemplate = `package tailor-sdk ${version}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 3eb5fe92a..030121cd8 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -13,6 +13,7 @@ const profileValue = "tailor-sdk --profile tailor-sdk deploy"; const shortProfileValue = "tailor-sdk -p tailor-sdk deploy"; const substitutedArgValue = `tailor-sdk function test-run --arg ${cond ? "tailor-sdk deploy" : "{}"}`; const substitutedSingleArgValue = `tailor-sdk function test-run --arg ${"tailor-sdk"}`; +const dynamicArgFlagValue = `tailor-sdk function test-run ${"--arg"} ${"tailor-sdk deploy"}`; const latest = "npx tailor-sdk@latest login"; const cacheRunner = "npx --cache .npm tailor-sdk login"; const registryRunner = "npx --registry=https://npm.example/tailor-sdk tailor-sdk login"; @@ -38,4 +39,5 @@ const cacheValue = "npx --cache tailor-sdk some-package"; const outputDir = ".tailor-sdk/"; const scaffold = "create-tailor-sdk"; const skills = "tailor-sdk-skills install"; +const packageTemplateSuffix = `tailor-sdk${suffix}`; const proseTemplate = `package tailor-sdk ${version}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts index 47fa15e00..b6c828606 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts @@ -51,6 +51,7 @@ const profileCommandPayload = ["tailor", "--profile", "tailor-sdk deploy", "depl const argCommandPayload = ["tailor", "function", "test-run", "--arg", "{\"cmd\":\"tailor-sdk deploy\"}"]; const argCommandPayloadExpression = ["tailor", "function", "test-run", "--arg", cond ? "tailor-sdk deploy" : "{}"]; const argCommandPayloadTemplate = ["tailor", "function", "test-run", "--arg", `tailor-sdk deploy`]; +const dynamicArgFlagPayload = ["tailor", "function", "test-run", includeArg ? "--arg" : "", "tailor-sdk deploy"]; const dynamicBinaryArgs = ["tailor", cmd]; const spreadBinaryArgs = ["tailor", ...args]; const wrappedCommandArgs = ["tailor", "login" as const]; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts index e1f0a8e77..8f498fdac 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts @@ -51,6 +51,7 @@ const profileCommandPayload = ["tailor-sdk", "--profile", "tailor-sdk deploy", " const argCommandPayload = ["tailor-sdk", "function", "test-run", "--arg", "{\"cmd\":\"tailor-sdk deploy\"}"]; const argCommandPayloadExpression = ["tailor-sdk", "function", "test-run", "--arg", cond ? "tailor-sdk deploy" : "{}"]; const argCommandPayloadTemplate = ["tailor-sdk", "function", "test-run", "--arg", `tailor-sdk deploy`]; +const dynamicArgFlagPayload = ["tailor-sdk", "function", "test-run", includeArg ? "--arg" : "", "tailor-sdk deploy"]; const dynamicBinaryArgs = ["tailor-sdk", cmd]; const spreadBinaryArgs = ["tailor-sdk", ...args]; const wrappedCommandArgs = ["tailor-sdk", "login" as const]; From 284cda9cbeffb08063da32860a1d3622d42f8412 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 15:58:58 +0900 Subject: [PATCH 301/618] fix(sdk-codemod): preserve wrapped package runners --- .../v2/rename-bin/scripts/transform.ts | 30 +++++++++++++++---- .../tests/source-template/expected.ts | 1 + .../rename-bin/tests/source-template/input.ts | 1 + .../tests/source-token-runner/expected.ts | 2 ++ .../tests/source-token-runner/input.ts | 2 ++ 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 0fd4560b8..5d166c8d6 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -660,8 +660,25 @@ function collectTokenizedRunnerEdits( return changed; }; + const sourceStringExpressionToken = ( + node: SgNode, + visited = new Set(), + ): SourceStringToken | undefined => { + const direct = sourceStringToken(node, source); + if (direct) return direct; + if (!SOURCE_STRING_WRAPPER_NODE_KINDS.has(node.kind())) return undefined; + const key = nodeRangeKey(node); + if (visited.has(key)) return undefined; + visited.add(key); + for (const child of node.children()) { + const token = sourceStringExpressionToken(child, visited); + if (token) return token; + } + return undefined; + }; + const protectTailorSdkStrings = (node: SgNode): void => { - const token = sourceStringToken(node, source); + const token = sourceStringExpressionToken(node); if (token) { protectToken(token); return; @@ -672,7 +689,7 @@ function collectTokenizedRunnerEdits( }; const collectPackageExpressionEdits = (node: SgNode): boolean => { - const token = sourceStringToken(node, source); + const token = sourceStringExpressionToken(node); if (token) { const replacement = runnerPackageReplacement(token.value); if (replacement) { @@ -699,7 +716,7 @@ function collectTokenizedRunnerEdits( }; const expressionStringValues = (node: SgNode): string[] => { - const token = sourceStringToken(node, source); + const token = sourceStringExpressionToken(node); if (token) return [token.value]; const values: string[] = []; const packageChildren = runnerPackageExpressionChildren(node); @@ -715,6 +732,9 @@ function collectTokenizedRunnerEdits( .map((value) => value.trim()) .filter((value) => value !== ""); if (values.length === 0) return null; + const runnerStates = values.map((value) => runnerStateAfterToken(value)); + if (runnerStates.every((state) => state === "await-package")) return "await-package"; + if (runnerStates.every((state) => state === "await-dlx")) return "await-dlx"; if (values.every((value) => isRunnerOpenPackageFlag(value))) { return "await-package-option-value"; } @@ -948,7 +968,7 @@ function collectTokenizedRunnerEdits( const visit = (node: SgNode, inheritedRunnerState: RunnerState = "none"): void => { if (inheritedRunnerState === "await-package") { - const token = sourceStringToken(node, source); + const token = sourceStringExpressionToken(node); if (token) { const replacement = runnerPackageReplacement(token.value); if (replacement) { @@ -975,7 +995,7 @@ function collectTokenizedRunnerEdits( let runnerState = inheritedRunnerState; let previousTokenValue: string | undefined; for (const child of node.children()) { - const token = sourceStringToken(child, source); + const token = sourceStringExpressionToken(child); if (token) { const previous = previousTokenValue; previousTokenValue = token.value; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 4b45b773e..e97907a5f 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -29,6 +29,7 @@ const dynamicPackageOptionCommand = `npx --package @tailor-platform/sdk tailor $ const packageOptionArgValue = `npx --package @tailor-platform/sdk tailor function test-run --arg ${cond ? "tailor-sdk deploy" : "{}"}`; const dynamicInlinePackageOptionCommand = `npx --package=${pkg} tailor ${subcommand}`; const dynamicFlagStaticTemplate = `npx ${yes ? "-y" : ""} @tailor-platform/sdk login`; +const staticSubstitutionRunner = `${"npx"} @tailor-platform/sdk login`; const dynamicFlagConditionTemplate = `npx ${mode === "prod" ? "-y" : "--yes"} @tailor-platform/sdk login`; const dynamicDlxValueFlagTemplate = `pnpm ${use ? "--filter" : ""} app dlx @tailor-platform/sdk login`; const dynamicDlxInlineValueFlagTemplate = `pnpm ${use ? "--filter=tailor-sdk" : ""} dlx @tailor-platform/sdk login`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 030121cd8..538745fdd 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -29,6 +29,7 @@ const dynamicPackageOptionCommand = `npx --package tailor-sdk tailor-sdk ${subco const packageOptionArgValue = `npx --package tailor-sdk tailor-sdk function test-run --arg ${cond ? "tailor-sdk deploy" : "{}"}`; const dynamicInlinePackageOptionCommand = `npx --package=${pkg} tailor-sdk ${subcommand}`; const dynamicFlagStaticTemplate = `npx ${yes ? "-y" : ""} tailor-sdk login`; +const staticSubstitutionRunner = `${"npx"} tailor-sdk login`; const dynamicFlagConditionTemplate = `npx ${mode === "prod" ? "-y" : "--yes"} tailor-sdk login`; const dynamicDlxValueFlagTemplate = `pnpm ${use ? "--filter" : ""} app dlx tailor-sdk login`; const dynamicDlxInlineValueFlagTemplate = `pnpm ${use ? "--filter=tailor-sdk" : ""} dlx tailor-sdk login`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts index b6c828606..3e4894819 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts @@ -1,5 +1,7 @@ const packageName = "tailor-sdk"; const runnerArgs = ["npx", "@tailor-platform/sdk@latest", "login"]; +const wrappedRunnerArgs = ["npx" as const, "@tailor-platform/sdk", "login"]; +const parenthesizedRunnerArgs = [("npx"), "@tailor-platform/sdk", "login"]; const dynamicPackageVariableRunner = ["npx", pkg, "tailor", "login"]; const dynamicDlxPackageVariableRunner = ["pnpm", "dlx", pkg, "tailor", "login"]; const dynamicFlagRunner = ["npx", yes && "-y", "@tailor-platform/sdk", "login"]; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts index 8f498fdac..4e3c81768 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts @@ -1,5 +1,7 @@ const packageName = "tailor-sdk"; const runnerArgs = ["npx", "tailor-sdk@latest", "login"]; +const wrappedRunnerArgs = ["npx" as const, "tailor-sdk", "login"]; +const parenthesizedRunnerArgs = [("npx"), "tailor-sdk", "login"]; const dynamicPackageVariableRunner = ["npx", pkg, "tailor-sdk", "login"]; const dynamicDlxPackageVariableRunner = ["pnpm", "dlx", pkg, "tailor-sdk", "login"]; const dynamicFlagRunner = ["npx", yes && "-y", "tailor-sdk", "login"]; From 82d70b4b97b89928ef07e41139d56d9101cd18c2 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 16:22:09 +0900 Subject: [PATCH 302/618] fix(sdk-codemod): preserve ambiguous runner args --- .../v2/cli-rename/scripts/transform.ts | 84 +++++++++++++++---- .../tests/source-template/expected.ts | 1 + .../cli-rename/tests/source-template/input.ts | 1 + .../tests/source-token-array/expected.ts | 3 + .../tests/source-token-array/input.ts | 3 + 5 files changed, 76 insertions(+), 16 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index 34e2c269a..c2e50a36d 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -65,6 +65,13 @@ const PACKAGE_RUNNER_VALUE_ARGS = new Set([ "-C", ]); const CLI_ARGUMENT_CALLEE_RE = /(?:^|\.)(?:spawn|spawnSync|execFile|execFileSync|execa|execaSync)$/; +const SOURCE_STRING_WRAPPER_NODE_KINDS = new Set([ + "as_expression", + "non_null_expression", + "parenthesized_expression", + "satisfies_expression", + "type_assertion", +]); interface TextRange { start: number; @@ -718,6 +725,24 @@ function sourceStringToken(node: SgNode, source: string): SourceStringToken | un }; } +function sourceStringExpressionToken( + node: SgNode, + source: string, + visited = new Set(), +): SourceStringToken | undefined { + const direct = sourceStringToken(node, source); + if (direct) return direct; + if (!SOURCE_STRING_WRAPPER_NODE_KINDS.has(node.kind())) return undefined; + const key = nodeRangeKey(node); + if (visited.has(key)) return undefined; + visited.add(key); + for (const child of node.children()) { + const token = sourceStringExpressionToken(child, source, visited); + if (token) return token; + } + return undefined; +} + function ensureTemplateToken(state: TemplateTokenState): TemplateToken { state.token ??= { value: "", segments: [], substitutions: [], quoted: false }; return state.token; @@ -866,6 +891,10 @@ function isPackageRunnerSeparateValueArg(value: string | undefined): boolean { return value != null && PACKAGE_RUNNER_VALUE_ARGS.has(value); } +function isPackageRunnerSeparateArg(value: string | undefined): boolean { + return isPackageRunnerSeparatePackageArg(value) || isPackageRunnerSeparateValueArg(value); +} + function replaceOptionsInToken(value: string): string { const equalsIndex = value.indexOf("="); if (equalsIndex !== -1 && value.startsWith("-")) { @@ -1048,7 +1077,7 @@ function collectCliExpressionEdits( source: string, renameCommand: boolean, ): Array<[number, number, string]> { - const token = sourceStringToken(node, source); + const token = sourceStringExpressionToken(node, source); if (token) { const replacement = rewriteTokenizedCliArg(token.value, renameCommand); return replacement === token.value ? [] : [[token.start, token.end, replacement]]; @@ -1064,7 +1093,7 @@ function collectCliExpressionEdits( } function sourceStringValues(node: SgNode, source: string): string[] { - const token = sourceStringToken(node, source); + const token = sourceStringExpressionToken(node, source); if (token) return [token.value]; const values: string[] = []; @@ -1127,6 +1156,14 @@ function dynamicCliValueArgState(node: SgNode, source: string): "skip-value" | n return values.every((value) => isOpenCliValueArg(value)) ? "skip-value" : null; } +function dynamicPackageRunnerArgState(node: SgNode, source: string): "skip-value" | null { + const values = sourceStringValues(node, source) + .map((value) => value.trim()) + .filter((value) => value !== ""); + if (values.length === 0) return null; + return values.some((value) => isPackageRunnerSeparateArg(value)) ? "skip-value" : null; +} + function advanceCliTemplateState(state: CliTemplateState, value: string): void { if (!state.afterTailorBinary) { if (TAILOR_BINARY_TOKEN.test(value)) { @@ -1195,12 +1232,13 @@ function collectCliTemplateEdits(node: SgNode, source: string): CliTemplateEditR } if (!state.afterTailorBinary) { + if (skipNextPackageRunnerValue) { + protectTemplateSubstitutions(token); + skipNextPackageRunnerValue = false; + continue; + } if (value !== undefined) { - if (skipNextPackageRunnerValue) { - skipNextPackageRunnerValue = false; - continue; - } - if (isPackageRunnerSeparatePackageArg(value) || isPackageRunnerSeparateValueArg(value)) { + if (isPackageRunnerSeparateArg(value)) { skipNextPackageRunnerValue = true; continue; } @@ -1211,6 +1249,14 @@ function collectCliTemplateEdits(node: SgNode, source: string): CliTemplateEditR continue; } advanceCliTemplateState(state, value); + } else { + const dynamicState = + token.value === TEMPLATE_SUBSTITUTION_PLACEHOLDER && token.substitutions.length === 1 + ? dynamicPackageRunnerArgState(token.substitutions[0]!, source) + : null; + if (dynamicState === "skip-value") { + skipNextPackageRunnerValue = true; + } } continue; } @@ -1441,7 +1487,7 @@ function collectTokenizedSourceEdits( const visit = (node: SgNode, inheritedAfterTailorBinary = false): void => { if (inheritedAfterTailorBinary) { - const token = sourceStringToken(node, source); + const token = sourceStringExpressionToken(node, source); if (token) { const replacement = rewriteTokenizedCliArg(token.value, true); if (replacement !== token.value) { @@ -1459,7 +1505,7 @@ function collectTokenizedSourceEdits( let skipNextPackageRunnerValue = false; let previousTokenValue: string | undefined; for (const child of node.children()) { - const token = sourceStringToken(child, source); + const token = sourceStringExpressionToken(child, source); if (token) { const previous = previousTokenValue; previousTokenValue = token.value; @@ -1469,10 +1515,7 @@ function collectTokenizedSourceEdits( skipNextPackageRunnerValue = false; continue; } - if ( - isPackageRunnerSeparatePackageArg(token.value) || - isPackageRunnerSeparateValueArg(token.value) - ) { + if (isPackageRunnerSeparateArg(token.value)) { skipNextPackageRunnerValue = true; continue; } @@ -1526,7 +1569,7 @@ function collectTokenizedSourceEdits( } if (!afterTailorBinary && skipNextPackageRunnerValue) { - visit(child); + protectNode(child); skipNextPackageRunnerValue = false; previousTokenValue = undefined; continue; @@ -1542,9 +1585,18 @@ function collectTokenizedSourceEdits( const childCanHoldCommand = isTokenSequenceNode(child) || cliArgExpressionChildren(child).length > 0; + if (!afterTailorBinary) { + visit(child); + const dynamicState = dynamicPackageRunnerArgState(child, source); + if (dynamicState === "skip-value") { + skipNextPackageRunnerValue = true; + } + continue; + } + if (afterTailorBinary && commandMayBeNext) { edits.push(...collectInterpolatedOptionEdits(child, source, commandMayBeNext)); - } else if (afterTailorBinary) { + } else { edits.push(...collectInterpolatedOptionEdits(child, source, false)); if (!templateStartsWithInlineOptionValue(child, source)) { edits.push(...collectCliExpressionEdits(child, source, false)); @@ -1565,7 +1617,7 @@ function collectTokenizedSourceEdits( continue; } commandMayBeNext = false; - } else if (afterTailorBinary) { + } else { const dynamicState = dynamicCliValueArgState(child, source); if (dynamicState === "skip-value") { skipNextValue = true; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts index e8de96451..f1a17eb5e 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts @@ -10,6 +10,7 @@ const separatedTemplate = `tailor-sdk crashreport; ${next}`; const packageRunnerTemplate = `npx --package tailor-sdk tailor-sdk crashreport --machine-user=ci`; const shortPackageRunnerTemplate = `npx -p tailor-sdk tailor-sdk crashreport --machine-user=ci`; const valueFlagPackageRunnerTemplate = `npx --cache tailor-sdk tailor-sdk crashreport --machine-user=ci`; +const dynamicValueFlagPackageRunnerTemplate = `npx ${useCache ? "--cache" : "--yes"} tailor-sdk crash-report --machineuser=ci`; const repeatedTemplate = `tailor-sdk crashreport --machine-user tailor-sdk crashreport --machine-user`; const repeatedSeparatedTemplate = `tailor-sdk crashreport; tailor-sdk crashreport --machine-user`; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts index 9ee570e0c..344f8b5b1 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts @@ -10,6 +10,7 @@ const separatedTemplate = `tailor-sdk crash-report; ${next}`; const packageRunnerTemplate = `npx --package tailor-sdk tailor-sdk crash-report --machineuser=ci`; const shortPackageRunnerTemplate = `npx -p tailor-sdk tailor-sdk crash-report --machineuser=ci`; const valueFlagPackageRunnerTemplate = `npx --cache tailor-sdk tailor-sdk crash-report --machineuser=ci`; +const dynamicValueFlagPackageRunnerTemplate = `npx ${useCache ? "--cache" : "--yes"} tailor-sdk crash-report --machineuser=ci`; const repeatedTemplate = `tailor-sdk crash-report --machineuser tailor-sdk crash-report --machineuser`; const repeatedSeparatedTemplate = `tailor-sdk crash-report; tailor-sdk crash-report --machineuser`; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts index 8bdb04710..abca91676 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts @@ -1,6 +1,9 @@ const args = ["tailor-sdk", "crashreport", "list", "--machine-user", "ci"]; const withRunner = ["npx", "tailor-sdk@latest", "crashreport", "--machine-user=ci"]; +const wrappedBinary = ["tailor-sdk" as const, "crashreport", "--machine-user"]; +const parenthesizedBinary = [("tailor-sdk"), "crashreport", "--machine-user"]; const packageRunnerValueArg = ["npx", "--cache", "tailor-sdk", "tailor-sdk", "crashreport", "--machine-user"]; +const dynamicPackageRunnerValueArg = ["npx", useCache ? "--cache" : "--yes", "tailor-sdk", "crash-report", "--machineuser"]; const packageRunner = ["npx", "--package", "tailor-sdk", "tailor-sdk", "crashreport"]; const shortPackageRunner = ["npx", "-p", "tailor-sdk", "tailor-sdk", "crashreport"]; const nested = spawn("tailor-sdk", ["crashreport", "list", "--machine-user", "ci"]); diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts index 5a408f2ac..0b5b632ff 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts @@ -1,6 +1,9 @@ const args = ["tailor-sdk", "crash-report", "list", "--machineuser", "ci"]; const withRunner = ["npx", "tailor-sdk@latest", "crash-report", "--machineuser=ci"]; +const wrappedBinary = ["tailor-sdk" as const, "crash-report", "--machineuser"]; +const parenthesizedBinary = [("tailor-sdk"), "crash-report", "--machineuser"]; const packageRunnerValueArg = ["npx", "--cache", "tailor-sdk", "tailor-sdk", "crash-report", "--machineuser"]; +const dynamicPackageRunnerValueArg = ["npx", useCache ? "--cache" : "--yes", "tailor-sdk", "crash-report", "--machineuser"]; const packageRunner = ["npx", "--package", "tailor-sdk", "tailor-sdk", "crash-report"]; const shortPackageRunner = ["npx", "-p", "tailor-sdk", "tailor-sdk", "crash-report"]; const nested = spawn("tailor-sdk", ["crash-report", "list", "--machineuser", "ci"]); From b90b92d7ddf891c5f960d57e4f1d9be79012fd69 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 16:35:30 +0900 Subject: [PATCH 303/618] refactor(sdk-codemod): narrow rename-bin source migration --- .changeset/fix-rename-bin-source-files.md | 2 +- .../codemods/v2/cli-rename/codemod.yaml | 2 +- .../v2/cli-rename/scripts/transform.ts | 1304 +----------- .../cli-rename/tests/basic-shell/expected.sh | 4 - .../v2/cli-rename/tests/basic-shell/input.sh | 4 - .../tests/declaration-comment/expected.d.ts | 9 - .../tests/declaration-comment/input.d.ts | 9 - .../tests/source-jsx-js/expected.js | 6 - .../cli-rename/tests/source-jsx-js/input.js | 6 - .../tests/source-template/expected.ts | 48 - .../cli-rename/tests/source-template/input.ts | 48 - .../tests/source-token-array/expected.ts | 62 - .../tests/source-token-array/input.ts | 62 - .../cli-rename/tests/source-tsx/expected.tsx | 6 - .../v2/cli-rename/tests/source-tsx/input.tsx | 6 - .../v2/rename-bin/scripts/transform.ts | 1751 +---------------- .../rename-bin/tests/basic-shell/expected.sh | 3 - .../v2/rename-bin/tests/basic-shell/input.sh | 3 - .../tests/declaration-comment/expected.d.ts | 7 +- .../tests/declaration-comment/input.d.ts | 7 +- .../tests/source-js-string/expected.js | 21 +- .../tests/source-js-string/input.js | 21 +- .../tests/source-template/expected.ts | 42 +- .../rename-bin/tests/source-template/input.ts | 40 +- .../tests/source-token-runner/expected.ts | 68 - .../tests/source-token-runner/input.ts | 68 - .../rename-bin/tests/source-tsx/expected.tsx | 7 - .../v2/rename-bin/tests/source-tsx/input.tsx | 7 - packages/sdk-codemod/src/registry.test.ts | 22 +- packages/sdk-codemod/src/registry.ts | 15 +- packages/sdk-codemod/src/runner.test.ts | 63 - packages/sdk-codemod/src/runner.ts | 2 +- packages/sdk/docs/migration/v2.md | 2 +- 33 files changed, 105 insertions(+), 3622 deletions(-) delete mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/expected.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/input.d.ts delete mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/expected.js delete mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/input.js delete mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/expected.tsx delete mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/input.tsx delete mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/expected.tsx delete mode 100644 packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/input.tsx diff --git a/.changeset/fix-rename-bin-source-files.md b/.changeset/fix-rename-bin-source-files.md index f9936f110..0ce99f44c 100644 --- a/.changeset/fix-rename-bin-source-files.md +++ b/.changeset/fix-rename-bin-source-files.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk-codemod": patch --- -Apply the v2 CLI rename codemods to SDK CLI command strings in TypeScript and JavaScript source files. +Apply the v2 `rename-bin` codemod to SDK CLI command strings in TypeScript and JavaScript source files. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/codemod.yaml b/packages/sdk-codemod/codemods/v2/cli-rename/codemod.yaml index 8cdb2f535..2937f57aa 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/codemod.yaml +++ b/packages/sdk-codemod/codemods/v2/cli-rename/codemod.yaml @@ -1,6 +1,6 @@ name: "@tailor-platform/cli-rename" version: "1.0.0" -description: "Apply v2 CLI naming conventions in scripts, source files, CI workflows, and documentation" +description: "Apply v2 CLI naming conventions: single-word command names and kebab-case option names" engine: jssg language: text since: "1.0.0" diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index c2e50a36d..7a498fef4 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -1,6 +1,4 @@ -import { parse, Lang } from "@ast-grep/napi"; import * as path from "pathe"; -import type { SgNode } from "@ast-grep/napi"; // Map of v1 multi-word command names to their v2 single-word replacements. const COMMAND_RENAMES: ReadonlyArray = [["crash-report", "crashreport"]]; @@ -9,69 +7,17 @@ const OPTION_RENAMES: ReadonlyArray = [ ]; const ARG_VALUE = `(?:[^\\s'"\`;&|]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`; +const BOOLEAN_GLOBAL_ARG = "(?:--verbose|--json|-j)"; +const VALUE_GLOBAL_ARG = "(?:--env-file|--env-file-if-exists|-e)"; +const GLOBAL_ARG_PATTERN = `(?:(?:\\s+${BOOLEAN_GLOBAL_ARG})|(?:\\s+${VALUE_GLOBAL_ARG}(?:=${ARG_VALUE}|\\s+${ARG_VALUE})))*`; const TAILOR_BINARY = `(? from).join("|")})\\b`, + "g", ); -const SHELL_SEPARATOR_TAILOR_PATTERN = new RegExp(`[;&|]\\s*${TAILOR_BINARY}(?=\\s|$)`); +const TAILOR_BINARY_PATTERN = new RegExp(TAILOR_BINARY, "g"); const COMMAND_MAP = new Map(COMMAND_RENAMES); -const OPTION_MAP = new Map(OPTION_RENAMES); -const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]); -const TAILOR_BINARY_TOKEN = /^tailor-sdk(?:@[^\s'"`;|&)]+)?$/; -const GLOBAL_BOOLEAN_ARGS = new Set(["--verbose", "--json", "--yes", "-j", "-y"]); -const GLOBAL_VALUE_ARGS = new Set([ - "--env-file", - "--env-file-if-exists", - "--profile", - "--config", - "--workspace-id", - "-e", - "-p", - "-c", - "-w", -]); -const COMMAND_VALUE_ARGS = [ - "--env-file-if-exists", - "--env-file", - "--profile", - "--config", - "--workspace-id", - "--arg", - "--query", - "--file", - "-e", - "-p", - "-c", - "-w", - "-a", - "-q", - "-f", -] as const; -const CLI_VALUE_ARG_SET = new Set(COMMAND_VALUE_ARGS); -const PACKAGE_RUNNER_VALUE_ARGS = new Set([ - "--cache", - "--userconfig", - "--registry", - "--prefix", - "--dir", - "--filter", - "--cwd", - "-C", -]); -const CLI_ARGUMENT_CALLEE_RE = /(?:^|\.)(?:spawn|spawnSync|execFile|execFileSync|execa|execaSync)$/; -const SOURCE_STRING_WRAPPER_NODE_KINDS = new Set([ - "as_expression", - "non_null_expression", - "parenthesized_expression", - "satisfies_expression", - "type_assertion", -]); interface TextRange { start: number; @@ -83,37 +29,6 @@ interface ActiveQuote { escaped: boolean; } -interface SourceStringToken { - value: string; - start: number; - end: number; -} - -interface TemplateToken { - value: string; - segments: SourceStringToken[]; - substitutions: SgNode[]; - quoted: boolean; -} - -interface TemplateTokenState { - quote: "'" | '"' | null; - token: TemplateToken | null; -} - -interface CliTemplateState { - afterTailorBinary: boolean; - commandMayBeNext: boolean; - skipNextValue: boolean; -} - -interface CliTemplateEditResult { - edits: Array<[number, number, string]>; - protectedRanges: TextRange[]; -} - -const TEMPLATE_SUBSTITUTION_PLACEHOLDER = "\u{E000}"; - function isOptionBoundaryChar(value: string | undefined): boolean { return value === undefined || !/[\w-]/.test(value); } @@ -229,17 +144,16 @@ function findMarkdownFencedCodeRanges(source: string): TextRange[] { for (const line of lines) { const body = line.replace(/\r?\n$/, ""); - const fenceBody = body.replace(/^ {0,3}\*\s?/, ""); if (open) { - const close = fenceBody.match(/^ {0,3}(`{3,}|~{3,})\s*$/); + const close = body.match(/^ {0,3}(`{3,}|~{3,})\s*$/); const marker = close?.[1]; if (marker && marker[0] === open.char && marker.length >= open.length) { ranges.push({ start: open.start, end: offset }); open = undefined; } } else { - const start = fenceBody.match(/^ {0,3}(`{3,}|~{3,}).*$/); + const start = body.match(/^ {0,3}(`{3,}|~{3,}).*$/); const marker = start?.[1]; if (marker) { open = { char: marker[0], length: marker.length, start: offset + line.length }; @@ -273,10 +187,6 @@ function startsCommandSubstitution(source: string, index: number): boolean { return source[index] === "$" && source[index + 1] === "(" && source[index - 1] !== "\\"; } -function startsTemplateSubstitution(source: string, index: number): boolean { - return source[index] === "$" && source[index + 1] === "{" && source[index - 1] !== "\\"; -} - function findCommandSubstitutionEnd(source: string, start: number): number | undefined { let depth = 1; let index = start + 2; @@ -340,51 +250,6 @@ function findCommandSubstitutionEnd(source: string, start: number): number | und return undefined; } -function findTemplateSubstitutionEnd(source: string, start: number): number | undefined { - let depth = 1; - let index = start + 2; - let quote: "'" | '"' | "`" | null = null; - - while (index < source.length) { - const ch = source[index]; - - if (quote !== null) { - if (ch === "\\" && index + 1 < source.length) { - index += 2; - continue; - } - if (quote === "`" && startsTemplateSubstitution(source, index)) { - const substitutionEnd = findTemplateSubstitutionEnd(source, index); - if (substitutionEnd !== undefined) { - index = substitutionEnd + 1; - continue; - } - } - if (ch === quote) { - quote = null; - } - index += 1; - continue; - } - - if (ch === "'" || ch === '"' || ch === "`") { - quote = ch; - index += 1; - continue; - } - - if (ch === "{") { - depth += 1; - } else if (ch === "}") { - depth -= 1; - if (depth === 0) return index; - } - index += 1; - } - - return undefined; -} - function findContainingRange( ranges: TextRange[] | undefined, index: number, @@ -489,75 +354,11 @@ function findOptionRename(command: string, index: number): readonly [string, str return OPTION_RENAMES.find( ([from]) => command.startsWith(from, index) && - command[index - 1] !== "=" && isOptionBoundaryChar(command[index - 1]) && isOptionBoundaryChar(command[index + from.length]), ); } -function findCliValueArg(command: string, index: number): string | undefined { - return COMMAND_VALUE_ARGS.find( - (arg) => - command.startsWith(arg, index) && - isOptionBoundaryChar(command[index - 1]) && - (command[index + arg.length] === "=" || isOptionBoundaryChar(command[index + arg.length])), - ); -} - -function findShellArgEnd(command: string, start: number): number { - let index = start; - let quote: ActiveQuote | null = null; - - while (index < command.length) { - const ch = command[index]; - if (quote !== null) { - if (quote.escaped) { - if (ch === "\\" && command[index + 1] === quote.char) { - quote = null; - index += 2; - continue; - } - } else if (ch === "\\" && quote.char === '"' && index + 1 < command.length) { - index += 2; - continue; - } else if (ch === quote.char) { - quote = null; - } - index += 1; - continue; - } - - if (ch === "'" || ch === '"') { - quote = { char: ch, escaped: false }; - index += 1; - continue; - } - if (ch === "\\" && (command[index + 1] === "'" || command[index + 1] === '"')) { - quote = { char: command[index + 1] as "'" | '"', escaped: true }; - index += 2; - continue; - } - if (startsCommandSubstitution(command, index)) { - const substitutionEnd = findCommandSubstitutionEnd(command, index); - if (substitutionEnd !== undefined) { - index = substitutionEnd + 1; - continue; - } - } - if (startsTemplateSubstitution(command, index)) { - const substitutionEnd = findTemplateSubstitutionEnd(command, index); - if (substitutionEnd !== undefined) { - index = substitutionEnd + 1; - continue; - } - } - if (/\s/.test(ch) || isCommandSeparator(command, index) || ch === ")") break; - index += 1; - } - - return index; -} - function replaceOptionsInCommand(command: string): string { let updated = ""; let index = 0; @@ -621,37 +422,10 @@ function replaceOptionsInCommand(command: string): string { } } - const valueArg = findCliValueArg(command, index); - if (valueArg) { - const afterName = index + valueArg.length; - updated += command.slice(index, afterName); - index = afterName; - if (command[index] === "=") { - const valueEnd = findShellArgEnd(command, index + 1); - updated += command.slice(index, valueEnd); - index = valueEnd; - continue; - } - const whitespace = command.slice(index).match(/^\s+/)?.[0]; - if (whitespace) { - updated += whitespace; - index += whitespace.length; - const valueEnd = findShellArgEnd(command, index); - updated += command.slice(index, valueEnd); - index = valueEnd; - } - continue; - } - const rename = findOptionRename(command, index); if (rename) { updated += rename[1]; index += rename[0].length; - if (command[index] === "=") { - const valueEnd = findShellArgEnd(command, index + 1); - updated += command.slice(index, valueEnd); - index = valueEnd; - } continue; } @@ -662,1042 +436,6 @@ function replaceOptionsInCommand(command: string): string { return updated; } -function replaceCommandNameInCommand(command: string): string { - const binary = command.match(TAILOR_BINARY_START_PATTERN)?.[0]; - if (!binary) return command; - - let index = binary.length; - for (;;) { - const whitespace = command.slice(index).match(/^\s+/)?.[0]; - if (whitespace) index += whitespace.length; - if (index >= command.length) return command; - - const tokenEnd = findShellArgEnd(command, index); - const token = command.slice(index, tokenEnd); - const name = optionName(token); - if (GLOBAL_BOOLEAN_ARGS.has(name)) { - index = tokenEnd; - continue; - } - if (isGlobalSeparateValueArg(name)) { - index = tokenEnd; - if (!token.includes("=") || token.endsWith("=")) { - const valueWhitespace = command.slice(index).match(/^\s+/)?.[0]; - if (!valueWhitespace) return command; - index += valueWhitespace.length; - index = findShellArgEnd(command, index); - } - continue; - } - if (token.startsWith("-")) return command; - - const replacement = COMMAND_MAP.get(token); - return replacement - ? `${command.slice(0, index)}${replacement}${command.slice(tokenEnd)}` - : command; - } -} - -function replaceCliRenamesInCommand(command: string): string { - return replaceOptionsInCommand(replaceCommandNameInCommand(command)); -} - -function sourceLang(filePath: string): Lang { - const ext = path.extname(filePath).toLowerCase(); - return ext === ".tsx" || ext === ".jsx" || ext === ".js" ? Lang.Tsx : Lang.TypeScript; -} - -function sourceStringToken(node: SgNode, source: string): SourceStringToken | undefined { - const kind = node.kind(); - if (kind !== "string" && kind !== "template_string") return undefined; - if ( - kind === "template_string" && - node.children().some((child: SgNode) => child.kind() === "template_substitution") - ) { - return undefined; - } - - const range = node.range(); - return { - value: source.slice(range.start.index + 1, range.end.index - 1), - start: range.start.index + 1, - end: range.end.index - 1, - }; -} - -function sourceStringExpressionToken( - node: SgNode, - source: string, - visited = new Set(), -): SourceStringToken | undefined { - const direct = sourceStringToken(node, source); - if (direct) return direct; - if (!SOURCE_STRING_WRAPPER_NODE_KINDS.has(node.kind())) return undefined; - const key = nodeRangeKey(node); - if (visited.has(key)) return undefined; - visited.add(key); - for (const child of node.children()) { - const token = sourceStringExpressionToken(child, source, visited); - if (token) return token; - } - return undefined; -} - -function ensureTemplateToken(state: TemplateTokenState): TemplateToken { - state.token ??= { value: "", segments: [], substitutions: [], quoted: false }; - return state.token; -} - -function appendTemplateTokenChar(state: TemplateTokenState, ch: string, sourceIndex: number): void { - const token = ensureTemplateToken(state); - token.value += ch; - const previous = token.segments.at(-1); - if (previous && previous.end === sourceIndex) { - previous.value += ch; - previous.end += 1; - } else { - token.segments.push({ value: ch, start: sourceIndex, end: sourceIndex + 1 }); - } -} - -function pushTemplateToken(tokens: TemplateToken[], state: TemplateTokenState): void { - if (!state.token) return; - tokens.push(state.token); - state.token = null; -} - -function scanTemplateTextTokens( - state: TemplateTokenState, - text: string, - offset: number, - tokens: TemplateToken[], -): void { - for (let index = 0; index < text.length; index += 1) { - const ch = text[index]!; - - if (state.quote !== null) { - appendTemplateTokenChar(state, ch, offset + index); - if (ch === "\\" && state.quote === '"' && index + 1 < text.length) { - index += 1; - appendTemplateTokenChar(state, text[index]!, offset + index); - continue; - } - if (ch === state.quote) { - state.quote = null; - } - continue; - } - - if (/\s/.test(ch)) { - pushTemplateToken(tokens, state); - continue; - } - - if (isCommandSeparator(text, index)) { - pushTemplateToken(tokens, state); - appendTemplateTokenChar(state, ch, offset + index); - pushTemplateToken(tokens, state); - continue; - } - - appendTemplateTokenChar(state, ch, offset + index); - if (ch === "'" || ch === '"') { - state.quote = ch; - ensureTemplateToken(state).quoted = true; - } - } -} - -function appendTemplateSubstitution(state: TemplateTokenState, substitution: SgNode): void { - const token = ensureTemplateToken(state); - token.value += TEMPLATE_SUBSTITUTION_PLACEHOLDER; - token.substitutions.push(substitution); -} - -function collectTemplateTokens(node: SgNode, source: string): TemplateToken[] { - const tokens: TemplateToken[] = []; - const state: TemplateTokenState = { quote: null, token: null }; - - for (const child of node.children()) { - if (child.kind() === "string_fragment") { - const range = child.range(); - scanTemplateTextTokens( - state, - source.slice(range.start.index, range.end.index), - range.start.index, - tokens, - ); - continue; - } - if (child.kind() === "template_substitution") { - appendTemplateSubstitution(state, child); - } - } - - pushTemplateToken(tokens, state); - return tokens; -} - -function staticTemplateTokenValue(token: TemplateToken): string | undefined { - return token.substitutions.length === 0 ? token.value : undefined; -} - -function pushTemplateTokenReplacement( - edits: Array<[number, number, string]>, - token: TemplateToken, - replacement: string, -): void { - if (token.segments.length !== 1) return; - const [{ start, end }] = token.segments; - edits.push([start, end, replacement]); -} - -function optionName(value: string): string { - const equalsIndex = value.indexOf("="); - return equalsIndex === -1 ? value : value.slice(0, equalsIndex); -} - -function isGlobalSeparateValueArg(value: string): boolean { - return GLOBAL_VALUE_ARGS.has(value); -} - -function isInlineGlobalValueArg(value: string): boolean { - return isGlobalSeparateValueArg(optionName(value)) && value.includes("=") && !value.endsWith("="); -} - -function isOpenGlobalValueArg(value: string): boolean { - return ( - isGlobalSeparateValueArg(optionName(value)) && (!value.includes("=") || value.endsWith("=")) - ); -} - -function isAnySeparateValueArg(value: string): boolean { - return CLI_VALUE_ARG_SET.has(value); -} - -function isOpenCliValueArg(value: string): boolean { - return isAnySeparateValueArg(optionName(value)) && (!value.includes("=") || value.endsWith("=")); -} - -function isInlineCliValueArg(value: string): boolean { - return isAnySeparateValueArg(optionName(value)) && value.includes("=") && !value.endsWith("="); -} - -function isPackageRunnerSeparatePackageArg(value: string | undefined): boolean { - return value === "--package" || value === "-p"; -} - -function isPackageRunnerSeparateValueArg(value: string | undefined): boolean { - return value != null && PACKAGE_RUNNER_VALUE_ARGS.has(value); -} - -function isPackageRunnerSeparateArg(value: string | undefined): boolean { - return isPackageRunnerSeparatePackageArg(value) || isPackageRunnerSeparateValueArg(value); -} - -function replaceOptionsInToken(value: string): string { - const equalsIndex = value.indexOf("="); - if (equalsIndex !== -1 && value.startsWith("-")) { - const name = value.slice(0, equalsIndex); - return `${OPTION_MAP.get(name) ?? name}${value.slice(equalsIndex)}`; - } - return replaceOptionsInCommand(value); -} - -function resetCliTemplateState(state: CliTemplateState): void { - state.afterTailorBinary = false; - state.commandMayBeNext = false; - state.skipNextValue = false; -} - -function tokenStart(token: TemplateToken): number | undefined { - return token.segments[0]?.start; -} - -function tokenEnd(token: TemplateToken): number | undefined { - return token.segments.at(-1)?.end; -} - -function hasUnescapedLineBreak(value: string): boolean { - for (let index = 0; index < value.length; index += 1) { - if (value[index] === "\n" && value[index - 1] !== "\\") return true; - } - return false; -} - -function hasTemplateCommandBoundaryBetween( - previous: TemplateToken, - next: TemplateToken, - source: string, -): boolean { - const previousEnd = tokenEnd(previous); - const nextStart = tokenStart(next); - if (previousEnd == null || nextStart == null) return false; - return hasUnescapedLineBreak(source.slice(previousEnd, nextStart)); -} - -function hasCommandSeparatorToken(value: string): boolean { - for (let index = 0; index < value.length; index += 1) { - if (isCommandSeparator(value, index)) return true; - } - return false; -} - -function isCommandSeparatorOnlyToken(value: string): boolean { - return value !== "" && /^[;&|]+$/.test(value) && hasCommandSeparatorToken(value); -} - -function isShellAssignmentToken(value: string): boolean { - return /^[A-Za-z_]\w*=/.test(value); -} - -function shellCommandPrefixEndIndex(values: string[]): number { - let index = values[0] === "env" ? 1 : 0; - const assignmentStart = index; - while (index < values.length && isShellAssignmentToken(values[index]!)) { - index += 1; - } - if (index > assignmentStart) return index; - return values[0] === "env" ? 1 : 0; -} - -function rewriteTokenizedCliArg(value: string, renameCommand: boolean): string { - const commandRenamed = renameCommand ? (COMMAND_MAP.get(value) ?? value) : value; - return replaceOptionsInToken(commandRenamed); -} - -function rewriteCommandToken(value: string): string { - for (let index = 0; index < value.length; index += 1) { - if (!isCommandSeparator(value, index)) continue; - const command = value.slice(0, index); - return `${COMMAND_MAP.get(command) ?? command}${value.slice(index)}`; - } - return COMMAND_MAP.get(value) ?? value; -} - -function isLikelySourceCommandString(value: string): boolean { - const trimmed = value.trimStart(); - if (TAILOR_BINARY_TOKEN.test(trimmed.split(/\s+/, 1)[0] ?? "")) return true; - if (/^(?:npx|bunx|pnpm|yarn)\b/.test(trimmed) && TAILOR_BINARY_PATTERN.test(trimmed)) { - TAILOR_BINARY_PATTERN.lastIndex = 0; - return true; - } - TAILOR_BINARY_PATTERN.lastIndex = 0; - return ( - SHELL_ASSIGNMENT_PREFIX_PATTERN.test(trimmed) || SHELL_SEPARATOR_TAILOR_PATTERN.test(trimmed) - ); -} - -function replaceSourceCommandString(value: string): string { - TAILOR_BINARY_PATTERN.lastIndex = 0; - if (!isLikelySourceCommandString(value)) return value; - TAILOR_BINARY_PATTERN.lastIndex = 0; - return replaceAll(value); -} - -function isPackageRunnerOptionValueReference(source: string, start: number): boolean { - return PACKAGE_RUNNER_OPTION_VALUE_PREFIX_PATTERN.test(source.slice(0, start)); -} - -function isTokenSequenceNode(node: SgNode): boolean { - return node.kind() === "array" || isCliArgumentsNode(node); -} - -function isSyntaxOnlyNode(node: SgNode): boolean { - const kind = node.kind(); - return ( - kind === "[" || - kind === "]" || - kind === "(" || - kind === ")" || - kind === "," || - kind === "comment" - ); -} - -function nodeRangeKey(node: SgNode): string { - const range = node.range(); - return `${range.start.index}:${range.end.index}`; -} - -function isCliArgumentsNode(node: SgNode): boolean { - if (node.kind() !== "arguments") return false; - const parent = node.parent(); - if (parent?.kind() !== "call_expression") return false; - const argumentRange = nodeRangeKey(node); - const callee = parent.children().find((child: SgNode) => nodeRangeKey(child) !== argumentRange); - const calleeText = callee?.text(); - return calleeText === "$" || (calleeText != null && CLI_ARGUMENT_CALLEE_RE.test(calleeText)); -} - -function collectInterpolatedOptionEdits( - node: SgNode, - source: string, - renameCommand = false, -): Array<[number, number, string]> { - if (node.kind() !== "template_string") return []; - if (!node.children().some((child: SgNode) => child.kind() === "template_substitution")) { - return []; - } - - const edits: Array<[number, number, string]> = []; - for (const fragment of node - .children() - .filter((child: SgNode) => child.kind() === "string_fragment")) { - const range = fragment.range(); - const text = source.slice(range.start.index, range.end.index); - const replacement = replaceOptionsInCommand(text); - if (replacement !== text) { - edits.push([range.start.index, range.end.index, replacement]); - } - } - if (templateStartsWithInlineOptionValue(node, source)) return edits; - for (const substitution of node - .children() - .filter((child: SgNode) => child.kind() === "template_substitution")) { - edits.push(...collectCliExpressionEdits(substitution, source, renameCommand)); - } - return edits; -} - -function cliArgExpressionChildren(node: SgNode): SgNode[] { - if (node.kind() === "parenthesized_expression") return node.children(); - if (node.kind() === "as_expression" || node.kind() === "satisfies_expression") { - return node.children().slice(0, 1); - } - if (node.kind() === "ternary_expression") return node.children().slice(1); - if (node.kind() === "binary_expression" && /&&|\|\|/.test(node.text())) { - return node.children().slice(1); - } - return []; -} - -function collectCliExpressionEdits( - node: SgNode, - source: string, - renameCommand: boolean, -): Array<[number, number, string]> { - const token = sourceStringExpressionToken(node, source); - if (token) { - const replacement = rewriteTokenizedCliArg(token.value, renameCommand); - return replacement === token.value ? [] : [[token.start, token.end, replacement]]; - } - - const edits: Array<[number, number, string]> = []; - const cliArgChildren = cliArgExpressionChildren(node); - const children = cliArgChildren.length > 0 ? cliArgChildren : node.children(); - for (const child of children) { - edits.push(...collectCliExpressionEdits(child, source, renameCommand)); - } - return edits; -} - -function sourceStringValues(node: SgNode, source: string): string[] { - const token = sourceStringExpressionToken(node, source); - if (token) return [token.value]; - - const values: string[] = []; - if (node.kind() === "template_string") { - for (const child of node.children()) { - if (child.kind() !== "string_fragment") continue; - const range = child.range(); - values.push(source.slice(range.start.index, range.end.index)); - } - } - const cliArgChildren = cliArgExpressionChildren(node); - const children = cliArgChildren.length > 0 ? cliArgChildren : node.children(); - for (const child of children) { - values.push(...sourceStringValues(child, source)); - } - return values; -} - -function templateStartsWithInlineOptionValue(node: SgNode, source: string): boolean { - if (node.kind() !== "template_string") return false; - const firstFragment = node.children().find((child: SgNode) => child.kind() === "string_fragment"); - if (!firstFragment) return false; - const range = firstFragment.range(); - const text = source.slice(range.start.index, range.end.index).trimStart(); - const equalsIndex = text.indexOf("="); - if (equalsIndex === -1) return false; - return text.startsWith("-") && !/\s/.test(text.slice(0, equalsIndex)); -} - -function dynamicCliArgState(node: SgNode, source: string): "keep-command" | "skip-value" | null { - if (templateStartsWithInlineOptionValue(node, source)) { - const values = sourceStringValues(node, source); - const first = values.find((value) => value.trim() !== "")?.trimStart() ?? ""; - const name = optionName(first); - return isGlobalSeparateValueArg(name) ? "keep-command" : null; - } - - const values = sourceStringValues(node, source) - .map((value) => value.trim()) - .filter((value) => value !== ""); - if (values.length === 0) return null; - if ( - values.every( - (value) => - GLOBAL_BOOLEAN_ARGS.has(optionName(value)) || - isInlineGlobalValueArg(value) || - isOpenGlobalValueArg(value), - ) - ) { - return values.some((value) => isOpenGlobalValueArg(value)) ? "skip-value" : "keep-command"; - } - return null; -} - -function dynamicCliValueArgState(node: SgNode, source: string): "skip-value" | null { - const values = sourceStringValues(node, source) - .map((value) => value.trim()) - .filter((value) => value !== ""); - if (values.length === 0) return null; - return values.every((value) => isOpenCliValueArg(value)) ? "skip-value" : null; -} - -function dynamicPackageRunnerArgState(node: SgNode, source: string): "skip-value" | null { - const values = sourceStringValues(node, source) - .map((value) => value.trim()) - .filter((value) => value !== ""); - if (values.length === 0) return null; - return values.some((value) => isPackageRunnerSeparateArg(value)) ? "skip-value" : null; -} - -function advanceCliTemplateState(state: CliTemplateState, value: string): void { - if (!state.afterTailorBinary) { - if (TAILOR_BINARY_TOKEN.test(value)) { - state.afterTailorBinary = true; - state.commandMayBeNext = true; - state.skipNextValue = false; - } - return; - } - - if (state.skipNextValue) { - state.skipNextValue = false; - return; - } - - const name = optionName(value); - if (state.commandMayBeNext) { - if (GLOBAL_BOOLEAN_ARGS.has(name)) return; - if (isGlobalSeparateValueArg(name)) { - state.skipNextValue = !value.includes("=") || value.endsWith("="); - return; - } - if (!value.startsWith("-")) { - state.commandMayBeNext = false; - } - return; - } - - if (isAnySeparateValueArg(name) && (!value.includes("=") || value.endsWith("="))) { - state.skipNextValue = true; - } -} - -function collectCliTemplateEdits(node: SgNode, source: string): CliTemplateEditResult { - if (node.kind() !== "template_string") return { edits: [], protectedRanges: [] }; - - const edits: Array<[number, number, string]> = []; - const protectedRanges: TextRange[] = []; - const state: CliTemplateState = { - afterTailorBinary: false, - commandMayBeNext: false, - skipNextValue: false, - }; - let skipNextPackageRunnerValue = false; - const protectTemplateSubstitutions = (token: TemplateToken): void => { - for (const substitution of token.substitutions) { - const range = substitution.range(); - protectedRanges.push({ start: range.start.index, end: range.end.index }); - } - }; - - const tokens = collectTemplateTokens(node, source); - for (const [index, token] of tokens.entries()) { - const previous = tokens[index - 1]; - const hasCommandBoundaryBefore = - previous != null && hasTemplateCommandBoundaryBetween(previous, token, source); - if (hasCommandBoundaryBefore) { - resetCliTemplateState(state); - } - - const value = staticTemplateTokenValue(token); - const tokenHasCommandSeparator = value !== undefined && hasCommandSeparatorToken(value); - if (value !== undefined && isCommandSeparatorOnlyToken(value)) { - resetCliTemplateState(state); - continue; - } - - if (!state.afterTailorBinary) { - if (skipNextPackageRunnerValue) { - protectTemplateSubstitutions(token); - skipNextPackageRunnerValue = false; - continue; - } - if (value !== undefined) { - if (isPackageRunnerSeparateArg(value)) { - skipNextPackageRunnerValue = true; - continue; - } - if ( - TAILOR_BINARY_TOKEN.test(value) && - !isLikelyTemplateCommandToken(tokens, index, hasCommandBoundaryBefore) - ) { - continue; - } - advanceCliTemplateState(state, value); - } else { - const dynamicState = - token.value === TEMPLATE_SUBSTITUTION_PLACEHOLDER && token.substitutions.length === 1 - ? dynamicPackageRunnerArgState(token.substitutions[0]!, source) - : null; - if (dynamicState === "skip-value") { - skipNextPackageRunnerValue = true; - } - } - continue; - } - - if (state.skipNextValue) { - protectTemplateSubstitutions(token); - state.skipNextValue = false; - continue; - } - - if (token.quoted) { - if (state.commandMayBeNext) { - state.commandMayBeNext = false; - } - continue; - } - - if (value !== undefined) { - const name = optionName(value); - if (state.commandMayBeNext) { - if (GLOBAL_BOOLEAN_ARGS.has(name)) { - if (tokenHasCommandSeparator) { - resetCliTemplateState(state); - } - continue; - } - if (isGlobalSeparateValueArg(name)) { - state.skipNextValue = !value.includes("=") || value.endsWith("="); - if (tokenHasCommandSeparator) { - resetCliTemplateState(state); - } - continue; - } - if (!value.startsWith("-")) { - const replacement = rewriteCommandToken(value); - if (replacement !== value) { - pushTemplateTokenReplacement(edits, token, replacement); - } - state.commandMayBeNext = false; - if (tokenHasCommandSeparator) { - resetCliTemplateState(state); - } - continue; - } - if (tokenHasCommandSeparator) { - resetCliTemplateState(state); - } - continue; - } - - if (isOpenCliValueArg(value)) { - state.skipNextValue = true; - continue; - } - - const replacement = replaceOptionsInToken(value); - if (replacement !== value) { - pushTemplateTokenReplacement(edits, token, replacement); - } - if (tokenHasCommandSeparator) { - resetCliTemplateState(state); - } - continue; - } - - if (state.commandMayBeNext) { - if (isInlineGlobalValueArg(token.value)) { - continue; - } - for (const substitution of token.substitutions) { - edits.push(...collectCliExpressionEdits(substitution, source, true)); - } - const dynamicState = - token.value === TEMPLATE_SUBSTITUTION_PLACEHOLDER && token.substitutions.length === 1 - ? dynamicCliArgState(token.substitutions[0]!, source) - : null; - if (dynamicState === "keep-command") { - continue; - } - if (dynamicState === "skip-value") { - state.skipNextValue = true; - continue; - } - state.commandMayBeNext = false; - continue; - } - - if (isInlineCliValueArg(token.value)) { - continue; - } - for (const substitution of token.substitutions) { - edits.push(...collectCliExpressionEdits(substitution, source, false)); - } - const dynamicState = - token.value === TEMPLATE_SUBSTITUTION_PLACEHOLDER && token.substitutions.length === 1 - ? dynamicCliValueArgState(token.substitutions[0]!, source) - : null; - if (dynamicState === "skip-value") { - state.skipNextValue = true; - } - } - - return { edits, protectedRanges }; -} - -function isLikelyTemplateCommandToken( - tokens: TemplateToken[], - tokenIndex: number, - hasCommandBoundaryBefore = false, -): boolean { - if (tokenIndex === 0 || hasCommandBoundaryBefore) return true; - - const previous = tokens[tokenIndex - 1]; - if (previous && staticTemplateTokenValue(previous) !== undefined) { - const value = staticTemplateTokenValue(previous)!; - if (isCommandSeparatorOnlyToken(value)) return true; - } - - const staticPrefixValues = tokens - .slice(0, tokenIndex) - .map((token) => staticTemplateTokenValue(token)) - .filter((value): value is string => value !== undefined); - const shellPrefixEndIndex = shellCommandPrefixEndIndex(staticPrefixValues); - if (shellPrefixEndIndex > 0 && shellPrefixEndIndex === staticPrefixValues.length) return true; - const [first] = staticPrefixValues.slice(shellPrefixEndIndex); - return first === "npx" || first === "bunx" || first === "pnpm" || first === "yarn"; -} - -function collectCliTemplateSourceEdits( - root: SgNode, - source: string, - protectedRanges: TextRange[] = [], -): CliTemplateEditResult { - const edits: Array<[number, number, string]> = []; - const templateProtectedRanges = [...protectedRanges]; - const isProtected = (node: SgNode): boolean => { - const range = node.range(); - return templateProtectedRanges.some( - (protectedRange) => - protectedRange.start <= range.start.index && range.end.index <= protectedRange.end, - ); - }; - - const visit = (node: SgNode): void => { - if (isProtected(node)) return; - if (node.kind() === "template_string") { - const result = collectCliTemplateEdits(node, source); - edits.push(...result.edits); - templateProtectedRanges.push(...result.protectedRanges); - } - for (const child of node.children()) { - visit(child); - } - }; - visit(root); - return { edits, protectedRanges: templateProtectedRanges }; -} - -function collectSourceLiteralCliCommandEdits( - root: SgNode, - source: string, - protectedRanges: TextRange[] = [], -): Array<[number, number, string]> { - const edits: Array<[number, number, string]> = []; - - const isProtected = (node: SgNode): boolean => { - const range = node.range(); - return protectedRanges.some( - (protectedRange) => - protectedRange.start <= range.start.index && range.end.index <= protectedRange.end, - ); - }; - - const visit = (node: SgNode): void => { - if (isProtected(node)) return; - - if (node.kind() === "comment") { - const range = node.range(); - const text = source.slice(range.start.index, range.end.index); - const replacement = replaceAll(text, false, true, true); - if (replacement !== text) { - edits.push([range.start.index, range.end.index, replacement]); - } - return; - } - - if (node.kind() === "string") { - const token = sourceStringToken(node, source); - if (token) { - const replacement = replaceSourceCommandString(token.value); - if (replacement !== token.value) { - edits.push([token.start, token.end, replacement]); - } - } - return; - } - - if (node.kind() === "jsx_text") { - const range = node.range(); - const text = source.slice(range.start.index, range.end.index); - const replacement = replaceSourceCommandString(text); - if (replacement !== text) { - edits.push([range.start.index, range.end.index, replacement]); - } - return; - } - - for (const child of node.children()) { - visit(child); - } - }; - - visit(root); - return edits; -} - -function collectTokenizedSourceEdits( - root: SgNode, - source: string, -): { edits: Array<[number, number, string]>; protectedRanges: TextRange[] } { - const edits: Array<[number, number, string]> = []; - const protectedRanges: TextRange[] = []; - - const protectNode = (node: SgNode): void => { - const range = node.range(); - protectedRanges.push({ start: range.start.index, end: range.end.index }); - }; - - const visit = (node: SgNode, inheritedAfterTailorBinary = false): void => { - if (inheritedAfterTailorBinary) { - const token = sourceStringExpressionToken(node, source); - if (token) { - const replacement = rewriteTokenizedCliArg(token.value, true); - if (replacement !== token.value) { - edits.push([token.start, token.end, replacement]); - } - return; - } - edits.push(...collectInterpolatedOptionEdits(node, source)); - } - - if (isTokenSequenceNode(node)) { - let afterTailorBinary = inheritedAfterTailorBinary; - let commandMayBeNext = inheritedAfterTailorBinary; - let skipNextValue = false; - let skipNextPackageRunnerValue = false; - let previousTokenValue: string | undefined; - for (const child of node.children()) { - const token = sourceStringExpressionToken(child, source); - if (token) { - const previous = previousTokenValue; - previousTokenValue = token.value; - - if (!afterTailorBinary) { - if (skipNextPackageRunnerValue) { - skipNextPackageRunnerValue = false; - continue; - } - if (isPackageRunnerSeparateArg(token.value)) { - skipNextPackageRunnerValue = true; - continue; - } - afterTailorBinary = TAILOR_BINARY_TOKEN.test(token.value); - commandMayBeNext = afterTailorBinary; - continue; - } - - if ( - skipNextValue || - (previous != null && - (commandMayBeNext ? isOpenGlobalValueArg(previous) : isOpenCliValueArg(previous))) - ) { - protectNode(child); - skipNextValue = false; - continue; - } - - const name = optionName(token.value); - const renameCommand = commandMayBeNext && !token.value.startsWith("-"); - const replacement = - commandMayBeNext && - token.value.startsWith("-") && - !GLOBAL_BOOLEAN_ARGS.has(name) && - !isOpenGlobalValueArg(token.value) - ? token.value - : rewriteTokenizedCliArg(token.value, renameCommand); - if (replacement !== token.value) { - edits.push([token.start, token.end, replacement]); - } - if (commandMayBeNext) { - if (GLOBAL_BOOLEAN_ARGS.has(name)) { - continue; - } - if (isOpenGlobalValueArg(token.value)) { - skipNextValue = true; - continue; - } - if (!token.value.startsWith("-")) { - commandMayBeNext = false; - } - } else if (isOpenCliValueArg(token.value)) { - skipNextValue = true; - } - continue; - } - - if (isSyntaxOnlyNode(child)) { - visit(child); - continue; - } - - if (!afterTailorBinary && skipNextPackageRunnerValue) { - protectNode(child); - skipNextPackageRunnerValue = false; - previousTokenValue = undefined; - continue; - } - - if (skipNextValue) { - protectNode(child); - skipNextValue = false; - previousTokenValue = undefined; - continue; - } - - const childCanHoldCommand = - isTokenSequenceNode(child) || cliArgExpressionChildren(child).length > 0; - - if (!afterTailorBinary) { - visit(child); - const dynamicState = dynamicPackageRunnerArgState(child, source); - if (dynamicState === "skip-value") { - skipNextPackageRunnerValue = true; - } - continue; - } - - if (afterTailorBinary && commandMayBeNext) { - edits.push(...collectInterpolatedOptionEdits(child, source, commandMayBeNext)); - } else { - edits.push(...collectInterpolatedOptionEdits(child, source, false)); - if (!templateStartsWithInlineOptionValue(child, source)) { - edits.push(...collectCliExpressionEdits(child, source, false)); - } - } - - visit( - child, - afterTailorBinary && commandMayBeNext && !skipNextValue && childCanHoldCommand, - ); - if (commandMayBeNext && afterTailorBinary) { - const dynamicState = dynamicCliArgState(child, source); - if (dynamicState === "keep-command") { - continue; - } - if (dynamicState === "skip-value") { - skipNextValue = true; - continue; - } - commandMayBeNext = false; - } else { - const dynamicState = dynamicCliValueArgState(child, source); - if (dynamicState === "skip-value") { - skipNextValue = true; - } - } - } - return; - } - - const cliArgChildren = inheritedAfterTailorBinary ? cliArgExpressionChildren(node) : []; - if (cliArgChildren.length > 0) { - for (const child of cliArgChildren) { - visit(child, true); - } - return; - } - - for (const child of node.children()) { - visit(child); - } - }; - - visit(root); - return { edits, protectedRanges }; -} - -function replaceTokenizedSourceCliCommands(source: string, filePath: string): string { - let root: SgNode; - try { - root = parse(sourceLang(filePath), source).root(); - } catch { - return source; - } - - let updated = source; - const tokenized = collectTokenizedSourceEdits(root, source); - const template = collectCliTemplateSourceEdits(root, source, tokenized.protectedRanges); - const protectedRanges = [...tokenized.protectedRanges, ...template.protectedRanges]; - const editByRange = new Map(); - for (const edit of [ - ...tokenized.edits, - ...template.edits, - ...collectSourceLiteralCliCommandEdits(root, source, protectedRanges), - ]) { - editByRange.set(`${edit[0]}:${edit[1]}`, edit); - } - const edits = Array.from(editByRange.values()).toSorted(([a], [b]) => b - a); - for (const [start, end, replacement] of edits) { - updated = `${updated.slice(0, start)}${replacement}${updated.slice(end)}`; - } - return updated; -} - -function protectCliValueStrings(source: string): { source: string; protectedValues: string[] } { - const protectedValues: string[] = []; - const updated = source.replace( - /(["'`])(?:--env-file-if-exists|--env-file|--arg|--query|--file|-e|-a|-q|-f)\1\s*,\s*(["'`])([^"'`]*?(?:crash-report|--machineuser)[^"'`]*)\2/g, - (match, _optionQuote: string, valueQuote: string, value: string) => { - const placeholder = `__TAILOR_CLI_RENAME_PROTECTED_${protectedValues.length}__`; - protectedValues.push(value); - return match.replace( - `${valueQuote}${value}${valueQuote}`, - `${valueQuote}${placeholder}${valueQuote}`, - ); - }, - ); - return { source: updated, protectedValues }; -} - -function restoreCliValueStrings(source: string, protectedValues: string[]): string { - let restored = source; - for (const [index, value] of protectedValues.entries()) { - restored = restored.replaceAll(`__TAILOR_CLI_RENAME_PROTECTED_${index}__`, value); - } - return restored.replace( - /((["'`])tailor-sdk\2\s*,\s*(["'`])(?:--env-file|--env-file-if-exists|-e)\3\s*,\s*(["'`])[^"'`]*\4\s*,\s*)(["'`])crash-report\5/g, - "$1$5crashreport$5", - ); -} - function replaceOptionsInTailorCommands( source: string, foldedYamlRanges?: TextRange[], @@ -1715,11 +453,6 @@ function replaceOptionsInTailorCommands( const start = match.index; if (start < cursor) continue; - if (isPackageRunnerOptionValueReference(source, start)) { - TAILOR_BINARY_PATTERN.lastIndex = start + match[0].length; - continue; - } - if ( requireDelimitedContext && !isDelimitedCommandContext(source, start, foldedYamlRanges, markdownFencedCodeRanges) @@ -1730,7 +463,7 @@ function replaceOptionsInTailorCommands( const end = findTailorCommandEnd(source, start, foldedYamlRanges, markdownFencedCodeRanges); updated += source.slice(cursor, start); - updated += replaceCliRenamesInCommand(source.slice(start, end)); + updated += replaceOptionsInCommand(source.slice(start, end)); cursor = end; TAILOR_BINARY_PATTERN.lastIndex = end; } @@ -1744,12 +477,17 @@ function replaceAll( requireDelimitedContext = false, parseMarkdownFencedCode = false, ): string { - const foldedYamlRanges = parseFoldedYaml ? findFoldedYamlRanges(value) : undefined; + const updated = value.replace( + COMMAND_PATTERN, + (match, _prefix: string, cmd: string) => + `${match.slice(0, -cmd.length)}${COMMAND_MAP.get(cmd) ?? cmd}`, + ); + const foldedYamlRanges = parseFoldedYaml ? findFoldedYamlRanges(updated) : undefined; const markdownFencedCodeRanges = parseMarkdownFencedCode - ? findMarkdownFencedCodeRanges(value) + ? findMarkdownFencedCodeRanges(updated) : undefined; return replaceOptionsInTailorCommands( - value, + updated, foldedYamlRanges, requireDelimitedContext, markdownFencedCodeRanges, @@ -1760,12 +498,6 @@ function transformText(source: string, filePath: string): string | null { const ext = path.extname(filePath).toLowerCase(); const isYaml = ext === ".yml" || ext === ".yaml"; const isMarkdown = ext === ".md"; - if (SOURCE_EXTENSIONS.has(ext)) { - const protectedSource = protectCliValueStrings(source); - let updated = replaceTokenizedSourceCliCommands(protectedSource.source, filePath); - updated = restoreCliValueStrings(updated, protectedSource.protectedValues); - return updated === source ? null : updated; - } const updated = replaceAll(source, isYaml, isMarkdown, isMarkdown); return updated === source ? null : updated; } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh index 550424df2..faba9713c 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh @@ -7,13 +7,9 @@ pnpm exec tailor-sdk workflow start approval --machine-user ci tailor-sdk query --query 'select 1' --machine-user ci tailor-sdk query 2>&1 --machine-user ci tailor-sdk query $(build-query --machineuser=ci) --machine-user ci -tailor-sdk login ${CI:+--machine-user ci} tailor-sdk query --query 'select 1;' --machine-user ci tailor-sdk query --query "select 1 | 2" --machine-user ci tailor-sdk workflow start approval --arg '{"ok":true}' \ --machine-user ci tailor-sdk --json crashreport list -tailor-sdk --profile --machineuser crashreport --machine-user ci -npx --package tailor-sdk tailor-sdk crashreport --machine-user=ci -npx --cache tailor-sdk tailor-sdk crashreport --machine-user=ci TOKEN=$(tailor-sdk query --machine-user ci) other-cli --machineuser=ci diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh index 429c7d878..5b1573060 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh @@ -7,13 +7,9 @@ pnpm exec tailor-sdk workflow start approval --machineuser ci tailor-sdk query --query 'select 1' --machineuser ci tailor-sdk query 2>&1 --machineuser ci tailor-sdk query $(build-query --machineuser=ci) --machineuser ci -tailor-sdk login ${CI:+--machineuser ci} tailor-sdk query --query 'select 1;' --machineuser ci tailor-sdk query --query "select 1 | 2" --machineuser ci tailor-sdk workflow start approval --arg '{"ok":true}' \ --machineuser ci tailor-sdk --json crash-report list -tailor-sdk --profile --machineuser crash-report --machineuser ci -npx --package tailor-sdk tailor-sdk crash-report --machineuser=ci -npx --cache tailor-sdk tailor-sdk crash-report --machineuser=ci TOKEN=$(tailor-sdk query --machineuser ci) other-cli --machineuser=ci diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/expected.d.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/expected.d.ts deleted file mode 100644 index 42fc9cc99..000000000 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/expected.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Plain prose mentions tailor-sdk crash-report --machineuser. - * Inspect crash reports with `tailor-sdk crashreport list --machine-user ci`. - * Example: - * ```sh - * tailor-sdk crashreport list --machine-user ci - * ``` - */ -export {}; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/input.d.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/input.d.ts deleted file mode 100644 index a97139391..000000000 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/declaration-comment/input.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Plain prose mentions tailor-sdk crash-report --machineuser. - * Inspect crash reports with `tailor-sdk crash-report list --machineuser ci`. - * Example: - * ```sh - * tailor-sdk crash-report list --machineuser ci - * ``` - */ -export {}; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/expected.js b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/expected.js deleted file mode 100644 index 361fec5c6..000000000 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/expected.js +++ /dev/null @@ -1,6 +0,0 @@ -const docs = ( - <> -

package tailor-sdk crash-report --machineuser

- tailor-sdk crashreport list --machine-user ci - -); diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/input.js b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/input.js deleted file mode 100644 index b789716d1..000000000 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-jsx-js/input.js +++ /dev/null @@ -1,6 +0,0 @@ -const docs = ( - <> -

package tailor-sdk crash-report --machineuser

- tailor-sdk crash-report list --machineuser ci - -); diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts deleted file mode 100644 index f1a17eb5e..000000000 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/expected.ts +++ /dev/null @@ -1,48 +0,0 @@ -const machineUser = "ci"; -const report = `tailor-sdk crashreport list --machine-user ${machineUser}`; -const conditionalArg = `tailor-sdk login ${isCI ? "--machine-user" : ""}`; -const conditionalCommand = `tailor-sdk ${showReports ? "crashreport" : "login"} list`; -const dynamicGlobalTemplate = `tailor-sdk ${verbose ? "--verbose" : ""} ${showReports ? "crashreport" : "login"} list`; -const staticCommandAfterDynamicGlobal = `tailor-sdk ${verbose ? "--verbose" : ""} crashreport list`; -const dynamicInlineGlobalTemplate = `tailor-sdk ${useEnv ? "--env-file=.env" : "--json"} ${showReports ? "crashreport" : "login"} list`; -const dynamicProfileTemplate = `tailor-sdk --profile ${profile} crashreport --machine-user`; -const separatedTemplate = `tailor-sdk crashreport; ${next}`; -const packageRunnerTemplate = `npx --package tailor-sdk tailor-sdk crashreport --machine-user=ci`; -const shortPackageRunnerTemplate = `npx -p tailor-sdk tailor-sdk crashreport --machine-user=ci`; -const valueFlagPackageRunnerTemplate = `npx --cache tailor-sdk tailor-sdk crashreport --machine-user=ci`; -const dynamicValueFlagPackageRunnerTemplate = `npx ${useCache ? "--cache" : "--yes"} tailor-sdk crash-report --machineuser=ci`; -const repeatedTemplate = `tailor-sdk crashreport --machine-user -tailor-sdk crashreport --machine-user`; -const repeatedSeparatedTemplate = `tailor-sdk crashreport; tailor-sdk crashreport --machine-user`; -const joinedRepeatedSeparatedTemplate = `tailor-sdk crashreport;tailor-sdk crashreport`; -const conditionalGlobalComparison = `tailor-sdk ${mode === "prod" ? "--verbose" : "--json"} crashreport list`; -const conditionalOptionComparison = `tailor-sdk login ${flag === "--machineuser" ? "--json" : "--machine-user"}`; -const conditionalCommandComparison = `tailor-sdk ${cmd === "crash-report" ? "login" : "crashreport"} list`; -const inlineEnvTemplate = `tailor-sdk --env-file=${envFile} ${showReports ? "crashreport" : "login"} list`; -const login = "tailor-sdk login --machine-user ci"; -const envFilePath = "tailor-sdk --env-file=/tmp/--machineuser crashreport"; -const envFileCommandSubstitutionPath = "tailor-sdk --env-file=$(pwd)/--machineuser crashreport --machine-user"; -const argPayload = "tailor-sdk function test-run --arg=--machineuser"; -const argCommandSubstitutionPayload = "tailor-sdk function test-run --arg=$(printf --machineuser) --machine-user"; -const quotedArgCommandPayload = "tailor-sdk function test-run --arg='tailor-sdk crash-report --machineuser' --machine-user"; -const substitutedArgCommandPayload = `tailor-sdk function test-run --arg ${cond ? "tailor-sdk crash-report --machineuser" : "{}"} --machine-user`; -const escapedQueryCommand = "tailor-sdk query --query \"select 1\" --machine-user"; -const profileCommand = "tailor-sdk --profile prod crashreport --machine-user"; -const profileMachineUserValue = "tailor-sdk --profile --machineuser crashreport --machine-user"; -const workspaceCommand = "tailor-sdk -w workspace-1 crashreport"; -const chainedShellCommand = "cd app && tailor-sdk crashreport --machine-user"; -const envShellCommand = "env FOO=bar tailor-sdk crashreport --machine-user"; -const assignmentShellCommand = "TAILOR=1 tailor-sdk crashreport --machine-user"; -const envDynamicCommandTemplate = `env FOO=bar tailor-sdk ${showReports ? "crashreport" : "login"} --machine-user`; -const assignmentDynamicCommandTemplate = `TAILOR=1 tailor-sdk ${showReports ? "crashreport" : "login"} --machine-user`; -const unknownInlineValue = "tailor-sdk login --name=--machineuser"; -const argTemplatePayload = `tailor-sdk function test-run --arg=${payload ? "--machineuser" : ""}`; -const dynamicArgTemplatePayload = `tailor-sdk function test-run ${includeArg ? "--arg" : ""} ${payload ? "--machineuser" : ""} --machine-user`; -const quotedQueryTemplate = `tailor-sdk query --query "select --machineuser ${where}" --machine-user`; -const inlineQueryTemplate = `tailor-sdk query --query=${payload ? "--machineuser" : ""} --machine-user`; -const postCommandConfigValue = ["tailor-sdk", "deploy", "--config", "/tmp/--machineuser"]; -const chainedCommandTemplate = `tailor-sdk login --machine-user && other-cli --machineuser`; -const joinedSeparatorTemplate = `tailor-sdk login --machine-user;other-cli --machineuser`; -const newlineCommandTemplate = `tailor-sdk login --machine-user -other-cli --machineuser`; -const other = "other-cli --machineuser ci"; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts deleted file mode 100644 index 344f8b5b1..000000000 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-template/input.ts +++ /dev/null @@ -1,48 +0,0 @@ -const machineUser = "ci"; -const report = `tailor-sdk crash-report list --machineuser ${machineUser}`; -const conditionalArg = `tailor-sdk login ${isCI ? "--machineuser" : ""}`; -const conditionalCommand = `tailor-sdk ${showReports ? "crash-report" : "login"} list`; -const dynamicGlobalTemplate = `tailor-sdk ${verbose ? "--verbose" : ""} ${showReports ? "crash-report" : "login"} list`; -const staticCommandAfterDynamicGlobal = `tailor-sdk ${verbose ? "--verbose" : ""} crash-report list`; -const dynamicInlineGlobalTemplate = `tailor-sdk ${useEnv ? "--env-file=.env" : "--json"} ${showReports ? "crash-report" : "login"} list`; -const dynamicProfileTemplate = `tailor-sdk --profile ${profile} crash-report --machineuser`; -const separatedTemplate = `tailor-sdk crash-report; ${next}`; -const packageRunnerTemplate = `npx --package tailor-sdk tailor-sdk crash-report --machineuser=ci`; -const shortPackageRunnerTemplate = `npx -p tailor-sdk tailor-sdk crash-report --machineuser=ci`; -const valueFlagPackageRunnerTemplate = `npx --cache tailor-sdk tailor-sdk crash-report --machineuser=ci`; -const dynamicValueFlagPackageRunnerTemplate = `npx ${useCache ? "--cache" : "--yes"} tailor-sdk crash-report --machineuser=ci`; -const repeatedTemplate = `tailor-sdk crash-report --machineuser -tailor-sdk crash-report --machineuser`; -const repeatedSeparatedTemplate = `tailor-sdk crash-report; tailor-sdk crash-report --machineuser`; -const joinedRepeatedSeparatedTemplate = `tailor-sdk crash-report;tailor-sdk crash-report`; -const conditionalGlobalComparison = `tailor-sdk ${mode === "prod" ? "--verbose" : "--json"} crash-report list`; -const conditionalOptionComparison = `tailor-sdk login ${flag === "--machineuser" ? "--json" : "--machineuser"}`; -const conditionalCommandComparison = `tailor-sdk ${cmd === "crash-report" ? "login" : "crash-report"} list`; -const inlineEnvTemplate = `tailor-sdk --env-file=${envFile} ${showReports ? "crash-report" : "login"} list`; -const login = "tailor-sdk login --machineuser ci"; -const envFilePath = "tailor-sdk --env-file=/tmp/--machineuser crash-report"; -const envFileCommandSubstitutionPath = "tailor-sdk --env-file=$(pwd)/--machineuser crash-report --machineuser"; -const argPayload = "tailor-sdk function test-run --arg=--machineuser"; -const argCommandSubstitutionPayload = "tailor-sdk function test-run --arg=$(printf --machineuser) --machineuser"; -const quotedArgCommandPayload = "tailor-sdk function test-run --arg='tailor-sdk crash-report --machineuser' --machineuser"; -const substitutedArgCommandPayload = `tailor-sdk function test-run --arg ${cond ? "tailor-sdk crash-report --machineuser" : "{}"} --machineuser`; -const escapedQueryCommand = "tailor-sdk query --query \"select 1\" --machineuser"; -const profileCommand = "tailor-sdk --profile prod crash-report --machineuser"; -const profileMachineUserValue = "tailor-sdk --profile --machineuser crash-report --machineuser"; -const workspaceCommand = "tailor-sdk -w workspace-1 crash-report"; -const chainedShellCommand = "cd app && tailor-sdk crash-report --machineuser"; -const envShellCommand = "env FOO=bar tailor-sdk crash-report --machineuser"; -const assignmentShellCommand = "TAILOR=1 tailor-sdk crash-report --machineuser"; -const envDynamicCommandTemplate = `env FOO=bar tailor-sdk ${showReports ? "crash-report" : "login"} --machineuser`; -const assignmentDynamicCommandTemplate = `TAILOR=1 tailor-sdk ${showReports ? "crash-report" : "login"} --machineuser`; -const unknownInlineValue = "tailor-sdk login --name=--machineuser"; -const argTemplatePayload = `tailor-sdk function test-run --arg=${payload ? "--machineuser" : ""}`; -const dynamicArgTemplatePayload = `tailor-sdk function test-run ${includeArg ? "--arg" : ""} ${payload ? "--machineuser" : ""} --machineuser`; -const quotedQueryTemplate = `tailor-sdk query --query "select --machineuser ${where}" --machineuser`; -const inlineQueryTemplate = `tailor-sdk query --query=${payload ? "--machineuser" : ""} --machineuser`; -const postCommandConfigValue = ["tailor-sdk", "deploy", "--config", "/tmp/--machineuser"]; -const chainedCommandTemplate = `tailor-sdk login --machineuser && other-cli --machineuser`; -const joinedSeparatorTemplate = `tailor-sdk login --machineuser;other-cli --machineuser`; -const newlineCommandTemplate = `tailor-sdk login --machineuser -other-cli --machineuser`; -const other = "other-cli --machineuser ci"; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts deleted file mode 100644 index abca91676..000000000 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/expected.ts +++ /dev/null @@ -1,62 +0,0 @@ -const args = ["tailor-sdk", "crashreport", "list", "--machine-user", "ci"]; -const withRunner = ["npx", "tailor-sdk@latest", "crashreport", "--machine-user=ci"]; -const wrappedBinary = ["tailor-sdk" as const, "crashreport", "--machine-user"]; -const parenthesizedBinary = [("tailor-sdk"), "crashreport", "--machine-user"]; -const packageRunnerValueArg = ["npx", "--cache", "tailor-sdk", "tailor-sdk", "crashreport", "--machine-user"]; -const dynamicPackageRunnerValueArg = ["npx", useCache ? "--cache" : "--yes", "tailor-sdk", "crash-report", "--machineuser"]; -const packageRunner = ["npx", "--package", "tailor-sdk", "tailor-sdk", "crashreport"]; -const shortPackageRunner = ["npx", "-p", "tailor-sdk", "tailor-sdk", "crashreport"]; -const nested = spawn("tailor-sdk", ["crashreport", "list", "--machine-user", "ci"]); -const forkedModule = child_process.fork("tailor-sdk/register", ["crash-report", "--machineuser"]); -const dynamic = ["tailor-sdk", "login", `--machine-user=${machineUser}`]; -const conditional = ["tailor-sdk", "login", isCI ? "--machine-user" : "--json"]; -const optional = ["tailor-sdk", "login", includeMachineUser && "--machine-user"]; -const compactOptional = ["tailor-sdk", "login", includeMachineUser&&"--machine-user"]; -const dynamicGlobalOption = ["tailor-sdk", verbose && "--verbose", "crashreport", "list"]; -const dynamicInlineGlobalOption = ["tailor-sdk", useEnv ? "--env-file=.env" : "--json", "crashreport", "list"]; -const profileGlobalOption = ["tailor-sdk", "--profile", "prod", "crashreport", "--machine-user"]; -const workspaceGlobalOption = ["tailor-sdk", "-w", "workspace-1", "crashreport"]; -const inlineEnvTemplate = ["tailor-sdk", `--env-file=${envFile}`, "crashreport", "list"]; -const openEnvFileValue = ["tailor-sdk", "--env-file=", ".env", "crashreport", "--machine-user"]; -const argValue = ["tailor-sdk", "function", "test-run", "--arg", "crash-report"]; -const argCommandPayload = ["tailor-sdk", "function", "test-run", "--arg", "tailor-sdk crash-report --machineuser"]; -const argCommandPayloadWithComment = [ - "tailor-sdk", - "function", - "test-run", - "--arg", - /* payload */ "tailor-sdk crash-report --machineuser", -]; -const argCommandPayloadExpression = ["tailor-sdk", "function", "test-run", "--arg", cond ? "tailor-sdk crash-report --machineuser" : "{}"]; -const argCommandPayloadTemplate = ["tailor-sdk", "function", "test-run", "--arg", `tailor-sdk crash-report --machineuser`]; -const openArgValue = ["tailor-sdk", "function", "test-run", "--arg=", "--machineuser", "--machine-user"]; -const shortArgValue = ["tailor-sdk", "function", "test-run", "-a", "--machineuser", "--machine-user"]; -const queryValue = ["tailor-sdk", "query", "--query", "select --machineuser", "--machine-user", "ci"]; -const queryValueWithComment = [ - "tailor-sdk", - "query", - "--query", - /* sql */ "select --machineuser", - "--machine-user", -]; -const inlineQueryValue = ["tailor-sdk", "query", `--query=${payload ? "--machineuser" : ""}`, "--machine-user"]; -const fileValue = ["tailor-sdk", "query", "--file", "queries/--machineuser.graphql", "--machine-user"]; -const envFileValue = ["tailor-sdk", "--env-file", "crash-report", "crashreport"]; -const envFileBackticks = [`tailor-sdk`, `--env-file`, `crash-report`, `crashreport`]; -const envFileExpression = ["tailor-sdk", "--env-file", envFile, "crashreport", "list"]; -const nestedProfile = spawn("tailor-sdk", ["--config", "tailor.config.ts", "crashreport"]); -const argExpression = ["tailor-sdk", "function", "test-run", "--arg", payload, "--machine-user"]; -const dynamicArgExpression = ["tailor-sdk", "function", "test-run", includeArg ? "--arg" : "", "--machineuser"]; -const inlineArgValue = ["tailor-sdk", "function", "test-run", "--arg=--machineuser"]; -const inlineEnvFileValue = ["tailor-sdk", "--env-file=/tmp/--machineuser", "crashreport"]; -const dynamicCommand = ["tailor-sdk", command, "crash-report", "--machine-user"]; -const otherCli = ["other-cli", "crash-report", "--machineuser"]; -const diagnostic = formatDiagnostic("tailor-sdk", "crash-report", "--machineuser"); -const postCommandConditionalArg = ["tailor-sdk", "secret", "set", cond ? "crash-report" : "x", cond ? "--machine-user" : "--json"]; -const postCommandLogicalArg = ["tailor-sdk", "secret", "set", cond && "crash-report", cond && "--machine-user"]; -const dynamicLabel = `--machineuser=${machineUser}`; -const label = "crash-report"; -const regexPattern = /tailor-sdk crash-report --machineuser/; -const prose = "package tailor-sdk crash-report --machineuser"; -const proseTemplate = `package tailor-sdk crash-report --machineuser`; -const packageOptionData = ["tailor-sdk", "--machineuser"]; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts deleted file mode 100644 index 0b5b632ff..000000000 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-token-array/input.ts +++ /dev/null @@ -1,62 +0,0 @@ -const args = ["tailor-sdk", "crash-report", "list", "--machineuser", "ci"]; -const withRunner = ["npx", "tailor-sdk@latest", "crash-report", "--machineuser=ci"]; -const wrappedBinary = ["tailor-sdk" as const, "crash-report", "--machineuser"]; -const parenthesizedBinary = [("tailor-sdk"), "crash-report", "--machineuser"]; -const packageRunnerValueArg = ["npx", "--cache", "tailor-sdk", "tailor-sdk", "crash-report", "--machineuser"]; -const dynamicPackageRunnerValueArg = ["npx", useCache ? "--cache" : "--yes", "tailor-sdk", "crash-report", "--machineuser"]; -const packageRunner = ["npx", "--package", "tailor-sdk", "tailor-sdk", "crash-report"]; -const shortPackageRunner = ["npx", "-p", "tailor-sdk", "tailor-sdk", "crash-report"]; -const nested = spawn("tailor-sdk", ["crash-report", "list", "--machineuser", "ci"]); -const forkedModule = child_process.fork("tailor-sdk/register", ["crash-report", "--machineuser"]); -const dynamic = ["tailor-sdk", "login", `--machineuser=${machineUser}`]; -const conditional = ["tailor-sdk", "login", isCI ? "--machineuser" : "--json"]; -const optional = ["tailor-sdk", "login", includeMachineUser && "--machineuser"]; -const compactOptional = ["tailor-sdk", "login", includeMachineUser&&"--machineuser"]; -const dynamicGlobalOption = ["tailor-sdk", verbose && "--verbose", "crash-report", "list"]; -const dynamicInlineGlobalOption = ["tailor-sdk", useEnv ? "--env-file=.env" : "--json", "crash-report", "list"]; -const profileGlobalOption = ["tailor-sdk", "--profile", "prod", "crash-report", "--machineuser"]; -const workspaceGlobalOption = ["tailor-sdk", "-w", "workspace-1", "crash-report"]; -const inlineEnvTemplate = ["tailor-sdk", `--env-file=${envFile}`, "crash-report", "list"]; -const openEnvFileValue = ["tailor-sdk", "--env-file=", ".env", "crash-report", "--machineuser"]; -const argValue = ["tailor-sdk", "function", "test-run", "--arg", "crash-report"]; -const argCommandPayload = ["tailor-sdk", "function", "test-run", "--arg", "tailor-sdk crash-report --machineuser"]; -const argCommandPayloadWithComment = [ - "tailor-sdk", - "function", - "test-run", - "--arg", - /* payload */ "tailor-sdk crash-report --machineuser", -]; -const argCommandPayloadExpression = ["tailor-sdk", "function", "test-run", "--arg", cond ? "tailor-sdk crash-report --machineuser" : "{}"]; -const argCommandPayloadTemplate = ["tailor-sdk", "function", "test-run", "--arg", `tailor-sdk crash-report --machineuser`]; -const openArgValue = ["tailor-sdk", "function", "test-run", "--arg=", "--machineuser", "--machineuser"]; -const shortArgValue = ["tailor-sdk", "function", "test-run", "-a", "--machineuser", "--machineuser"]; -const queryValue = ["tailor-sdk", "query", "--query", "select --machineuser", "--machineuser", "ci"]; -const queryValueWithComment = [ - "tailor-sdk", - "query", - "--query", - /* sql */ "select --machineuser", - "--machineuser", -]; -const inlineQueryValue = ["tailor-sdk", "query", `--query=${payload ? "--machineuser" : ""}`, "--machineuser"]; -const fileValue = ["tailor-sdk", "query", "--file", "queries/--machineuser.graphql", "--machineuser"]; -const envFileValue = ["tailor-sdk", "--env-file", "crash-report", "crash-report"]; -const envFileBackticks = [`tailor-sdk`, `--env-file`, `crash-report`, `crash-report`]; -const envFileExpression = ["tailor-sdk", "--env-file", envFile, "crash-report", "list"]; -const nestedProfile = spawn("tailor-sdk", ["--config", "tailor.config.ts", "crash-report"]); -const argExpression = ["tailor-sdk", "function", "test-run", "--arg", payload, "--machineuser"]; -const dynamicArgExpression = ["tailor-sdk", "function", "test-run", includeArg ? "--arg" : "", "--machineuser"]; -const inlineArgValue = ["tailor-sdk", "function", "test-run", "--arg=--machineuser"]; -const inlineEnvFileValue = ["tailor-sdk", "--env-file=/tmp/--machineuser", "crash-report"]; -const dynamicCommand = ["tailor-sdk", command, "crash-report", "--machineuser"]; -const otherCli = ["other-cli", "crash-report", "--machineuser"]; -const diagnostic = formatDiagnostic("tailor-sdk", "crash-report", "--machineuser"); -const postCommandConditionalArg = ["tailor-sdk", "secret", "set", cond ? "crash-report" : "x", cond ? "--machineuser" : "--json"]; -const postCommandLogicalArg = ["tailor-sdk", "secret", "set", cond && "crash-report", cond && "--machineuser"]; -const dynamicLabel = `--machineuser=${machineUser}`; -const label = "crash-report"; -const regexPattern = /tailor-sdk crash-report --machineuser/; -const prose = "package tailor-sdk crash-report --machineuser"; -const proseTemplate = `package tailor-sdk crash-report --machineuser`; -const packageOptionData = ["tailor-sdk", "--machineuser"]; diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/expected.tsx b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/expected.tsx deleted file mode 100644 index 361fec5c6..000000000 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/expected.tsx +++ /dev/null @@ -1,6 +0,0 @@ -const docs = ( - <> -

package tailor-sdk crash-report --machineuser

- tailor-sdk crashreport list --machine-user ci - -); diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/input.tsx b/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/input.tsx deleted file mode 100644 index b789716d1..000000000 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/source-tsx/input.tsx +++ /dev/null @@ -1,6 +0,0 @@ -const docs = ( - <> -

package tailor-sdk crash-report --machineuser

- tailor-sdk crash-report list --machineuser ci - -); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 5d166c8d6..7a7d3ee0c 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -2,39 +2,13 @@ import { parse, Lang } from "@ast-grep/napi"; import * as path from "pathe"; import type { SgNode } from "@ast-grep/napi"; -const SHELL_ARG_VALUE = `(?:[^\\s'"\`;|&]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`; -const RUNNER_VALUE_FLAG = "(?:--cache|--userconfig|--registry|--prefix|--dir|--filter|--cwd|-C)"; -const RUNNER_VALUE_ARG = `${RUNNER_VALUE_FLAG}(?:=${SHELL_ARG_VALUE}|\\s+${SHELL_ARG_VALUE})`; -const RUNNER_BOOLEAN_ARG = `(?:(?!-p(?:\\s|$))-\\w+|--(?!package(?:=|\\s|$))\\w[\\w-]*(?:=${SHELL_ARG_VALUE})?)`; -const RUNNER_OPTION = `(?:${RUNNER_VALUE_ARG}|${RUNNER_BOOLEAN_ARG})`; -const DIRECT_PKG_RUNNER = `(?:npx|bunx)(?:\\s+${RUNNER_OPTION})*`; -const DLX_PKG_RUNNER = `(?:pnpm|yarn)(?:\\s+${RUNNER_OPTION})*\\s+dlx(?:\\s+${RUNNER_OPTION})*`; - -// Package-runner forms resolve npm package names, so `tailor-sdk@...` must -// become `@tailor-platform/sdk@...`; rewriting to `tailor@...` would download -// the unrelated CSS Sprites Generator instead. Runner options, including -// options with values, are captured as part of the runner group. -const PKG_RUNNER_RE = new RegExp( - `\\b((?:${DIRECT_PKG_RUNNER}|${DLX_PKG_RUNNER}))\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?`, - "g", -); -const PACKAGE_RUNNER_EXECUTABLE_PREFIX_RE = new RegExp( - `(?:^|[;&|\n\`])\\s*(?:env\\s+)?(?:[A-Za-z_]\\w*=${SHELL_ARG_VALUE}\\s+)*(?:${DIRECT_PKG_RUNNER}|${DLX_PKG_RUNNER})\\s+(?:["'])?$`, -); -const RUNNER_VALUE_REFERENCE_RE = new RegExp( - `(${RUNNER_VALUE_FLAG}(?:=|\\s+))(${SHELL_ARG_VALUE})`, - "g", -); -const RUNNER_PACKAGE_VALUE_REFERENCE_RE = new RegExp( - `\\b((?:${DIRECT_PKG_RUNNER}|${DLX_PKG_RUNNER})\\s+(?:--package|-p)(?:=|\\s+))(${SHELL_ARG_VALUE})`, - "g", -); -const TAILOR_CLI_VALUE_FLAG = - "(?:--env-file-if-exists|--env-file|--profile|--config|--workspace-id|--arg|--query|--file|-e|-p|-c|-w|-a|-q|-f)"; -const TAILOR_CLI_VALUE_REFERENCE_RE = new RegExp( - `(${TAILOR_CLI_VALUE_FLAG}(?:=|\\s+))(${SHELL_ARG_VALUE})`, - "g", -); +// Package-runner forms (`npx`, `pnpm dlx`, `yarn dlx`, `bunx`) resolve npm package +// names, so `tailor-sdk@...` must become `@tailor-platform/sdk@...` — rewriting +// to `tailor@...` would download the unrelated CSS Sprites Generator instead. +// Optional flags (e.g. `-y`, `--yes`) between the runner and the package name are +// captured as part of the runner group so the replacement preserves them. +const PKG_RUNNER_RE = + /\b((?:npx|pnpm\s+dlx|yarn\s+dlx|bunx)(?:\s+(?:-\w+|--\w[\w-]*))*)\s+tailor-sdk(?![\w-])(@[^\s'"`;|&)]+)?/g; // Match the `tailor-sdk` binary, optionally with a version pin (`@latest`, // `@2.0.0`, etc.). Lookbehind excludes `.tailor-sdk` (preceded by `.`) and @@ -42,11 +16,7 @@ const TAILOR_CLI_VALUE_REFERENCE_RE = new RegExp( // (e.g. `tailor-sdk-skills`) to avoid partial-match rewrites. const TAILOR_SDK_RE = /(? - version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, + const withRunners = value.replace(PKG_RUNNER_RE, (_, runner: string, version?: string) => + version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, ); - const updated = withRunners.replace(TAILOR_SDK_RE, (_match, version?: string) => + return withRunners.replace(TAILOR_SDK_RE, (_match, version?: string) => version ? `@tailor-platform/sdk${version}` : "tailor", ); - const cliValueRestored = restoreProtectedValues( - updated, - protectedCliValue.protectedValues, - "__TAILOR_SDK_CODEMOD_CLI_VALUE_", +} + +function renameSourceCommandText(value: string): string { + const withRunners = value.replace(SOURCE_PKG_RUNNER_RE, (_, runner: string, version?: string) => + version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, ); - return restoreProtectedValues( - cliValueRestored, - protectedRunnerValue.protectedValues, - "__TAILOR_SDK_CODEMOD_SHELL_VALUE_", + return withRunners.replace(SOURCE_TAILOR_SDK_RE, (_match, version?: string) => + version ? `@tailor-platform/sdk${version}` : "tailor", ); } @@ -193,1558 +81,64 @@ function sourceLang(filePath: string): Lang { return ext === ".tsx" || ext === ".jsx" || ext === ".js" ? Lang.Tsx : Lang.TypeScript; } -function isTokenSequenceNode(node: SgNode): boolean { - return node.kind() === "array" || isCliArgumentsNode(node); -} - -function isSyntaxOnlyNode(node: SgNode): boolean { - const kind = node.kind(); - return ( - kind === "[" || - kind === "]" || - kind === "(" || - kind === ")" || - kind === "," || - kind === "comment" - ); -} - -function isProtectedRunnerValuePlaceholder(node: SgNode): boolean { - return /^__TAILOR_SDK_CODEMOD_VALUE_PROTECTED_\d+__$/.test(node.text()); -} - -function nodeRangeKey(node: SgNode): string { - const range = node.range(); - return `${range.start.index}:${range.end.index}`; -} - -function isCliArgumentsNode(node: SgNode): boolean { - if (node.kind() !== "arguments") return false; - const parent = node.parent(); - if (parent?.kind() !== "call_expression") return false; - const argumentRange = nodeRangeKey(node); - const callee = parent.children().find((child: SgNode) => nodeRangeKey(child) !== argumentRange); - const calleeText = callee?.text(); - return calleeText === "$" || (calleeText != null && CLI_ARGUMENT_CALLEE_RE.test(calleeText)); -} - -function sourceStringToken(node: SgNode, source: string): SourceStringToken | undefined { - const kind = node.kind(); - if (kind !== "string" && kind !== "template_string") return undefined; - if ( - kind === "template_string" && - node.children().some((child: SgNode) => child.kind() === "template_substitution") - ) { - return undefined; - } - - const range = node.range(); - return { - value: source.slice(range.start.index + 1, range.end.index - 1), - start: range.start.index + 1, - end: range.end.index - 1, - }; -} - -function ensureTemplateToken(state: TemplateTokenState): TemplateToken { - state.token ??= { value: "", segments: [], substitutions: [], quoted: false }; - return state.token; -} - -function appendTemplateTokenChar(state: TemplateTokenState, ch: string, sourceIndex: number): void { - const token = ensureTemplateToken(state); - token.value += ch; - const previous = token.segments.at(-1); - if (previous && previous.end === sourceIndex) { - previous.value += ch; - previous.end += 1; - } else { - token.segments.push({ value: ch, start: sourceIndex, end: sourceIndex + 1 }); - } -} - -function pushTemplateToken(tokens: TemplateToken[], state: TemplateTokenState): void { - if (!state.token) return; - tokens.push(state.token); - state.token = null; -} - -function scanTemplateTextTokens( - state: TemplateTokenState, - text: string, - offset: number, - tokens: TemplateToken[], -): void { - for (let index = 0; index < text.length; index += 1) { - const ch = text[index]!; - - if (state.quote !== null) { - appendTemplateTokenChar(state, ch, offset + index); - if (ch === "\\" && state.quote === '"' && index + 1 < text.length) { - index += 1; - appendTemplateTokenChar(state, text[index]!, offset + index); - continue; - } - if (ch === state.quote) { - state.quote = null; - } - continue; - } - - if (/\s/.test(ch)) { - pushTemplateToken(tokens, state); - continue; - } - - appendTemplateTokenChar(state, ch, offset + index); - if (ch === "'" || ch === '"') { - state.quote = ch; - ensureTemplateToken(state).quoted = true; - } - } -} - -function appendTemplateSubstitution(state: TemplateTokenState, substitution: SgNode): void { - const token = ensureTemplateToken(state); - token.value += TEMPLATE_SUBSTITUTION_PLACEHOLDER; - token.substitutions.push(substitution); -} - -function collectTemplateTokens(node: SgNode, source: string): TemplateToken[] { - const tokens: TemplateToken[] = []; - const state: TemplateTokenState = { quote: null, token: null }; - - for (const child of node.children()) { - if (child.kind() === "string_fragment") { - const range = child.range(); - scanTemplateTextTokens( - state, - source.slice(range.start.index, range.end.index), - range.start.index, - tokens, - ); - continue; - } - if (child.kind() === "template_substitution") { - appendTemplateSubstitution(state, child); - } - } - - pushTemplateToken(tokens, state); - return tokens; -} - -function staticTemplateTokenValue(token: TemplateToken): string | undefined { - return token.substitutions.length === 0 ? token.value : undefined; -} - -function isRunnerFlag(value: string): boolean { - if (isRunnerPackageFlag(value)) return false; - return /^-\w+$|^--\w[\w-]*(?:=.*)?$/.test(value); -} - -function isRunnerValueFlag(value: string): boolean { - return /^(?:--cache|--userconfig|--registry|--prefix|--dir|--filter|--cwd|-C)(?:=|$)/.test(value); -} - -function isRunnerOpenValueFlag(value: string): boolean { - return isRunnerValueFlag(value) && (!value.includes("=") || value.endsWith("=")); -} - -function isRunnerSeparateValueFlag(value: string | undefined): boolean { - return ( - value === "--cache" || - value === "--userconfig" || - value === "--registry" || - value === "--prefix" || - value === "--dir" || - value === "--filter" || - value === "--cwd" || - value === "-C" - ); -} - -function isRunnerPackageFlag(value: string): boolean { - return /^(?:--package|-p)(?:=|$)/.test(value); -} - -function isRunnerOpenPackageFlag(value: string): boolean { - return isRunnerPackageFlag(value) && (!value.includes("=") || value.endsWith("=")); -} - -function isRunnerSeparatePackageFlag(value: string | undefined): boolean { - return value === "--package" || value === "-p"; -} - -function rewriteRunnerPackageOptionValue( - token: SourceStringToken, -): Array<[number, number, string]> { - const equalsIndex = token.value.indexOf("="); - if (equalsIndex === -1) return []; - const value = token.value.slice(equalsIndex + 1); - const replacement = rewriteRunnerPackageValue(value); - return replacement ? [[token.start + equalsIndex + 1, token.end, replacement]] : []; -} - -function restoreProtectedValues(source: string, protectedValues: string[], prefix: string): string { - let restored = source; - for (const [index, value] of protectedValues.entries()) { - restored = restored.replaceAll(`${prefix}${index}__`, value); - } - return restored; -} - -function protectShellRunnerValueReferences(source: string): TokenizedRunnerRewrite { - const protectedValues: string[] = []; - const updated = source.replace( - RUNNER_VALUE_REFERENCE_RE, - (match, prefix: string, value: string) => { - if (!value.includes("tailor-sdk")) return match; - const placeholder = `__TAILOR_SDK_CODEMOD_SHELL_VALUE_${protectedValues.length}__`; - protectedValues.push(value); - return `${prefix}${placeholder}`; - }, - ); - return { source: updated, protectedValues }; -} - -function protectTailorCliValueReferences(source: string): TokenizedRunnerRewrite { - const protectedValues: string[] = []; - const updated = source.replace( - TAILOR_CLI_VALUE_REFERENCE_RE, - (match, prefix: string, value: string, offset: number) => { - if (!value.includes("tailor-sdk")) return match; - if (isPackageRunnerExecutableReference(source, offset + prefix.length)) return match; - const placeholder = `__TAILOR_SDK_CODEMOD_CLI_VALUE_${protectedValues.length}__`; - protectedValues.push(value); - return `${prefix}${placeholder}`; - }, - ); - return { source: updated, protectedValues }; -} - -function isPackageRunnerExecutableReference(source: string, start: number): boolean { - return PACKAGE_RUNNER_EXECUTABLE_PREFIX_RE.test(source.slice(0, start)); -} - -function rewriteRunnerPackageValue(value: string): string | undefined { - const quote = value[0] === "'" || value[0] === '"' ? value[0] : ""; - const inner = quote ? value.slice(1, -1) : value; - const replacement = runnerPackageReplacement(inner); - if (!replacement) return undefined; - return quote ? `${quote}${replacement}${quote}` : replacement; -} - -function rewriteRunnerPackageValueReferences(source: string): string { - return source.replace( - RUNNER_PACKAGE_VALUE_REFERENCE_RE, - (match, prefix: string, value: string) => { - const replacement = rewriteRunnerPackageValue(value); - return replacement ? `${prefix}${replacement}` : match; - }, - ); -} - -function runnerStateAfterToken(value: string): RunnerState { - if (value === "npx" || value === "bunx") return "await-package"; - if (value === "pnpm" || value === "yarn") return "await-dlx"; - return "none"; -} - -function runnerPackageReplacement(value: string): string | undefined { - const match = TAILOR_SDK_TOKEN.exec(value); - if (!match) return undefined; - return `@tailor-platform/sdk${match[1] ?? ""}`; -} - -function isPathLikeRunnerFlagValue(value: string): boolean { - return value.startsWith(".") || value.startsWith("/") || value.startsWith("~"); -} - -function isTailorCliCommandToken(value: string): boolean { - const token = value.replace(/[),.:!?]+$/, ""); - return TAILOR_CLI_COMMANDS.has(token) || isTailorCliGlobalFlag(token); -} - -function optionName(value: string): string { - const equalsIndex = value.indexOf("="); - return equalsIndex === -1 ? value : value.slice(0, equalsIndex); -} - -function isTailorCliGlobalFlag(value: string): boolean { - return TAILOR_CLI_GLOBAL_FLAGS.has(optionName(value)); -} - -function tailorCliValueFlagName(value: string | undefined): string | undefined { - if (value == null) return undefined; - return Array.from(TAILOR_CLI_SEPARATE_VALUE_FLAGS).find( - (flag) => value === flag || value.startsWith(`${flag}=`), - ); -} - -function isOpenTailorCliValueFlag(value: string | undefined): boolean { - const flag = tailorCliValueFlagName(value); - return flag != null && (value === flag || value === `${flag}=`); -} - -function hasInlineTailorCliValue(value: string | undefined): boolean { - const flag = tailorCliValueFlagName(value); - return ( - flag != null && value != null && value.startsWith(`${flag}=`) && value.length > flag.length + 1 - ); -} - -function isLikelyDynamicTailorArgvRemainder(node: SgNode): boolean { - if (node.kind() === "spread_element") return true; - return /\b(?:argv|args?|cmd|command|subcommand)\b/i.test(node.text()); -} - -function collectPackageRunnerTokenRanges(value: string): TextRange[] { - const ranges: TextRange[] = []; - PKG_RUNNER_RE.lastIndex = 0; - for (;;) { - const match = PKG_RUNNER_RE.exec(value); - if (!match) break; - const tokenStartInMatch = match[0].lastIndexOf("tailor-sdk"); - if (tokenStartInMatch === -1) continue; - const start = match.index + tokenStartInMatch; - ranges.push({ start, end: start + "tailor-sdk".length + (match[2]?.length ?? 0) }); - } - PKG_RUNNER_RE.lastIndex = 0; - return ranges; -} - -function collectPackageRunnerOptionValueRanges(value: string): TextRange[] { - const ranges: TextRange[] = []; - RUNNER_PACKAGE_VALUE_REFERENCE_RE.lastIndex = 0; - for (;;) { - const match = RUNNER_PACKAGE_VALUE_REFERENCE_RE.exec(value); - if (!match) break; - const optionValue = match[2]!; - if (!rewriteRunnerPackageValue(optionValue)) continue; - const quoteOffset = optionValue[0] === "'" || optionValue[0] === '"' ? 1 : 0; - const start = match.index + match[1]!.length + quoteOffset; - ranges.push({ start, end: start + optionValue.length - quoteOffset * 2 }); - } - RUNNER_PACKAGE_VALUE_REFERENCE_RE.lastIndex = 0; - return ranges; -} - -function collectTailorCommandTokenRanges(value: string): TextRange[] { - const ranges: TextRange[] = []; - TAILOR_SDK_COMMAND_TOKEN_RE.lastIndex = 0; - for (;;) { - const match = TAILOR_SDK_COMMAND_TOKEN_RE.exec(value); - if (!match) break; - if (!isTailorCliCommandToken(match[1]!)) { - TAILOR_SDK_COMMAND_TOKEN_RE.lastIndex = match.index + 1; - continue; - } - const token = /^tailor-sdk(@[^\s'"`;|&)]+)?/.exec(match[0]); - if (!token) continue; - ranges.push({ start: match.index, end: match.index + token[0].length }); - } - TAILOR_SDK_COMMAND_TOKEN_RE.lastIndex = 0; - return ranges; -} - -function collectDynamicTemplateTailorCommandRanges(value: string): TextRange[] { - const ranges: TextRange[] = []; - const commandBoundary = String.raw`(?:^|[;&|\n])\s*`; - const shellPrefix = String.raw`(?:(?:env\s+)?(?:[A-Za-z_]\w*=${SHELL_ARG_VALUE}\s+)*)`; - const tailorToken = "tailor-sdk(?:@[^\\s'\"`;|&)]+)?"; - const patterns = [ - new RegExp(`${commandBoundary}${shellPrefix}${tailorToken}(?=\\s|$)`, "gu"), - new RegExp( - `${commandBoundary}${shellPrefix}(?:pnpm|yarn)(?:\\s+${RUNNER_OPTION})*\\s+exec(?:\\s+${RUNNER_OPTION})*\\s+${tailorToken}(?=\\s|$)`, - "gu", - ), - new RegExp( - `${commandBoundary}${shellPrefix}(?:pnpm|yarn)(?:\\s+${RUNNER_OPTION})*\\s+${tailorToken}(?=\\s|$)`, - "gu", - ), - new RegExp( - `${commandBoundary}${shellPrefix}(?:npx|bunx)(?:\\s+${RUNNER_OPTION})*\\s+(?:--package|-p)(?:=${SHELL_ARG_VALUE}|\\s+${SHELL_ARG_VALUE})(?:\\s+${RUNNER_OPTION})*\\s+${tailorToken}(?=\\s|$)`, - "gu", - ), - ]; - for (const pattern of patterns) { - for (;;) { - const match = pattern.exec(value); - if (!match) break; - const tokenStartInMatch = match[0].lastIndexOf("tailor-sdk"); - if (tokenStartInMatch === -1) continue; - const version = /^tailor-sdk(@[^\s'"`;|&)]+)?/.exec(match[0].slice(tokenStartInMatch))?.[1]; - const start = match.index + tokenStartInMatch; - ranges.push({ start, end: start + "tailor-sdk".length + (version?.length ?? 0) }); - } - } - return ranges; -} - -function collectRewriteableTailorSdkRanges(value: string): TextRange[] { - return [ - ...collectPackageRunnerTokenRanges(value), - ...collectPackageRunnerOptionValueRanges(value), - ...collectTailorCommandTokenRanges(value), - ]; -} - -function containsRange(ranges: TextRange[], start: number, end: number): boolean { - return ranges.some((range) => range.start <= start && end <= range.end); -} - -function collectNonCommandTailorSdkRanges( - value: string, - offset: number, - rewriteableRanges = collectRewriteableTailorSdkRanges(value), - rewriteableOffset = 0, -): Array<[number, number, string]> { - const protectedRanges: Array<[number, number, string]> = []; - TAILOR_SDK_RE.lastIndex = 0; - for (;;) { - const match = TAILOR_SDK_RE.exec(value); - if (!match) break; - const start = match.index; - const end = start + match[0].length; - if (!containsRange(rewriteableRanges, rewriteableOffset + start, rewriteableOffset + end)) { - protectedRanges.push([offset + start, offset + end, match[0]]); - } - } - TAILOR_SDK_RE.lastIndex = 0; - return protectedRanges; -} - -function runnerPackageExpressionChildren(node: SgNode): SgNode[] { - if (node.kind() === "parenthesized_expression") return node.children(); - if (node.kind() === "ternary_expression") return node.children().slice(1); - if (node.kind() === "binary_expression" && /&&|\|\|/.test(node.text())) { - return node.children().slice(1); - } - return []; -} - -function collectTokenizedRunnerEdits( - root: SgNode, +function pushSourceTextEdit( + edits: Array<[number, number, string]>, source: string, -): { edits: Array<[number, number, string]>; protectedRanges: Array<[number, number, string]> } { - const edits: Array<[number, number, string]> = []; - const protectedRanges: Array<[number, number, string]> = []; - - const protectToken = (token: SourceStringToken): void => { - if (token.value.includes("tailor-sdk")) { - protectedRanges.push([token.start, token.end, token.value]); - } - }; - - const protectTemplateToken = (token: TemplateToken): void => { - for (const segment of token.segments) { - protectToken(segment); - } - for (const substitution of token.substitutions) { - protectTailorSdkStrings(substitution); - } - }; - - const pushTemplateTokenReplacement = (token: TemplateToken, replacement: string): void => { - if (token.segments.length !== 1) return; - const [{ start, end }] = token.segments; + start: number, + end: number, +): void { + const text = source.slice(start, end); + const replacement = renameSourceCommandText(text); + if (replacement !== text) { edits.push([start, end, replacement]); - }; - - const collectTemplatePackageExpressionEdits = (token: TemplateToken): boolean => { - let changed = false; - for (const substitution of token.substitutions) { - changed = collectPackageExpressionEdits(substitution) || changed; - } - return changed; - }; - - const sourceStringExpressionToken = ( - node: SgNode, - visited = new Set(), - ): SourceStringToken | undefined => { - const direct = sourceStringToken(node, source); - if (direct) return direct; - if (!SOURCE_STRING_WRAPPER_NODE_KINDS.has(node.kind())) return undefined; - const key = nodeRangeKey(node); - if (visited.has(key)) return undefined; - visited.add(key); - for (const child of node.children()) { - const token = sourceStringExpressionToken(child, visited); - if (token) return token; - } - return undefined; - }; - - const protectTailorSdkStrings = (node: SgNode): void => { - const token = sourceStringExpressionToken(node); - if (token) { - protectToken(token); - return; - } - for (const child of node.children()) { - protectTailorSdkStrings(child); - } - }; - - const collectPackageExpressionEdits = (node: SgNode): boolean => { - const token = sourceStringExpressionToken(node); - if (token) { - const replacement = runnerPackageReplacement(token.value); - if (replacement) { - edits.push([token.start, token.end, replacement]); - return true; - } - return false; - } - let changed = false; - const packageChildren = runnerPackageExpressionChildren(node); - if (packageChildren.length > 0) { - const packageChildRanges = new Set(packageChildren.map((child) => nodeRangeKey(child))); - for (const child of node.children()) { - if (!packageChildRanges.has(nodeRangeKey(child))) { - protectTailorSdkStrings(child); - } - } - } - const children = packageChildren.length > 0 ? packageChildren : node.children(); - for (const child of children) { - changed = collectPackageExpressionEdits(child) || changed; - } - return changed; - }; - - const expressionStringValues = (node: SgNode): string[] => { - const token = sourceStringExpressionToken(node); - if (token) return [token.value]; - const values: string[] = []; - const packageChildren = runnerPackageExpressionChildren(node); - const children = packageChildren.length > 0 ? packageChildren : node.children(); - for (const child of children) { - values.push(...expressionStringValues(child)); - } - return values; - }; - - const dynamicRunnerOptionState = (node: SgNode): RunnerState | null => { - const values = expressionStringValues(node) - .map((value) => value.trim()) - .filter((value) => value !== ""); - if (values.length === 0) return null; - const runnerStates = values.map((value) => runnerStateAfterToken(value)); - if (runnerStates.every((state) => state === "await-package")) return "await-package"; - if (runnerStates.every((state) => state === "await-dlx")) return "await-dlx"; - if (values.every((value) => isRunnerOpenPackageFlag(value))) { - return "await-package-option-value"; - } - if (!values.every((value) => isRunnerFlag(value) || isRunnerValueFlag(value))) return null; - const hasOpenValueFlag = values.some((value) => isRunnerOpenValueFlag(value)); - if (hasOpenValueFlag) { - return "await-flag-value"; - } - return "await-package"; - }; - - const dynamicRunnerOptionStateFor = ( - runnerState: RunnerState, - node: SgNode, - ): RunnerState | null => { - const dynamicState = dynamicRunnerOptionState(node); - if (runnerState === "await-executable") { - if (dynamicState === "await-flag-value") return "await-executable-flag-value"; - if (dynamicState === "await-package") return "await-executable"; - return dynamicState; - } - if (runnerState !== "await-dlx") return dynamicState; - if (dynamicState === "await-flag-value") return "await-dlx-flag-value"; - if (dynamicState === "await-package") return "await-dlx"; - return dynamicState; - }; - - const advanceRunnerState = (runnerState: RunnerState, value: string): RunnerState => { - if (runnerState === "await-flag-value") return "await-package"; - if (runnerState === "await-dlx-flag-value") return "await-dlx"; - if (runnerState === "await-executable-flag-value") return "await-executable"; - if (runnerState === "await-package-option-value") return "await-executable"; - - if (runnerState === "await-executable") { - if (isRunnerSeparatePackageFlag(value)) return "await-package-option-value"; - if (isRunnerPackageFlag(value)) { - return isRunnerOpenPackageFlag(value) ? "await-package-option-value" : "await-executable"; - } - if (isRunnerSeparateValueFlag(value)) return "await-executable-flag-value"; - if (isRunnerValueFlag(value)) { - return value.includes("=") && !value.endsWith("=") - ? "await-executable" - : "await-executable-flag-value"; - } - if (isRunnerFlag(value)) return "await-executable"; - return "none"; - } - - if (runnerState === "await-dlx") { - if (value === "dlx") return "await-package"; - if (isRunnerSeparateValueFlag(value)) return "await-dlx-flag-value"; - if (isRunnerValueFlag(value)) { - return value.includes("=") && !value.endsWith("=") ? "await-dlx" : "await-dlx-flag-value"; - } - if (isRunnerFlag(value)) return "await-dlx"; - return runnerStateAfterToken(value); - } - - if (runnerState === "await-package") { - if (isRunnerSeparatePackageFlag(value)) return "await-package-option-value"; - if (isRunnerPackageFlag(value)) { - return isRunnerOpenPackageFlag(value) ? "await-package-option-value" : "await-executable"; - } - if (isRunnerSeparateValueFlag(value)) return "await-flag-value"; - if (isRunnerValueFlag(value)) { - return value.includes("=") && !value.endsWith("=") ? "await-package" : "await-flag-value"; - } - if (isRunnerFlag(value)) return "await-package"; - return "none"; - } - - return runnerStateAfterToken(value); - }; - - const processRunnerTemplateToken = ( - runnerState: RunnerState, - token: TemplateToken, - ): RunnerState => { - const nextState = runnerState; - const value = staticTemplateTokenValue(token); - - if (nextState === "await-flag-value") { - protectTemplateToken(token); - return "await-package"; - } - if (nextState === "await-dlx-flag-value") { - protectTemplateToken(token); - return "await-dlx"; - } - if (nextState === "await-package-option-value") { - if (value !== undefined) { - const replacement = rewriteRunnerPackageValue(value); - if (replacement) { - pushTemplateTokenReplacement(token, replacement); - } - } else { - collectTemplatePackageExpressionEdits(token); - } - return "await-executable"; - } - if (nextState === "await-package") { - if (value !== undefined) { - if (isRunnerPackageFlag(value)) { - for (const segment of token.segments) { - edits.push(...rewriteRunnerPackageOptionValue(segment)); - } - return isRunnerOpenPackageFlag(value) ? "await-package-option-value" : "await-executable"; - } - if (isRunnerFlag(value) || isRunnerValueFlag(value)) { - if (isRunnerValueFlag(value) && value.includes("=")) { - protectTemplateToken(token); - } - return advanceRunnerState(nextState, value); - } - const replacement = rewriteRunnerPackageValue(value); - if (replacement) { - pushTemplateTokenReplacement(token, replacement); - } - return "none"; - } - - if (isRunnerPackageFlag(token.value)) { - collectTemplatePackageExpressionEdits(token); - return isRunnerOpenPackageFlag(token.value) - ? "await-package-option-value" - : "await-executable"; - } - if (isRunnerValueFlag(token.value)) { - protectTemplateToken(token); - return token.value.includes("=") && !token.value.endsWith("=") - ? "await-package" - : "await-flag-value"; - } - if (isRunnerFlag(token.value)) { - return "await-package"; - } - const changed = collectTemplatePackageExpressionEdits(token); - return changed ? "none" : (dynamicRunnerOptionState(token.substitutions[0]!) ?? "none"); - } - - if (nextState === "await-executable") { - if (value !== undefined) { - if (isRunnerPackageFlag(value)) { - for (const segment of token.segments) { - edits.push(...rewriteRunnerPackageOptionValue(segment)); - } - return isRunnerOpenPackageFlag(value) ? "await-package-option-value" : "await-executable"; - } - if (isRunnerValueFlag(value)) { - if (value.includes("=")) { - protectTemplateToken(token); - } - return value.includes("=") && !value.endsWith("=") - ? "await-executable" - : "await-executable-flag-value"; - } - if (isRunnerFlag(value)) return "await-executable"; - if (TAILOR_SDK_TOKEN.test(value)) { - pushTemplateTokenReplacement(token, "tailor"); - } - return "none"; - } - - if (isRunnerPackageFlag(token.value)) { - collectTemplatePackageExpressionEdits(token); - return isRunnerOpenPackageFlag(token.value) - ? "await-package-option-value" - : "await-executable"; - } - if (isRunnerValueFlag(token.value)) { - protectTemplateToken(token); - return token.value.includes("=") && !token.value.endsWith("=") - ? "await-executable" - : "await-executable-flag-value"; - } - if (isRunnerFlag(token.value)) return "await-executable"; - return dynamicRunnerOptionStateFor("await-executable", token.substitutions[0]!) ?? "none"; - } - - if (nextState === "await-executable-flag-value") { - protectTemplateToken(token); - return "await-executable"; - } - - if (nextState === "await-dlx" && value === undefined) { - return dynamicRunnerOptionStateFor(nextState, token.substitutions[0]!) ?? "none"; - } - - if (value !== undefined) { - return advanceRunnerState(nextState, value); - } - return dynamicRunnerOptionStateFor(nextState, token.substitutions[0]!) ?? "none"; - }; - - const visitTemplate = (node: SgNode, inheritedRunnerState: RunnerState): RunnerState => { - let runnerState = inheritedRunnerState; - for (const token of collectTemplateTokens(node, source)) { - if (token.substitutions.length === 0) { - runnerState = processRunnerTemplateToken(runnerState, token); - continue; - } - - if ( - runnerState === "await-package" || - runnerState === "await-package-option-value" || - runnerState === "await-flag-value" || - runnerState === "await-executable" || - runnerState === "await-executable-flag-value" || - runnerState === "await-dlx" || - runnerState === "await-dlx-flag-value" - ) { - runnerState = processRunnerTemplateToken(runnerState, token); - continue; - } - - for (const substitution of token.substitutions) { - visit(substitution); - } - const dynamicState = dynamicRunnerOptionStateFor(runnerState, token.substitutions[0]!); - if (dynamicState) { - runnerState = dynamicState; - } else { - const value = staticTemplateTokenValue(token); - if (value !== undefined) { - runnerState = advanceRunnerState(runnerState, value); - } - } - } - return runnerState; - }; - - const visit = (node: SgNode, inheritedRunnerState: RunnerState = "none"): void => { - if (inheritedRunnerState === "await-package") { - const token = sourceStringExpressionToken(node); - if (token) { - const replacement = runnerPackageReplacement(token.value); - if (replacement) { - edits.push([token.start, token.end, replacement]); - } - return; - } - - const packageChildren = runnerPackageExpressionChildren(node); - if (packageChildren.length > 0) { - for (const child of packageChildren) { - visit(child, "await-package"); - } - return; - } - } - - if (node.kind() === "template_string") { - visitTemplate(node, inheritedRunnerState); - return; - } - - if (isTokenSequenceNode(node)) { - let runnerState = inheritedRunnerState; - let previousTokenValue: string | undefined; - for (const child of node.children()) { - const token = sourceStringExpressionToken(child); - if (token) { - const previous = previousTokenValue; - previousTokenValue = token.value; - - if (runnerState === "await-flag-value") { - protectToken(token); - runnerState = "await-package"; - continue; - } - - if (runnerState === "await-dlx-flag-value") { - protectToken(token); - runnerState = "await-dlx"; - continue; - } - - if (runnerState === "await-executable-flag-value") { - protectToken(token); - runnerState = "await-executable"; - continue; - } - - if (runnerState === "await-package-option-value") { - const replacement = rewriteRunnerPackageValue(token.value); - if (replacement) { - edits.push([token.start, token.end, replacement]); - } - runnerState = "await-executable"; - previousTokenValue = undefined; - continue; - } - - if (runnerState === "await-executable") { - if (isRunnerSeparatePackageFlag(token.value)) { - runnerState = "await-package-option-value"; - continue; - } - if (isRunnerPackageFlag(token.value)) { - edits.push(...rewriteRunnerPackageOptionValue(token)); - runnerState = isRunnerOpenPackageFlag(token.value) - ? "await-package-option-value" - : "await-executable"; - continue; - } - if (isRunnerSeparateValueFlag(previous)) { - protectToken(token); - continue; - } - if (isRunnerSeparateValueFlag(token.value)) { - runnerState = "await-executable-flag-value"; - continue; - } - if (isRunnerValueFlag(token.value)) { - if (token.value.includes("=")) { - protectToken(token); - } - runnerState = token.value.includes("=") - ? "await-executable" - : "await-executable-flag-value"; - continue; - } - if (isRunnerFlag(token.value)) continue; - if (TAILOR_SDK_TOKEN.test(token.value)) { - edits.push([token.start, token.end, "tailor"]); - } else if (isPathLikeRunnerFlagValue(token.value)) { - protectToken(token); - } - runnerState = "none"; - continue; - } - - if (runnerState === "await-dlx") { - if (token.value === "dlx") { - runnerState = "await-package"; - continue; - } - if (isRunnerSeparateValueFlag(token.value)) { - runnerState = "await-dlx-flag-value"; - continue; - } - if (isRunnerValueFlag(token.value)) { - if (token.value.includes("=")) { - protectToken(token); - } - continue; - } - if (isRunnerFlag(token.value)) continue; - runnerState = runnerStateAfterToken(token.value); - continue; - } - - if (runnerState === "await-package") { - if (isRunnerSeparatePackageFlag(token.value)) { - runnerState = "await-package-option-value"; - continue; - } - if (isRunnerPackageFlag(token.value)) { - edits.push(...rewriteRunnerPackageOptionValue(token)); - runnerState = isRunnerOpenPackageFlag(token.value) - ? "await-package-option-value" - : "await-executable"; - continue; - } - if (isRunnerSeparateValueFlag(previous)) { - protectToken(token); - continue; - } - if (isRunnerSeparateValueFlag(token.value)) { - runnerState = "await-flag-value"; - continue; - } - if (isRunnerValueFlag(token.value)) { - if (token.value.includes("=")) { - protectToken(token); - } - runnerState = token.value.includes("=") ? "await-package" : "await-flag-value"; - continue; - } - if (isRunnerFlag(token.value)) continue; - const replacement = runnerPackageReplacement(token.value); - if (replacement) { - edits.push([token.start, token.end, replacement]); - } else if (isPathLikeRunnerFlagValue(token.value)) { - protectToken(token); - runnerState = "none"; - continue; - } - runnerState = "none"; - continue; - } - - runnerState = runnerStateAfterToken(token.value); - continue; - } - - if (isSyntaxOnlyNode(child)) { - visit(child); - continue; - } - - if ( - (runnerState === "await-package" || runnerState === "await-dlx") && - runnerPackageExpressionChildren(child).length > 0 - ) { - const changed = collectPackageExpressionEdits(child); - runnerState = changed - ? "none" - : (dynamicRunnerOptionStateFor(runnerState, child) ?? "none"); - continue; - } - - if ( - (runnerState === "await-package" || runnerState === "await-dlx") && - child.kind() === "spread_element" - ) { - runnerState = dynamicRunnerOptionStateFor(runnerState, child) ?? runnerState; - previousTokenValue = undefined; - continue; - } - - if (runnerState === "await-flag-value") { - protectTailorSdkStrings(child); - runnerState = "await-package"; - previousTokenValue = undefined; - continue; - } - - if (runnerState === "await-package-option-value") { - collectPackageExpressionEdits(child); - runnerState = "await-executable"; - previousTokenValue = undefined; - continue; - } - - if (runnerState === "await-executable-flag-value") { - protectTailorSdkStrings(child); - runnerState = "await-executable"; - previousTokenValue = undefined; - continue; - } - - if (child.kind() === "template_string") { - runnerState = visitTemplate(child, runnerState); - previousTokenValue = undefined; - continue; - } - - if (runnerState === "await-dlx-flag-value") { - protectTailorSdkStrings(child); - runnerState = "await-dlx"; - previousTokenValue = undefined; - continue; - } - - if (runnerState === "await-package" && isProtectedRunnerValuePlaceholder(child)) { - continue; - } - - if (runnerState === "await-executable") { - visit( - child, - isTokenSequenceNode(child) || child.kind() === "template_string" ? runnerState : "none", - ); - runnerState = "none"; - previousTokenValue = undefined; - continue; - } - - if (runnerState === "await-package") { - visit(child, isTokenSequenceNode(child) ? runnerState : "none"); - runnerState = "none"; - previousTokenValue = undefined; - continue; - } - - visit( - child, - isTokenSequenceNode(child) || child.kind() === "template_string" ? runnerState : "none", - ); - } - return; - } - - for (const child of node.children()) { - visit(child); - } - }; - - visit(root); - return { edits, protectedRanges }; -} - -function rewriteTokenizedPackageRunners(source: string, filePath: string): TokenizedRunnerRewrite { - let root: SgNode; - try { - root = parse(sourceLang(filePath), source).root(); - } catch { - return { source, protectedValues: [] }; } - - let updated = source; - const { edits, protectedRanges } = collectTokenizedRunnerEdits(root, source); - const protectedValues: string[] = []; - const protectionEdits = protectedRanges.map(([start, end, value], index) => { - protectedValues.push(value); - return [start, end, `__TAILOR_SDK_CODEMOD_PROTECTED_${index}__`] as [number, number, string]; - }); - const editByRange = new Map(); - for (const edit of [...edits, ...protectionEdits]) { - editByRange.set(`${edit[0]}:${edit[1]}`, edit); - } - const allEdits = Array.from(editByRange.values()).toSorted(([a], [b]) => b - a); - for (const [start, end, replacement] of allEdits) { - updated = `${updated.slice(0, start)}${replacement}${updated.slice(end)}`; - } - return { source: updated, protectedValues }; } -function protectRunnerValueStrings(source: string): TokenizedRunnerRewrite { - const protectedValues: string[] = []; - const protectValueToken = (match: string, valueQuote: string, value: string): string => { - const placeholder = `__TAILOR_SDK_CODEMOD_VALUE_PROTECTED_${protectedValues.length}__`; - protectedValues.push(value); - return match.replace( - `${valueQuote}${value}${valueQuote}`, - `${valueQuote}${placeholder}${valueQuote}`, - ); - }; - const inlineProtected = source.replace( - /(["'`])((?:--cache|--userconfig|--registry|--prefix|--dir|--filter|--cwd|-C)=)([^"'`]*tailor-sdk[^"'`]*)\1/g, - (match, quote: string, prefix: string, value: string) => { - const placeholder = `__TAILOR_SDK_CODEMOD_VALUE_PROTECTED_${protectedValues.length}__`; - protectedValues.push(value); - return `${quote}${prefix}${placeholder}${quote}`; - }, - ); - const updated = inlineProtected.replace( - /(["'])(?:--cache|--userconfig|--registry|--prefix|--dir|--filter|--cwd|-C)\1\s*,\s*(["'`])([^"'`]*tailor-sdk[^"'`]*)\2/g, - (match, _flagQuote: string, valueQuote: string, value: string) => { - return protectValueToken(match, valueQuote, value); - }, - ); - return { source: updated, protectedValues }; -} - -function protectStandaloneTailorSdkSourceStrings( - source: string, - filePath: string, -): TokenizedRunnerRewrite { +function transformSourceFile(source: string, filePath: string): string | null { let root: SgNode; try { root = parse(sourceLang(filePath), source).root(); } catch { - return { source, protectedValues: [] }; + return null; } - const ranges: Array<[number, number, string]> = []; - const isSourceStringWrapperNode = (node: SgNode): boolean => { - return SOURCE_STRING_WRAPPER_NODE_KINDS.has(node.kind()); - }; - const sourceStringExpressionToken = ( - node: SgNode, - visited = new Set(), - ): SourceStringToken | undefined => { - const direct = sourceStringToken(node, source); - if (direct) return direct; - if (!isSourceStringWrapperNode(node)) return undefined; - const key = nodeRangeKey(node); - if (visited.has(key)) return undefined; - visited.add(key); - for (const child of node.children()) { - const token = sourceStringExpressionToken(child, visited); - if (token) return token; - } - return undefined; - }; - const arrayElementForSourceString = (node: SgNode): SgNode | undefined => { - let element = node; - let parent = element.parent(); - while (parent && isSourceStringWrapperNode(parent)) { - element = parent; - parent = element.parent(); - } - return parent?.kind() === "array" ? element : undefined; - }; - const isSingleElementArrayToken = (node: SgNode): boolean => { - const element = arrayElementForSourceString(node) ?? node; - const parent = element.parent(); - if (parent?.kind() !== "array") return false; - return parent.children().filter((child: SgNode) => !isSyntaxOnlyNode(child)).length === 1; - }; - const arrayElementNodes = (node: SgNode): SgNode[] => { - return node.children().filter((child: SgNode) => !isSyntaxOnlyNode(child)); - }; - const arrayElementTokens = (node: SgNode): SourceStringToken[] => { - return arrayElementNodes(node) - .map((child: SgNode) => sourceStringExpressionToken(child)) - .filter((token): token is SourceStringToken => token != null); - }; - const hasTailorCommandTokenPair = (tokens: SourceStringToken[]): boolean => { - return tokens.some((token, index) => { - const next = tokens[index + 1]; - return ( - TAILOR_CLI_CONTEXT_TOKEN.test(token.value) && - next != null && - isTailorCliCommandToken(next.value) - ); - }); - }; - const hasTailorPackageRunnerToken = (tokens: SourceStringToken[]): boolean => { - let runnerState: RunnerState = "none"; - for (const token of tokens) { - if (runnerState === "await-flag-value") { - runnerState = "await-package"; - continue; - } - if (runnerState === "await-dlx-flag-value") { - runnerState = "await-dlx"; - continue; - } - if (runnerState === "await-package-option-value") { - if (runnerPackageReplacement(token.value)) return true; - runnerState = "await-executable"; - continue; - } - if (runnerState === "await-executable") { - if (isRunnerSeparatePackageFlag(token.value)) { - runnerState = "await-package-option-value"; - continue; - } - if (isRunnerPackageFlag(token.value)) { - if (rewriteRunnerPackageOptionValue(token).length > 0) return true; - runnerState = isRunnerOpenPackageFlag(token.value) - ? "await-package-option-value" - : "await-executable"; - continue; - } - if (isRunnerSeparateValueFlag(token.value)) { - runnerState = "await-executable-flag-value"; - continue; - } - if (isRunnerValueFlag(token.value) || isRunnerFlag(token.value)) continue; - if (TAILOR_SDK_TOKEN.test(token.value)) return true; - runnerState = "none"; - continue; - } - if (runnerState === "await-executable-flag-value") { - runnerState = "await-executable"; - continue; - } - if (runnerState === "await-dlx") { - if (token.value === "dlx") { - runnerState = "await-package"; - continue; - } - if (isRunnerSeparateValueFlag(token.value)) { - runnerState = "await-dlx-flag-value"; - continue; - } - if (isRunnerValueFlag(token.value) || isRunnerFlag(token.value)) continue; - runnerState = runnerStateAfterToken(token.value); - continue; - } - if (runnerState === "await-package") { - if (isRunnerSeparatePackageFlag(token.value)) { - runnerState = "await-package-option-value"; - continue; - } - if (isRunnerPackageFlag(token.value)) { - if (rewriteRunnerPackageOptionValue(token).length > 0) return true; - runnerState = isRunnerOpenPackageFlag(token.value) - ? "await-package-option-value" - : "await-executable"; - continue; - } - if (isRunnerSeparateValueFlag(token.value)) { - runnerState = "await-flag-value"; - continue; - } - if (isRunnerValueFlag(token.value) || isRunnerFlag(token.value)) continue; - if (runnerPackageReplacement(token.value)) return true; - runnerState = "none"; - continue; - } - runnerState = runnerStateAfterToken(token.value); - } - return false; - }; - const isTailorExecRunnerToken = (tokens: SourceStringToken[], tokenIndex: number): boolean => { - const execIndex = tokens.findIndex((token) => token.value === "exec"); - if (execIndex === -1 || tokenIndex <= execIndex) return false; - let expectFlagValue = false; - for (let index = execIndex + 1; index < tokens.length; index += 1) { - const token = tokens[index]!; - if (expectFlagValue) { - expectFlagValue = false; - continue; - } - if (isRunnerSeparateValueFlag(token.value)) { - expectFlagValue = true; - continue; - } - if (isRunnerValueFlag(token.value) || isRunnerFlag(token.value)) continue; - return index === tokenIndex && TAILOR_SDK_TOKEN.test(token.value); - } - return false; - }; - const hasTailorExecRunnerToken = (tokens: SourceStringToken[]): boolean => { - return tokens.some((_, index) => isTailorExecRunnerToken(tokens, index)); - }; - const hasDynamicTailorArgvRemainder = (node: SgNode, tokens: SourceStringToken[]): boolean => { - const [first] = tokens; - if (!first || !TAILOR_CLI_CONTEXT_TOKEN.test(first.value)) return false; - if (node.parent()?.kind() === "array") return false; - const firstElementIndex = arrayElementNodes(node).findIndex((child) => { - const token = sourceStringExpressionToken(child); - return token?.start === first.start && token.end === first.end; - }); - if (firstElementIndex === -1) return false; - return arrayElementNodes(node) - .slice(firstElementIndex + 1) - .some( - (child) => - sourceStringToken(child, source) == null && isLikelyDynamicTailorArgvRemainder(child), - ); - }; - const isRewriteableTailorArgvToken = (node: SgNode, token: SourceStringToken): boolean => { - const element = arrayElementForSourceString(node); - const parent = element?.parent(); - if (!element || parent?.kind() !== "array") return false; - const tokens = arrayElementTokens(parent); - const tokenIndex = tokens.findIndex( - (candidate) => candidate.start === token.start && candidate.end === token.end, - ); - if (tokenIndex === -1 || !TAILOR_CLI_CONTEXT_TOKEN.test(token.value)) return false; - if (isOpenTailorCliValueFlag(tokens[tokenIndex - 1]?.value)) return false; - const next = tokens[tokenIndex + 1]; - if (next != null && isTailorCliCommandToken(next.value)) return true; - if (tokenIndex === 0 && hasDynamicTailorArgvRemainder(parent, tokens)) return true; - return isTailorExecRunnerToken(tokens, tokenIndex); - }; - const isLikelyTailorArgvArray = (node: SgNode): boolean => { - if (node.kind() !== "array") return false; - const tokens = arrayElementTokens(node); - const [first, second] = tokens; - if (!first) return false; - if (hasTailorPackageRunnerToken(tokens) || hasTailorCommandTokenPair(tokens)) return true; - if (hasDynamicTailorArgvRemainder(node, tokens)) return true; - if (first.value === "pnpm" || first.value === "yarn") { - return hasTailorExecRunnerToken(tokens); - } - return ( - TAILOR_CLI_CONTEXT_TOKEN.test(first.value) && - second != null && - isTailorCliCommandToken(second.value) - ); - }; - const isProtectedArrayDataToken = (node: SgNode): boolean => { - const element = arrayElementForSourceString(node) ?? node; - const parent = element.parent(); - return parent?.kind() === "array" && !isLikelyTailorArgvArray(parent); - }; - const templateFragmentsWithOffsets = ( - node: SgNode, - ): Array<{ fragment: SourceStringToken; offset: number }> => { - const fragments: Array<{ fragment: SourceStringToken; offset: number }> = []; - let offset = 0; - for (const child of node.children()) { - if (child.kind() === "string_fragment") { - const range = child.range(); - const fragment = { - value: source.slice(range.start.index, range.end.index), - start: range.start.index, - end: range.end.index, - }; - fragments.push({ fragment, offset }); - offset += fragment.value.length; - } else if (child.kind() === "template_substitution") { - offset += TEMPLATE_SUBSTITUTION_PLACEHOLDER.length; - } - } - return fragments; - }; - const templateStaticText = (node: SgNode): string => { - let text = ""; - for (const child of node.children()) { - if (child.kind() === "string_fragment") { - const range = child.range(); - text += source.slice(range.start.index, range.end.index); - } else if (child.kind() === "template_substitution") { - text += TEMPLATE_SUBSTITUTION_PLACEHOLDER; - } - } - return text; - }; - const expressionStringValues = (node: SgNode): string[] => { - const token = sourceStringExpressionToken(node); - if (token) return [token.value]; - const values: string[] = []; - const expressionChildren = runnerPackageExpressionChildren(node); - const children = expressionChildren.length > 0 ? expressionChildren : node.children(); - for (const child of children) { - values.push(...expressionStringValues(child)); - } - return values; - }; - const isDynamicOpenTailorCliValueFlag = (node: SgNode): boolean => { - const values = expressionStringValues(node) - .map((value) => value.trim()) - .filter((value) => value !== ""); - return values.length > 0 && values.every((value) => isOpenTailorCliValueFlag(value)); - }; - const isDynamicOpenTailorCliValueFlagToken = (token: TemplateToken): boolean => { - return token.substitutions.some((substitution) => - isDynamicOpenTailorCliValueFlag(substitution), - ); - }; - const protectTemplateSubstitutions = (token: TemplateToken): void => { - for (const substitution of token.substitutions) { - protectNodeText(substitution); - } - }; - const protectTailorCliTemplateValues = (node: SgNode): void => { - let afterTailorBinary = false; - let skipNextTailorValue = false; - - for (const token of collectTemplateTokens(node, source)) { - const value = staticTemplateTokenValue(token); - if (!afterTailorBinary) { - if (value !== undefined && TAILOR_CLI_CONTEXT_TOKEN.test(value)) { - afterTailorBinary = true; - } - continue; - } - - if (skipNextTailorValue) { - protectTemplateSubstitutions(token); - skipNextTailorValue = false; - continue; - } - - if (value !== undefined) { - if (isOpenTailorCliValueFlag(value)) { - skipNextTailorValue = true; - } - continue; - } - - if (hasInlineTailorCliValue(token.value)) { - protectTemplateSubstitutions(token); - continue; - } - if (isOpenTailorCliValueFlag(token.value)) { - skipNextTailorValue = true; - continue; - } - if (isDynamicOpenTailorCliValueFlagToken(token)) { - skipNextTailorValue = true; - } - } - }; - const protectNodeText = (node: SgNode): void => { - const range = node.range(); - ranges.push([ - range.start.index, - range.end.index, - source.slice(range.start.index, range.end.index), - ]); - }; - const visitTokenSequence = (node: SgNode): void => { - let afterTailorBinary = false; - let skipNextTailorValue = false; - - for (const child of node.children()) { - if (isSyntaxOnlyNode(child)) { - visit(child); - continue; - } - - if (skipNextTailorValue) { - if (child.text().includes("tailor-sdk")) { - protectNodeText(child); - } - skipNextTailorValue = false; - continue; - } - - const token = sourceStringExpressionToken(child); - if (token && !afterTailorBinary) { - if (isRewriteableTailorArgvToken(child, token)) { - afterTailorBinary = true; - } - visit(child); - continue; - } - - if (token && afterTailorBinary) { - if (hasInlineTailorCliValue(token.value)) { - if (token.value.includes("tailor-sdk")) { - ranges.push([token.start, token.end, token.value]); - } - visit(child); - continue; - } - if (isOpenTailorCliValueFlag(token.value)) { - skipNextTailorValue = true; - visit(child); - continue; - } - } - - if (afterTailorBinary && isDynamicOpenTailorCliValueFlag(child)) { - skipNextTailorValue = true; - visit(child); - continue; - } - - visit(child); - } - }; + const edits: Array<[number, number, string]> = []; const visit = (node: SgNode): void => { const kind = node.kind(); - if (isTokenSequenceNode(node)) { - visitTokenSequence(node); - return; - } - if (kind === "regex" && node.text().includes("tailor-sdk")) { - protectNodeText(node); - return; - } - if (kind === "comment" && node.text().includes("tailor-sdk")) { - const range = node.range(); - ranges.push(...collectNonCommandTailorSdkRanges(node.text(), range.start.index)); + const range = node.range(); + + if (kind === "comment" || kind === "jsx_text" || kind === "string_fragment") { + pushSourceTextEdit(edits, source, range.start.index, range.end.index); return; } - if (kind === "jsx_text" && node.text().includes("tailor-sdk")) { - const range = node.range(); - ranges.push( - ...collectNonCommandTailorSdkRanges( - source.slice(range.start.index, range.end.index), - range.start.index, - ), - ); + + if (kind === "string") { + pushSourceTextEdit(edits, source, range.start.index + 1, range.end.index - 1); return; } - if ( - kind === "template_string" && - node.children().some((child: SgNode) => child.kind() === "template_substitution") - ) { - const fragments = templateFragmentsWithOffsets(node); - const staticText = templateStaticText(node); - if (fragments.some(({ fragment }) => fragment.value.includes("tailor-sdk"))) { - const rewriteableRanges = [ - ...collectRewriteableTailorSdkRanges(staticText), - ...collectDynamicTemplateTailorCommandRanges(staticText), - ]; - for (const { fragment, offset } of fragments) { - ranges.push( - ...collectNonCommandTailorSdkRanges( - fragment.value, - fragment.start, - rewriteableRanges, - offset, - ), - ); - } - } - protectTailorCliTemplateValues(node); - } - const token = sourceStringToken(node, source); - if (token) { - const parent = node.parent(); - const arrayElement = arrayElementForSourceString(node); - const arrayParent = arrayElement?.parent(); - if ( - token.value.includes("tailor-sdk") && - (parent == null || - (!isTokenSequenceNode(parent) && arrayParent?.kind() !== "array") || - isSingleElementArrayToken(node) || - isProtectedArrayDataToken(node) || - (arrayParent?.kind() === "array" && !isRewriteableTailorArgvToken(node, token))) - ) { - ranges.push(...collectNonCommandTailorSdkRanges(token.value, token.start)); + + if (kind === "template_string") { + const hasSubstitution = node + .children() + .some((child: SgNode) => child.kind() === "template_substitution"); + if (!hasSubstitution) { + pushSourceTextEdit(edits, source, range.start.index + 1, range.end.index - 1); + return; } - return; } + for (const child of node.children()) { visit(child); } }; visit(root); + if (edits.length === 0) return null; let updated = source; - const protectedValues: string[] = []; - const nonOverlappingRanges: Array<[number, number, string]> = []; - let coveredEnd = -1; - for (const range of ranges.toSorted(([aStart, aEnd], [bStart, bEnd]) => { - return aStart - bStart || bEnd - aEnd; - })) { - if (range[0] < coveredEnd) continue; - nonOverlappingRanges.push(range); - coveredEnd = range[1]; - } - for (const [start, end, value] of nonOverlappingRanges.toSorted(([a], [b]) => b - a)) { - const placeholder = `__TAILOR_SDK_CODEMOD_STANDALONE_${protectedValues.length}__`; - protectedValues.push(value); - updated = `${updated.slice(0, start)}${placeholder}${updated.slice(end)}`; + for (const [start, end, replacement] of edits.toSorted(([a], [b]) => b - a)) { + updated = `${updated.slice(0, start)}${replacement}${updated.slice(end)}`; } - return { source: updated, protectedValues }; + return updated === source ? null : updated; } function transformPackageJson(source: string): string | null { @@ -1792,31 +186,8 @@ export default function transform(source: string, filePath: string): string | nu const ext = path.extname(filePath).toLowerCase(); if (ext === ".json") return transformPackageJson(source); + if (SOURCE_EXTENSIONS.has(ext)) return transformSourceFile(source, filePath); - const valueProtected = SOURCE_EXTENSIONS.has(ext) - ? protectRunnerValueStrings(source) - : { source, protectedValues: [] }; - const tokenizedRunnerRewrite = SOURCE_EXTENSIONS.has(ext) - ? rewriteTokenizedPackageRunners(valueProtected.source, filePath) - : { source: valueProtected.source, protectedValues: [] }; - const standaloneProtected = SOURCE_EXTENSIONS.has(ext) - ? protectStandaloneTailorSdkSourceStrings(tokenizedRunnerRewrite.source, filePath) - : { source: tokenizedRunnerRewrite.source, protectedValues: [] }; - let updated = renameBinary(standaloneProtected.source); - updated = restoreProtectedValues( - updated, - standaloneProtected.protectedValues, - "__TAILOR_SDK_CODEMOD_STANDALONE_", - ); - updated = restoreProtectedValues( - updated, - tokenizedRunnerRewrite.protectedValues, - "__TAILOR_SDK_CODEMOD_PROTECTED_", - ); - updated = restoreProtectedValues( - updated, - valueProtected.protectedValues, - "__TAILOR_SDK_CODEMOD_VALUE_PROTECTED_", - ); + const updated = renameBinary(source); return updated === source ? null : updated; } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh index 1db6d922a..1752dc093 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/expected.sh @@ -3,9 +3,6 @@ set -euo pipefail pnpm exec tailor deploy tailor login -tailor -p tailor-sdk deploy @tailor-platform/sdk@latest deploy npx @tailor-platform/sdk@2.0.0 workspace list npx -y @tailor-platform/sdk login -npx -f @tailor-platform/sdk login -npx -q @tailor-platform/sdk query diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh index 1f515c34f..e7743b6b7 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/basic-shell/input.sh @@ -3,9 +3,6 @@ set -euo pipefail pnpm exec tailor-sdk deploy tailor-sdk login -tailor-sdk -p tailor-sdk deploy tailor-sdk@latest deploy npx tailor-sdk@2.0.0 workspace list npx -y tailor-sdk login -npx -f tailor-sdk login -npx -q tailor-sdk query diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/expected.d.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/expected.d.ts index 9bc34fa5f..6d5f65e20 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/expected.d.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/expected.d.ts @@ -1,7 +1,2 @@ -/** - * Run `tailor deploy` after editing the app config. - * Regenerate types with `tailor generate`. - * Install a pinned CLI with `npx --package @tailor-platform/sdk tailor login`. - * Keep the generated output directory `.tailor-sdk/` in place until v2 setup runs. - */ +// Regenerated automatically when running 'tailor deploy' or 'tailor generate' export {}; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/input.d.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/input.d.ts index 1835c8a56..e31ff90b5 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/input.d.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/declaration-comment/input.d.ts @@ -1,7 +1,2 @@ -/** - * Run `tailor-sdk deploy` after editing the app config. - * Regenerate types with `tailor-sdk generate`. - * Install a pinned CLI with `npx --package tailor-sdk tailor-sdk login`. - * Keep the generated output directory `.tailor-sdk/` in place until v2 setup runs. - */ +// Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate' export {}; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 34979c5e7..baecfb514 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -1,25 +1,8 @@ -const packageName = "tailor-sdk"; -const packageList = ["tailor-sdk"]; -const resolvedPackage = require.resolve("tailor-sdk/package.json"); -const resolvedTemplatePackage = require.resolve(`tailor-sdk/${subpath}`); -const packageTemplate = `tailor-sdk/${version}`; -const packageRegex = /tailor-sdk/; -// package tailor-sdk -// package tailor-sdk is installed -const packageMessage = "package tailor-sdk is installed"; -const escapedPackageMessage = "package \"tailor-sdk\" is installed"; -// Install tailor-sdk before running tailor deploy -const mixedPackageAndCommand = "Install tailor-sdk before running tailor deploy"; -const dynamicImport = import("tailor-sdk"); -const installedPackage = installPackage("tailor-sdk"); -const forkedModule = child_process.fork("tailor-sdk/register", ["crash-report"]); -const migrate = "bunx @tailor-platform/sdk@2.0.0-next.2 generate"; -const escapedQuery = "tailor query --query \"select 1\""; -const script = ["tailor", "deploy"].join(" "); -const skills = "tailor-sdk-skills"; +const script = "tailor deploy"; const docs = ( <>

package tailor-sdk is installed

tailor deploy + npx @tailor-platform/sdk@latest login ); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 4786fa118..4cf181816 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -1,25 +1,8 @@ -const packageName = "tailor-sdk"; -const packageList = ["tailor-sdk"]; -const resolvedPackage = require.resolve("tailor-sdk/package.json"); -const resolvedTemplatePackage = require.resolve(`tailor-sdk/${subpath}`); -const packageTemplate = `tailor-sdk/${version}`; -const packageRegex = /tailor-sdk/; -// package tailor-sdk -// package tailor-sdk is installed -const packageMessage = "package tailor-sdk is installed"; -const escapedPackageMessage = "package \"tailor-sdk\" is installed"; -// Install tailor-sdk before running tailor-sdk deploy -const mixedPackageAndCommand = "Install tailor-sdk before running tailor-sdk deploy"; -const dynamicImport = import("tailor-sdk"); -const installedPackage = installPackage("tailor-sdk"); -const forkedModule = child_process.fork("tailor-sdk/register", ["crash-report"]); -const migrate = "bunx tailor-sdk@2.0.0-next.2 generate"; -const escapedQuery = "tailor-sdk query --query \"select 1\""; -const script = ["tailor-sdk", "deploy"].join(" "); -const skills = "tailor-sdk-skills"; +const script = "tailor-sdk deploy"; const docs = ( <>

package tailor-sdk is installed

tailor-sdk deploy + npx tailor-sdk@latest login ); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index e97907a5f..b004a6a97 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -1,44 +1,12 @@ const siteName = "portal"; const setup = `pnpm tailor staticwebsite deploy --name ${siteName}`; const deploy = "tailor deploy"; -const dynamicCommand = `tailor ${subcommand}`; -const dynamicCommandAfterSeparator = `cd app && tailor ${subcommand}`; -const dynamicCommandAfterEnv = `env FOO=bar tailor ${subcommand}`; -const dynamicCommandAfterAssignment = `TAILOR=1 tailor ${subcommand}`; -const dynamicPnpmCommand = `pnpm tailor ${subcommand}`; -const dynamicPnpmExecCommand = `pnpm exec tailor ${subcommand}`; -const dynamicCommandWithFlags = `tailor ${flags} deploy`; -const dynamicCommandWithConditionalFlags = `tailor ${useJson ? "--json" : ""} deploy`; -const profileValue = "tailor --profile tailor-sdk deploy"; -const shortProfileValue = "tailor -p tailor-sdk deploy"; -const substitutedArgValue = `tailor function test-run --arg ${cond ? "tailor-sdk deploy" : "{}"}`; -const substitutedSingleArgValue = `tailor function test-run --arg ${"tailor-sdk"}`; -const dynamicArgFlagValue = `tailor function test-run ${"--arg"} ${"tailor-sdk deploy"}`; +const generated = "Run tailor generate after changes"; +const dynamicCommand = `tailor-sdk ${subcommand}`; const latest = "npx @tailor-platform/sdk@latest login"; -const cacheRunner = "npx --cache .npm @tailor-platform/sdk login"; -const registryRunner = "npx --registry=https://npm.example/tailor-sdk @tailor-platform/sdk login"; -const pnpmOptionRunner = "pnpm --dir . dlx @tailor-platform/sdk login"; -const inlineBooleanFlagRunner = "npx --yes=true @tailor-platform/sdk login"; -const packageOptionRunner = "npx --package=@tailor-platform/sdk tailor login"; -const separatePackageOptionRunner = "npx --package @tailor-platform/sdk tailor login"; -const separatePackageOptionTemplate = `npx --package @tailor-platform/sdk tailor login`; -const quotedPackageOptionRunner = 'npx --package="@tailor-platform/sdk" tailor login'; -const dynamicPackageOptionRunner = `npx --package=${useLocal ? "@tailor-platform/sdk" : pkg} tailor login`; -const dynamicSeparatePackageOptionRunner = `npx ${useLocal ? "--package" : ""} ${useLocal ? "@tailor-platform/sdk" : pkg} tailor login`; -const dynamicPackageOptionCommand = `npx --package @tailor-platform/sdk tailor ${subcommand}`; -const packageOptionArgValue = `npx --package @tailor-platform/sdk tailor function test-run --arg ${cond ? "tailor-sdk deploy" : "{}"}`; -const dynamicInlinePackageOptionCommand = `npx --package=${pkg} tailor ${subcommand}`; -const dynamicFlagStaticTemplate = `npx ${yes ? "-y" : ""} @tailor-platform/sdk login`; -const staticSubstitutionRunner = `${"npx"} @tailor-platform/sdk login`; -const dynamicFlagConditionTemplate = `npx ${mode === "prod" ? "-y" : "--yes"} @tailor-platform/sdk login`; -const dynamicDlxValueFlagTemplate = `pnpm ${use ? "--filter" : ""} app dlx @tailor-platform/sdk login`; -const dynamicDlxInlineValueFlagTemplate = `pnpm ${use ? "--filter=tailor-sdk" : ""} dlx @tailor-platform/sdk login`; -const packageConditionTemplate = `npx ${pkg === "tailor-sdk" ? pkg : "@tailor-platform/sdk"} login`; -const quotedCacheTemplate = `npx --cache 'tailor-sdk/${cache}' @tailor-platform/sdk login`; -const quotedRegistryTemplate = `npx --registry "https://npm.example/tailor-sdk/${scope}" @tailor-platform/sdk login`; -const cacheValue = "npx --cache tailor-sdk some-package"; +const packageName = "tailor-sdk"; +const packageMessage = "package tailor-sdk is installed"; +const mixedPackageAndCommand = "Install tailor-sdk before running tailor deploy"; const outputDir = ".tailor-sdk/"; const scaffold = "create-tailor-sdk"; const skills = "tailor-sdk-skills install"; -const packageTemplateSuffix = `tailor-sdk${suffix}`; -const proseTemplate = `package tailor-sdk ${version}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 538745fdd..3daa92829 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -1,44 +1,12 @@ const siteName = "portal"; const setup = `pnpm tailor-sdk staticwebsite deploy --name ${siteName}`; const deploy = "tailor-sdk deploy"; +const generated = "Run tailor-sdk generate after changes"; const dynamicCommand = `tailor-sdk ${subcommand}`; -const dynamicCommandAfterSeparator = `cd app && tailor-sdk ${subcommand}`; -const dynamicCommandAfterEnv = `env FOO=bar tailor-sdk ${subcommand}`; -const dynamicCommandAfterAssignment = `TAILOR=1 tailor-sdk ${subcommand}`; -const dynamicPnpmCommand = `pnpm tailor-sdk ${subcommand}`; -const dynamicPnpmExecCommand = `pnpm exec tailor-sdk ${subcommand}`; -const dynamicCommandWithFlags = `tailor-sdk ${flags} deploy`; -const dynamicCommandWithConditionalFlags = `tailor-sdk ${useJson ? "--json" : ""} deploy`; -const profileValue = "tailor-sdk --profile tailor-sdk deploy"; -const shortProfileValue = "tailor-sdk -p tailor-sdk deploy"; -const substitutedArgValue = `tailor-sdk function test-run --arg ${cond ? "tailor-sdk deploy" : "{}"}`; -const substitutedSingleArgValue = `tailor-sdk function test-run --arg ${"tailor-sdk"}`; -const dynamicArgFlagValue = `tailor-sdk function test-run ${"--arg"} ${"tailor-sdk deploy"}`; const latest = "npx tailor-sdk@latest login"; -const cacheRunner = "npx --cache .npm tailor-sdk login"; -const registryRunner = "npx --registry=https://npm.example/tailor-sdk tailor-sdk login"; -const pnpmOptionRunner = "pnpm --dir . dlx tailor-sdk login"; -const inlineBooleanFlagRunner = "npx --yes=true tailor-sdk login"; -const packageOptionRunner = "npx --package=tailor-sdk tailor-sdk login"; -const separatePackageOptionRunner = "npx --package tailor-sdk tailor-sdk login"; -const separatePackageOptionTemplate = `npx --package tailor-sdk tailor-sdk login`; -const quotedPackageOptionRunner = 'npx --package="tailor-sdk" tailor-sdk login'; -const dynamicPackageOptionRunner = `npx --package=${useLocal ? "tailor-sdk" : pkg} tailor-sdk login`; -const dynamicSeparatePackageOptionRunner = `npx ${useLocal ? "--package" : ""} ${useLocal ? "tailor-sdk" : pkg} tailor-sdk login`; -const dynamicPackageOptionCommand = `npx --package tailor-sdk tailor-sdk ${subcommand}`; -const packageOptionArgValue = `npx --package tailor-sdk tailor-sdk function test-run --arg ${cond ? "tailor-sdk deploy" : "{}"}`; -const dynamicInlinePackageOptionCommand = `npx --package=${pkg} tailor-sdk ${subcommand}`; -const dynamicFlagStaticTemplate = `npx ${yes ? "-y" : ""} tailor-sdk login`; -const staticSubstitutionRunner = `${"npx"} tailor-sdk login`; -const dynamicFlagConditionTemplate = `npx ${mode === "prod" ? "-y" : "--yes"} tailor-sdk login`; -const dynamicDlxValueFlagTemplate = `pnpm ${use ? "--filter" : ""} app dlx tailor-sdk login`; -const dynamicDlxInlineValueFlagTemplate = `pnpm ${use ? "--filter=tailor-sdk" : ""} dlx tailor-sdk login`; -const packageConditionTemplate = `npx ${pkg === "tailor-sdk" ? pkg : "tailor-sdk"} login`; -const quotedCacheTemplate = `npx --cache 'tailor-sdk/${cache}' tailor-sdk login`; -const quotedRegistryTemplate = `npx --registry "https://npm.example/tailor-sdk/${scope}" tailor-sdk login`; -const cacheValue = "npx --cache tailor-sdk some-package"; +const packageName = "tailor-sdk"; +const packageMessage = "package tailor-sdk is installed"; +const mixedPackageAndCommand = "Install tailor-sdk before running tailor-sdk deploy"; const outputDir = ".tailor-sdk/"; const scaffold = "create-tailor-sdk"; const skills = "tailor-sdk-skills install"; -const packageTemplateSuffix = `tailor-sdk${suffix}`; -const proseTemplate = `package tailor-sdk ${version}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts deleted file mode 100644 index 3e4894819..000000000 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/expected.ts +++ /dev/null @@ -1,68 +0,0 @@ -const packageName = "tailor-sdk"; -const runnerArgs = ["npx", "@tailor-platform/sdk@latest", "login"]; -const wrappedRunnerArgs = ["npx" as const, "@tailor-platform/sdk", "login"]; -const parenthesizedRunnerArgs = [("npx"), "@tailor-platform/sdk", "login"]; -const dynamicPackageVariableRunner = ["npx", pkg, "tailor", "login"]; -const dynamicDlxPackageVariableRunner = ["pnpm", "dlx", pkg, "tailor", "login"]; -const dynamicFlagRunner = ["npx", yes && "-y", "@tailor-platform/sdk", "login"]; -const dynamicAmbiguousValueFlagRunner = ["npx", cond ? "--cache" : "--yes", "tailor-sdk", "login"]; -const localPackageRunner = ["npx", "./tailor-sdk", "tailor", "login"]; -const dynamicInlineValueFlagRunner = ["npx", useRegistry ? "--registry=https://npm.example" : "-y", "@tailor-platform/sdk", "login"]; -const dynamicFlagConditionRunner = ["npx", mode === "prod" ? "-y" : "--yes", "@tailor-platform/sdk", "login"]; -const packageConditionRunner = ["npx", pkg === "tailor-sdk" ? pkg : "@tailor-platform/sdk", "login"]; -const inlineBooleanFlagRunner = ["npx", "--yes=true", "@tailor-platform/sdk", "login"]; -const nestedRunner = spawn("npx", ["-y", "@tailor-platform/sdk", "login"]); -const pnpmRunner = spawn("pnpm", ["dlx", "@tailor-platform/sdk@2.0.0", "login"]); -const pnpmExecRunner = spawn("pnpm", ["exec", "tailor", "login"]); -const yarnExecRunner = ["yarn", "exec", "tailor", "login"]; -const pnpmExecOtherCommand = ["pnpm", "exec", "other-cli", "tailor-sdk"]; -const valuedFlagRunner = ["npx", "--cache", ".npm", "@tailor-platform/sdk", "login"]; -const namedValueFlagRunner = ["npx", "--cache", "npm-cache", "@tailor-platform/sdk", "login"]; -const registryValueFlagRunner = ["npx", "--registry", "https://npm.example", "@tailor-platform/sdk", "login"]; -const prefixValueFlagRunner = ["npx", "--prefix", "npm-cache", "@tailor-platform/sdk", "login"]; -const protectedValueFlagRunner = ["npx", "--cache", "tailor-sdk", "some-package"]; -const expressionValueFlagRunner = ["npx", "--cache", cacheDir, "@tailor-platform/sdk", "login"]; -const protectedExpressionValueFlagRunner = ["npx", "--cache", cacheDir || "tailor-sdk", "@tailor-platform/sdk", "login"]; -const inlineValueFlagRunner = ["npx", "--registry=https://npm.example/tailor-sdk", "@tailor-platform/sdk"]; -const inlinePackageFlagRunner = ["npx", "--package=@tailor-platform/sdk", "tailor", "login"]; -const dynamicInlinePackageFlagRunner = ["npx", `--package=${useLocal ? "@tailor-platform/sdk" : pkg}`, "tailor", "login"]; -const dynamicSeparatePackageFlagRunner = ["npx", useLocal ? "--package" : "", useLocal ? "@tailor-platform/sdk" : pkg, "tailor", "login"]; -const dynamicPackageOptionCommandRunner = ["npx", "--package", "@tailor-platform/sdk", "tailor", cmd]; -const packageOptionArgPayloadRunner = ["npx", "--package", "@tailor-platform/sdk", "tailor", "function", "test-run", "--arg", cond ? "tailor-sdk deploy" : "{}"]; -const dynamicInlinePackageOptionCommandRunner = ["npx", `--package=${pkg}`, "tailor", cmd]; -const templateConditionalRunner = `npx ${useLatest ? "@tailor-platform/sdk@latest" : "@tailor-platform/sdk"} login`; -const templateDynamicInlineValueFlagRunner = `npx ${useRegistry ? "--registry=https://npm.example" : "-y"} @tailor-platform/sdk login`; -const pnpmOptionRunner = spawn("pnpm", ["--dir", ".", "dlx", "@tailor-platform/sdk", "login"]); -const pnpmFilterRunner = ["pnpm", "--filter", "app", "dlx", "@tailor-platform/sdk", "login"]; -const pnpmRegistryRunner = ["pnpm", "--registry", "https://npm.example", "dlx", "@tailor-platform/sdk", "login"]; -const dynamicPnpmFilterRunner = ["pnpm", use ? "--filter" : "", "app", "dlx", "@tailor-platform/sdk", "login"]; -const dynamicInlinePnpmFilterRunner = ["pnpm", use ? "--filter=tailor-sdk" : "", "dlx", "@tailor-platform/sdk", "login"]; -const dynamicInlineRegistryRunner = ["npx", use ? "--registry=https://npm.example/tailor-sdk" : "-y", "@tailor-platform/sdk", "login"]; -const spreadFlagRunner = ["npx", ...["-y"], "@tailor-platform/sdk", "login"]; -const unknownSpreadFlagRunner = ["npx", ...flags, "@tailor-platform/sdk", "login"]; -const commentedRunner = ["npx", /* install CLI */ "@tailor-platform/sdk", "login"]; -const commentedPackageFlagRunner = ["npx", "--package", /* package */ "@tailor-platform/sdk", "tailor", "login"]; -const commentedPnpmDlxRunner = ["pnpm", /* comment */ "dlx", "@tailor-platform/sdk", "login"]; -const commentedValueFlagRunner = ["npx", "--cache", /* cache dir */ ".npm", "@tailor-platform/sdk", "login"]; -const unrelatedTailorPackageRunner = ["npx", "--cache", ".npm", "tailor", "sprite"]; -const conditionalRunner = ["npx", useLatest ? "@tailor-platform/sdk@latest" : "@tailor-platform/sdk", "login"]; -const binaryArgs = ["tailor", "login"]; -const profileArgValue = ["tailor", "--profile", "tailor-sdk", "deploy"]; -const shortProfileArgValue = ["tailor", "-p", "tailor-sdk", "deploy"]; -const profileCommandPayload = ["tailor", "--profile", "tailor-sdk deploy", "deploy"]; -const argCommandPayload = ["tailor", "function", "test-run", "--arg", "{\"cmd\":\"tailor-sdk deploy\"}"]; -const argCommandPayloadExpression = ["tailor", "function", "test-run", "--arg", cond ? "tailor-sdk deploy" : "{}"]; -const argCommandPayloadTemplate = ["tailor", "function", "test-run", "--arg", `tailor-sdk deploy`]; -const dynamicArgFlagPayload = ["tailor", "function", "test-run", includeArg ? "--arg" : "", "tailor-sdk deploy"]; -const dynamicBinaryArgs = ["tailor", cmd]; -const spreadBinaryArgs = ["tailor", ...args]; -const wrappedCommandArgs = ["tailor", "login" as const]; -const parenthesizedCommandArgs = ["tailor", ("deploy")]; -const commandArgPackageName = ["tailor", "login", "tailor-sdk"]; -const packageManagerNames = ["pnpm", "tailor-sdk"]; -const unrelatedNpxValues = ["npx", "create-tailor-sdk", "tailor-sdk-skills", "tailor-sdk"]; -const packageNames = ["tailor-sdk", "react"]; -const packageTuple = [["tailor-sdk", version]]; -const packageTupleValue = ["tailor-sdk", version]; -const packageOptionData = ["tailor-sdk", "--not-cli"]; -const outputDir = ".tailor-sdk"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts deleted file mode 100644 index 4e3c81768..000000000 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-token-runner/input.ts +++ /dev/null @@ -1,68 +0,0 @@ -const packageName = "tailor-sdk"; -const runnerArgs = ["npx", "tailor-sdk@latest", "login"]; -const wrappedRunnerArgs = ["npx" as const, "tailor-sdk", "login"]; -const parenthesizedRunnerArgs = [("npx"), "tailor-sdk", "login"]; -const dynamicPackageVariableRunner = ["npx", pkg, "tailor-sdk", "login"]; -const dynamicDlxPackageVariableRunner = ["pnpm", "dlx", pkg, "tailor-sdk", "login"]; -const dynamicFlagRunner = ["npx", yes && "-y", "tailor-sdk", "login"]; -const dynamicAmbiguousValueFlagRunner = ["npx", cond ? "--cache" : "--yes", "tailor-sdk", "login"]; -const localPackageRunner = ["npx", "./tailor-sdk", "tailor-sdk", "login"]; -const dynamicInlineValueFlagRunner = ["npx", useRegistry ? "--registry=https://npm.example" : "-y", "tailor-sdk", "login"]; -const dynamicFlagConditionRunner = ["npx", mode === "prod" ? "-y" : "--yes", "tailor-sdk", "login"]; -const packageConditionRunner = ["npx", pkg === "tailor-sdk" ? pkg : "tailor-sdk", "login"]; -const inlineBooleanFlagRunner = ["npx", "--yes=true", "tailor-sdk", "login"]; -const nestedRunner = spawn("npx", ["-y", "tailor-sdk", "login"]); -const pnpmRunner = spawn("pnpm", ["dlx", "tailor-sdk@2.0.0", "login"]); -const pnpmExecRunner = spawn("pnpm", ["exec", "tailor-sdk", "login"]); -const yarnExecRunner = ["yarn", "exec", "tailor-sdk", "login"]; -const pnpmExecOtherCommand = ["pnpm", "exec", "other-cli", "tailor-sdk"]; -const valuedFlagRunner = ["npx", "--cache", ".npm", "tailor-sdk", "login"]; -const namedValueFlagRunner = ["npx", "--cache", "npm-cache", "tailor-sdk", "login"]; -const registryValueFlagRunner = ["npx", "--registry", "https://npm.example", "tailor-sdk", "login"]; -const prefixValueFlagRunner = ["npx", "--prefix", "npm-cache", "tailor-sdk", "login"]; -const protectedValueFlagRunner = ["npx", "--cache", "tailor-sdk", "some-package"]; -const expressionValueFlagRunner = ["npx", "--cache", cacheDir, "tailor-sdk", "login"]; -const protectedExpressionValueFlagRunner = ["npx", "--cache", cacheDir || "tailor-sdk", "tailor-sdk", "login"]; -const inlineValueFlagRunner = ["npx", "--registry=https://npm.example/tailor-sdk", "tailor-sdk"]; -const inlinePackageFlagRunner = ["npx", "--package=tailor-sdk", "tailor-sdk", "login"]; -const dynamicInlinePackageFlagRunner = ["npx", `--package=${useLocal ? "tailor-sdk" : pkg}`, "tailor-sdk", "login"]; -const dynamicSeparatePackageFlagRunner = ["npx", useLocal ? "--package" : "", useLocal ? "tailor-sdk" : pkg, "tailor-sdk", "login"]; -const dynamicPackageOptionCommandRunner = ["npx", "--package", "tailor-sdk", "tailor-sdk", cmd]; -const packageOptionArgPayloadRunner = ["npx", "--package", "tailor-sdk", "tailor-sdk", "function", "test-run", "--arg", cond ? "tailor-sdk deploy" : "{}"]; -const dynamicInlinePackageOptionCommandRunner = ["npx", `--package=${pkg}`, "tailor-sdk", cmd]; -const templateConditionalRunner = `npx ${useLatest ? "tailor-sdk@latest" : "tailor-sdk"} login`; -const templateDynamicInlineValueFlagRunner = `npx ${useRegistry ? "--registry=https://npm.example" : "-y"} tailor-sdk login`; -const pnpmOptionRunner = spawn("pnpm", ["--dir", ".", "dlx", "tailor-sdk", "login"]); -const pnpmFilterRunner = ["pnpm", "--filter", "app", "dlx", "tailor-sdk", "login"]; -const pnpmRegistryRunner = ["pnpm", "--registry", "https://npm.example", "dlx", "tailor-sdk", "login"]; -const dynamicPnpmFilterRunner = ["pnpm", use ? "--filter" : "", "app", "dlx", "tailor-sdk", "login"]; -const dynamicInlinePnpmFilterRunner = ["pnpm", use ? "--filter=tailor-sdk" : "", "dlx", "tailor-sdk", "login"]; -const dynamicInlineRegistryRunner = ["npx", use ? "--registry=https://npm.example/tailor-sdk" : "-y", "tailor-sdk", "login"]; -const spreadFlagRunner = ["npx", ...["-y"], "tailor-sdk", "login"]; -const unknownSpreadFlagRunner = ["npx", ...flags, "tailor-sdk", "login"]; -const commentedRunner = ["npx", /* install CLI */ "tailor-sdk", "login"]; -const commentedPackageFlagRunner = ["npx", "--package", /* package */ "tailor-sdk", "tailor-sdk", "login"]; -const commentedPnpmDlxRunner = ["pnpm", /* comment */ "dlx", "tailor-sdk", "login"]; -const commentedValueFlagRunner = ["npx", "--cache", /* cache dir */ ".npm", "tailor-sdk", "login"]; -const unrelatedTailorPackageRunner = ["npx", "--cache", ".npm", "tailor", "sprite"]; -const conditionalRunner = ["npx", useLatest ? "tailor-sdk@latest" : "tailor-sdk", "login"]; -const binaryArgs = ["tailor-sdk", "login"]; -const profileArgValue = ["tailor-sdk", "--profile", "tailor-sdk", "deploy"]; -const shortProfileArgValue = ["tailor-sdk", "-p", "tailor-sdk", "deploy"]; -const profileCommandPayload = ["tailor-sdk", "--profile", "tailor-sdk deploy", "deploy"]; -const argCommandPayload = ["tailor-sdk", "function", "test-run", "--arg", "{\"cmd\":\"tailor-sdk deploy\"}"]; -const argCommandPayloadExpression = ["tailor-sdk", "function", "test-run", "--arg", cond ? "tailor-sdk deploy" : "{}"]; -const argCommandPayloadTemplate = ["tailor-sdk", "function", "test-run", "--arg", `tailor-sdk deploy`]; -const dynamicArgFlagPayload = ["tailor-sdk", "function", "test-run", includeArg ? "--arg" : "", "tailor-sdk deploy"]; -const dynamicBinaryArgs = ["tailor-sdk", cmd]; -const spreadBinaryArgs = ["tailor-sdk", ...args]; -const wrappedCommandArgs = ["tailor-sdk", "login" as const]; -const parenthesizedCommandArgs = ["tailor-sdk", ("deploy")]; -const commandArgPackageName = ["tailor-sdk", "login", "tailor-sdk"]; -const packageManagerNames = ["pnpm", "tailor-sdk"]; -const unrelatedNpxValues = ["npx", "create-tailor-sdk", "tailor-sdk-skills", "tailor-sdk"]; -const packageNames = ["tailor-sdk", "react"]; -const packageTuple = [["tailor-sdk", version]]; -const packageTupleValue = ["tailor-sdk", version]; -const packageOptionData = ["tailor-sdk", "--not-cli"]; -const outputDir = ".tailor-sdk"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/expected.tsx b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/expected.tsx deleted file mode 100644 index edf3495b7..000000000 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/expected.tsx +++ /dev/null @@ -1,7 +0,0 @@ -const docs = ( - <> -

package tailor-sdk is installed

- tailor deploy - npx --package @tailor-platform/sdk tailor login - -); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/input.tsx b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/input.tsx deleted file mode 100644 index da53e41a1..000000000 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-tsx/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -const docs = ( - <> -

package tailor-sdk is installed

- tailor-sdk deploy - npx --package tailor-sdk tailor-sdk login - -); diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 995fea754..140b5b028 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -56,28 +56,16 @@ describe("getApplicableCodemods", () => { ); }); - test("CLI command codemods scan source files and declaration comments", () => { + test("rename-bin scans source files and declaration comments", () => { const sourcePattern = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"; - const cliRename = getApplicableCodemods("1.67.1", "2.0.0").find( - (codemod) => codemod.id === "v2/cli-rename", - ); const renameBin = getApplicableCodemods("1.67.1", "2.0.0").find( (codemod) => codemod.id === "v2/rename-bin", ); - for (const codemod of [cliRename, renameBin]) { - expect(codemod?.filePatterns).toEqual(expect.arrayContaining([sourcePattern])); - const matches = picomatch(codemod?.filePatterns ?? [], { dot: true }); - expect(matches("packages/app/frontend/e2e/global-setup.ts")).toBe(true); - expect(matches("tailor.d.ts")).toBe(true); - } - expect(cliRename?.sourceStringLegacyPatterns).toEqual( - expect.arrayContaining([ - ["tailor-sdk", "crash-report"], - [/(?:^|[\s;&|])tailor(?=[\s;&|]|$)/, "crash-report"], - "--machineuser", - ]), - ); + expect(renameBin?.filePatterns).toEqual(expect.arrayContaining([sourcePattern])); + const matches = picomatch(renameBin?.filePatterns ?? [], { dot: true }); + expect(matches("packages/app/frontend/e2e/global-setup.ts")).toBe(true); + expect(matches("tailor.d.ts")).toBe(true); }); test("flags CommonJS TypeScript files for runtime globals review", () => { diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 473240d5e..84e6dd9a4 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -4,7 +4,6 @@ import { lt, gte, valid } from "semver"; import type { CodemodPackage } from "./types"; const CODEMODS_ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "codemods"); -const TAILOR_COMMAND_TOKEN_RE = /(?:^|[\s;&|])tailor(?=[\s;&|]|$)/; /** All registered codemods, in registration order. */ export const allCodemods: CodemodPackage[] = [ @@ -205,22 +204,12 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/cli-rename", name: "v2 CLI rename", description: - "Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, source files, and docs", + "Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs", since: "1.0.0", until: "2.0.0", scriptPath: "v2/cli-rename/scripts/transform.js", - filePatterns: [ - "**/package.json", - "**/*.{sh,bash,zsh,yml,yaml}", - "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}", - "**/*.md", - ], + filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], legacyPatterns: ["tailor-sdk crash-report", "--machineuser"], - sourceStringLegacyPatterns: [ - ["tailor-sdk", "crash-report"], - [TAILOR_COMMAND_TOKEN_RE, "crash-report"], - "--machineuser", - ], examples: [ { lang: "sh", diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 8c794425d..080ae1173 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -315,7 +315,6 @@ describe("runCodemods", () => { describe("legacy pattern warnings", () => { const partialTransformPath = path.join(os.tmpdir(), "transform-partial.ts"); - const renameBinaryTransformPath = path.join(os.tmpdir(), "transform-rename-binary.ts"); beforeEach(async () => { await fs.promises.writeFile( @@ -325,18 +324,10 @@ describe("runCodemods", () => { }`, "utf-8", ); - await fs.promises.writeFile( - renameBinaryTransformPath, - `export default function transform(source) { - return source.replaceAll("tailor-sdk", "tailor"); - }`, - "utf-8", - ); }); afterEach(async () => { await fs.promises.rm(partialTransformPath, { force: true }); - await fs.promises.rm(renameBinaryTransformPath, { force: true }); }); test("warns when legacy patterns remain after a partial migration", async () => { @@ -502,60 +493,6 @@ describe("runCodemods", () => { ]); }); - test("checks source string warnings after chained transforms", async () => { - const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); - tmpDir = dir; - await fs.promises.writeFile( - path.join(dir, "cli.ts"), - 'const command = ["tailor-sdk", mode, "crash-report"];', - "utf-8", - ); - - const result = await runCodemods( - [ - { - codemod: makeCodemod("test/rename-bin", renameBinaryTransformPath, ["**/*.ts"]), - scriptPath: renameBinaryTransformPath, - }, - { - codemod: makeCodemod("test/cli-rename", undefined, ["**/*.ts"], [], { - sourceStringLegacyPatterns: [[/(?:^|[\s;&|])tailor(?=[\s;&|]|$)/, "crash-report"]], - }), - }, - ], - dir, - false, - ); - - expect(result.warnings).toEqual([ - "cli.ts: contains /(?:^|[\\s;&|])tailor(?=[\\s;&|]|$)/ + crash-report but was not migrated automatically (rule: test/cli-rename). Manual migration may be needed.", - ]); - }); - - test("does not treat SDK package specifiers as tailor command tokens", async () => { - const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); - tmpDir = dir; - await fs.promises.writeFile( - path.join(dir, "cli.ts"), - ['import "@tailor-platform/sdk";', 'const commandName = "crash-report";'].join("\n"), - "utf-8", - ); - - const result = await runCodemods( - [ - { - codemod: makeCodemod("test/cli-rename", undefined, ["**/*.ts"], [], { - sourceStringLegacyPatterns: [[/(?:^|[\s;&|])tailor(?=[\s;&|]|$)/, "crash-report"]], - }), - }, - ], - dir, - false, - ); - - expect(result.warnings).toEqual([]); - }); - test("flags files matching a suspicious pattern for LLM review", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index cbf54d2cd..4bf94a544 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -173,7 +173,7 @@ function sourceStringContentForResidualMatching(relative: string, content: strin function sourceLang(relative: string): Lang { const ext = path.extname(relative).toLowerCase(); - return ext === ".tsx" || ext === ".jsx" || ext === ".js" ? Lang.Tsx : Lang.TypeScript; + return ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : Lang.TypeScript; } function isProcessEnvSubscriptKey(node: SgNode): boolean { diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index a6066606b..407334850 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -231,7 +231,7 @@ tailor-sdk deploy --profile prod **Migration:** Partially automatic -Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, source files, and docs +Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs Before: From 4f8aaf28e943dfd5290b1ea988141fc120f535b5 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 16:51:39 +0900 Subject: [PATCH 304/618] fix(sdk-codemod): cover source binary edge cases --- .../v2/rename-bin/scripts/transform.ts | 145 +++++++++++++++++- .../tests/source-js-string/expected.js | 1 + .../tests/source-js-string/input.js | 1 + .../tests/source-template/expected.ts | 5 +- .../rename-bin/tests/source-template/input.ts | 3 + packages/sdk-codemod/src/runner.test.ts | 22 +++ packages/sdk-codemod/src/runner.ts | 1 + 7 files changed, 170 insertions(+), 8 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 7a7d3ee0c..e9a67acc1 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -16,6 +16,7 @@ const PKG_RUNNER_RE = // (e.g. `tailor-sdk-skills`) to avoid partial-match rewrites. const TAILOR_SDK_RE = /(? @@ -67,13 +85,52 @@ function renameBinary(value: string): string { ); } +function protectSourceCliValueReferences(value: string): { + source: string; + protectedValues: string[]; +} { + const protectedValues: string[] = []; + const source = value.replace( + SOURCE_CLI_VALUE_REFERENCE_RE, + (match, prefix: string, arg: string) => { + if (!arg.includes("tailor-sdk")) return match; + const placeholder = `__TAILOR_SDK_SOURCE_VALUE_${protectedValues.length}__`; + protectedValues.push(arg); + return `${prefix}${placeholder}`; + }, + ); + return { source, protectedValues }; +} + +function restoreSourceCliValueReferences(value: string, protectedValues: string[]): string { + let restored = value; + for (const [index, protectedValue] of protectedValues.entries()) { + restored = restored.replaceAll(`__TAILOR_SDK_SOURCE_VALUE_${index}__`, protectedValue); + } + return restored; +} + function renameSourceCommandText(value: string): string { - const withRunners = value.replace(SOURCE_PKG_RUNNER_RE, (_, runner: string, version?: string) => - version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, + const protectedValue = protectSourceCliValueReferences(value); + const withRunners = protectedValue.source.replace( + SOURCE_PKG_RUNNER_RE, + (_, runner: string, version?: string) => + version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, ); - return withRunners.replace(SOURCE_TAILOR_SDK_RE, (_match, version?: string) => + const withCommands = withRunners.replace(SOURCE_TAILOR_SDK_RE, (_match, version?: string) => version ? `@tailor-platform/sdk${version}` : "tailor", ); + const withDynamicRunners = withCommands.replace( + SOURCE_DYNAMIC_PKG_RUNNER_RE, + (_, runner: string, version?: string) => + version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, + ); + const updated = withDynamicRunners.replace( + SOURCE_DYNAMIC_TAILOR_SDK_RE, + (_match, prefix: string, version?: string) => + version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}tailor`, + ); + return restoreSourceCliValueReferences(updated, protectedValue.protectedValues); } function sourceLang(filePath: string): Lang { @@ -94,6 +151,80 @@ function pushSourceTextEdit( } } +function nodeRangeKey(node: SgNode): string { + const range = node.range(); + return `${range.start.index}:${range.end.index}`; +} + +function sourceStringContent(node: SgNode, source: string): string | null { + const kind = node.kind(); + if (kind !== "string" && kind !== "template_string") return null; + if ( + kind === "template_string" && + node.children().some((child: SgNode) => child.kind() === "template_substitution") + ) { + return null; + } + const range = node.range(); + return source.slice(range.start.index + 1, range.end.index - 1); +} + +function isSyntaxOnlyNode(node: SgNode): boolean { + const kind = node.kind(); + return ( + kind === "[" || + kind === "]" || + kind === "(" || + kind === ")" || + kind === "," || + kind === "comment" + ); +} + +function isStaticTailorCommandNode(node: SgNode, source: string): boolean { + const value = sourceStringContent(node, source); + return ( + value != null && TAILOR_CLI_COMMANDS.includes(value as (typeof TAILOR_CLI_COMMANDS)[number]) + ); +} + +function isCliBinaryArgument(node: SgNode, source: string): boolean { + const parent = node.parent(); + if (parent?.kind() === "array") { + const elements = parent.children().filter((child: SgNode) => !isSyntaxOnlyNode(child)); + const index = elements.findIndex((child: SgNode) => nodeRangeKey(child) === nodeRangeKey(node)); + const next = elements[index + 1]; + return index === 0 && next != null && isStaticTailorCommandNode(next, source); + } + + if (parent?.kind() !== "arguments") return false; + const parentRange = nodeRangeKey(parent); + const call = parent.parent(); + if (call?.kind() !== "call_expression") return false; + const callee = call.children().find((child: SgNode) => nodeRangeKey(child) !== parentRange); + if (!CLI_ARGUMENT_CALLEE_RE.test(callee?.text() ?? "")) return false; + const args = parent.children().filter((child: SgNode) => !isSyntaxOnlyNode(child)); + return args[0] != null && nodeRangeKey(args[0]) === nodeRangeKey(node); +} + +function pushSourceStringEdit( + edits: Array<[number, number, string]>, + source: string, + node: SgNode, +): void { + const range = node.range(); + const start = range.start.index + 1; + const end = range.end.index - 1; + const text = source.slice(start, end); + const replacement = + TAILOR_SDK_TOKEN_RE.test(text) && isCliBinaryArgument(node, source) + ? renameBinary(text) + : renameSourceCommandText(text); + if (replacement !== text) { + edits.push([start, end, replacement]); + } +} + function transformSourceFile(source: string, filePath: string): string | null { let root: SgNode; try { @@ -113,7 +244,7 @@ function transformSourceFile(source: string, filePath: string): string | null { } if (kind === "string") { - pushSourceTextEdit(edits, source, range.start.index + 1, range.end.index - 1); + pushSourceStringEdit(edits, source, node); return; } @@ -122,7 +253,7 @@ function transformSourceFile(source: string, filePath: string): string | null { .children() .some((child: SgNode) => child.kind() === "template_substitution"); if (!hasSubstitution) { - pushSourceTextEdit(edits, source, range.start.index + 1, range.end.index - 1); + pushSourceStringEdit(edits, source, node); return; } } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index baecfb514..fb22e64f3 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -1,4 +1,5 @@ const script = "tailor deploy"; +const spawned = spawn("tailor", ["deploy"]); const docs = ( <>

package tailor-sdk is installed

diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 4cf181816..93869962c 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -1,4 +1,5 @@ const script = "tailor-sdk deploy"; +const spawned = spawn("tailor-sdk", ["deploy"]); const docs = ( <>

package tailor-sdk is installed

diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index b004a6a97..91e2966da 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -1,8 +1,11 @@ const siteName = "portal"; const setup = `pnpm tailor staticwebsite deploy --name ${siteName}`; const deploy = "tailor deploy"; +const envFileDeploy = "tailor --env-file .env deploy"; +const profileValue = "tailor --profile tailor-sdk deploy"; const generated = "Run tailor generate after changes"; -const dynamicCommand = `tailor-sdk ${subcommand}`; +const dynamicCommand = `tailor ${subcommand}`; +const dynamicPnpmCommand = `pnpm tailor ${subcommand}`; const latest = "npx @tailor-platform/sdk@latest login"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 3daa92829..7de83af7d 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -1,8 +1,11 @@ const siteName = "portal"; const setup = `pnpm tailor-sdk staticwebsite deploy --name ${siteName}`; const deploy = "tailor-sdk deploy"; +const envFileDeploy = "tailor-sdk --env-file .env deploy"; +const profileValue = "tailor-sdk --profile tailor-sdk deploy"; const generated = "Run tailor-sdk generate after changes"; const dynamicCommand = `tailor-sdk ${subcommand}`; +const dynamicPnpmCommand = `pnpm tailor-sdk ${subcommand}`; const latest = "npx tailor-sdk@latest login"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 080ae1173..d8c8adced 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -395,6 +395,28 @@ describe("runCodemods", () => { expect(result.warnings).toEqual([]); }); + test("ignores JSX text for legacy warnings", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "docs.tsx"), + "export const docs =

package tailor-sdk is installed

;", + "utf-8", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", undefined, ["**/*.tsx"], ["tailor-sdk"]), + }, + ], + dir, + true, + ); + + expect(result.warnings).toEqual([]); + }); + test("keeps legacy warnings for source identifiers", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 4bf94a544..4e906f240 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -58,6 +58,7 @@ const MASKED_SOURCE_NODE_KINDS: ReadonlySet> = new Se "string", "regex", "string_fragment", + "jsx_text", ]); function shouldSkipDirectory(name: string): boolean { From 5b4d2f366786ccb95a8d427d8b3871200a0afbb6 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 17:07:58 +0900 Subject: [PATCH 305/618] fix(sdk-codemod): preserve package runner source args --- .../v2/rename-bin/scripts/transform.ts | 90 ++++++++++++++++--- .../tests/source-js-string/expected.js | 4 + .../tests/source-js-string/input.js | 4 + .../tests/source-template/expected.ts | 2 + .../rename-bin/tests/source-template/input.ts | 2 + packages/sdk-codemod/src/runner.test.ts | 6 +- packages/sdk-codemod/src/runner.ts | 2 +- 7 files changed, 94 insertions(+), 16 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index e9a67acc1..13656e43d 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -70,11 +70,13 @@ const SOURCE_DYNAMIC_PKG_RUNNER_RE = new RegExp( "g", ); const SOURCE_DYNAMIC_TAILOR_SDK_RE = new RegExp( - `(^|[;&|]\\s*|\\b(?:pnpm|npm|yarn)(?:\\s+exec)?\\s+)tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=\\s+$)`, + `(^\\s*|[;&|]\\s*|\\b(?:pnpm|npm|yarn)(?:\\s+exec)?\\s+)tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=\\s+$)`, "g", ); const TAILOR_SDK_TOKEN_RE = /^tailor-sdk(@[^\s'"`;|&)]+)?$/; const CLI_ARGUMENT_CALLEE_RE = /(?:^|\.)(?:spawn|spawnSync|execFile|execFileSync|execa|execaSync)$/; +const SOURCE_PACKAGE_RUNNERS = new Set(["bunx", "npx"]); +const SOURCE_DLX_PACKAGE_RUNNERS = new Set(["pnpm", "yarn"]); function renameBinary(value: string): string { const withRunners = value.replace(PKG_RUNNER_RE, (_, runner: string, version?: string) => @@ -85,6 +87,12 @@ function renameBinary(value: string): string { ); } +function renamePackageName(value: string): string { + return value.replace(TAILOR_SDK_TOKEN_RE, (_match, version?: string) => + version ? `@tailor-platform/sdk${version}` : "@tailor-platform/sdk", + ); +} + function protectSourceCliValueReferences(value: string): { source: string; protectedValues: string[]; @@ -188,22 +196,78 @@ function isStaticTailorCommandNode(node: SgNode, source: string): boolean { ); } +function sourceArrayElements(node: SgNode): SgNode[] { + return node.children().filter((child: SgNode) => !isSyntaxOnlyNode(child)); +} + +function nodeIndex(nodes: SgNode[], node: SgNode): number { + return nodes.findIndex((child: SgNode) => nodeRangeKey(child) === nodeRangeKey(node)); +} + +function callExpressionCalleeText(argumentsNode: SgNode): string | null { + const parentRange = nodeRangeKey(argumentsNode); + const call = argumentsNode.parent(); + if (call?.kind() !== "call_expression") return null; + const callee = call.children().find((child: SgNode) => nodeRangeKey(child) !== parentRange); + return callee?.text() ?? null; +} + +function firstNonOptionIndex(elements: SgNode[], start: number, source: string): number | null { + for (let index = start; index < elements.length; index += 1) { + const value = sourceStringContent(elements[index]!, source); + if (value == null) return null; + if (!value.startsWith("-")) return index; + } + return null; +} + +function isPackageRunnerPackageArgument(node: SgNode, source: string): boolean { + const parent = node.parent(); + if (parent?.kind() !== "array") return false; + + const argumentsNode = parent.parent(); + if (argumentsNode?.kind() !== "arguments") return false; + const callee = callExpressionCalleeText(argumentsNode); + if (!CLI_ARGUMENT_CALLEE_RE.test(callee ?? "")) return false; + + const callArgs = sourceArrayElements(argumentsNode); + const executableNode = callArgs[0]; + if (executableNode == null) return false; + const executable = sourceStringContent(executableNode, source); + if (executable == null) return false; + + const elements = sourceArrayElements(parent); + const index = nodeIndex(elements, node); + const next = elements[index + 1]; + if (index < 0 || next == null || !isStaticTailorCommandNode(next, source)) return false; + + if (SOURCE_PACKAGE_RUNNERS.has(executable)) { + return firstNonOptionIndex(elements, 0, source) === index; + } + + if (SOURCE_DLX_PACKAGE_RUNNERS.has(executable)) { + const dlxIndex = firstNonOptionIndex(elements, 0, source); + if (dlxIndex == null || sourceStringContent(elements[dlxIndex]!, source) !== "dlx") { + return false; + } + return firstNonOptionIndex(elements, dlxIndex + 1, source) === index; + } + + return false; +} + function isCliBinaryArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() === "array") { - const elements = parent.children().filter((child: SgNode) => !isSyntaxOnlyNode(child)); - const index = elements.findIndex((child: SgNode) => nodeRangeKey(child) === nodeRangeKey(node)); + const elements = sourceArrayElements(parent); + const index = nodeIndex(elements, node); const next = elements[index + 1]; return index === 0 && next != null && isStaticTailorCommandNode(next, source); } if (parent?.kind() !== "arguments") return false; - const parentRange = nodeRangeKey(parent); - const call = parent.parent(); - if (call?.kind() !== "call_expression") return false; - const callee = call.children().find((child: SgNode) => nodeRangeKey(child) !== parentRange); - if (!CLI_ARGUMENT_CALLEE_RE.test(callee?.text() ?? "")) return false; - const args = parent.children().filter((child: SgNode) => !isSyntaxOnlyNode(child)); + if (!CLI_ARGUMENT_CALLEE_RE.test(callExpressionCalleeText(parent) ?? "")) return false; + const args = sourceArrayElements(parent); return args[0] != null && nodeRangeKey(args[0]) === nodeRangeKey(node); } @@ -217,9 +281,11 @@ function pushSourceStringEdit( const end = range.end.index - 1; const text = source.slice(start, end); const replacement = - TAILOR_SDK_TOKEN_RE.test(text) && isCliBinaryArgument(node, source) - ? renameBinary(text) - : renameSourceCommandText(text); + TAILOR_SDK_TOKEN_RE.test(text) && isPackageRunnerPackageArgument(node, source) + ? renamePackageName(text) + : TAILOR_SDK_TOKEN_RE.test(text) && isCliBinaryArgument(node, source) + ? renameBinary(text) + : renameSourceCommandText(text); if (replacement !== text) { edits.push([start, end, replacement]); } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index fb22e64f3..60e59a4c2 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -1,5 +1,9 @@ const script = "tailor deploy"; const spawned = spawn("tailor", ["deploy"]); +const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); +const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); +const pnpmDlxSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk", "login"]); +const pnpmBinarySpawned = spawn("pnpm", ["tailor", "deploy"]); const docs = ( <>

package tailor-sdk is installed

diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 93869962c..491770506 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -1,5 +1,9 @@ const script = "tailor-sdk deploy"; const spawned = spawn("tailor-sdk", ["deploy"]); +const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); +const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); +const pnpmDlxSpawned = spawn("pnpm", ["dlx", "tailor-sdk", "login"]); +const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const docs = ( <>

package tailor-sdk is installed

diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 91e2966da..cb351d464 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -6,6 +6,8 @@ const profileValue = "tailor --profile tailor-sdk deploy"; const generated = "Run tailor generate after changes"; const dynamicCommand = `tailor ${subcommand}`; const dynamicPnpmCommand = `pnpm tailor ${subcommand}`; +const indentedDynamicCommand = ` + tailor ${subcommand}`; const latest = "npx @tailor-platform/sdk@latest login"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 7de83af7d..d6eb4210e 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -6,6 +6,8 @@ const profileValue = "tailor-sdk --profile tailor-sdk deploy"; const generated = "Run tailor-sdk generate after changes"; const dynamicCommand = `tailor-sdk ${subcommand}`; const dynamicPnpmCommand = `pnpm tailor-sdk ${subcommand}`; +const indentedDynamicCommand = ` + tailor-sdk ${subcommand}`; const latest = "npx tailor-sdk@latest login"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index d8c8adced..684839098 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -395,11 +395,11 @@ describe("runCodemods", () => { expect(result.warnings).toEqual([]); }); - test("ignores JSX text for legacy warnings", async () => { + test("ignores JSX text for legacy warnings in JavaScript files", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); tmpDir = dir; await fs.promises.writeFile( - path.join(dir, "docs.tsx"), + path.join(dir, "docs.js"), "export const docs =

package tailor-sdk is installed

;", "utf-8", ); @@ -407,7 +407,7 @@ describe("runCodemods", () => { const result = await runCodemods( [ { - codemod: makeCodemod("test/rename-bin", undefined, ["**/*.tsx"], ["tailor-sdk"]), + codemod: makeCodemod("test/rename-bin", undefined, ["**/*.js"], ["tailor-sdk"]), }, ], dir, diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 4e906f240..57f249209 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -174,7 +174,7 @@ function sourceStringContentForResidualMatching(relative: string, content: strin function sourceLang(relative: string): Lang { const ext = path.extname(relative).toLowerCase(); - return ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : Lang.TypeScript; + return ext === ".tsx" || ext === ".jsx" || ext === ".js" ? Lang.Tsx : Lang.TypeScript; } function isProcessEnvSubscriptKey(node: SgNode): boolean { From 8010ae20fa42f044f33a0a4a53580b9a86d78c3b Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 17:23:40 +0900 Subject: [PATCH 306/618] fix(sdk-codemod): handle package runner option layouts --- .../v2/rename-bin/scripts/transform.ts | 59 +++++++++++++++---- .../tests/source-js-string/expected.js | 2 + .../tests/source-js-string/input.js | 2 + .../tests/source-template/expected.ts | 1 + .../rename-bin/tests/source-template/input.ts | 1 + 5 files changed, 52 insertions(+), 13 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 13656e43d..636b9095b 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -52,13 +52,31 @@ const TAILOR_CLI_COMMANDS = [ const TAILOR_CLI_COMMAND_PATTERN = `(?:${TAILOR_CLI_COMMANDS.join("|")})`; const TAILOR_CLI_VALUE_FLAG = "(?:--env-file-if-exists|--env-file|--profile|--config|--workspace-id|--arg|--query|--file|-e|-p|-c|-w|-a|-q|-f)"; +const TAILOR_CLI_VALUE_FLAGS = new Set([ + "--env-file-if-exists", + "--env-file", + "--profile", + "--config", + "--workspace-id", + "--arg", + "--query", + "--file", + "-e", + "-p", + "-c", + "-w", + "-a", + "-q", + "-f", +]); const SOURCE_COMMAND_GAP = `(?:\\s+--?[\\w-]+(?:=${SOURCE_ARG_VALUE})?(?:\\s+${SOURCE_ARG_VALUE})?)*`; const SOURCE_CLI_VALUE_REFERENCE_RE = new RegExp( `(${TAILOR_CLI_VALUE_FLAG}(?:=|\\s+))(${SOURCE_ARG_VALUE})`, "g", ); +const SOURCE_CLI_COMMAND_LOOKAHEAD = `(?:${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b|\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b)`; const SOURCE_PKG_RUNNER_RE = new RegExp( - `\\b((?:npx|pnpm\\s+dlx|yarn\\s+dlx|bunx)(?:\\s+(?:-\\w+|--\\w[\\w-]*))*)\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b)`, + `\\b((?:npx|pnpm\\s+dlx|yarn\\s+dlx|bunx)(?:\\s+(?:-\\w+|--\\w[\\w-]*))*)\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_CLI_COMMAND_LOOKAHEAD})`, "g", ); const SOURCE_TAILOR_SDK_RE = new RegExp( @@ -77,6 +95,7 @@ const TAILOR_SDK_TOKEN_RE = /^tailor-sdk(@[^\s'"`;|&)]+)?$/; const CLI_ARGUMENT_CALLEE_RE = /(?:^|\.)(?:spawn|spawnSync|execFile|execFileSync|execa|execaSync)$/; const SOURCE_PACKAGE_RUNNERS = new Set(["bunx", "npx"]); const SOURCE_DLX_PACKAGE_RUNNERS = new Set(["pnpm", "yarn"]); +const NPX_PACKAGE_FLAG_CONTEXT_RE = /(?:^|[;&|]\s*)npx(?:\s+(?:-\w+|--\w[\w-]*))*\s*$/; function renameBinary(value: string): string { const withRunners = value.replace(PKG_RUNNER_RE, (_, runner: string, version?: string) => @@ -100,8 +119,11 @@ function protectSourceCliValueReferences(value: string): { const protectedValues: string[] = []; const source = value.replace( SOURCE_CLI_VALUE_REFERENCE_RE, - (match, prefix: string, arg: string) => { + (match: string, prefix: string, arg: string, offset: number) => { if (!arg.includes("tailor-sdk")) return match; + if (prefix.startsWith("-p") && NPX_PACKAGE_FLAG_CONTEXT_RE.test(value.slice(0, offset))) { + return match; + } const placeholder = `__TAILOR_SDK_SOURCE_VALUE_${protectedValues.length}__`; protectedValues.push(arg); return `${prefix}${placeholder}`; @@ -189,13 +211,6 @@ function isSyntaxOnlyNode(node: SgNode): boolean { ); } -function isStaticTailorCommandNode(node: SgNode, source: string): boolean { - const value = sourceStringContent(node, source); - return ( - value != null && TAILOR_CLI_COMMANDS.includes(value as (typeof TAILOR_CLI_COMMANDS)[number]) - ); -} - function sourceArrayElements(node: SgNode): SgNode[] { return node.children().filter((child: SgNode) => !isSyntaxOnlyNode(child)); } @@ -221,6 +236,26 @@ function firstNonOptionIndex(elements: SgNode[], start: number, source: string): return null; } +function isTailorCliValueFlag(value: string): boolean { + return TAILOR_CLI_VALUE_FLAGS.has(value.split("=", 1)[0]!); +} + +function hasTailorCommandAfter(elements: SgNode[], start: number, source: string): boolean { + for (let index = start; index < elements.length; index += 1) { + const value = sourceStringContent(elements[index]!, source); + if (value == null) return false; + if (TAILOR_CLI_COMMANDS.includes(value as (typeof TAILOR_CLI_COMMANDS)[number])) return true; + if (value.startsWith("-")) { + if (isTailorCliValueFlag(value) && !value.includes("=")) { + index += 1; + } + continue; + } + return false; + } + return false; +} + function isPackageRunnerPackageArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() !== "array") return false; @@ -238,8 +273,7 @@ function isPackageRunnerPackageArgument(node: SgNode, source: string): boolean { const elements = sourceArrayElements(parent); const index = nodeIndex(elements, node); - const next = elements[index + 1]; - if (index < 0 || next == null || !isStaticTailorCommandNode(next, source)) return false; + if (index < 0 || !hasTailorCommandAfter(elements, index + 1, source)) return false; if (SOURCE_PACKAGE_RUNNERS.has(executable)) { return firstNonOptionIndex(elements, 0, source) === index; @@ -261,8 +295,7 @@ function isCliBinaryArgument(node: SgNode, source: string): boolean { if (parent?.kind() === "array") { const elements = sourceArrayElements(parent); const index = nodeIndex(elements, node); - const next = elements[index + 1]; - return index === 0 && next != null && isStaticTailorCommandNode(next, source); + return index === 0 && hasTailorCommandAfter(elements, index + 1, source); } if (parent?.kind() !== "arguments") return false; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 60e59a4c2..667cbc69d 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -2,8 +2,10 @@ const script = "tailor deploy"; const spawned = spawn("tailor", ["deploy"]); const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); +const npxProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "dev", "login"]); const pnpmDlxSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor", "deploy"]); +const arrayCommand = ["tailor", "--profile", "dev", "deploy"]; const docs = ( <>

package tailor-sdk is installed

diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 491770506..595da9d7f 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -2,8 +2,10 @@ const script = "tailor-sdk deploy"; const spawned = spawn("tailor-sdk", ["deploy"]); const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); +const npxProfileSpawned = spawn("npx", ["tailor-sdk", "--profile", "dev", "login"]); const pnpmDlxSpawned = spawn("pnpm", ["dlx", "tailor-sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); +const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; const docs = ( <>

package tailor-sdk is installed

diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index cb351d464..5c33c7c52 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -9,6 +9,7 @@ const dynamicPnpmCommand = `pnpm tailor ${subcommand}`; const indentedDynamicCommand = ` tailor ${subcommand}`; const latest = "npx @tailor-platform/sdk@latest login"; +const npxPackageFlag = "npx -p @tailor-platform/sdk tailor login"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; const mixedPackageAndCommand = "Install tailor-sdk before running tailor deploy"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index d6eb4210e..b84e8e065 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -9,6 +9,7 @@ const dynamicPnpmCommand = `pnpm tailor-sdk ${subcommand}`; const indentedDynamicCommand = ` tailor-sdk ${subcommand}`; const latest = "npx tailor-sdk@latest login"; +const npxPackageFlag = "npx -p tailor-sdk tailor-sdk login"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; const mixedPackageAndCommand = "Install tailor-sdk before running tailor-sdk deploy"; From 6ccf31715dea49312be63914ab5a4371ad8d3967 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 17:40:09 +0900 Subject: [PATCH 307/618] fix(sdk-codemod): avoid silent source command misses --- .../v2/rename-bin/scripts/transform.ts | 72 +++++++++++++++++-- .../tests/source-js-string/expected.js | 1 + .../tests/source-js-string/input.js | 1 + .../tests/source-template/expected.ts | 4 ++ .../rename-bin/tests/source-template/input.ts | 4 ++ packages/sdk-codemod/src/registry.test.ts | 1 + packages/sdk-codemod/src/registry.ts | 3 + 7 files changed, 80 insertions(+), 6 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 636b9095b..41b7a6dd6 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -74,27 +74,43 @@ const SOURCE_CLI_VALUE_REFERENCE_RE = new RegExp( `(${TAILOR_CLI_VALUE_FLAG}(?:=|\\s+))(${SOURCE_ARG_VALUE})`, "g", ); -const SOURCE_CLI_COMMAND_LOOKAHEAD = `(?:${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b|\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b)`; +const SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD = "\\s+(?:--help|-h|--version|-v)\\b"; +const SOURCE_DIRECT_INVOCATION_LOOKAHEAD = `(?:${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b|${SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD})`; +const SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD = `(?:${SOURCE_DIRECT_INVOCATION_LOOKAHEAD}|\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_DIRECT_INVOCATION_LOOKAHEAD})`; +const SOURCE_DYNAMIC_OPTION_VALUE_LOOKAHEAD = `(?=\\s+${TAILOR_CLI_VALUE_FLAG}\\s*$)`; +const SOURCE_NPX_PACKAGE_FLAG_VALUE_RE = new RegExp( + `\\b(npx(?:\\s+(?!(?:-p|--package)=)(?:-\\w+|--\\w[\\w-]*))*\\s+(?:-p|--package)=)tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_DIRECT_INVOCATION_LOOKAHEAD})`, + "g", +); const SOURCE_PKG_RUNNER_RE = new RegExp( - `\\b((?:npx|pnpm\\s+dlx|yarn\\s+dlx|bunx)(?:\\s+(?:-\\w+|--\\w[\\w-]*))*)\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_CLI_COMMAND_LOOKAHEAD})`, + `\\b((?:npx|pnpm\\s+dlx|yarn\\s+dlx|bunx)(?:\\s+(?:-\\w+|--\\w[\\w-]*))*)\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})`, "g", ); const SOURCE_TAILOR_SDK_RE = new RegExp( - `(? + version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}@tailor-platform/sdk`, + ); + const withRunners = withNpxPackageValues.replace( SOURCE_PKG_RUNNER_RE, (_, runner: string, version?: string) => version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, @@ -155,11 +176,21 @@ function renameSourceCommandText(value: string): string { (_, runner: string, version?: string) => version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, ); - const updated = withDynamicRunners.replace( + const withDynamicOptionRunners = withDynamicRunners.replace( + SOURCE_DYNAMIC_OPTION_PKG_RUNNER_RE, + (_, runner: string, version?: string) => + version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, + ); + const withDynamicCommands = withDynamicOptionRunners.replace( SOURCE_DYNAMIC_TAILOR_SDK_RE, (_match, prefix: string, version?: string) => version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}tailor`, ); + const updated = withDynamicCommands.replace( + SOURCE_DYNAMIC_OPTION_TAILOR_SDK_RE, + (_match, prefix: string, version?: string) => + version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}tailor`, + ); return restoreSourceCliValueReferences(updated, protectedValue.protectedValues); } @@ -290,12 +321,41 @@ function isPackageRunnerPackageArgument(node: SgNode, source: string): boolean { return false; } +function isPackageManagerExecBinaryArgument(node: SgNode, source: string): boolean { + const parent = node.parent(); + if (parent?.kind() !== "array") return false; + + const argumentsNode = parent.parent(); + if (argumentsNode?.kind() !== "arguments") return false; + const callee = callExpressionCalleeText(argumentsNode); + if (!CLI_ARGUMENT_CALLEE_RE.test(callee ?? "")) return false; + + const callArgs = sourceArrayElements(argumentsNode); + const executableNode = callArgs[0]; + if (executableNode == null) return false; + const executable = sourceStringContent(executableNode, source); + if (executable == null || !SOURCE_EXEC_PACKAGE_MANAGERS.has(executable)) return false; + + const elements = sourceArrayElements(parent); + const index = nodeIndex(elements, node); + if (index < 0 || !hasTailorCommandAfter(elements, index + 1, source)) return false; + + const execIndex = firstNonOptionIndex(elements, 0, source); + if (execIndex == null || sourceStringContent(elements[execIndex]!, source) !== "exec") { + return false; + } + return firstNonOptionIndex(elements, execIndex + 1, source) === index; +} + function isCliBinaryArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() === "array") { const elements = sourceArrayElements(parent); const index = nodeIndex(elements, node); - return index === 0 && hasTailorCommandAfter(elements, index + 1, source); + return ( + (index === 0 && hasTailorCommandAfter(elements, index + 1, source)) || + isPackageManagerExecBinaryArgument(node, source) + ); } if (parent?.kind() !== "arguments") return false; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 667cbc69d..542583094 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -5,6 +5,7 @@ const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", " const npxProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "dev", "login"]); const pnpmDlxSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor", "deploy"]); +const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor", "deploy"]); const arrayCommand = ["tailor", "--profile", "dev", "deploy"]; const docs = ( <> diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 595da9d7f..d7452afc9 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -5,6 +5,7 @@ const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["tailor-sdk", "--profile", "dev", "login"]); const pnpmDlxSpawned = spawn("pnpm", ["dlx", "tailor-sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); +const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor-sdk", "deploy"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; const docs = ( <> diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 5c33c7c52..4d26309b7 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -3,13 +3,17 @@ const setup = `pnpm tailor staticwebsite deploy --name ${siteName}`; const deploy = "tailor deploy"; const envFileDeploy = "tailor --env-file .env deploy"; const profileValue = "tailor --profile tailor-sdk deploy"; +const help = "tailor --help"; +const npxVersion = "npx @tailor-platform/sdk --version"; const generated = "Run tailor generate after changes"; const dynamicCommand = `tailor ${subcommand}`; const dynamicPnpmCommand = `pnpm tailor ${subcommand}`; +const dynamicProfileCommand = `tailor --profile ${profile} deploy`; const indentedDynamicCommand = ` tailor ${subcommand}`; const latest = "npx @tailor-platform/sdk@latest login"; const npxPackageFlag = "npx -p @tailor-platform/sdk tailor login"; +const npxPackageFlagEquals = "npx --package=@tailor-platform/sdk tailor login"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; const mixedPackageAndCommand = "Install tailor-sdk before running tailor deploy"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index b84e8e065..72e9a5153 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -3,13 +3,17 @@ const setup = `pnpm tailor-sdk staticwebsite deploy --name ${siteName}`; const deploy = "tailor-sdk deploy"; const envFileDeploy = "tailor-sdk --env-file .env deploy"; const profileValue = "tailor-sdk --profile tailor-sdk deploy"; +const help = "tailor-sdk --help"; +const npxVersion = "npx tailor-sdk --version"; const generated = "Run tailor-sdk generate after changes"; const dynamicCommand = `tailor-sdk ${subcommand}`; const dynamicPnpmCommand = `pnpm tailor-sdk ${subcommand}`; +const dynamicProfileCommand = `tailor-sdk --profile ${profile} deploy`; const indentedDynamicCommand = ` tailor-sdk ${subcommand}`; const latest = "npx tailor-sdk@latest login"; const npxPackageFlag = "npx -p tailor-sdk tailor-sdk login"; +const npxPackageFlagEquals = "npx --package=tailor-sdk tailor-sdk login"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; const mixedPackageAndCommand = "Install tailor-sdk before running tailor-sdk deploy"; diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 140b5b028..7989ef2e2 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -63,6 +63,7 @@ describe("getApplicableCodemods", () => { ); expect(renameBin?.filePatterns).toEqual(expect.arrayContaining([sourcePattern])); + expect(renameBin?.sourceStringLegacyPatterns).toHaveLength(1); const matches = picomatch(renameBin?.filePatterns ?? [], { dot: true }); expect(matches("packages/app/frontend/e2e/global-setup.ts")).toBe(true); expect(matches("tailor.d.ts")).toBe(true); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 84e6dd9a4..729a57e98 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -536,6 +536,9 @@ export const allCodemods: CodemodPackage[] = [ "**/*.md", ], legacyPatterns: ["tailor-sdk"], + sourceStringLegacyPatterns: [ + /(? Date: Sat, 27 Jun 2026 17:56:01 +0900 Subject: [PATCH 308/618] refactor(sdk-codemod): limit source package runner rewrites --- .../v2/rename-bin/scripts/transform.ts | 95 ++++--------------- .../tests/source-js-string/expected.js | 10 +- .../tests/source-template/expected.ts | 8 +- 3 files changed, 29 insertions(+), 84 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 41b7a6dd6..10688c1ab 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -76,28 +76,11 @@ const SOURCE_CLI_VALUE_REFERENCE_RE = new RegExp( ); const SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD = "\\s+(?:--help|-h|--version|-v)\\b"; const SOURCE_DIRECT_INVOCATION_LOOKAHEAD = `(?:${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b|${SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD})`; -const SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD = `(?:${SOURCE_DIRECT_INVOCATION_LOOKAHEAD}|\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_DIRECT_INVOCATION_LOOKAHEAD})`; const SOURCE_DYNAMIC_OPTION_VALUE_LOOKAHEAD = `(?=\\s+${TAILOR_CLI_VALUE_FLAG}\\s*$)`; -const SOURCE_NPX_PACKAGE_FLAG_VALUE_RE = new RegExp( - `\\b(npx(?:\\s+(?!(?:-p|--package)=)(?:-\\w+|--\\w[\\w-]*))*\\s+(?:-p|--package)=)tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_DIRECT_INVOCATION_LOOKAHEAD})`, - "g", -); -const SOURCE_PKG_RUNNER_RE = new RegExp( - `\\b((?:npx|pnpm\\s+dlx|yarn\\s+dlx|bunx)(?:\\s+(?:-\\w+|--\\w[\\w-]*))*)\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})`, - "g", -); const SOURCE_TAILOR_SDK_RE = new RegExp( `(? @@ -122,12 +104,6 @@ function renameBinary(value: string): string { ); } -function renamePackageName(value: string): string { - return value.replace(TAILOR_SDK_TOKEN_RE, (_match, version?: string) => - version ? `@tailor-platform/sdk${version}` : "@tailor-platform/sdk", - ); -} - function protectSourceCliValueReferences(value: string): { source: string; protectedValues: string[]; @@ -135,11 +111,8 @@ function protectSourceCliValueReferences(value: string): { const protectedValues: string[] = []; const source = value.replace( SOURCE_CLI_VALUE_REFERENCE_RE, - (match: string, prefix: string, arg: string, offset: number) => { + (match: string, prefix: string, arg: string) => { if (!arg.includes("tailor-sdk")) return match; - if (prefix.startsWith("-p") && NPX_PACKAGE_FLAG_CONTEXT_RE.test(value.slice(0, offset))) { - return match; - } const placeholder = `__TAILOR_SDK_SOURCE_VALUE_${protectedValues.length}__`; protectedValues.push(arg); return `${prefix}${placeholder}`; @@ -157,31 +130,15 @@ function restoreSourceCliValueReferences(value: string, protectedValues: string[ } function renameSourceCommandText(value: string): string { + if (SOURCE_PACKAGE_RUNNER_CONTEXT_RE.test(value) || SOURCE_ESCAPED_QUOTE_RE.test(value)) { + return value; + } const protectedValue = protectSourceCliValueReferences(value); - const withNpxPackageValues = protectedValue.source.replace( - SOURCE_NPX_PACKAGE_FLAG_VALUE_RE, - (_match, prefix: string, version?: string) => - version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}@tailor-platform/sdk`, - ); - const withRunners = withNpxPackageValues.replace( - SOURCE_PKG_RUNNER_RE, - (_, runner: string, version?: string) => - version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, + const withCommands = protectedValue.source.replace( + SOURCE_TAILOR_SDK_RE, + (_match, version?: string) => (version ? `@tailor-platform/sdk${version}` : "tailor"), ); - const withCommands = withRunners.replace(SOURCE_TAILOR_SDK_RE, (_match, version?: string) => - version ? `@tailor-platform/sdk${version}` : "tailor", - ); - const withDynamicRunners = withCommands.replace( - SOURCE_DYNAMIC_PKG_RUNNER_RE, - (_, runner: string, version?: string) => - version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, - ); - const withDynamicOptionRunners = withDynamicRunners.replace( - SOURCE_DYNAMIC_OPTION_PKG_RUNNER_RE, - (_, runner: string, version?: string) => - version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, - ); - const withDynamicCommands = withDynamicOptionRunners.replace( + const withDynamicCommands = withCommands.replace( SOURCE_DYNAMIC_TAILOR_SDK_RE, (_match, prefix: string, version?: string) => version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}tailor`, @@ -287,7 +244,7 @@ function hasTailorCommandAfter(elements: SgNode[], start: number, source: string return false; } -function isPackageRunnerPackageArgument(node: SgNode, source: string): boolean { +function isPackageRunnerArrayArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() !== "array") return false; @@ -302,23 +259,12 @@ function isPackageRunnerPackageArgument(node: SgNode, source: string): boolean { const executable = sourceStringContent(executableNode, source); if (executable == null) return false; - const elements = sourceArrayElements(parent); - const index = nodeIndex(elements, node); - if (index < 0 || !hasTailorCommandAfter(elements, index + 1, source)) return false; - - if (SOURCE_PACKAGE_RUNNERS.has(executable)) { - return firstNonOptionIndex(elements, 0, source) === index; - } + if (executable === "bunx" || executable === "npx") return true; + if (executable !== "pnpm" && executable !== "yarn") return false; - if (SOURCE_DLX_PACKAGE_RUNNERS.has(executable)) { - const dlxIndex = firstNonOptionIndex(elements, 0, source); - if (dlxIndex == null || sourceStringContent(elements[dlxIndex]!, source) !== "dlx") { - return false; - } - return firstNonOptionIndex(elements, dlxIndex + 1, source) === index; - } - - return false; + const elements = sourceArrayElements(parent); + const dlxIndex = firstNonOptionIndex(elements, 0, source); + return dlxIndex != null && sourceStringContent(elements[dlxIndex]!, source) === "dlx"; } function isPackageManagerExecBinaryArgument(node: SgNode, source: string): boolean { @@ -350,6 +296,7 @@ function isPackageManagerExecBinaryArgument(node: SgNode, source: string): boole function isCliBinaryArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() === "array") { + if (isPackageRunnerArrayArgument(node, source)) return false; const elements = sourceArrayElements(parent); const index = nodeIndex(elements, node); return ( @@ -374,11 +321,9 @@ function pushSourceStringEdit( const end = range.end.index - 1; const text = source.slice(start, end); const replacement = - TAILOR_SDK_TOKEN_RE.test(text) && isPackageRunnerPackageArgument(node, source) - ? renamePackageName(text) - : TAILOR_SDK_TOKEN_RE.test(text) && isCliBinaryArgument(node, source) - ? renameBinary(text) - : renameSourceCommandText(text); + TAILOR_SDK_TOKEN_RE.test(text) && isCliBinaryArgument(node, source) + ? renameBinary(text) + : renameSourceCommandText(text); if (replacement !== text) { edits.push([start, end, replacement]); } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 542583094..9a7e5d0a3 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -1,9 +1,9 @@ const script = "tailor deploy"; const spawned = spawn("tailor", ["deploy"]); -const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); -const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); -const npxProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "dev", "login"]); -const pnpmDlxSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk", "login"]); +const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); +const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); +const npxProfileSpawned = spawn("npx", ["tailor-sdk", "--profile", "dev", "login"]); +const pnpmDlxSpawned = spawn("pnpm", ["dlx", "tailor-sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor", "deploy"]); const arrayCommand = ["tailor", "--profile", "dev", "deploy"]; @@ -11,6 +11,6 @@ const docs = ( <>

package tailor-sdk is installed

tailor deploy - npx @tailor-platform/sdk@latest login + npx tailor-sdk@latest login ); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 4d26309b7..61da9f89f 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -4,16 +4,16 @@ const deploy = "tailor deploy"; const envFileDeploy = "tailor --env-file .env deploy"; const profileValue = "tailor --profile tailor-sdk deploy"; const help = "tailor --help"; -const npxVersion = "npx @tailor-platform/sdk --version"; +const npxVersion = "npx tailor-sdk --version"; const generated = "Run tailor generate after changes"; const dynamicCommand = `tailor ${subcommand}`; const dynamicPnpmCommand = `pnpm tailor ${subcommand}`; const dynamicProfileCommand = `tailor --profile ${profile} deploy`; const indentedDynamicCommand = ` tailor ${subcommand}`; -const latest = "npx @tailor-platform/sdk@latest login"; -const npxPackageFlag = "npx -p @tailor-platform/sdk tailor login"; -const npxPackageFlagEquals = "npx --package=@tailor-platform/sdk tailor login"; +const latest = "npx tailor-sdk@latest login"; +const npxPackageFlag = "npx -p tailor-sdk tailor-sdk login"; +const npxPackageFlagEquals = "npx --package=tailor-sdk tailor-sdk login"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; const mixedPackageAndCommand = "Install tailor-sdk before running tailor deploy"; From 00ed50b8610e23d1bc875fac95fa397feb81bcaa Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 18:07:42 +0900 Subject: [PATCH 309/618] fix(sdk-codemod): keep source runner rewrites conservative --- .../v2/rename-bin/scripts/transform.ts | 26 +++++++++++++------ .../tests/source-js-string/expected.js | 8 +++--- .../tests/source-js-string/input.js | 2 ++ .../tests/source-template/expected.ts | 4 +-- packages/sdk-codemod/src/registry.ts | 2 +- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 10688c1ab..6707924ff 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -77,6 +77,10 @@ const SOURCE_CLI_VALUE_REFERENCE_RE = new RegExp( const SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD = "\\s+(?:--help|-h|--version|-v)\\b"; const SOURCE_DIRECT_INVOCATION_LOOKAHEAD = `(?:${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b|${SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD})`; const SOURCE_DYNAMIC_OPTION_VALUE_LOOKAHEAD = `(?=\\s+${TAILOR_CLI_VALUE_FLAG}\\s*$)`; +const SOURCE_SIMPLE_PKG_RUNNER_RE = new RegExp( + `\\b((?:npx|pnpm\\s+dlx|yarn\\s+dlx|bunx))\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_DIRECT_INVOCATION_LOOKAHEAD})`, + "g", +); const SOURCE_TAILOR_SDK_RE = new RegExp( `(? + version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, + ); + if (SOURCE_PACKAGE_RUNNER_CONTEXT_RE.test(withSimplePackageRunners)) { + return restoreSourceCliValueReferences( + withSimplePackageRunners, + protectedValue.protectedValues, + ); + } + const withCommands = withSimplePackageRunners.replace( SOURCE_TAILOR_SDK_RE, (_match, version?: string) => (version ? `@tailor-platform/sdk${version}` : "tailor"), ); @@ -297,12 +312,7 @@ function isCliBinaryArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() === "array") { if (isPackageRunnerArrayArgument(node, source)) return false; - const elements = sourceArrayElements(parent); - const index = nodeIndex(elements, node); - return ( - (index === 0 && hasTailorCommandAfter(elements, index + 1, source)) || - isPackageManagerExecBinaryArgument(node, source) - ); + return isPackageManagerExecBinaryArgument(node, source); } if (parent?.kind() !== "arguments") return false; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 9a7e5d0a3..98ce50e15 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -4,13 +4,15 @@ const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["tailor-sdk", "--profile", "dev", "login"]); const pnpmDlxSpawned = spawn("pnpm", ["dlx", "tailor-sdk", "login"]); -const pnpmBinarySpawned = spawn("pnpm", ["tailor", "deploy"]); +const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor", "deploy"]); -const arrayCommand = ["tailor", "--profile", "dev", "deploy"]; +const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; +const npxArgs = ["tailor-sdk", "login"]; +spawn("npx", npxArgs); const docs = ( <>

package tailor-sdk is installed

tailor deploy - npx tailor-sdk@latest login + npx @tailor-platform/sdk@latest login ); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index d7452afc9..63272c3cc 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -7,6 +7,8 @@ const pnpmDlxSpawned = spawn("pnpm", ["dlx", "tailor-sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor-sdk", "deploy"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; +const npxArgs = ["tailor-sdk", "login"]; +spawn("npx", npxArgs); const docs = ( <>

package tailor-sdk is installed

diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 61da9f89f..dfc4cbb56 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -4,14 +4,14 @@ const deploy = "tailor deploy"; const envFileDeploy = "tailor --env-file .env deploy"; const profileValue = "tailor --profile tailor-sdk deploy"; const help = "tailor --help"; -const npxVersion = "npx tailor-sdk --version"; +const npxVersion = "npx @tailor-platform/sdk --version"; const generated = "Run tailor generate after changes"; const dynamicCommand = `tailor ${subcommand}`; const dynamicPnpmCommand = `pnpm tailor ${subcommand}`; const dynamicProfileCommand = `tailor --profile ${profile} deploy`; const indentedDynamicCommand = ` tailor ${subcommand}`; -const latest = "npx tailor-sdk@latest login"; +const latest = "npx @tailor-platform/sdk@latest login"; const npxPackageFlag = "npx -p tailor-sdk tailor-sdk login"; const npxPackageFlagEquals = "npx --package=tailor-sdk tailor-sdk login"; const packageName = "tailor-sdk"; diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 729a57e98..2b6eced7f 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -537,7 +537,7 @@ export const allCodemods: CodemodPackage[] = [ ], legacyPatterns: ["tailor-sdk"], sourceStringLegacyPatterns: [ - /(? Date: Sat, 27 Jun 2026 18:24:19 +0900 Subject: [PATCH 310/618] fix(sdk-codemod): handle source package runners safely --- .../v2/rename-bin/scripts/transform.ts | 121 ++++++++++++++---- .../tests/source-js-string/expected.js | 8 +- .../tests/source-template/expected.ts | 6 +- .../rename-bin/tests/source-template/input.ts | 2 + 4 files changed, 108 insertions(+), 29 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 6707924ff..67b6f7288 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -76,9 +76,14 @@ const SOURCE_CLI_VALUE_REFERENCE_RE = new RegExp( ); const SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD = "\\s+(?:--help|-h|--version|-v)\\b"; const SOURCE_DIRECT_INVOCATION_LOOKAHEAD = `(?:${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b|${SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD})`; +const SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD = `(?:${SOURCE_DIRECT_INVOCATION_LOOKAHEAD}|\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_DIRECT_INVOCATION_LOOKAHEAD})`; const SOURCE_DYNAMIC_OPTION_VALUE_LOOKAHEAD = `(?=\\s+${TAILOR_CLI_VALUE_FLAG}\\s*$)`; -const SOURCE_SIMPLE_PKG_RUNNER_RE = new RegExp( - `\\b((?:npx|pnpm\\s+dlx|yarn\\s+dlx|bunx))\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_DIRECT_INVOCATION_LOOKAHEAD})`, +const SOURCE_PKG_RUNNER_RE = new RegExp( + `\\b((?:npx|pnpm\\s+dlx|yarn\\s+dlx|bunx)(?:(?!\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})\\s+${SOURCE_ARG_VALUE})*)\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})`, + "g", +); +const SOURCE_NPX_PACKAGE_FLAG_VALUE_RE = new RegExp( + `\\b(npx(?:(?!\\s+(?:-p|--package)(?:=|\\s+))\\s+${SOURCE_ARG_VALUE})*\\s+(?:-p|--package)(?:=|\\s+))tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_DIRECT_INVOCATION_LOOKAHEAD})`, "g", ); const SOURCE_TAILOR_SDK_RE = new RegExp( @@ -96,8 +101,11 @@ const SOURCE_DYNAMIC_OPTION_TAILOR_SDK_RE = new RegExp( const TAILOR_SDK_TOKEN_RE = /^tailor-sdk(@[^\s'"`;|&)]+)?$/; const CLI_ARGUMENT_CALLEE_RE = /(?:^|\.)(?:spawn|spawnSync|execFile|execFileSync|execa|execaSync)$/; const SOURCE_EXEC_PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn"]); -const SOURCE_PACKAGE_RUNNER_CONTEXT_RE = /\b(?:npx|pnpm\s+dlx|yarn\s+dlx|bunx)\b/; -const SOURCE_ESCAPED_QUOTE_RE = /\\["']/; +const SOURCE_PACKAGE_RUNNERS = new Set(["bunx", "npx"]); +const SOURCE_DLX_PACKAGE_RUNNERS = new Set(["pnpm", "yarn"]); +const SOURCE_PACKAGE_FLAG_RE = /^(?:-p|--package)(?:=.*)?$/; +const NPX_PACKAGE_FLAG_CONTEXT_RE = /(?:^|[;&|]\s*)npx(?:\s+(?:-\w+|--\w[\w-]*))*\s*$/; +const SOURCE_ESCAPED_QUOTED_VALUE_RE = /\\"(?:\\\\.|[^"\\])*\\"|\\'(?:\\\\.|[^'\\])*\\'/g; function renameBinary(value: string): string { const withRunners = value.replace(PKG_RUNNER_RE, (_, runner: string, version?: string) => @@ -108,15 +116,30 @@ function renameBinary(value: string): string { ); } +function renamePackageName(value: string): string { + return value.replace(TAILOR_SDK_TOKEN_RE, (_match, version?: string) => + version ? `@tailor-platform/sdk${version}` : "@tailor-platform/sdk", + ); +} + function protectSourceCliValueReferences(value: string): { source: string; protectedValues: string[]; } { const protectedValues: string[] = []; - const source = value.replace( + const withEscapedQuotedValues = value.replace(SOURCE_ESCAPED_QUOTED_VALUE_RE, (match) => { + if (!match.includes("tailor-sdk")) return match; + const placeholder = `__TAILOR_SDK_SOURCE_VALUE_${protectedValues.length}__`; + protectedValues.push(match); + return placeholder; + }); + const source = withEscapedQuotedValues.replace( SOURCE_CLI_VALUE_REFERENCE_RE, - (match: string, prefix: string, arg: string) => { + (match: string, prefix: string, arg: string, offset: number) => { if (!arg.includes("tailor-sdk")) return match; + if (prefix.startsWith("-p") && NPX_PACKAGE_FLAG_CONTEXT_RE.test(value.slice(0, offset))) { + return match; + } const placeholder = `__TAILOR_SDK_SOURCE_VALUE_${protectedValues.length}__`; protectedValues.push(arg); return `${prefix}${placeholder}`; @@ -134,22 +157,22 @@ function restoreSourceCliValueReferences(value: string, protectedValues: string[ } function renameSourceCommandText(value: string): string { - if (SOURCE_ESCAPED_QUOTE_RE.test(value)) { - return value; - } const protectedValue = protectSourceCliValueReferences(value); - const withSimplePackageRunners = protectedValue.source.replace( - SOURCE_SIMPLE_PKG_RUNNER_RE, - (_match, runner: string, version?: string) => - version ? `${runner} @tailor-platform/sdk${version}` : `${runner} @tailor-platform/sdk`, + const withPackageFlagValues = protectedValue.source.replace( + SOURCE_NPX_PACKAGE_FLAG_VALUE_RE, + (_match, prefix: string, version?: string) => + version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}@tailor-platform/sdk`, ); - if (SOURCE_PACKAGE_RUNNER_CONTEXT_RE.test(withSimplePackageRunners)) { - return restoreSourceCliValueReferences( - withSimplePackageRunners, - protectedValue.protectedValues, - ); - } - const withCommands = withSimplePackageRunners.replace( + const withPackageRunners = withPackageFlagValues.replace( + SOURCE_PKG_RUNNER_RE, + (match: string, runner: string, version?: string) => { + if (/\s(?:-p|--package)(?:=|\s|$)/.test(runner)) return match; + return version + ? `${runner} @tailor-platform/sdk${version}` + : `${runner} @tailor-platform/sdk`; + }, + ); + const withCommands = withPackageRunners.replace( SOURCE_TAILOR_SDK_RE, (_match, version?: string) => (version ? `@tailor-platform/sdk${version}` : "tailor"), ); @@ -259,6 +282,24 @@ function hasTailorCommandAfter(elements: SgNode[], start: number, source: string return false; } +function hasNpxPackageFlag(elements: SgNode[], source: string): boolean { + return elements.some((element) => { + const value = sourceStringContent(element, source); + return value != null && SOURCE_PACKAGE_FLAG_RE.test(value); + }); +} + +function firstTailorPackageIndex(elements: SgNode[], start: number, source: string): number | null { + for (let index = start; index < elements.length; index += 1) { + const value = sourceStringContent(elements[index]!, source); + if (value == null) return null; + if (TAILOR_SDK_TOKEN_RE.test(value) && hasTailorCommandAfter(elements, index + 1, source)) { + return index; + } + } + return null; +} + function isPackageRunnerArrayArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() !== "array") return false; @@ -282,6 +323,38 @@ function isPackageRunnerArrayArgument(node: SgNode, source: string): boolean { return dlxIndex != null && sourceStringContent(elements[dlxIndex]!, source) === "dlx"; } +function isPackageRunnerPackageArgument(node: SgNode, source: string): boolean { + const parent = node.parent(); + if (parent?.kind() !== "array" || !isPackageRunnerArrayArgument(node, source)) return false; + + const argumentsNode = parent.parent(); + if (argumentsNode?.kind() !== "arguments") return false; + const callArgs = sourceArrayElements(argumentsNode); + const executableNode = callArgs[0]; + if (executableNode == null) return false; + const executable = sourceStringContent(executableNode, source); + if (executable == null) return false; + + const elements = sourceArrayElements(parent); + if (hasNpxPackageFlag(elements, source)) return false; + const index = nodeIndex(elements, node); + if (index < 0) return false; + + if (SOURCE_PACKAGE_RUNNERS.has(executable)) { + return firstTailorPackageIndex(elements, 0, source) === index; + } + + if (SOURCE_DLX_PACKAGE_RUNNERS.has(executable)) { + const dlxIndex = firstNonOptionIndex(elements, 0, source); + if (dlxIndex == null || sourceStringContent(elements[dlxIndex]!, source) !== "dlx") { + return false; + } + return firstTailorPackageIndex(elements, dlxIndex + 1, source) === index; + } + + return false; +} + function isPackageManagerExecBinaryArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() !== "array") return false; @@ -331,9 +404,11 @@ function pushSourceStringEdit( const end = range.end.index - 1; const text = source.slice(start, end); const replacement = - TAILOR_SDK_TOKEN_RE.test(text) && isCliBinaryArgument(node, source) - ? renameBinary(text) - : renameSourceCommandText(text); + TAILOR_SDK_TOKEN_RE.test(text) && isPackageRunnerPackageArgument(node, source) + ? renamePackageName(text) + : TAILOR_SDK_TOKEN_RE.test(text) && isCliBinaryArgument(node, source) + ? renameBinary(text) + : renameSourceCommandText(text); if (replacement !== text) { edits.push([start, end, replacement]); } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 98ce50e15..97d7363f6 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -1,9 +1,9 @@ const script = "tailor deploy"; const spawned = spawn("tailor", ["deploy"]); -const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); -const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); -const npxProfileSpawned = spawn("npx", ["tailor-sdk", "--profile", "dev", "login"]); -const pnpmDlxSpawned = spawn("pnpm", ["dlx", "tailor-sdk", "login"]); +const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); +const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); +const npxProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "dev", "login"]); +const pnpmDlxSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor", "deploy"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index dfc4cbb56..576133ae7 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -12,8 +12,10 @@ const dynamicProfileCommand = `tailor --profile ${profile} deploy`; const indentedDynamicCommand = ` tailor ${subcommand}`; const latest = "npx @tailor-platform/sdk@latest login"; -const npxPackageFlag = "npx -p tailor-sdk tailor-sdk login"; -const npxPackageFlagEquals = "npx --package=tailor-sdk tailor-sdk login"; +const latestWithRunnerOption = "npx --yes @tailor-platform/sdk@latest login"; +const npxPackageFlag = "npx -p @tailor-platform/sdk tailor login"; +const npxPackageFlagEquals = "npx --package=@tailor-platform/sdk tailor login"; +const escapedArg = "tailor --arg \"tailor-sdk deploy\" deploy"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; const mixedPackageAndCommand = "Install tailor-sdk before running tailor deploy"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 72e9a5153..0809cd666 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -12,8 +12,10 @@ const dynamicProfileCommand = `tailor-sdk --profile ${profile} deploy`; const indentedDynamicCommand = ` tailor-sdk ${subcommand}`; const latest = "npx tailor-sdk@latest login"; +const latestWithRunnerOption = "npx --yes tailor-sdk@latest login"; const npxPackageFlag = "npx -p tailor-sdk tailor-sdk login"; const npxPackageFlagEquals = "npx --package=tailor-sdk tailor-sdk login"; +const escapedArg = "tailor-sdk --arg \"tailor-sdk deploy\" deploy"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; const mixedPackageAndCommand = "Install tailor-sdk before running tailor-sdk deploy"; From 109f8b15429fdabbef66a4b494160e7d8b8dbdfe Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 18:36:44 +0900 Subject: [PATCH 311/618] fix(sdk-codemod): preserve argv command values --- .../v2/rename-bin/scripts/transform.ts | 76 +++++++++++++++++-- .../tests/source-js-string/expected.js | 5 ++ .../tests/source-js-string/input.js | 5 ++ 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 67b6f7288..b8aa6c3e4 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -49,6 +49,7 @@ const TAILOR_CLI_COMMANDS = [ "workflow", "workspace", ] as const; +const TAILOR_CLI_STANDALONE_FLAGS = new Set(["--help", "-h", "--version", "-v"]); const TAILOR_CLI_COMMAND_PATTERN = `(?:${TAILOR_CLI_COMMANDS.join("|")})`; const TAILOR_CLI_VALUE_FLAG = "(?:--env-file-if-exists|--env-file|--profile|--config|--workspace-id|--arg|--query|--file|-e|-p|-c|-w|-a|-q|-f)"; @@ -270,6 +271,7 @@ function hasTailorCommandAfter(elements: SgNode[], start: number, source: string for (let index = start; index < elements.length; index += 1) { const value = sourceStringContent(elements[index]!, source); if (value == null) return false; + if (TAILOR_CLI_STANDALONE_FLAGS.has(value)) return true; if (TAILOR_CLI_COMMANDS.includes(value as (typeof TAILOR_CLI_COMMANDS)[number])) return true; if (value.startsWith("-")) { if (isTailorCliValueFlag(value) && !value.includes("=")) { @@ -289,6 +291,31 @@ function hasNpxPackageFlag(elements: SgNode[], source: string): boolean { }); } +function isNpxSplitPackageFlagValue(elements: SgNode[], index: number, source: string): boolean { + const previous = elements[index - 1]; + if (previous == null) return false; + const previousValue = sourceStringContent(previous, source); + return previousValue === "-p" || previousValue === "--package"; +} + +function sourcePackageFlagReplacement( + node: SgNode, + source: string, +): { text: string; replacement: string } | null { + const text = sourceStringContent(node, source); + if (text == null) return null; + const match = /^(?-p=|--package=)tailor-sdk(?@[^\s'"`;|&)]+)?$/.exec(text); + if (match?.groups == null) return null; + const parent = node.parent(); + if (parent?.kind() !== "array" || !isPackageRunnerArrayArgument(node, source)) return null; + return { + text, + replacement: `${match.groups.prefix}${renamePackageName( + `tailor-sdk${match.groups.version ?? ""}`, + )}`, + }; +} + function firstTailorPackageIndex(elements: SgNode[], start: number, source: string): number | null { for (let index = start; index < elements.length; index += 1) { const value = sourceStringContent(elements[index]!, source); @@ -336,10 +363,13 @@ function isPackageRunnerPackageArgument(node: SgNode, source: string): boolean { if (executable == null) return false; const elements = sourceArrayElements(parent); - if (hasNpxPackageFlag(elements, source)) return false; const index = nodeIndex(elements, node); if (index < 0) return false; + if (hasNpxPackageFlag(elements, source)) { + return isNpxSplitPackageFlagValue(elements, index, source); + } + if (SOURCE_PACKAGE_RUNNERS.has(executable)) { return firstTailorPackageIndex(elements, 0, source) === index; } @@ -355,6 +385,22 @@ function isPackageRunnerPackageArgument(node: SgNode, source: string): boolean { return false; } +function isPackageRunnerCommandBinaryArgument(node: SgNode, source: string): boolean { + const parent = node.parent(); + if (parent?.kind() !== "array" || !isPackageRunnerArrayArgument(node, source)) return false; + + const elements = sourceArrayElements(parent); + const index = nodeIndex(elements, node); + if (index < 0 || !hasNpxPackageFlag(elements, source)) return false; + + const text = sourceStringContent(node, source); + return ( + text != null && + TAILOR_SDK_TOKEN_RE.test(text) && + hasTailorCommandAfter(elements, index + 1, source) + ); +} + function isPackageManagerExecBinaryArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() !== "array") return false; @@ -384,6 +430,7 @@ function isPackageManagerExecBinaryArgument(node: SgNode, source: string): boole function isCliBinaryArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() === "array") { + if (isPackageRunnerCommandBinaryArgument(node, source)) return true; if (isPackageRunnerArrayArgument(node, source)) return false; return isPackageManagerExecBinaryArgument(node, source); } @@ -394,6 +441,18 @@ function isCliBinaryArgument(node: SgNode, source: string): boolean { return args[0] != null && nodeRangeKey(args[0]) === nodeRangeKey(node); } +function isCliValueArgument(node: SgNode, source: string): boolean { + const parent = node.parent(); + if (parent?.kind() !== "array") return false; + const elements = sourceArrayElements(parent); + const index = nodeIndex(elements, node); + if (index <= 0) return false; + const previousValue = sourceStringContent(elements[index - 1]!, source); + return ( + previousValue != null && isTailorCliValueFlag(previousValue) && !previousValue.includes("=") + ); +} + function pushSourceStringEdit( edits: Array<[number, number, string]>, source: string, @@ -403,12 +462,17 @@ function pushSourceStringEdit( const start = range.start.index + 1; const end = range.end.index - 1; const text = source.slice(start, end); + const packageFlagReplacement = sourcePackageFlagReplacement(node, source); const replacement = - TAILOR_SDK_TOKEN_RE.test(text) && isPackageRunnerPackageArgument(node, source) - ? renamePackageName(text) - : TAILOR_SDK_TOKEN_RE.test(text) && isCliBinaryArgument(node, source) - ? renameBinary(text) - : renameSourceCommandText(text); + packageFlagReplacement != null + ? packageFlagReplacement.replacement + : TAILOR_SDK_TOKEN_RE.test(text) && isPackageRunnerPackageArgument(node, source) + ? renamePackageName(text) + : isCliValueArgument(node, source) + ? text + : TAILOR_SDK_TOKEN_RE.test(text) && isCliBinaryArgument(node, source) + ? renameBinary(text) + : renameSourceCommandText(text); if (replacement !== text) { edits.push([start, end, replacement]); } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 97d7363f6..f2a325c19 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -1,11 +1,16 @@ const script = "tailor deploy"; const spawned = spawn("tailor", ["deploy"]); +const argSpawned = spawn("tailor", ["--arg", "tailor-sdk deploy", "deploy"]); const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "dev", "login"]); +const npxVersionSpawned = spawn("npx", ["@tailor-platform/sdk", "--version"]); +const npxPackageFlagSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "tailor", "login"]); +const npxPackageEqualsSpawned = spawn("npx", ["--package=@tailor-platform/sdk", "tailor", "login"]); const pnpmDlxSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor", "deploy"]); +const pnpmExecHelpSpawned = spawn("pnpm", ["exec", "tailor", "--help"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; const npxArgs = ["tailor-sdk", "login"]; spawn("npx", npxArgs); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 63272c3cc..b6b5759e2 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -1,11 +1,16 @@ const script = "tailor-sdk deploy"; const spawned = spawn("tailor-sdk", ["deploy"]); +const argSpawned = spawn("tailor-sdk", ["--arg", "tailor-sdk deploy", "deploy"]); const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["tailor-sdk", "--profile", "dev", "login"]); +const npxVersionSpawned = spawn("npx", ["tailor-sdk", "--version"]); +const npxPackageFlagSpawned = spawn("npx", ["-p", "tailor-sdk", "tailor-sdk", "login"]); +const npxPackageEqualsSpawned = spawn("npx", ["--package=tailor-sdk", "tailor-sdk", "login"]); const pnpmDlxSpawned = spawn("pnpm", ["dlx", "tailor-sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor-sdk", "deploy"]); +const pnpmExecHelpSpawned = spawn("pnpm", ["exec", "tailor-sdk", "--help"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; const npxArgs = ["tailor-sdk", "login"]; spawn("npx", npxArgs); From 534c3f75457eae426b9a067cd6d40d3b418b616d Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 18:56:18 +0900 Subject: [PATCH 312/618] fix(sdk-codemod): handle dynamic package runners --- .../v2/rename-bin/scripts/transform.ts | 25 ++++++++++++++----- .../tests/source-js-string/expected.js | 5 ++++ .../tests/source-js-string/input.js | 5 ++++ .../tests/source-template/expected.ts | 6 +++++ .../rename-bin/tests/source-template/input.ts | 6 +++++ 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index b8aa6c3e4..315d434d3 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -77,14 +77,19 @@ const SOURCE_CLI_VALUE_REFERENCE_RE = new RegExp( ); const SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD = "\\s+(?:--help|-h|--version|-v)\\b"; const SOURCE_DIRECT_INVOCATION_LOOKAHEAD = `(?:${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b|${SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD})`; -const SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD = `(?:${SOURCE_DIRECT_INVOCATION_LOOKAHEAD}|\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_DIRECT_INVOCATION_LOOKAHEAD})`; +const SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD = `(?:${SOURCE_DIRECT_INVOCATION_LOOKAHEAD}|\\s*$)`; +const SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD = `(?:${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD}|\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})`; const SOURCE_DYNAMIC_OPTION_VALUE_LOOKAHEAD = `(?=\\s+${TAILOR_CLI_VALUE_FLAG}\\s*$)`; const SOURCE_PKG_RUNNER_RE = new RegExp( `\\b((?:npx|pnpm\\s+dlx|yarn\\s+dlx|bunx)(?:(?!\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})\\s+${SOURCE_ARG_VALUE})*)\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})`, "g", ); const SOURCE_NPX_PACKAGE_FLAG_VALUE_RE = new RegExp( - `\\b(npx(?:(?!\\s+(?:-p|--package)(?:=|\\s+))\\s+${SOURCE_ARG_VALUE})*\\s+(?:-p|--package)(?:=|\\s+))tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_DIRECT_INVOCATION_LOOKAHEAD})`, + `\\b(npx(?:(?!\\s+(?:-p|--package)(?:=|\\s+))\\s+${SOURCE_ARG_VALUE})*\\s+(?:-p|--package)(?:=|\\s+))tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})`, + "g", +); +const SOURCE_NPX_PACKAGE_FLAG_BINARY_RE = new RegExp( + `\\b(npx(?:(?!\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})\\s+${SOURCE_ARG_VALUE})*\\s+)tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})`, "g", ); const SOURCE_TAILOR_SDK_RE = new RegExp( @@ -173,7 +178,12 @@ function renameSourceCommandText(value: string): string { : `${runner} @tailor-platform/sdk`; }, ); - const withCommands = withPackageRunners.replace( + const withPackageFlagBinaries = withPackageRunners.replace( + SOURCE_NPX_PACKAGE_FLAG_BINARY_RE, + (_match, prefix: string, version?: string) => + version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}tailor`, + ); + const withCommands = withPackageFlagBinaries.replace( SOURCE_TAILOR_SDK_RE, (_match, version?: string) => (version ? `@tailor-platform/sdk${version}` : "tailor"), ); @@ -285,7 +295,10 @@ function hasTailorCommandAfter(elements: SgNode[], start: number, source: string } function hasNpxPackageFlag(elements: SgNode[], source: string): boolean { - return elements.some((element) => { + const firstPackageIndex = firstTailorPackageIndex(elements, 0, source); + if (firstPackageIndex == null) return false; + return elements.some((element, index) => { + if (index >= firstPackageIndex) return false; const value = sourceStringContent(element, source); return value != null && SOURCE_PACKAGE_FLAG_RE.test(value); }); @@ -320,7 +333,7 @@ function firstTailorPackageIndex(elements: SgNode[], start: number, source: stri for (let index = start; index < elements.length; index += 1) { const value = sourceStringContent(elements[index]!, source); if (value == null) return null; - if (TAILOR_SDK_TOKEN_RE.test(value) && hasTailorCommandAfter(elements, index + 1, source)) { + if (TAILOR_SDK_TOKEN_RE.test(value)) { return index; } } @@ -397,7 +410,7 @@ function isPackageRunnerCommandBinaryArgument(node: SgNode, source: string): boo return ( text != null && TAILOR_SDK_TOKEN_RE.test(text) && - hasTailorCommandAfter(elements, index + 1, source) + !isNpxSplitPackageFlagValue(elements, index, source) ); } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index f2a325c19..2c111f66e 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -4,10 +4,15 @@ const argSpawned = spawn("tailor", ["--arg", "tailor-sdk deploy", "deploy"]); const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "dev", "login"]); +const npxShortProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "-p", "dev", "login"]); const npxVersionSpawned = spawn("npx", ["@tailor-platform/sdk", "--version"]); +const npxDynamicSpawned = spawn("npx", ["@tailor-platform/sdk", subcommand]); const npxPackageFlagSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "tailor", "login"]); const npxPackageEqualsSpawned = spawn("npx", ["--package=@tailor-platform/sdk", "tailor", "login"]); +const npxPackageFlagDynamicSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "tailor", subcommand]); +const npxPackageEqualsDynamicSpawned = spawn("npx", ["--package=@tailor-platform/sdk", "tailor", subcommand]); const pnpmDlxSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk", "login"]); +const pnpmDlxDynamicSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor", "deploy"]); const pnpmExecHelpSpawned = spawn("pnpm", ["exec", "tailor", "--help"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index b6b5759e2..ce709bb6f 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -4,10 +4,15 @@ const argSpawned = spawn("tailor-sdk", ["--arg", "tailor-sdk deploy", "deploy"]) const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["tailor-sdk", "--profile", "dev", "login"]); +const npxShortProfileSpawned = spawn("npx", ["tailor-sdk", "-p", "dev", "login"]); const npxVersionSpawned = spawn("npx", ["tailor-sdk", "--version"]); +const npxDynamicSpawned = spawn("npx", ["tailor-sdk", subcommand]); const npxPackageFlagSpawned = spawn("npx", ["-p", "tailor-sdk", "tailor-sdk", "login"]); const npxPackageEqualsSpawned = spawn("npx", ["--package=tailor-sdk", "tailor-sdk", "login"]); +const npxPackageFlagDynamicSpawned = spawn("npx", ["-p", "tailor-sdk", "tailor-sdk", subcommand]); +const npxPackageEqualsDynamicSpawned = spawn("npx", ["--package=tailor-sdk", "tailor-sdk", subcommand]); const pnpmDlxSpawned = spawn("pnpm", ["dlx", "tailor-sdk", "login"]); +const pnpmDlxDynamicSpawned = spawn("pnpm", ["dlx", "tailor-sdk"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor-sdk", "deploy"]); const pnpmExecHelpSpawned = spawn("pnpm", ["exec", "tailor-sdk", "--help"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 576133ae7..52e4f9391 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -12,9 +12,15 @@ const dynamicProfileCommand = `tailor --profile ${profile} deploy`; const indentedDynamicCommand = ` tailor ${subcommand}`; const latest = "npx @tailor-platform/sdk@latest login"; +const latestOnly = "npx @tailor-platform/sdk@latest"; const latestWithRunnerOption = "npx --yes @tailor-platform/sdk@latest login"; +const dynamicNpxCommand = `npx @tailor-platform/sdk ${subcommand}`; +const dynamicBunxCommand = `bunx @tailor-platform/sdk ${subcommand}`; +const dynamicDlxCommand = `pnpm dlx @tailor-platform/sdk ${subcommand}`; const npxPackageFlag = "npx -p @tailor-platform/sdk tailor login"; const npxPackageFlagEquals = "npx --package=@tailor-platform/sdk tailor login"; +const npxPackageFlagDynamic = `npx -p @tailor-platform/sdk tailor ${subcommand}`; +const npxPackageEqualsDynamic = `npx --package=@tailor-platform/sdk tailor ${subcommand}`; const escapedArg = "tailor --arg \"tailor-sdk deploy\" deploy"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 0809cd666..654f10874 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -12,9 +12,15 @@ const dynamicProfileCommand = `tailor-sdk --profile ${profile} deploy`; const indentedDynamicCommand = ` tailor-sdk ${subcommand}`; const latest = "npx tailor-sdk@latest login"; +const latestOnly = "npx tailor-sdk@latest"; const latestWithRunnerOption = "npx --yes tailor-sdk@latest login"; +const dynamicNpxCommand = `npx tailor-sdk ${subcommand}`; +const dynamicBunxCommand = `bunx tailor-sdk ${subcommand}`; +const dynamicDlxCommand = `pnpm dlx tailor-sdk ${subcommand}`; const npxPackageFlag = "npx -p tailor-sdk tailor-sdk login"; const npxPackageFlagEquals = "npx --package=tailor-sdk tailor-sdk login"; +const npxPackageFlagDynamic = `npx -p tailor-sdk tailor-sdk ${subcommand}`; +const npxPackageEqualsDynamic = `npx --package=tailor-sdk tailor-sdk ${subcommand}`; const escapedArg = "tailor-sdk --arg \"tailor-sdk deploy\" deploy"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; From f8c7525f4bdd9599c465f09d6e6f82561ae4bbc2 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 19:11:43 +0900 Subject: [PATCH 313/618] fix(sdk-codemod): support runner option variants --- .../v2/rename-bin/scripts/transform.ts | 19 +++++++++++++------ .../tests/source-js-string/expected.js | 2 ++ .../tests/source-js-string/input.js | 2 ++ .../tests/source-template/expected.ts | 6 ++++++ .../rename-bin/tests/source-template/input.ts | 6 ++++++ 5 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 315d434d3..c2742131f 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -2,13 +2,18 @@ import { parse, Lang } from "@ast-grep/napi"; import * as path from "pathe"; import type { SgNode } from "@ast-grep/napi"; +const SOURCE_ARG_VALUE = `(?:[^\\s'"\`;|&]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`; +const PACKAGE_RUNNER_COMMAND = `(?:npx|bunx|(?:pnpm|yarn)(?:\\s+(?:-\\w+|--\\w[\\w-]*(?:=${SOURCE_ARG_VALUE})?))*\\s+dlx)`; + // Package-runner forms (`npx`, `pnpm dlx`, `yarn dlx`, `bunx`) resolve npm package // names, so `tailor-sdk@...` must become `@tailor-platform/sdk@...` — rewriting // to `tailor@...` would download the unrelated CSS Sprites Generator instead. // Optional flags (e.g. `-y`, `--yes`) between the runner and the package name are // captured as part of the runner group so the replacement preserves them. -const PKG_RUNNER_RE = - /\b((?:npx|pnpm\s+dlx|yarn\s+dlx|bunx)(?:\s+(?:-\w+|--\w[\w-]*))*)\s+tailor-sdk(?![\w-])(@[^\s'"`;|&)]+)?/g; +const PKG_RUNNER_RE = new RegExp( + `\\b(${PACKAGE_RUNNER_COMMAND}(?:\\s+(?:-\\w+|--\\w[\\w-]*))*)\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?`, + "g", +); // Match the `tailor-sdk` binary, optionally with a version pin (`@latest`, // `@2.0.0`, etc.). Lookbehind excludes `.tailor-sdk` (preceded by `.`) and @@ -16,7 +21,6 @@ const PKG_RUNNER_RE = // (e.g. `tailor-sdk-skills`) to avoid partial-match rewrites. const TAILOR_SDK_RE = /(? Date: Sat, 27 Jun 2026 19:24:54 +0900 Subject: [PATCH 314/618] fix(sdk-codemod): preserve template runner context --- .../v2/rename-bin/scripts/transform.ts | 44 +++++++++++++++++-- .../tests/source-template/expected.ts | 3 ++ .../rename-bin/tests/source-template/input.ts | 3 ++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index c2742131f..3e1ca33b7 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -3,7 +3,7 @@ import * as path from "pathe"; import type { SgNode } from "@ast-grep/napi"; const SOURCE_ARG_VALUE = `(?:[^\\s'"\`;|&]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`; -const PACKAGE_RUNNER_COMMAND = `(?:npx|bunx|(?:pnpm|yarn)(?:\\s+(?:-\\w+|--\\w[\\w-]*(?:=${SOURCE_ARG_VALUE})?))*\\s+dlx)`; +const PACKAGE_RUNNER_COMMAND = `(?:npx|bunx|(?:pnpm|yarn)(?:\\s+(?:-\\w+|--\\w[\\w-]*)(?:=${SOURCE_ARG_VALUE})?(?:\\s+(?!dlx\\b|-)${SOURCE_ARG_VALUE})?)*\\s+dlx)`; // Package-runner forms (`npx`, `pnpm dlx`, `yarn dlx`, `bunx`) resolve npm package // names, so `tailor-sdk@...` must become `@tailor-platform/sdk@...` — rewriting @@ -79,9 +79,10 @@ const SOURCE_CLI_VALUE_REFERENCE_RE = new RegExp( `(${TAILOR_CLI_VALUE_FLAG}(?:=|\\s+))(${SOURCE_ARG_VALUE})`, "g", ); +const SOURCE_TEMPLATE_EXPR_PLACEHOLDER = "__TAILOR_SDK_TEMPLATE_EXPR_\\d+__"; const SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD = "\\s+(?:--help|-h|--version|-v)\\b"; const SOURCE_DIRECT_INVOCATION_LOOKAHEAD = `(?:${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b|${SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD})`; -const SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD = `(?:${SOURCE_DIRECT_INVOCATION_LOOKAHEAD}|\\s*$)`; +const SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD = `(?:${SOURCE_DIRECT_INVOCATION_LOOKAHEAD}|\\s+${SOURCE_TEMPLATE_EXPR_PLACEHOLDER}\\s*$|\\s*$)`; const SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD = `(?:${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD}|\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})`; const SOURCE_DYNAMIC_OPTION_VALUE_LOOKAHEAD = `(?=\\s+${TAILOR_CLI_VALUE_FLAG}(?:=|\\s+)\\s*$)`; const SOURCE_PKG_RUNNER_RE = new RegExp( @@ -101,7 +102,7 @@ const SOURCE_TAILOR_SDK_RE = new RegExp( "g", ); const SOURCE_DYNAMIC_TAILOR_SDK_RE = new RegExp( - `(^\\s*|[;&|]\\s*|\\b(?:pnpm|npm|yarn)(?:\\s+exec)?\\s+)tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=\\s+$)`, + `(^\\s*|[;&|]\\s*|\\b(?:pnpm|npm|yarn)(?:\\s+exec)?\\s+)tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=\\s+(?:${SOURCE_TEMPLATE_EXPR_PLACEHOLDER}\\s*)?$)`, "g", ); const SOURCE_DYNAMIC_OPTION_TAILOR_SDK_RE = new RegExp( @@ -498,6 +499,41 @@ function pushSourceStringEdit( } } +function pushTemplateStringEdit( + edits: Array<[number, number, string]>, + source: string, + node: SgNode, +): void { + const range = node.range(); + const start = range.start.index + 1; + const end = range.end.index - 1; + let text = source.slice(start, end); + const substitutions: Array<{ placeholder: string; text: string }> = []; + + const substitutionNodes = node + .children() + .filter((child: SgNode) => child.kind() === "template_substitution"); + for (const child of substitutionNodes.toReversed()) { + const childRange = child.range(); + const childStart = childRange.start.index - start; + const childEnd = childRange.end.index - start; + const placeholder = `__TAILOR_SDK_TEMPLATE_EXPR_${substitutions.length}__`; + substitutions.push({ + placeholder, + text: source.slice(childRange.start.index, childRange.end.index), + }); + text = `${text.slice(0, childStart)}${placeholder}${text.slice(childEnd)}`; + } + + let replacement = renameSourceCommandText(text); + for (const substitution of substitutions) { + replacement = replacement.replaceAll(substitution.placeholder, substitution.text); + } + if (replacement !== source.slice(start, end)) { + edits.push([start, end, replacement]); + } +} + function transformSourceFile(source: string, filePath: string): string | null { let root: SgNode; try { @@ -529,6 +565,8 @@ function transformSourceFile(source: string, filePath: string): string | null { pushSourceStringEdit(edits, source, node); return; } + pushTemplateStringEdit(edits, source, node); + return; } for (const child of node.children()) { diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index eb10211a7..f9084130b 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -17,9 +17,12 @@ const latest = "npx @tailor-platform/sdk@latest login"; const latestOnly = "npx @tailor-platform/sdk@latest"; const latestWithRunnerOption = "npx --yes @tailor-platform/sdk@latest login"; const dynamicNpxCommand = `npx @tailor-platform/sdk ${subcommand}`; +const dynamicNpxRegistryCommand = `npx --registry ${registry} @tailor-platform/sdk login`; const dynamicBunxCommand = `bunx @tailor-platform/sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx @tailor-platform/sdk ${subcommand}`; const pnpmDlxWithOption = "pnpm --silent dlx @tailor-platform/sdk login"; +const pnpmDlxWithOptionValue = "pnpm --filter app dlx @tailor-platform/sdk login"; +const dynamicPnpmDlxWithOptionValue = `pnpm --filter ${app} dlx @tailor-platform/sdk login`; const yarnDlxWithOption = "yarn --quiet dlx @tailor-platform/sdk login"; const npxPackageFlag = "npx -p @tailor-platform/sdk tailor login"; const npxPackageFlagEquals = "npx --package=@tailor-platform/sdk tailor login"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 4c6b0e6eb..7b9e579e2 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -17,9 +17,12 @@ const latest = "npx tailor-sdk@latest login"; const latestOnly = "npx tailor-sdk@latest"; const latestWithRunnerOption = "npx --yes tailor-sdk@latest login"; const dynamicNpxCommand = `npx tailor-sdk ${subcommand}`; +const dynamicNpxRegistryCommand = `npx --registry ${registry} tailor-sdk login`; const dynamicBunxCommand = `bunx tailor-sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx tailor-sdk ${subcommand}`; const pnpmDlxWithOption = "pnpm --silent dlx tailor-sdk login"; +const pnpmDlxWithOptionValue = "pnpm --filter app dlx tailor-sdk login"; +const dynamicPnpmDlxWithOptionValue = `pnpm --filter ${app} dlx tailor-sdk login`; const yarnDlxWithOption = "yarn --quiet dlx tailor-sdk login"; const npxPackageFlag = "npx -p tailor-sdk tailor-sdk login"; const npxPackageFlagEquals = "npx --package=tailor-sdk tailor-sdk login"; From a82d7d017d431f2f17e81907f91334f05d7dc7e7 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 19:39:21 +0900 Subject: [PATCH 315/618] fix(sdk-codemod): avoid runner residual false positives --- .../v2/rename-bin/scripts/transform.ts | 17 ++++- .../tests/source-js-string/expected.js | 2 + .../tests/source-js-string/input.js | 2 + .../tests/source-template/expected.ts | 2 + .../rename-bin/tests/source-template/input.ts | 2 + packages/sdk-codemod/src/registry.test.ts | 3 + packages/sdk-codemod/src/registry.ts | 70 ++++++++++++++++++- 7 files changed, 94 insertions(+), 4 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 3e1ca33b7..e234b4052 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -114,6 +114,7 @@ const CLI_ARGUMENT_CALLEE_RE = /(?:^|\.)(?:spawn|spawnSync|execFile|execFileSync const SOURCE_EXEC_PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn"]); const SOURCE_PACKAGE_RUNNERS = new Set(["bunx", "npx"]); const SOURCE_DLX_PACKAGE_RUNNERS = new Set(["pnpm", "yarn"]); +const PACKAGE_MANAGER_OPTION_VALUE_FLAGS = new Set(["--filter", "-F", "--dir", "-C", "--cwd"]); const SOURCE_PACKAGE_FLAG_RE = /^(?:-p|--package)(?:=.*)?$/; const NPX_OPTION_WITH_VALUE = "(?:--registry|--cache|--userconfig|--prefix)"; const NPX_PACKAGE_FLAG_CONTEXT_RE = new RegExp( @@ -277,6 +278,9 @@ function firstNonOptionIndex(elements: SgNode[], start: number, source: string): const value = sourceStringContent(elements[index]!, source); if (value == null) return null; if (!value.startsWith("-")) return index; + if (PACKAGE_MANAGER_OPTION_VALUE_FLAGS.has(value.split("=", 1)[0]!) && !value.includes("=")) { + index += 1; + } } return null; } @@ -520,7 +524,9 @@ function pushTemplateStringEdit( const placeholder = `__TAILOR_SDK_TEMPLATE_EXPR_${substitutions.length}__`; substitutions.push({ placeholder, - text: source.slice(childRange.start.index, childRange.end.index), + text: transformTemplateSubstitutionText( + source.slice(childRange.start.index, childRange.end.index), + ), }); text = `${text.slice(0, childStart)}${placeholder}${text.slice(childEnd)}`; } @@ -534,6 +540,15 @@ function pushTemplateStringEdit( } } +function transformTemplateSubstitutionText(value: string): string { + if (!value.includes("tailor-sdk") || !value.startsWith("${") || !value.endsWith("}")) { + return value; + } + const expression = value.slice(2, -1); + const transformed = transformSourceFile(expression, "template-expression.ts"); + return transformed == null ? value : `\${${transformed}}`; +} + function transformSourceFile(source: string, filePath: string): string | null { let root: SgNode; try { diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index a5d9077cb..2579591dd 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -14,6 +14,8 @@ const npxPackageEqualsDynamicSpawned = spawn("npx", ["--package=@tailor-platform const pnpmDlxSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk", "login"]); const pnpmDlxDynamicSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk"]); const pnpmDlxOptionSpawned = spawn("pnpm", ["--silent", "dlx", "@tailor-platform/sdk", "login"]); +const pnpmDlxSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "dlx", "@tailor-platform/sdk", "login"]); +const pnpmExecSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "exec", "tailor", "deploy"]); const yarnDlxOptionSpawned = spawn("yarn", ["--quiet", "dlx", "@tailor-platform/sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor", "deploy"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index f4963f82b..69d9c2c57 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -14,6 +14,8 @@ const npxPackageEqualsDynamicSpawned = spawn("npx", ["--package=tailor-sdk", "ta const pnpmDlxSpawned = spawn("pnpm", ["dlx", "tailor-sdk", "login"]); const pnpmDlxDynamicSpawned = spawn("pnpm", ["dlx", "tailor-sdk"]); const pnpmDlxOptionSpawned = spawn("pnpm", ["--silent", "dlx", "tailor-sdk", "login"]); +const pnpmDlxSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "dlx", "tailor-sdk", "login"]); +const pnpmExecSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "exec", "tailor-sdk", "deploy"]); const yarnDlxOptionSpawned = spawn("yarn", ["--quiet", "dlx", "tailor-sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor-sdk", "deploy"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index f9084130b..da0c23efd 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -24,6 +24,8 @@ const pnpmDlxWithOption = "pnpm --silent dlx @tailor-platform/sdk login"; const pnpmDlxWithOptionValue = "pnpm --filter app dlx @tailor-platform/sdk login"; const dynamicPnpmDlxWithOptionValue = `pnpm --filter ${app} dlx @tailor-platform/sdk login`; const yarnDlxWithOption = "yarn --quiet dlx @tailor-platform/sdk login"; +const nestedCommand = `run ${"tailor deploy"}`; +const nestedTailorCommand = `tailor deploy ${"tailor login"}`; const npxPackageFlag = "npx -p @tailor-platform/sdk tailor login"; const npxPackageFlagEquals = "npx --package=@tailor-platform/sdk tailor login"; const npxPackageFlagDynamic = `npx -p @tailor-platform/sdk tailor ${subcommand}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 7b9e579e2..04c0b2e54 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -24,6 +24,8 @@ const pnpmDlxWithOption = "pnpm --silent dlx tailor-sdk login"; const pnpmDlxWithOptionValue = "pnpm --filter app dlx tailor-sdk login"; const dynamicPnpmDlxWithOptionValue = `pnpm --filter ${app} dlx tailor-sdk login`; const yarnDlxWithOption = "yarn --quiet dlx tailor-sdk login"; +const nestedCommand = `run ${"tailor-sdk deploy"}`; +const nestedTailorCommand = `tailor deploy ${"tailor-sdk login"}`; const npxPackageFlag = "npx -p tailor-sdk tailor-sdk login"; const npxPackageFlagEquals = "npx --package=tailor-sdk tailor-sdk login"; const npxPackageFlagDynamic = `npx -p tailor-sdk tailor-sdk ${subcommand}`; diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 7989ef2e2..a524489b0 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -64,6 +64,9 @@ describe("getApplicableCodemods", () => { expect(renameBin?.filePatterns).toEqual(expect.arrayContaining([sourcePattern])); expect(renameBin?.sourceStringLegacyPatterns).toHaveLength(1); + const sourceStringPattern = renameBin?.sourceStringLegacyPatterns?.[0] as RegExp; + expect(sourceStringPattern.test("tailor-sdk deploy")).toBe(true); + expect(sourceStringPattern.test("tailor --profile tailor-sdk deploy")).toBe(false); const matches = picomatch(renameBin?.filePatterns ?? [], { dot: true }); expect(matches("packages/app/frontend/e2e/global-setup.ts")).toBe(true); expect(matches("tailor.d.ts")).toBe(true); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 2b6eced7f..b1eaafea8 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -4,6 +4,72 @@ import { lt, gte, valid } from "semver"; import type { CodemodPackage } from "./types"; const CODEMODS_ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "codemods"); +const RENAME_BIN_SOURCE_VALUE_FLAGS = [ + "--env-file-if-exists", + "--env-file", + "--profile", + "--config", + "--workspace-id", + "--arg", + "--query", + "--file", + "-e", + "-p", + "-c", + "-w", + "-a", + "-q", + "-f", +]; +const RENAME_BIN_SOURCE_COMMANDS = [ + "api", + "apply", + "authconnection", + "completion", + "crash-report", + "crashreport", + "deploy", + "executor", + "function", + "generate", + "init", + "login", + "logout", + "machineuser", + "oauth2client", + "open", + "organization", + "profile", + "query", + "remove", + "secret", + "setup", + "show", + "skills", + "staticwebsite", + "tailordb", + "upgrade", + "user", + "workflow", + "workspace", +]; + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +const RENAME_BIN_SOURCE_VALUE_GUARDS = RENAME_BIN_SOURCE_VALUE_FLAGS.flatMap((flag) => { + const escaped = escapeRegExp(flag); + return [`(? Date: Sat, 27 Jun 2026 19:57:59 +0900 Subject: [PATCH 316/618] fix(sdk-codemod): scope source residual checks --- .../v2/rename-bin/scripts/transform.ts | 8 +- .../tests/source-template/expected.ts | 2 + .../rename-bin/tests/source-template/input.ts | 2 + packages/sdk-codemod/src/runner.test.ts | 29 ++++++++ packages/sdk-codemod/src/runner.ts | 73 ++++++++++++++++++- 5 files changed, 110 insertions(+), 4 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index e234b4052..933ce707b 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -90,7 +90,7 @@ const SOURCE_PKG_RUNNER_RE = new RegExp( "g", ); const SOURCE_NPX_PACKAGE_FLAG_VALUE_RE = new RegExp( - `\\b(npx(?:(?!\\s+(?:-p|--package)(?:=|\\s+))\\s+${SOURCE_ARG_VALUE})*\\s+(?:-p|--package)(?:=|\\s+))tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})`, + `\\b(npx(?:(?!\\s+(?:-p|--package)(?:=|\\s+))\\s+${SOURCE_ARG_VALUE})*\\s+(?:-p|--package)(?:=|\\s+))tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})`, "g", ); const SOURCE_NPX_PACKAGE_FLAG_BINARY_RE = new RegExp( @@ -189,8 +189,10 @@ function renameSourceCommandText(value: string): string { ); const withPackageFlagBinaries = withPackageRunners.replace( SOURCE_NPX_PACKAGE_FLAG_BINARY_RE, - (_match, prefix: string, version?: string) => - version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}tailor`, + (match: string, prefix: string, version?: string) => { + if (/(?:^|\s)(?:-p|--package)\s+$/.test(prefix)) return match; + return version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}tailor`; + }, ); const withCommands = withPackageFlagBinaries.replace( SOURCE_TAILOR_SDK_RE, diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index da0c23efd..feb762621 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -27,6 +27,8 @@ const yarnDlxWithOption = "yarn --quiet dlx @tailor-platform/sdk login"; const nestedCommand = `run ${"tailor deploy"}`; const nestedTailorCommand = `tailor deploy ${"tailor login"}`; const npxPackageFlag = "npx -p @tailor-platform/sdk tailor login"; +const npxPackageSplitOnly = "npx --package @tailor-platform/sdk"; +const npxPackageSplitHelp = "npx -p @tailor-platform/sdk --help"; const npxPackageFlagEquals = "npx --package=@tailor-platform/sdk tailor login"; const npxPackageFlagDynamic = `npx -p @tailor-platform/sdk tailor ${subcommand}`; const npxPackageEqualsDynamic = `npx --package=@tailor-platform/sdk tailor ${subcommand}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 04c0b2e54..2540cfc6c 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -27,6 +27,8 @@ const yarnDlxWithOption = "yarn --quiet dlx tailor-sdk login"; const nestedCommand = `run ${"tailor-sdk deploy"}`; const nestedTailorCommand = `tailor deploy ${"tailor-sdk login"}`; const npxPackageFlag = "npx -p tailor-sdk tailor-sdk login"; +const npxPackageSplitOnly = "npx --package tailor-sdk"; +const npxPackageSplitHelp = "npx -p tailor-sdk --help"; const npxPackageFlagEquals = "npx --package=tailor-sdk tailor-sdk login"; const npxPackageFlagDynamic = `npx -p tailor-sdk tailor-sdk ${subcommand}`; const npxPackageEqualsDynamic = `npx --package=tailor-sdk tailor-sdk ${subcommand}`; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 684839098..9923c028c 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -515,6 +515,35 @@ describe("runCodemods", () => { ]); }); + test("keeps source string residual checks inside each literal", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "commands.ts"), + [ + 'const packageName = "tailor-sdk";', + 'const command = "deploy";', + 'spawn("tailor", ["--arg", "tailor-sdk deploy", "deploy"]);', + ].join("\n"), + "utf-8", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: [/tailor-sdk(?=\s+deploy)/], + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toEqual([]); + }); + test("flags files matching a suspicious pattern for LLM review", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 57f249209..ef4dd3a10 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -60,6 +60,24 @@ const MASKED_SOURCE_NODE_KINDS: ReadonlySet> = new Se "string_fragment", "jsx_text", ]); +const SOURCE_STRING_FRAGMENT_SEPARATOR = "\u0000"; +const SOURCE_VALUE_FLAGS = new Set([ + "--env-file-if-exists", + "--env-file", + "--profile", + "--config", + "--workspace-id", + "--arg", + "--query", + "--file", + "-e", + "-p", + "-c", + "-w", + "-a", + "-q", + "-f", +]); function shouldSkipDirectory(name: string): boolean { return EXCLUDE_DIRS.has(name) || (name.startsWith(".") && !ALLOWED_DOT_DIRS.has(name)); @@ -161,6 +179,7 @@ function sourceStringContentForResidualMatching(relative: string, content: strin const fragments: string[] = []; const visit = (node: SgNode): void => { if (node.kind() === "string_fragment") { + if (isSourceValueArgument(node, content)) return; fragments.push(node.text()); return; } @@ -169,7 +188,59 @@ function sourceStringContentForResidualMatching(relative: string, content: strin } }; visit(root); - return fragments.join("\n"); + return fragments.join(SOURCE_STRING_FRAGMENT_SEPARATOR); +} + +function isSyntaxOnlyNode(node: SgNode): boolean { + const kind = node.kind(); + return ( + kind === "[" || + kind === "]" || + kind === "(" || + kind === ")" || + kind === "," || + kind === "comment" + ); +} + +function sourceArrayElements(node: SgNode): SgNode[] { + return node.children().filter((child: SgNode) => !isSyntaxOnlyNode(child)); +} + +function nodeRangeKey(node: SgNode): string { + const range = node.range(); + return `${range.start.index}:${range.end.index}`; +} + +function sourceStringNodeContent(node: SgNode, source: string): string | null { + const kind = node.kind(); + if (kind !== "string" && kind !== "template_string") return null; + if ( + kind === "template_string" && + node.children().some((child: SgNode) => child.kind() === "template_substitution") + ) { + return null; + } + const range = node.range(); + return source.slice(range.start.index + 1, range.end.index - 1); +} + +function isSourceValueArgument(fragment: SgNode, source: string): boolean { + const stringNode = fragment.parent(); + if (stringNode == null) return false; + const parent = stringNode.parent(); + if (parent?.kind() !== "array") return false; + + const elements = sourceArrayElements(parent); + const index = elements.findIndex((element) => nodeRangeKey(element) === nodeRangeKey(stringNode)); + if (index <= 0) return false; + + const previous = sourceStringNodeContent(elements[index - 1]!, source); + return ( + previous != null && + SOURCE_VALUE_FLAGS.has(previous.split("=", 1)[0]!) && + !previous.includes("=") + ); } function sourceLang(relative: string): Lang { From fa96526c40d958eaf6f0cc7b79215bb9ac5c2f18 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 20:16:31 +0900 Subject: [PATCH 317/618] fix(sdk-codemod): keep runner package positions --- .../v2/rename-bin/scripts/transform.ts | 114 ++++++++++++++---- .../tests/source-js-string/expected.js | 3 + .../tests/source-js-string/input.js | 3 + .../tests/source-template/expected.ts | 2 + .../rename-bin/tests/source-template/input.ts | 2 + 5 files changed, 101 insertions(+), 23 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 933ce707b..b0c9f5965 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -53,7 +53,6 @@ const TAILOR_CLI_COMMANDS = [ "workflow", "workspace", ] as const; -const TAILOR_CLI_STANDALONE_FLAGS = new Set(["--help", "-h", "--version", "-v"]); const TAILOR_CLI_COMMAND_PATTERN = `(?:${TAILOR_CLI_COMMANDS.join("|")})`; const TAILOR_CLI_VALUE_FLAG = "(?:--env-file-if-exists|--env-file|--profile|--config|--workspace-id|--arg|--query|--file|-e|-p|-c|-w|-a|-q|-f)"; @@ -75,18 +74,20 @@ const TAILOR_CLI_VALUE_FLAGS = new Set([ "-f", ]); const SOURCE_COMMAND_GAP = `(?:\\s+--?[\\w-]+(?:=${SOURCE_ARG_VALUE})?(?:\\s+${SOURCE_ARG_VALUE})?)*`; +const SOURCE_RUNNER_OPTION_GAP = `(?:\\s+(?:-\\w+|--\\w[\\w-]*)(?:=${SOURCE_ARG_VALUE})?(?:\\s+(?!tailor-sdk(?![\\w-])|-)${SOURCE_ARG_VALUE})?)*`; const SOURCE_CLI_VALUE_REFERENCE_RE = new RegExp( `(${TAILOR_CLI_VALUE_FLAG}(?:=|\\s+))(${SOURCE_ARG_VALUE})`, "g", ); const SOURCE_TEMPLATE_EXPR_PLACEHOLDER = "__TAILOR_SDK_TEMPLATE_EXPR_\\d+__"; +const SOURCE_TEMPLATE_DYNAMIC_ARGS = `\\s+${SOURCE_TEMPLATE_EXPR_PLACEHOLDER}(?:\\s+${SOURCE_ARG_VALUE})*\\s*$`; const SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD = "\\s+(?:--help|-h|--version|-v)\\b"; const SOURCE_DIRECT_INVOCATION_LOOKAHEAD = `(?:${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b|${SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD})`; -const SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD = `(?:${SOURCE_DIRECT_INVOCATION_LOOKAHEAD}|\\s+${SOURCE_TEMPLATE_EXPR_PLACEHOLDER}\\s*$|\\s*$)`; +const SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD = `(?:${SOURCE_DIRECT_INVOCATION_LOOKAHEAD}|${SOURCE_TEMPLATE_DYNAMIC_ARGS}|\\s*$)`; const SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD = `(?:${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD}|\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})`; const SOURCE_DYNAMIC_OPTION_VALUE_LOOKAHEAD = `(?=\\s+${TAILOR_CLI_VALUE_FLAG}(?:=|\\s+)\\s*$)`; const SOURCE_PKG_RUNNER_RE = new RegExp( - `\\b(${PACKAGE_RUNNER_COMMAND}(?:(?!\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})\\s+${SOURCE_ARG_VALUE})*)\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})`, + `\\b(${PACKAGE_RUNNER_COMMAND}${SOURCE_RUNNER_OPTION_GAP})\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})`, "g", ); const SOURCE_NPX_PACKAGE_FLAG_VALUE_RE = new RegExp( @@ -102,7 +103,7 @@ const SOURCE_TAILOR_SDK_RE = new RegExp( "g", ); const SOURCE_DYNAMIC_TAILOR_SDK_RE = new RegExp( - `(^\\s*|[;&|]\\s*|\\b(?:pnpm|npm|yarn)(?:\\s+exec)?\\s+)tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=\\s+(?:${SOURCE_TEMPLATE_EXPR_PLACEHOLDER}\\s*)?$)`, + `(^\\s*|[;&|]\\s*|\\b(?:pnpm|npm|yarn)(?:\\s+exec)?\\s+)tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_TEMPLATE_DYNAMIC_ARGS}|\\s+$)`, "g", ); const SOURCE_DYNAMIC_OPTION_TAILOR_SDK_RE = new RegExp( @@ -121,6 +122,18 @@ const NPX_PACKAGE_FLAG_CONTEXT_RE = new RegExp( `(?:^|[;&|]\\s*)npx(?:\\s+(?:${NPX_OPTION_WITH_VALUE}\\s+${SOURCE_ARG_VALUE}|-\\w+|--\\w[\\w-]*(?:=${SOURCE_ARG_VALUE})?))*\\s*$`, ); const SOURCE_ESCAPED_QUOTED_VALUE_RE = /\\"(?:\\\\.|[^"\\])*\\"|\\'(?:\\\\.|[^'\\])*\\'/g; +const SOURCE_TOKEN_RE = new RegExp(SOURCE_ARG_VALUE, "g"); +const RUNNER_OPTION_VALUE_FLAGS = new Set([ + "--registry", + "--cache", + "--userconfig", + "--prefix", + "--filter", + "-F", + "--dir", + "-C", + "--cwd", +]); function renameBinary(value: string): string { const withRunners = value.replace(PKG_RUNNER_RE, (_, runner: string, version?: string) => @@ -171,6 +184,69 @@ function restoreSourceCliValueReferences(value: string, protectedValues: string[ return restored; } +function sourceTokens(value: string): string[] | null { + const tokens: string[] = []; + SOURCE_TOKEN_RE.lastIndex = 0; + let lastEnd = 0; + for (const match of value.matchAll(SOURCE_TOKEN_RE)) { + if (value.slice(lastEnd, match.index).trim() !== "") return null; + tokens.push(match[0]); + lastEnd = (match.index ?? 0) + match[0].length; + } + return value.slice(lastEnd).trim() === "" ? tokens : null; +} + +function skipsRunnerOptionValue(token: string): boolean { + return RUNNER_OPTION_VALUE_FLAGS.has(token.split("=", 1)[0]!) && !token.includes("="); +} + +function firstRunnerPackageToken(tokens: string[]): string | null { + const executable = tokens[0]; + let index: number; + if (executable === "npx" || executable === "bunx") { + index = 1; + } else if (executable === "pnpm" || executable === "yarn") { + index = 1; + for (; index < tokens.length; index += 1) { + const token = tokens[index]!; + if (token === "dlx") { + index += 1; + break; + } + if (token.startsWith("-")) { + if (skipsRunnerOptionValue(token)) index += 1; + continue; + } + return null; + } + } else { + return null; + } + + for (; index < tokens.length; index += 1) { + const token = tokens[index]!; + if (token.startsWith("-")) { + if (skipsRunnerOptionValue(token)) index += 1; + continue; + } + return token; + } + return null; +} + +function isAfterOtherPackageRunner(source: string, offset: number): boolean { + const segmentStart = Math.max( + source.lastIndexOf(";", offset - 1), + source.lastIndexOf("&", offset - 1), + source.lastIndexOf("|", offset - 1), + ); + const segment = source.slice(segmentStart + 1, offset).trim(); + const tokens = sourceTokens(segment); + if (tokens == null) return false; + const packageToken = firstRunnerPackageToken(tokens); + return packageToken != null && !TAILOR_SDK_TOKEN_RE.test(packageToken); +} + function renameSourceCommandText(value: string): string { const protectedValue = protectSourceCliValueReferences(value); const withPackageFlagValues = protectedValue.source.replace( @@ -190,13 +266,17 @@ function renameSourceCommandText(value: string): string { const withPackageFlagBinaries = withPackageRunners.replace( SOURCE_NPX_PACKAGE_FLAG_BINARY_RE, (match: string, prefix: string, version?: string) => { + if (!/\s(?:-p|--package)(?:=|\s)/.test(prefix)) return match; if (/(?:^|\s)(?:-p|--package)\s+$/.test(prefix)) return match; return version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}tailor`; }, ); const withCommands = withPackageFlagBinaries.replace( SOURCE_TAILOR_SDK_RE, - (_match, version?: string) => (version ? `@tailor-platform/sdk${version}` : "tailor"), + (match: string, version: string | undefined, offset: number, source: string) => { + if (isAfterOtherPackageRunner(source, offset)) return match; + return version ? `@tailor-platform/sdk${version}` : "tailor"; + }, ); const withDynamicCommands = withCommands.replace( SOURCE_DYNAMIC_TAILOR_SDK_RE, @@ -291,23 +371,6 @@ function isTailorCliValueFlag(value: string): boolean { return TAILOR_CLI_VALUE_FLAGS.has(value.split("=", 1)[0]!); } -function hasTailorCommandAfter(elements: SgNode[], start: number, source: string): boolean { - for (let index = start; index < elements.length; index += 1) { - const value = sourceStringContent(elements[index]!, source); - if (value == null) return false; - if (TAILOR_CLI_STANDALONE_FLAGS.has(value)) return true; - if (TAILOR_CLI_COMMANDS.includes(value as (typeof TAILOR_CLI_COMMANDS)[number])) return true; - if (value.startsWith("-")) { - if (isTailorCliValueFlag(value) && !value.includes("=")) { - index += 1; - } - continue; - } - return false; - } - return false; -} - function hasNpxPackageFlag(elements: SgNode[], source: string): boolean { const firstPackageIndex = firstTailorPackageIndex(elements, 0, source); if (firstPackageIndex == null) return false; @@ -347,9 +410,14 @@ function firstTailorPackageIndex(elements: SgNode[], start: number, source: stri for (let index = start; index < elements.length; index += 1) { const value = sourceStringContent(elements[index]!, source); if (value == null) return null; + if (value.startsWith("-")) { + if (skipsRunnerOptionValue(value)) index += 1; + continue; + } if (TAILOR_SDK_TOKEN_RE.test(value)) { return index; } + return null; } return null; } @@ -445,7 +513,7 @@ function isPackageManagerExecBinaryArgument(node: SgNode, source: string): boole const elements = sourceArrayElements(parent); const index = nodeIndex(elements, node); - if (index < 0 || !hasTailorCommandAfter(elements, index + 1, source)) return false; + if (index < 0) return false; const execIndex = firstNonOptionIndex(elements, 0, source); if (execIndex == null || sourceStringContent(elements[execIndex]!, source) !== "exec") { diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 2579591dd..f3bf1c9e3 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -7,18 +7,21 @@ const npxProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "de const npxShortProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "-p", "dev", "login"]); const npxVersionSpawned = spawn("npx", ["@tailor-platform/sdk", "--version"]); const npxDynamicSpawned = spawn("npx", ["@tailor-platform/sdk", subcommand]); +const npxOtherPackageSpawned = spawn("npx", ["foo", "tailor-sdk", "login"]); const npxPackageFlagSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "tailor", "login"]); const npxPackageEqualsSpawned = spawn("npx", ["--package=@tailor-platform/sdk", "tailor", "login"]); const npxPackageFlagDynamicSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "tailor", subcommand]); const npxPackageEqualsDynamicSpawned = spawn("npx", ["--package=@tailor-platform/sdk", "tailor", subcommand]); const pnpmDlxSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk", "login"]); const pnpmDlxDynamicSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk"]); +const pnpmDlxOtherPackageSpawned = spawn("pnpm", ["dlx", "foo", "tailor-sdk", "login"]); const pnpmDlxOptionSpawned = spawn("pnpm", ["--silent", "dlx", "@tailor-platform/sdk", "login"]); const pnpmDlxSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "dlx", "@tailor-platform/sdk", "login"]); const pnpmExecSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "exec", "tailor", "deploy"]); const yarnDlxOptionSpawned = spawn("yarn", ["--quiet", "dlx", "@tailor-platform/sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor", "deploy"]); +const pnpmExecDynamicSpawned = spawn("pnpm", ["exec", "tailor", subcommand]); const pnpmExecHelpSpawned = spawn("pnpm", ["exec", "tailor", "--help"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; const npxArgs = ["tailor-sdk", "login"]; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 69d9c2c57..8288795ed 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -7,18 +7,21 @@ const npxProfileSpawned = spawn("npx", ["tailor-sdk", "--profile", "dev", "login const npxShortProfileSpawned = spawn("npx", ["tailor-sdk", "-p", "dev", "login"]); const npxVersionSpawned = spawn("npx", ["tailor-sdk", "--version"]); const npxDynamicSpawned = spawn("npx", ["tailor-sdk", subcommand]); +const npxOtherPackageSpawned = spawn("npx", ["foo", "tailor-sdk", "login"]); const npxPackageFlagSpawned = spawn("npx", ["-p", "tailor-sdk", "tailor-sdk", "login"]); const npxPackageEqualsSpawned = spawn("npx", ["--package=tailor-sdk", "tailor-sdk", "login"]); const npxPackageFlagDynamicSpawned = spawn("npx", ["-p", "tailor-sdk", "tailor-sdk", subcommand]); const npxPackageEqualsDynamicSpawned = spawn("npx", ["--package=tailor-sdk", "tailor-sdk", subcommand]); const pnpmDlxSpawned = spawn("pnpm", ["dlx", "tailor-sdk", "login"]); const pnpmDlxDynamicSpawned = spawn("pnpm", ["dlx", "tailor-sdk"]); +const pnpmDlxOtherPackageSpawned = spawn("pnpm", ["dlx", "foo", "tailor-sdk", "login"]); const pnpmDlxOptionSpawned = spawn("pnpm", ["--silent", "dlx", "tailor-sdk", "login"]); const pnpmDlxSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "dlx", "tailor-sdk", "login"]); const pnpmExecSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "exec", "tailor-sdk", "deploy"]); const yarnDlxOptionSpawned = spawn("yarn", ["--quiet", "dlx", "tailor-sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor-sdk", "deploy"]); +const pnpmExecDynamicSpawned = spawn("pnpm", ["exec", "tailor-sdk", subcommand]); const pnpmExecHelpSpawned = spawn("pnpm", ["exec", "tailor-sdk", "--help"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; const npxArgs = ["tailor-sdk", "login"]; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index feb762621..a5b58f61e 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -7,6 +7,7 @@ const help = "tailor --help"; const npxVersion = "npx @tailor-platform/sdk --version"; const generated = "Run tailor generate after changes"; const dynamicCommand = `tailor ${subcommand}`; +const dynamicCommandWithTrailingFlag = `tailor ${subcommand} --json`; const dynamicPnpmCommand = `pnpm tailor ${subcommand}`; const dynamicProfileCommand = `tailor --profile ${profile} deploy`; const dynamicEqualsProfileCommand = `tailor --profile=${profile} deploy`; @@ -18,6 +19,7 @@ const latestOnly = "npx @tailor-platform/sdk@latest"; const latestWithRunnerOption = "npx --yes @tailor-platform/sdk@latest login"; const dynamicNpxCommand = `npx @tailor-platform/sdk ${subcommand}`; const dynamicNpxRegistryCommand = `npx --registry ${registry} @tailor-platform/sdk login`; +const npxOtherPackage = "npx foo tailor-sdk login"; const dynamicBunxCommand = `bunx @tailor-platform/sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx @tailor-platform/sdk ${subcommand}`; const pnpmDlxWithOption = "pnpm --silent dlx @tailor-platform/sdk login"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 2540cfc6c..59be94ddb 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -7,6 +7,7 @@ const help = "tailor-sdk --help"; const npxVersion = "npx tailor-sdk --version"; const generated = "Run tailor-sdk generate after changes"; const dynamicCommand = `tailor-sdk ${subcommand}`; +const dynamicCommandWithTrailingFlag = `tailor-sdk ${subcommand} --json`; const dynamicPnpmCommand = `pnpm tailor-sdk ${subcommand}`; const dynamicProfileCommand = `tailor-sdk --profile ${profile} deploy`; const dynamicEqualsProfileCommand = `tailor-sdk --profile=${profile} deploy`; @@ -18,6 +19,7 @@ const latestOnly = "npx tailor-sdk@latest"; const latestWithRunnerOption = "npx --yes tailor-sdk@latest login"; const dynamicNpxCommand = `npx tailor-sdk ${subcommand}`; const dynamicNpxRegistryCommand = `npx --registry ${registry} tailor-sdk login`; +const npxOtherPackage = "npx foo tailor-sdk login"; const dynamicBunxCommand = `bunx tailor-sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx tailor-sdk ${subcommand}`; const pnpmDlxWithOption = "pnpm --silent dlx tailor-sdk login"; From e0ee1e082d3fa8c46dd92acd09d1886570f321d4 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 20:33:23 +0900 Subject: [PATCH 318/618] fix(sdk-codemod): handle dynamic shell continuations --- .../sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts | 2 +- .../codemods/v2/rename-bin/tests/source-template/expected.ts | 2 ++ .../codemods/v2/rename-bin/tests/source-template/input.ts | 2 ++ packages/sdk-codemod/src/registry.test.ts | 1 + packages/sdk-codemod/src/registry.ts | 2 ++ 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index b0c9f5965..59e2af56b 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -80,7 +80,7 @@ const SOURCE_CLI_VALUE_REFERENCE_RE = new RegExp( "g", ); const SOURCE_TEMPLATE_EXPR_PLACEHOLDER = "__TAILOR_SDK_TEMPLATE_EXPR_\\d+__"; -const SOURCE_TEMPLATE_DYNAMIC_ARGS = `\\s+${SOURCE_TEMPLATE_EXPR_PLACEHOLDER}(?:\\s+${SOURCE_ARG_VALUE})*\\s*$`; +const SOURCE_TEMPLATE_DYNAMIC_ARGS = `\\s+${SOURCE_TEMPLATE_EXPR_PLACEHOLDER}(?:\\s+${SOURCE_ARG_VALUE})*(?=\\s*(?:$|[;&|]))`; const SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD = "\\s+(?:--help|-h|--version|-v)\\b"; const SOURCE_DIRECT_INVOCATION_LOOKAHEAD = `(?:${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b|${SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD})`; const SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD = `(?:${SOURCE_DIRECT_INVOCATION_LOOKAHEAD}|${SOURCE_TEMPLATE_DYNAMIC_ARGS}|\\s*$)`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index a5b58f61e..d9ce6e93d 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -8,7 +8,9 @@ const npxVersion = "npx @tailor-platform/sdk --version"; const generated = "Run tailor generate after changes"; const dynamicCommand = `tailor ${subcommand}`; const dynamicCommandWithTrailingFlag = `tailor ${subcommand} --json`; +const dynamicCommandBeforeSeparator = `tailor ${subcommand} && echo done`; const dynamicPnpmCommand = `pnpm tailor ${subcommand}`; +const dynamicPnpmCommandBeforePipe = `pnpm tailor ${subcommand} | jq`; const dynamicProfileCommand = `tailor --profile ${profile} deploy`; const dynamicEqualsProfileCommand = `tailor --profile=${profile} deploy`; const dynamicEqualsWorkspaceCommand = `tailor --workspace-id=${workspaceId} deploy`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 59be94ddb..1323cdb0a 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -8,7 +8,9 @@ const npxVersion = "npx tailor-sdk --version"; const generated = "Run tailor-sdk generate after changes"; const dynamicCommand = `tailor-sdk ${subcommand}`; const dynamicCommandWithTrailingFlag = `tailor-sdk ${subcommand} --json`; +const dynamicCommandBeforeSeparator = `tailor-sdk ${subcommand} && echo done`; const dynamicPnpmCommand = `pnpm tailor-sdk ${subcommand}`; +const dynamicPnpmCommandBeforePipe = `pnpm tailor-sdk ${subcommand} | jq`; const dynamicProfileCommand = `tailor-sdk --profile ${profile} deploy`; const dynamicEqualsProfileCommand = `tailor-sdk --profile=${profile} deploy`; const dynamicEqualsWorkspaceCommand = `tailor-sdk --workspace-id=${workspaceId} deploy`; diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index a524489b0..65f7589d5 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -67,6 +67,7 @@ describe("getApplicableCodemods", () => { const sourceStringPattern = renameBin?.sourceStringLegacyPatterns?.[0] as RegExp; expect(sourceStringPattern.test("tailor-sdk deploy")).toBe(true); expect(sourceStringPattern.test("tailor --profile tailor-sdk deploy")).toBe(false); + expect(sourceStringPattern.test('tailor --arg "tailor-sdk deploy" deploy')).toBe(false); const matches = picomatch(renameBin?.filePatterns ?? [], { dot: true }); expect(matches("packages/app/frontend/e2e/global-setup.ts")).toBe(true); expect(matches("tailor.d.ts")).toBe(true); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index b1eaafea8..bdea53db6 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -65,6 +65,8 @@ const RENAME_BIN_SOURCE_VALUE_GUARDS = RENAME_BIN_SOURCE_VALUE_FLAGS.flatMap((fl const RENAME_BIN_SOURCE_LEGACY_PATTERN = new RegExp( [ "(? Date: Sat, 27 Jun 2026 20:42:57 +0900 Subject: [PATCH 319/618] fix(sdk-codemod): limit value guards to tailor args --- .../v2/rename-bin/scripts/transform.ts | 16 +++++++++++ .../tests/source-js-string/expected.js | 1 + .../tests/source-js-string/input.js | 1 + packages/sdk-codemod/src/runner.test.ts | 27 +++++++++++++++++++ packages/sdk-codemod/src/runner.ts | 17 ++++++++++++ 5 files changed, 62 insertions(+) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 59e2af56b..5d0aa2af5 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -536,12 +536,28 @@ function isCliBinaryArgument(node: SgNode, source: string): boolean { return args[0] != null && nodeRangeKey(args[0]) === nodeRangeKey(node); } +function isTailorCliArgumentArray(arrayNode: SgNode, index: number, source: string): boolean { + const argumentsNode = arrayNode.parent(); + if (argumentsNode?.kind() === "arguments") { + const callArgs = sourceArrayElements(argumentsNode); + const executable = callArgs[0] == null ? null : sourceStringContent(callArgs[0]!, source); + if (executable != null && TAILOR_SDK_TOKEN_RE.test(executable)) return true; + } + + const elements = sourceArrayElements(arrayNode); + return elements.slice(0, index).some((element) => { + const value = sourceStringContent(element, source); + return value != null && (value === "tailor" || TAILOR_SDK_TOKEN_RE.test(value)); + }); +} + function isCliValueArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() !== "array") return false; const elements = sourceArrayElements(parent); const index = nodeIndex(elements, node); if (index <= 0) return false; + if (!isTailorCliArgumentArray(parent, index, source)) return false; const previousValue = sourceStringContent(elements[index - 1]!, source); return ( previousValue != null && isTailorCliValueFlag(previousValue) && !previousValue.includes("=") diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index f3bf1c9e3..15389bb6c 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -1,6 +1,7 @@ const script = "tailor deploy"; const spawned = spawn("tailor", ["deploy"]); const argSpawned = spawn("tailor", ["--arg", "tailor-sdk deploy", "deploy"]); +const shellSpawned = spawn("sh", ["-c", "tailor deploy"]); const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "dev", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 8288795ed..336c182f1 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -1,6 +1,7 @@ const script = "tailor-sdk deploy"; const spawned = spawn("tailor-sdk", ["deploy"]); const argSpawned = spawn("tailor-sdk", ["--arg", "tailor-sdk deploy", "deploy"]); +const shellSpawned = spawn("sh", ["-c", "tailor-sdk deploy"]); const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["tailor-sdk", "--profile", "dev", "login"]); diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 9923c028c..17c5c1c72 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -544,6 +544,33 @@ describe("runCodemods", () => { expect(result.warnings).toEqual([]); }); + test("keeps source string residual checks in non-Tailor option values", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "env.ts"), + 'spawn("node", ["-e", "process.env.LOG_LEVEL"]);', + "utf-8", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/env", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: ["LOG_LEVEL"], + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toEqual([ + "env.ts: contains LOG_LEVEL but was not migrated automatically (rule: test/env). Manual migration may be needed.", + ]); + }); + test("flags files matching a suspicious pattern for LLM review", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index ef4dd3a10..49db1534f 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -78,6 +78,7 @@ const SOURCE_VALUE_FLAGS = new Set([ "-q", "-f", ]); +const SOURCE_CLI_BINARY_RE = /^(?:tailor|tailor-sdk(?:@[^\s'"`;|&)]+)?)$/; function shouldSkipDirectory(name: string): boolean { return EXCLUDE_DIRS.has(name) || (name.startsWith(".") && !ALLOWED_DOT_DIRS.has(name)); @@ -234,6 +235,7 @@ function isSourceValueArgument(fragment: SgNode, source: string): boolean { const elements = sourceArrayElements(parent); const index = elements.findIndex((element) => nodeRangeKey(element) === nodeRangeKey(stringNode)); if (index <= 0) return false; + if (!isTailorCliArgumentArray(parent, index, source)) return false; const previous = sourceStringNodeContent(elements[index - 1]!, source); return ( @@ -243,6 +245,21 @@ function isSourceValueArgument(fragment: SgNode, source: string): boolean { ); } +function isTailorCliArgumentArray(arrayNode: SgNode, index: number, source: string): boolean { + const argumentsNode = arrayNode.parent(); + if (argumentsNode?.kind() === "arguments") { + const callArgs = sourceArrayElements(argumentsNode); + const executable = callArgs[0] == null ? null : sourceStringNodeContent(callArgs[0]!, source); + if (executable != null && SOURCE_CLI_BINARY_RE.test(executable)) return true; + } + + const elements = sourceArrayElements(arrayNode); + return elements.slice(0, index).some((element) => { + const value = sourceStringNodeContent(element, source); + return value != null && SOURCE_CLI_BINARY_RE.test(value); + }); +} + function sourceLang(relative: string): Lang { const ext = path.extname(relative).toLowerCase(); return ext === ".tsx" || ext === ".jsx" || ext === ".js" ? Lang.Tsx : Lang.TypeScript; From e728ad360a81bac7feaeb9f9adc1fdc7a8ef8e20 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 20:54:43 +0900 Subject: [PATCH 320/618] fix(sdk-codemod): preserve residual value context --- .../v2/rename-bin/scripts/transform.ts | 6 ++-- packages/sdk-codemod/src/runner.test.ts | 28 +++++++++++++++++++ packages/sdk-codemod/src/runner.ts | 11 ++++++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 5d0aa2af5..abbf7a829 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -111,6 +111,8 @@ const SOURCE_DYNAMIC_OPTION_TAILOR_SDK_RE = new RegExp( "g", ); const TAILOR_SDK_TOKEN_RE = /^tailor-sdk(@[^\s'"`;|&)]+)?$/; +const TAILOR_CLI_TOKEN_RE = + /^(?:tailor|tailor-sdk(?:@[^\s'"`;|&)]+)?|@tailor-platform\/sdk(?:@[^\s'"`;|&)]+)?)$/; const CLI_ARGUMENT_CALLEE_RE = /(?:^|\.)(?:spawn|spawnSync|execFile|execFileSync|execa|execaSync)$/; const SOURCE_EXEC_PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn"]); const SOURCE_PACKAGE_RUNNERS = new Set(["bunx", "npx"]); @@ -541,13 +543,13 @@ function isTailorCliArgumentArray(arrayNode: SgNode, index: number, source: stri if (argumentsNode?.kind() === "arguments") { const callArgs = sourceArrayElements(argumentsNode); const executable = callArgs[0] == null ? null : sourceStringContent(callArgs[0]!, source); - if (executable != null && TAILOR_SDK_TOKEN_RE.test(executable)) return true; + if (executable != null && TAILOR_CLI_TOKEN_RE.test(executable)) return true; } const elements = sourceArrayElements(arrayNode); return elements.slice(0, index).some((element) => { const value = sourceStringContent(element, source); - return value != null && (value === "tailor" || TAILOR_SDK_TOKEN_RE.test(value)); + return value != null && TAILOR_CLI_TOKEN_RE.test(value); }); } diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 17c5c1c72..6b9722a9c 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -2,6 +2,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "pathe"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { allCodemods } from "./registry"; import { runCodemods } from "./runner"; import type { CodemodPackage } from "./types"; @@ -524,6 +525,7 @@ describe("runCodemods", () => { 'const packageName = "tailor-sdk";', 'const command = "deploy";', 'spawn("tailor", ["--arg", "tailor-sdk deploy", "deploy"]);', + 'spawn("npx", ["@tailor-platform/sdk", "--arg", "tailor-sdk deploy", "deploy"]);', ].join("\n"), "utf-8", ); @@ -544,6 +546,32 @@ describe("runCodemods", () => { expect(result.warnings).toEqual([]); }); + test("keeps escaped quoted Tailor values out of rename-bin residual warnings", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "commands.ts"), + 'const command = "tailor --arg \\"tailor-sdk deploy\\" deploy";', + "utf-8", + ); + const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: renameBin?.sourceStringLegacyPatterns, + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toEqual([]); + }); + test("keeps source string residual checks in non-Tailor option values", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 49db1534f..5456f1c55 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -78,7 +78,8 @@ const SOURCE_VALUE_FLAGS = new Set([ "-q", "-f", ]); -const SOURCE_CLI_BINARY_RE = /^(?:tailor|tailor-sdk(?:@[^\s'"`;|&)]+)?)$/; +const SOURCE_CLI_BINARY_RE = + /^(?:tailor|tailor-sdk(?:@[^\s'"`;|&)]+)?|@tailor-platform\/sdk(?:@[^\s'"`;|&)]+)?)$/; function shouldSkipDirectory(name: string): boolean { return EXCLUDE_DIRS.has(name) || (name.startsWith(".") && !ALLOWED_DOT_DIRS.has(name)); @@ -179,6 +180,12 @@ function sourceStringContentForResidualMatching(relative: string, content: strin const fragments: string[] = []; const visit = (node: SgNode): void => { + if (node.kind() === "string") { + if (isSourceValueArgument(node, content)) return; + const value = sourceStringNodeContent(node, content); + if (value != null) fragments.push(value); + return; + } if (node.kind() === "string_fragment") { if (isSourceValueArgument(node, content)) return; fragments.push(node.text()); @@ -227,7 +234,7 @@ function sourceStringNodeContent(node: SgNode, source: string): string | null { } function isSourceValueArgument(fragment: SgNode, source: string): boolean { - const stringNode = fragment.parent(); + const stringNode = fragment.kind() === "string_fragment" ? fragment.parent() : fragment; if (stringNode == null) return false; const parent = stringNode.parent(); if (parent?.kind() !== "array") return false; From 9d15fe88b7c1317e29df6ab15c6b52d0444fb739 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 21:10:50 +0900 Subject: [PATCH 321/618] fix(sdk-codemod): scope quoted value protection --- .../v2/rename-bin/scripts/transform.ts | 39 +++++++++++-------- .../tests/source-js-string/expected.js | 2 + .../tests/source-js-string/input.js | 2 + .../tests/source-template/expected.ts | 1 + .../rename-bin/tests/source-template/input.ts | 1 + 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index abbf7a829..223c44875 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -73,10 +73,12 @@ const TAILOR_CLI_VALUE_FLAGS = new Set([ "-q", "-f", ]); -const SOURCE_COMMAND_GAP = `(?:\\s+--?[\\w-]+(?:=${SOURCE_ARG_VALUE})?(?:\\s+${SOURCE_ARG_VALUE})?)*`; +const SOURCE_ESCAPED_QUOTED_VALUE = String.raw`\\"(?:\\\\.|[^"\\])*\\"|\\'(?:\\\\.|[^'\\])*\\'`; +const SOURCE_CLI_ARG_VALUE = `(?:${SOURCE_ESCAPED_QUOTED_VALUE}|${SOURCE_ARG_VALUE})`; +const SOURCE_COMMAND_GAP = `(?:\\s+--?[\\w-]+(?:=${SOURCE_CLI_ARG_VALUE})?(?:\\s+${SOURCE_CLI_ARG_VALUE})?)*`; const SOURCE_RUNNER_OPTION_GAP = `(?:\\s+(?:-\\w+|--\\w[\\w-]*)(?:=${SOURCE_ARG_VALUE})?(?:\\s+(?!tailor-sdk(?![\\w-])|-)${SOURCE_ARG_VALUE})?)*`; const SOURCE_CLI_VALUE_REFERENCE_RE = new RegExp( - `(${TAILOR_CLI_VALUE_FLAG}(?:=|\\s+))(${SOURCE_ARG_VALUE})`, + `(${TAILOR_CLI_VALUE_FLAG}(?:=|\\s+))(${SOURCE_CLI_ARG_VALUE})`, "g", ); const SOURCE_TEMPLATE_EXPR_PLACEHOLDER = "__TAILOR_SDK_TEMPLATE_EXPR_\\d+__"; @@ -123,7 +125,6 @@ const NPX_OPTION_WITH_VALUE = "(?:--registry|--cache|--userconfig|--prefix)"; const NPX_PACKAGE_FLAG_CONTEXT_RE = new RegExp( `(?:^|[;&|]\\s*)npx(?:\\s+(?:${NPX_OPTION_WITH_VALUE}\\s+${SOURCE_ARG_VALUE}|-\\w+|--\\w[\\w-]*(?:=${SOURCE_ARG_VALUE})?))*\\s*$`, ); -const SOURCE_ESCAPED_QUOTED_VALUE_RE = /\\"(?:\\\\.|[^"\\])*\\"|\\'(?:\\\\.|[^'\\])*\\'/g; const SOURCE_TOKEN_RE = new RegExp(SOURCE_ARG_VALUE, "g"); const RUNNER_OPTION_VALUE_FLAGS = new Set([ "--registry", @@ -157,16 +158,11 @@ function protectSourceCliValueReferences(value: string): { protectedValues: string[]; } { const protectedValues: string[] = []; - const withEscapedQuotedValues = value.replace(SOURCE_ESCAPED_QUOTED_VALUE_RE, (match) => { - if (!match.includes("tailor-sdk")) return match; - const placeholder = `__TAILOR_SDK_SOURCE_VALUE_${protectedValues.length}__`; - protectedValues.push(match); - return placeholder; - }); - const source = withEscapedQuotedValues.replace( + const source = value.replace( SOURCE_CLI_VALUE_REFERENCE_RE, (match: string, prefix: string, arg: string, offset: number) => { if (!arg.includes("tailor-sdk")) return match; + if (!isAfterTailorCliToken(value, offset)) return match; if (prefix.startsWith("-p") && NPX_PACKAGE_FLAG_CONTEXT_RE.test(value.slice(0, offset))) { return match; } @@ -249,6 +245,17 @@ function isAfterOtherPackageRunner(source: string, offset: number): boolean { return packageToken != null && !TAILOR_SDK_TOKEN_RE.test(packageToken); } +function isAfterTailorCliToken(source: string, offset: number): boolean { + const segmentStart = Math.max( + source.lastIndexOf(";", offset - 1), + source.lastIndexOf("&", offset - 1), + source.lastIndexOf("|", offset - 1), + ); + const segment = source.slice(segmentStart + 1, offset).trim(); + const tokens = sourceTokens(segment); + return tokens != null && tokens.some((token) => TAILOR_CLI_TOKEN_RE.test(token)); +} + function renameSourceCommandText(value: string): string { const protectedValue = protectSourceCliValueReferences(value); const withPackageFlagValues = protectedValue.source.replace( @@ -373,11 +380,9 @@ function isTailorCliValueFlag(value: string): boolean { return TAILOR_CLI_VALUE_FLAGS.has(value.split("=", 1)[0]!); } -function hasNpxPackageFlag(elements: SgNode[], source: string): boolean { - const firstPackageIndex = firstTailorPackageIndex(elements, 0, source); - if (firstPackageIndex == null) return false; - return elements.some((element, index) => { - if (index >= firstPackageIndex) return false; +function hasNpxPackageFlagBefore(elements: SgNode[], index: number, source: string): boolean { + return elements.some((element, currentIndex) => { + if (currentIndex >= index) return false; const value = sourceStringContent(element, source); return value != null && SOURCE_PACKAGE_FLAG_RE.test(value); }); @@ -463,7 +468,7 @@ function isPackageRunnerPackageArgument(node: SgNode, source: string): boolean { const index = nodeIndex(elements, node); if (index < 0) return false; - if (hasNpxPackageFlag(elements, source)) { + if (hasNpxPackageFlagBefore(elements, index, source)) { return isNpxSplitPackageFlagValue(elements, index, source); } @@ -488,7 +493,7 @@ function isPackageRunnerCommandBinaryArgument(node: SgNode, source: string): boo const elements = sourceArrayElements(parent); const index = nodeIndex(elements, node); - if (index < 0 || !hasNpxPackageFlag(elements, source)) return false; + if (index < 0 || !hasNpxPackageFlagBefore(elements, index, source)) return false; const text = sourceStringContent(node, source); return ( diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 15389bb6c..3d07524cd 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -13,6 +13,8 @@ const npxPackageFlagSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "tailo const npxPackageEqualsSpawned = spawn("npx", ["--package=@tailor-platform/sdk", "tailor", "login"]); const npxPackageFlagDynamicSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "tailor", subcommand]); const npxPackageEqualsDynamicSpawned = spawn("npx", ["--package=@tailor-platform/sdk", "tailor", subcommand]); +const npxPackageMigratedSpawned = spawn("npx", ["--package", "@tailor-platform/sdk", "tailor", "login"]); +const npxPackageDynamicSpawned = spawn("npx", ["-p", pkg, "tailor", "login"]); const pnpmDlxSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk", "login"]); const pnpmDlxDynamicSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk"]); const pnpmDlxOtherPackageSpawned = spawn("pnpm", ["dlx", "foo", "tailor-sdk", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 336c182f1..cae6f1817 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -13,6 +13,8 @@ const npxPackageFlagSpawned = spawn("npx", ["-p", "tailor-sdk", "tailor-sdk", "l const npxPackageEqualsSpawned = spawn("npx", ["--package=tailor-sdk", "tailor-sdk", "login"]); const npxPackageFlagDynamicSpawned = spawn("npx", ["-p", "tailor-sdk", "tailor-sdk", subcommand]); const npxPackageEqualsDynamicSpawned = spawn("npx", ["--package=tailor-sdk", "tailor-sdk", subcommand]); +const npxPackageMigratedSpawned = spawn("npx", ["--package", "@tailor-platform/sdk", "tailor-sdk", "login"]); +const npxPackageDynamicSpawned = spawn("npx", ["-p", pkg, "tailor-sdk", "login"]); const pnpmDlxSpawned = spawn("pnpm", ["dlx", "tailor-sdk", "login"]); const pnpmDlxDynamicSpawned = spawn("pnpm", ["dlx", "tailor-sdk"]); const pnpmDlxOtherPackageSpawned = spawn("pnpm", ["dlx", "foo", "tailor-sdk", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index d9ce6e93d..382f0416a 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -38,6 +38,7 @@ const npxPackageFlagDynamic = `npx -p @tailor-platform/sdk tailor ${subcommand}` const npxPackageEqualsDynamic = `npx --package=@tailor-platform/sdk tailor ${subcommand}`; const npxRegistryPackageFlag = "npx --registry https://registry.npmjs.org -p @tailor-platform/sdk tailor login"; const npxProfileValue = "npx @tailor-platform/sdk -p tailor-sdk login"; +const shellWrapped = "sh -c \"tailor deploy\""; const escapedArg = "tailor --arg \"tailor-sdk deploy\" deploy"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 1323cdb0a..a1ff0b0fb 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -38,6 +38,7 @@ const npxPackageFlagDynamic = `npx -p tailor-sdk tailor-sdk ${subcommand}`; const npxPackageEqualsDynamic = `npx --package=tailor-sdk tailor-sdk ${subcommand}`; const npxRegistryPackageFlag = "npx --registry https://registry.npmjs.org -p tailor-sdk tailor-sdk login"; const npxProfileValue = "npx tailor-sdk -p tailor-sdk login"; +const shellWrapped = "sh -c \"tailor-sdk deploy\""; const escapedArg = "tailor-sdk --arg \"tailor-sdk deploy\" deploy"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; From 5fe2121fc59f9eda5dd3ff96e8215d3cc196deaf Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 21:30:12 +0900 Subject: [PATCH 322/618] fix(sdk-codemod): preserve dynamic runner edge cases --- .../v2/rename-bin/scripts/transform.ts | 29 +++++++++++++++++-- .../tests/source-js-string/expected.js | 2 ++ .../tests/source-js-string/input.js | 2 ++ .../tests/source-template/expected.ts | 1 + .../rename-bin/tests/source-template/input.ts | 1 + 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 223c44875..5e18f24f1 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -119,7 +119,17 @@ const CLI_ARGUMENT_CALLEE_RE = /(?:^|\.)(?:spawn|spawnSync|execFile|execFileSync const SOURCE_EXEC_PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn"]); const SOURCE_PACKAGE_RUNNERS = new Set(["bunx", "npx"]); const SOURCE_DLX_PACKAGE_RUNNERS = new Set(["pnpm", "yarn"]); -const PACKAGE_MANAGER_OPTION_VALUE_FLAGS = new Set(["--filter", "-F", "--dir", "-C", "--cwd"]); +const PACKAGE_MANAGER_OPTION_VALUE_FLAGS = new Set([ + "--registry", + "--cache", + "--userconfig", + "--prefix", + "--filter", + "-F", + "--dir", + "-C", + "--cwd", +]); const SOURCE_PACKAGE_FLAG_RE = /^(?:-p|--package)(?:=.*)?$/; const NPX_OPTION_WITH_VALUE = "(?:--registry|--cache|--userconfig|--prefix)"; const NPX_PACKAGE_FLAG_CONTEXT_RE = new RegExp( @@ -256,6 +266,17 @@ function isAfterTailorCliToken(source: string, offset: number): boolean { return tokens != null && tokens.some((token) => TAILOR_CLI_TOKEN_RE.test(token)); } +function isAfterTemplatePlaceholder(source: string, offset: number): boolean { + const segmentStart = Math.max( + source.lastIndexOf(";", offset - 1), + source.lastIndexOf("&", offset - 1), + source.lastIndexOf("|", offset - 1), + ); + const segment = source.slice(segmentStart + 1, offset).trim(); + const tokens = sourceTokens(segment); + return tokens != null && tokens.some((token) => /^__TAILOR_SDK_TEMPLATE_EXPR_\d+__$/.test(token)); +} + function renameSourceCommandText(value: string): string { const protectedValue = protectSourceCliValueReferences(value); const withPackageFlagValues = protectedValue.source.replace( @@ -284,6 +305,7 @@ function renameSourceCommandText(value: string): string { SOURCE_TAILOR_SDK_RE, (match: string, version: string | undefined, offset: number, source: string) => { if (isAfterOtherPackageRunner(source, offset)) return match; + if (isAfterTemplatePlaceholder(source, offset)) return match; return version ? `@tailor-platform/sdk${version}` : "tailor"; }, ); @@ -563,8 +585,11 @@ function isCliValueArgument(node: SgNode, source: string): boolean { if (parent?.kind() !== "array") return false; const elements = sourceArrayElements(parent); const index = nodeIndex(elements, node); - if (index <= 0) return false; + if (index < 0) return false; if (!isTailorCliArgumentArray(parent, index, source)) return false; + const text = sourceStringContent(node, source); + if (text != null && isTailorCliValueFlag(text) && text.includes("=")) return true; + if (index === 0) return false; const previousValue = sourceStringContent(elements[index - 1]!, source); return ( previousValue != null && isTailorCliValueFlag(previousValue) && !previousValue.includes("=") diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 3d07524cd..ef86a80fa 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -1,6 +1,7 @@ const script = "tailor deploy"; const spawned = spawn("tailor", ["deploy"]); const argSpawned = spawn("tailor", ["--arg", "tailor-sdk deploy", "deploy"]); +const inlineArgSpawned = spawn("tailor", ["--arg=tailor-sdk deploy", "deploy"]); const shellSpawned = spawn("sh", ["-c", "tailor deploy"]); const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); @@ -20,6 +21,7 @@ const pnpmDlxDynamicSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk"]); const pnpmDlxOtherPackageSpawned = spawn("pnpm", ["dlx", "foo", "tailor-sdk", "login"]); const pnpmDlxOptionSpawned = spawn("pnpm", ["--silent", "dlx", "@tailor-platform/sdk", "login"]); const pnpmDlxSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "dlx", "@tailor-platform/sdk", "login"]); +const pnpmDlxRegistrySpawned = spawn("pnpm", ["--registry", registry, "dlx", "@tailor-platform/sdk", "login"]); const pnpmExecSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "exec", "tailor", "deploy"]); const yarnDlxOptionSpawned = spawn("yarn", ["--quiet", "dlx", "@tailor-platform/sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index cae6f1817..ed4b47e51 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -1,6 +1,7 @@ const script = "tailor-sdk deploy"; const spawned = spawn("tailor-sdk", ["deploy"]); const argSpawned = spawn("tailor-sdk", ["--arg", "tailor-sdk deploy", "deploy"]); +const inlineArgSpawned = spawn("tailor-sdk", ["--arg=tailor-sdk deploy", "deploy"]); const shellSpawned = spawn("sh", ["-c", "tailor-sdk deploy"]); const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); @@ -20,6 +21,7 @@ const pnpmDlxDynamicSpawned = spawn("pnpm", ["dlx", "tailor-sdk"]); const pnpmDlxOtherPackageSpawned = spawn("pnpm", ["dlx", "foo", "tailor-sdk", "login"]); const pnpmDlxOptionSpawned = spawn("pnpm", ["--silent", "dlx", "tailor-sdk", "login"]); const pnpmDlxSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "dlx", "tailor-sdk", "login"]); +const pnpmDlxRegistrySpawned = spawn("pnpm", ["--registry", registry, "dlx", "tailor-sdk", "login"]); const pnpmExecSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "exec", "tailor-sdk", "deploy"]); const yarnDlxOptionSpawned = spawn("yarn", ["--quiet", "dlx", "tailor-sdk", "login"]); const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 382f0416a..21407f795 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -21,6 +21,7 @@ const latestOnly = "npx @tailor-platform/sdk@latest"; const latestWithRunnerOption = "npx --yes @tailor-platform/sdk@latest login"; const dynamicNpxCommand = `npx @tailor-platform/sdk ${subcommand}`; const dynamicNpxRegistryCommand = `npx --registry ${registry} @tailor-platform/sdk login`; +const dynamicRunnerCommand = `${runner} tailor-sdk login`; const npxOtherPackage = "npx foo tailor-sdk login"; const dynamicBunxCommand = `bunx @tailor-platform/sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx @tailor-platform/sdk ${subcommand}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index a1ff0b0fb..1c9e11b59 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -21,6 +21,7 @@ const latestOnly = "npx tailor-sdk@latest"; const latestWithRunnerOption = "npx --yes tailor-sdk@latest login"; const dynamicNpxCommand = `npx tailor-sdk ${subcommand}`; const dynamicNpxRegistryCommand = `npx --registry ${registry} tailor-sdk login`; +const dynamicRunnerCommand = `${runner} tailor-sdk login`; const npxOtherPackage = "npx foo tailor-sdk login"; const dynamicBunxCommand = `bunx tailor-sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx tailor-sdk ${subcommand}`; From 32d50f44b763ad0cf25ed9257a2b59666eaff047 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 21:49:56 +0900 Subject: [PATCH 323/618] fix(sdk-codemod): preserve npx profile values --- .../codemods/v2/rename-bin/scripts/transform.ts | 13 ++++++------- .../rename-bin/tests/source-js-string/expected.js | 3 +++ .../v2/rename-bin/tests/source-js-string/input.js | 3 +++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 5e18f24f1..1789865e2 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -606,16 +606,15 @@ function pushSourceStringEdit( const end = range.end.index - 1; const text = source.slice(start, end); const packageFlagReplacement = sourcePackageFlagReplacement(node, source); - const replacement = - packageFlagReplacement != null + const replacement = isCliValueArgument(node, source) + ? text + : packageFlagReplacement != null ? packageFlagReplacement.replacement : TAILOR_SDK_TOKEN_RE.test(text) && isPackageRunnerPackageArgument(node, source) ? renamePackageName(text) - : isCliValueArgument(node, source) - ? text - : TAILOR_SDK_TOKEN_RE.test(text) && isCliBinaryArgument(node, source) - ? renameBinary(text) - : renameSourceCommandText(text); + : TAILOR_SDK_TOKEN_RE.test(text) && isCliBinaryArgument(node, source) + ? renameBinary(text) + : renameSourceCommandText(text); if (replacement !== text) { edits.push([start, end, replacement]); } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index ef86a80fa..c327e6ade 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -7,6 +7,9 @@ const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "dev", "login"]); const npxShortProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "-p", "dev", "login"]); +const npxProfileNamedTailorSdkSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "tailor-sdk", "login"]); +const npxShortProfileNamedTailorSdkSpawned = spawn("npx", ["@tailor-platform/sdk", "-p", "tailor-sdk", "login"]); +const npxInlineProfileNamedTailorSdkSpawned = spawn("npx", ["@tailor-platform/sdk", "-p=tailor-sdk", "login"]); const npxVersionSpawned = spawn("npx", ["@tailor-platform/sdk", "--version"]); const npxDynamicSpawned = spawn("npx", ["@tailor-platform/sdk", subcommand]); const npxOtherPackageSpawned = spawn("npx", ["foo", "tailor-sdk", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index ed4b47e51..6c6ca8e68 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -7,6 +7,9 @@ const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["tailor-sdk", "--profile", "dev", "login"]); const npxShortProfileSpawned = spawn("npx", ["tailor-sdk", "-p", "dev", "login"]); +const npxProfileNamedTailorSdkSpawned = spawn("npx", ["tailor-sdk", "--profile", "tailor-sdk", "login"]); +const npxShortProfileNamedTailorSdkSpawned = spawn("npx", ["tailor-sdk", "-p", "tailor-sdk", "login"]); +const npxInlineProfileNamedTailorSdkSpawned = spawn("npx", ["tailor-sdk", "-p=tailor-sdk", "login"]); const npxVersionSpawned = spawn("npx", ["tailor-sdk", "--version"]); const npxDynamicSpawned = spawn("npx", ["tailor-sdk", subcommand]); const npxOtherPackageSpawned = spawn("npx", ["foo", "tailor-sdk", "login"]); From 58a4f0e6ac6d2d7fc036d0e3fcd3d42f45a4d436 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 22:11:23 +0900 Subject: [PATCH 324/618] fix(sdk-codemod): preserve template cli values --- .../v2/rename-bin/scripts/transform.ts | 38 +++++++++++++++++-- .../tests/source-js-string/expected.js | 2 + .../tests/source-js-string/input.js | 2 + .../tests/source-template/expected.ts | 1 + .../rename-bin/tests/source-template/input.ts | 1 + 5 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 1789865e2..24c543029 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -81,7 +81,8 @@ const SOURCE_CLI_VALUE_REFERENCE_RE = new RegExp( `(${TAILOR_CLI_VALUE_FLAG}(?:=|\\s+))(${SOURCE_CLI_ARG_VALUE})`, "g", ); -const SOURCE_TEMPLATE_EXPR_PLACEHOLDER = "__TAILOR_SDK_TEMPLATE_EXPR_\\d+__"; +const SOURCE_TEMPLATE_EXPR_PLACEHOLDER = "__TAILOR_SDK_TEMPLATE_EXPR_\\d+_\\d+__"; +const SOURCE_TEMPLATE_EXPR_PLACEHOLDER_RE = /^__TAILOR_SDK_TEMPLATE_EXPR_\d+_\d+__$/; const SOURCE_TEMPLATE_DYNAMIC_ARGS = `\\s+${SOURCE_TEMPLATE_EXPR_PLACEHOLDER}(?:\\s+${SOURCE_ARG_VALUE})*(?=\\s*(?:$|[;&|]))`; const SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD = "\\s+(?:--help|-h|--version|-v)\\b"; const SOURCE_DIRECT_INVOCATION_LOOKAHEAD = `(?:${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b|${SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD})`; @@ -274,7 +275,7 @@ function isAfterTemplatePlaceholder(source: string, offset: number): boolean { ); const segment = source.slice(segmentStart + 1, offset).trim(); const tokens = sourceTokens(segment); - return tokens != null && tokens.some((token) => /^__TAILOR_SDK_TEMPLATE_EXPR_\d+__$/.test(token)); + return tokens != null && tokens.some((token) => SOURCE_TEMPLATE_EXPR_PLACEHOLDER_RE.test(token)); } function renameSourceCommandText(value: string): string { @@ -358,6 +359,13 @@ function sourceStringContent(node: SgNode, source: string): string | null { return source.slice(range.start.index + 1, range.end.index - 1); } +function sourceStringRawContent(node: SgNode, source: string): string | null { + const kind = node.kind(); + if (kind !== "string" && kind !== "template_string") return null; + const range = node.range(); + return source.slice(range.start.index + 1, range.end.index - 1); +} + function isSyntaxOnlyNode(node: SgNode): boolean { const kind = node.kind(); return ( @@ -587,7 +595,7 @@ function isCliValueArgument(node: SgNode, source: string): boolean { const index = nodeIndex(elements, node); if (index < 0) return false; if (!isTailorCliArgumentArray(parent, index, source)) return false; - const text = sourceStringContent(node, source); + const text = sourceStringContent(node, source) ?? sourceStringRawContent(node, source); if (text != null && isTailorCliValueFlag(text) && text.includes("=")) return true; if (index === 0) return false; const previousValue = sourceStringContent(elements[index - 1]!, source); @@ -620,6 +628,22 @@ function pushSourceStringEdit( } } +function templateSubstitutionPlaceholder( + index: number, + text: string, + usedPlaceholders: Set, +): string { + let attempt = 0; + while (true) { + const placeholder = `__TAILOR_SDK_TEMPLATE_EXPR_${index}_${attempt}__`; + if (!text.includes(placeholder) && !usedPlaceholders.has(placeholder)) { + usedPlaceholders.add(placeholder); + return placeholder; + } + attempt += 1; + } +} + function pushTemplateStringEdit( edits: Array<[number, number, string]>, source: string, @@ -630,6 +654,7 @@ function pushTemplateStringEdit( const end = range.end.index - 1; let text = source.slice(start, end); const substitutions: Array<{ placeholder: string; text: string }> = []; + const usedPlaceholders = new Set(); const substitutionNodes = node .children() @@ -638,7 +663,11 @@ function pushTemplateStringEdit( const childRange = child.range(); const childStart = childRange.start.index - start; const childEnd = childRange.end.index - start; - const placeholder = `__TAILOR_SDK_TEMPLATE_EXPR_${substitutions.length}__`; + const placeholder = templateSubstitutionPlaceholder( + substitutions.length, + text, + usedPlaceholders, + ); substitutions.push({ placeholder, text: transformTemplateSubstitutionText( @@ -697,6 +726,7 @@ function transformSourceFile(source: string, filePath: string): string | null { pushSourceStringEdit(edits, source, node); return; } + if (isCliValueArgument(node, source)) return; pushTemplateStringEdit(edits, source, node); return; } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index c327e6ade..3edd7bee1 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -2,6 +2,8 @@ const script = "tailor deploy"; const spawned = spawn("tailor", ["deploy"]); const argSpawned = spawn("tailor", ["--arg", "tailor-sdk deploy", "deploy"]); const inlineArgSpawned = spawn("tailor", ["--arg=tailor-sdk deploy", "deploy"]); +const templateArgSpawned = spawn("tailor", ["--arg", `tailor-sdk ${cmd}`, "deploy"]); +const inlineTemplateArgSpawned = spawn("tailor", [`--arg=tailor-sdk ${cmd}`, "deploy"]); const shellSpawned = spawn("sh", ["-c", "tailor deploy"]); const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 6c6ca8e68..85311eed6 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -2,6 +2,8 @@ const script = "tailor-sdk deploy"; const spawned = spawn("tailor-sdk", ["deploy"]); const argSpawned = spawn("tailor-sdk", ["--arg", "tailor-sdk deploy", "deploy"]); const inlineArgSpawned = spawn("tailor-sdk", ["--arg=tailor-sdk deploy", "deploy"]); +const templateArgSpawned = spawn("tailor-sdk", ["--arg", `tailor-sdk ${cmd}`, "deploy"]); +const inlineTemplateArgSpawned = spawn("tailor-sdk", [`--arg=tailor-sdk ${cmd}`, "deploy"]); const shellSpawned = spawn("sh", ["-c", "tailor-sdk deploy"]); const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 21407f795..ce165e8f4 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -22,6 +22,7 @@ const latestWithRunnerOption = "npx --yes @tailor-platform/sdk@latest login"; const dynamicNpxCommand = `npx @tailor-platform/sdk ${subcommand}`; const dynamicNpxRegistryCommand = `npx --registry ${registry} @tailor-platform/sdk login`; const dynamicRunnerCommand = `${runner} tailor-sdk login`; +const literalPlaceholderCommand = `prefix __TAILOR_SDK_TEMPLATE_EXPR_0__ tailor-sdk ${subcommand}`; const npxOtherPackage = "npx foo tailor-sdk login"; const dynamicBunxCommand = `bunx @tailor-platform/sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx @tailor-platform/sdk ${subcommand}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 1c9e11b59..6f5e766c4 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -22,6 +22,7 @@ const latestWithRunnerOption = "npx --yes tailor-sdk@latest login"; const dynamicNpxCommand = `npx tailor-sdk ${subcommand}`; const dynamicNpxRegistryCommand = `npx --registry ${registry} tailor-sdk login`; const dynamicRunnerCommand = `${runner} tailor-sdk login`; +const literalPlaceholderCommand = `prefix __TAILOR_SDK_TEMPLATE_EXPR_0__ tailor-sdk ${subcommand}`; const npxOtherPackage = "npx foo tailor-sdk login"; const dynamicBunxCommand = `bunx tailor-sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx tailor-sdk ${subcommand}`; From a1e0389ac35768993010f3671ccbaf432974b6bb Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 22:30:14 +0900 Subject: [PATCH 325/618] fix(sdk-codemod): handle source package flag edges --- .../v2/rename-bin/scripts/transform.ts | 48 ++++++++++++++++--- .../tests/source-template/expected.ts | 5 ++ .../rename-bin/tests/source-template/input.ts | 5 ++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 24c543029..0e4f61fa3 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -93,12 +93,13 @@ const SOURCE_PKG_RUNNER_RE = new RegExp( `\\b(${PACKAGE_RUNNER_COMMAND}${SOURCE_RUNNER_OPTION_GAP})\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})`, "g", ); -const SOURCE_NPX_PACKAGE_FLAG_VALUE_RE = new RegExp( - `\\b(npx(?:(?!\\s+(?:-p|--package)(?:=|\\s+))\\s+${SOURCE_ARG_VALUE})*\\s+(?:-p|--package)(?:=|\\s+))tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})`, +const SOURCE_RUNNER_OPTION_NO_PACKAGE_FLAG_GAP = `(?:\\s+(?!(?:-p|--package)(?:=|\\s|$))(?:-\\w+|--\\w[\\w-]*)(?:=${SOURCE_ARG_VALUE})?(?:\\s+(?!tailor-sdk(?![\\w-])|-)${SOURCE_ARG_VALUE})?)*`; +const SOURCE_PACKAGE_FLAG_VALUE_RE = new RegExp( + `\\b(${PACKAGE_RUNNER_COMMAND}${SOURCE_RUNNER_OPTION_NO_PACKAGE_FLAG_GAP}\\s+(?:-p|--package)(?:=|\\s+))tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})`, "g", ); -const SOURCE_NPX_PACKAGE_FLAG_BINARY_RE = new RegExp( - `\\b(npx(?:(?!\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})\\s+${SOURCE_ARG_VALUE})*\\s+)tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})`, +const SOURCE_PACKAGE_FLAG_BINARY_RE = new RegExp( + `\\b(${PACKAGE_RUNNER_COMMAND}(?:(?!\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})\\s+${SOURCE_ARG_VALUE})*\\s+)tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})`, "g", ); const SOURCE_TAILOR_SDK_RE = new RegExp( @@ -205,6 +206,33 @@ function sourceTokens(value: string): string[] | null { return value.slice(lastEnd).trim() === "" ? tokens : null; } +function activeQuoteStart(source: string, start: number, offset: number): number | null { + let quote: { delimiter: string; escaped: boolean; start: number } | null = null; + for (let index = start; index < offset; index += 1) { + const char = source[index]; + if (quote != null) { + if (quote.escaped) { + if (char === "\\" && source[index + 1] === quote.delimiter) { + quote = null; + index += 1; + } + } else if (char === "\\") { + index += 1; + } else if (char === quote.delimiter) { + quote = null; + } + continue; + } + if (char === "\\" && (source[index + 1] === '"' || source[index + 1] === "'")) { + quote = { delimiter: source[index + 1]!, escaped: true, start: index }; + index += 1; + } else if (char === '"' || char === "'") { + quote = { delimiter: char, escaped: false, start: index }; + } + } + return quote?.start ?? null; +} + function skipsRunnerOptionValue(token: string): boolean { return RUNNER_OPTION_VALUE_FLAGS.has(token.split("=", 1)[0]!) && !token.includes("="); } @@ -251,7 +279,13 @@ function isAfterOtherPackageRunner(source: string, offset: number): boolean { ); const segment = source.slice(segmentStart + 1, offset).trim(); const tokens = sourceTokens(segment); - if (tokens == null) return false; + if (tokens == null) { + const quoteStart = activeQuoteStart(source, segmentStart + 1, offset); + if (quoteStart == null) return false; + const prefixTokens = sourceTokens(source.slice(segmentStart + 1, quoteStart).trim()); + const packageToken = prefixTokens == null ? null : firstRunnerPackageToken(prefixTokens); + return packageToken != null && !TAILOR_SDK_TOKEN_RE.test(packageToken); + } const packageToken = firstRunnerPackageToken(tokens); return packageToken != null && !TAILOR_SDK_TOKEN_RE.test(packageToken); } @@ -281,7 +315,7 @@ function isAfterTemplatePlaceholder(source: string, offset: number): boolean { function renameSourceCommandText(value: string): string { const protectedValue = protectSourceCliValueReferences(value); const withPackageFlagValues = protectedValue.source.replace( - SOURCE_NPX_PACKAGE_FLAG_VALUE_RE, + SOURCE_PACKAGE_FLAG_VALUE_RE, (_match, prefix: string, version?: string) => version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}@tailor-platform/sdk`, ); @@ -295,7 +329,7 @@ function renameSourceCommandText(value: string): string { }, ); const withPackageFlagBinaries = withPackageRunners.replace( - SOURCE_NPX_PACKAGE_FLAG_BINARY_RE, + SOURCE_PACKAGE_FLAG_BINARY_RE, (match: string, prefix: string, version?: string) => { if (!/\s(?:-p|--package)(?:=|\s)/.test(prefix)) return match; if (/(?:^|\s)(?:-p|--package)\s+$/.test(prefix)) return match; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index ce165e8f4..1093412f0 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -40,6 +40,11 @@ const npxPackageFlagDynamic = `npx -p @tailor-platform/sdk tailor ${subcommand}` const npxPackageEqualsDynamic = `npx --package=@tailor-platform/sdk tailor ${subcommand}`; const npxRegistryPackageFlag = "npx --registry https://registry.npmjs.org -p @tailor-platform/sdk tailor login"; const npxProfileValue = "npx @tailor-platform/sdk -p tailor-sdk login"; +const pnpmDlxPackageFlag = "pnpm dlx --package @tailor-platform/sdk tailor login"; +const pnpmDlxPackageFlagEquals = "pnpm dlx --package=@tailor-platform/sdk tailor login"; +const yarnDlxPackageFlag = "yarn dlx --package @tailor-platform/sdk tailor login"; +const npxOtherPackageQuoted = "npx foo \"tailor-sdk login\""; +const npxOtherPackageSingleQuoted = "npx foo 'tailor-sdk login'"; const shellWrapped = "sh -c \"tailor deploy\""; const escapedArg = "tailor --arg \"tailor-sdk deploy\" deploy"; const packageName = "tailor-sdk"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 6f5e766c4..8d5ebaae7 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -40,6 +40,11 @@ const npxPackageFlagDynamic = `npx -p tailor-sdk tailor-sdk ${subcommand}`; const npxPackageEqualsDynamic = `npx --package=tailor-sdk tailor-sdk ${subcommand}`; const npxRegistryPackageFlag = "npx --registry https://registry.npmjs.org -p tailor-sdk tailor-sdk login"; const npxProfileValue = "npx tailor-sdk -p tailor-sdk login"; +const pnpmDlxPackageFlag = "pnpm dlx --package tailor-sdk tailor-sdk login"; +const pnpmDlxPackageFlagEquals = "pnpm dlx --package=tailor-sdk tailor-sdk login"; +const yarnDlxPackageFlag = "yarn dlx --package tailor-sdk tailor-sdk login"; +const npxOtherPackageQuoted = "npx foo \"tailor-sdk login\""; +const npxOtherPackageSingleQuoted = "npx foo 'tailor-sdk login'"; const shellWrapped = "sh -c \"tailor-sdk deploy\""; const escapedArg = "tailor-sdk --arg \"tailor-sdk deploy\" deploy"; const packageName = "tailor-sdk"; From d3216d810c51763159a1baf1c4b32881c3ef1f3d Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 22:52:28 +0900 Subject: [PATCH 326/618] fix(sdk-codemod): preserve package runner arguments --- .../v2/rename-bin/scripts/transform.ts | 145 +++++++++++++----- .../tests/source-js-string/expected.js | 5 + .../tests/source-js-string/input.js | 5 + .../tests/source-template/expected.ts | 3 + .../rename-bin/tests/source-template/input.ts | 3 + 5 files changed, 126 insertions(+), 35 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 0e4f61fa3..e049c15b5 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -115,6 +115,7 @@ const SOURCE_DYNAMIC_OPTION_TAILOR_SDK_RE = new RegExp( "g", ); const TAILOR_SDK_TOKEN_RE = /^tailor-sdk(@[^\s'"`;|&)]+)?$/; +const TAILOR_SDK_PATH_RE = /(?:^|[\\/])tailor-sdk(?![\w-])(@[^\s'"`;|&)]+)?/; const TAILOR_CLI_TOKEN_RE = /^(?:tailor|tailor-sdk(?:@[^\s'"`;|&)]+)?|@tailor-platform\/sdk(?:@[^\s'"`;|&)]+)?)$/; const CLI_ARGUMENT_CALLEE_RE = /(?:^|\.)(?:spawn|spawnSync|execFile|execFileSync|execa|execaSync)$/; @@ -237,18 +238,16 @@ function skipsRunnerOptionValue(token: string): boolean { return RUNNER_OPTION_VALUE_FLAGS.has(token.split("=", 1)[0]!) && !token.includes("="); } -function firstRunnerPackageToken(tokens: string[]): string | null { +function packageRunnerPackageStartTokenIndex(tokens: readonly string[]): number | null { const executable = tokens[0]; - let index: number; if (executable === "npx" || executable === "bunx") { - index = 1; + return 1; } else if (executable === "pnpm" || executable === "yarn") { - index = 1; + let index = 1; for (; index < tokens.length; index += 1) { const token = tokens[index]!; if (token === "dlx") { - index += 1; - break; + return index + 1; } if (token.startsWith("-")) { if (skipsRunnerOptionValue(token)) index += 1; @@ -256,11 +255,31 @@ function firstRunnerPackageToken(tokens: string[]): string | null { } return null; } - } else { - return null; } + return null; +} + +function hasPackageBeforePackageFlag( + tokens: readonly string[], + start: number, + end: number, +): boolean { + for (let index = start; index < end; index += 1) { + const token = tokens[index]!; + if (SOURCE_PACKAGE_FLAG_RE.test(token)) return false; + if (token.startsWith("-")) { + if (skipsRunnerOptionValue(token)) index += 1; + continue; + } + return true; + } + return false; +} - for (; index < tokens.length; index += 1) { +function firstRunnerPackageToken(tokens: string[]): string | null { + const start = packageRunnerPackageStartTokenIndex(tokens); + if (start == null) return null; + for (let index = start; index < tokens.length; index += 1) { const token = tokens[index]!; if (token.startsWith("-")) { if (skipsRunnerOptionValue(token)) index += 1; @@ -271,6 +290,13 @@ function firstRunnerPackageToken(tokens: string[]): string | null { return null; } +function sourceHasPackageBeforePackageFlag(source: string): boolean { + const tokens = sourceTokens(source.trim()); + if (tokens == null) return false; + const start = packageRunnerPackageStartTokenIndex(tokens); + return start != null && hasPackageBeforePackageFlag(tokens, start, tokens.length); +} + function isAfterOtherPackageRunner(source: string, offset: number): boolean { const segmentStart = Math.max( source.lastIndexOf(";", offset - 1), @@ -333,6 +359,7 @@ function renameSourceCommandText(value: string): string { (match: string, prefix: string, version?: string) => { if (!/\s(?:-p|--package)(?:=|\s)/.test(prefix)) return match; if (/(?:^|\s)(?:-p|--package)\s+$/.test(prefix)) return match; + if (sourceHasPackageBeforePackageFlag(prefix)) return match; return version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}tailor`; }, ); @@ -444,19 +471,52 @@ function isTailorCliValueFlag(value: string): boolean { return TAILOR_CLI_VALUE_FLAGS.has(value.split("=", 1)[0]!); } -function hasNpxPackageFlagBefore(elements: SgNode[], index: number, source: string): boolean { - return elements.some((element, currentIndex) => { - if (currentIndex >= index) return false; - const value = sourceStringContent(element, source); - return value != null && SOURCE_PACKAGE_FLAG_RE.test(value); - }); +function packageRunnerPackageStartIndex( + executable: string, + elements: SgNode[], + source: string, +): number | null { + if (SOURCE_PACKAGE_RUNNERS.has(executable)) return 0; + if (!SOURCE_DLX_PACKAGE_RUNNERS.has(executable)) return null; + const dlxIndex = firstNonOptionIndex(elements, 0, source); + if (dlxIndex == null || sourceStringContent(elements[dlxIndex]!, source) !== "dlx") { + return null; + } + return dlxIndex + 1; +} + +function hasPackageFlagBeforeArrayPackage( + elements: SgNode[], + index: number, + source: string, + start: number, +): boolean { + for (let currentIndex = start; currentIndex < index; currentIndex += 1) { + const value = sourceStringContent(elements[currentIndex]!, source); + if (value == null) return false; + if (SOURCE_PACKAGE_FLAG_RE.test(value)) return true; + if (value.startsWith("-")) { + if (skipsRunnerOptionValue(value)) currentIndex += 1; + continue; + } + return false; + } + return false; } -function isNpxSplitPackageFlagValue(elements: SgNode[], index: number, source: string): boolean { +function isSplitPackageFlagValue( + elements: SgNode[], + index: number, + source: string, + start: number, +): boolean { const previous = elements[index - 1]; if (previous == null) return false; const previousValue = sourceStringContent(previous, source); - return previousValue === "-p" || previousValue === "--package"; + return ( + (previousValue === "-p" || previousValue === "--package") && + hasPackageFlagBeforeArrayPackage(elements, index, source, start) + ); } function sourcePackageFlagReplacement( @@ -469,6 +529,18 @@ function sourcePackageFlagReplacement( if (match?.groups == null) return null; const parent = node.parent(); if (parent?.kind() !== "array" || !isPackageRunnerArrayArgument(node, source)) return null; + const argumentsNode = parent.parent(); + if (argumentsNode?.kind() !== "arguments") return null; + const callArgs = sourceArrayElements(argumentsNode); + const executableNode = callArgs[0]; + if (executableNode == null) return null; + const executable = sourceStringContent(executableNode, source); + if (executable == null) return null; + const elements = sourceArrayElements(parent); + const index = nodeIndex(elements, node); + const start = packageRunnerPackageStartIndex(executable, elements, source); + if (index < 0 || start == null) return null; + if (!hasPackageFlagBeforeArrayPackage(elements, index + 1, source, start)) return null; return { text, replacement: `${match.groups.prefix}${renamePackageName( @@ -531,24 +603,14 @@ function isPackageRunnerPackageArgument(node: SgNode, source: string): boolean { const elements = sourceArrayElements(parent); const index = nodeIndex(elements, node); if (index < 0) return false; + const start = packageRunnerPackageStartIndex(executable, elements, source); + if (start == null) return false; - if (hasNpxPackageFlagBefore(elements, index, source)) { - return isNpxSplitPackageFlagValue(elements, index, source); - } - - if (SOURCE_PACKAGE_RUNNERS.has(executable)) { - return firstTailorPackageIndex(elements, 0, source) === index; - } - - if (SOURCE_DLX_PACKAGE_RUNNERS.has(executable)) { - const dlxIndex = firstNonOptionIndex(elements, 0, source); - if (dlxIndex == null || sourceStringContent(elements[dlxIndex]!, source) !== "dlx") { - return false; - } - return firstTailorPackageIndex(elements, dlxIndex + 1, source) === index; + if (hasPackageFlagBeforeArrayPackage(elements, index, source, start)) { + return isSplitPackageFlagValue(elements, index, source, start); } - return false; + return firstTailorPackageIndex(elements, start, source) === index; } function isPackageRunnerCommandBinaryArgument(node: SgNode, source: string): boolean { @@ -557,13 +619,25 @@ function isPackageRunnerCommandBinaryArgument(node: SgNode, source: string): boo const elements = sourceArrayElements(parent); const index = nodeIndex(elements, node); - if (index < 0 || !hasNpxPackageFlagBefore(elements, index, source)) return false; + if (index < 0) return false; + + const argumentsNode = parent.parent(); + if (argumentsNode?.kind() !== "arguments") return false; + const callArgs = sourceArrayElements(argumentsNode); + const executableNode = callArgs[0]; + if (executableNode == null) return false; + const executable = sourceStringContent(executableNode, source); + if (executable == null) return false; + const start = packageRunnerPackageStartIndex(executable, elements, source); + if (start == null || !hasPackageFlagBeforeArrayPackage(elements, index, source, start)) { + return false; + } const text = sourceStringContent(node, source); return ( text != null && TAILOR_SDK_TOKEN_RE.test(text) && - !isNpxSplitPackageFlagValue(elements, index, source) + !isSplitPackageFlagValue(elements, index, source, start) ); } @@ -654,7 +728,8 @@ function pushSourceStringEdit( ? packageFlagReplacement.replacement : TAILOR_SDK_TOKEN_RE.test(text) && isPackageRunnerPackageArgument(node, source) ? renamePackageName(text) - : TAILOR_SDK_TOKEN_RE.test(text) && isCliBinaryArgument(node, source) + : (TAILOR_SDK_TOKEN_RE.test(text) || TAILOR_SDK_PATH_RE.test(text)) && + isCliBinaryArgument(node, source) ? renameBinary(text) : renameSourceCommandText(text); if (replacement !== text) { diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 3edd7bee1..9f94dcb7d 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -15,6 +15,9 @@ const npxInlineProfileNamedTailorSdkSpawned = spawn("npx", ["@tailor-platform/sd const npxVersionSpawned = spawn("npx", ["@tailor-platform/sdk", "--version"]); const npxDynamicSpawned = spawn("npx", ["@tailor-platform/sdk", subcommand]); const npxOtherPackageSpawned = spawn("npx", ["foo", "tailor-sdk", "login"]); +const npxOtherPackageFlagSpawned = spawn("npx", ["foo", "-p", "tailor-sdk", "tailor-sdk", "login"]); +const npxOtherPackageEqualsSpawned = spawn("npx", ["foo", "--package=tailor-sdk", "tailor-sdk", "login"]); +const npxOtherPackageSplitSpawned = spawn("npx", ["foo", "--package", "tailor-sdk", "tailor-sdk", "login"]); const npxPackageFlagSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "tailor", "login"]); const npxPackageEqualsSpawned = spawn("npx", ["--package=@tailor-platform/sdk", "tailor", "login"]); const npxPackageFlagDynamicSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "tailor", subcommand]); @@ -24,6 +27,7 @@ const npxPackageDynamicSpawned = spawn("npx", ["-p", pkg, "tailor", "login"]); const pnpmDlxSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk", "login"]); const pnpmDlxDynamicSpawned = spawn("pnpm", ["dlx", "@tailor-platform/sdk"]); const pnpmDlxOtherPackageSpawned = spawn("pnpm", ["dlx", "foo", "tailor-sdk", "login"]); +const pnpmDlxOtherPackageFlagSpawned = spawn("pnpm", ["dlx", "foo", "-p", "tailor-sdk", "tailor-sdk", "login"]); const pnpmDlxOptionSpawned = spawn("pnpm", ["--silent", "dlx", "@tailor-platform/sdk", "login"]); const pnpmDlxSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "dlx", "@tailor-platform/sdk", "login"]); const pnpmDlxRegistrySpawned = spawn("pnpm", ["--registry", registry, "dlx", "@tailor-platform/sdk", "login"]); @@ -33,6 +37,7 @@ const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor", "deploy"]); const pnpmExecDynamicSpawned = spawn("pnpm", ["exec", "tailor", subcommand]); const pnpmExecHelpSpawned = spawn("pnpm", ["exec", "tailor", "--help"]); +const pathQualifiedSpawned = spawn("./node_modules/.bin/tailor", ["deploy"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; const npxArgs = ["tailor-sdk", "login"]; spawn("npx", npxArgs); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 85311eed6..105571904 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -15,6 +15,9 @@ const npxInlineProfileNamedTailorSdkSpawned = spawn("npx", ["tailor-sdk", "-p=ta const npxVersionSpawned = spawn("npx", ["tailor-sdk", "--version"]); const npxDynamicSpawned = spawn("npx", ["tailor-sdk", subcommand]); const npxOtherPackageSpawned = spawn("npx", ["foo", "tailor-sdk", "login"]); +const npxOtherPackageFlagSpawned = spawn("npx", ["foo", "-p", "tailor-sdk", "tailor-sdk", "login"]); +const npxOtherPackageEqualsSpawned = spawn("npx", ["foo", "--package=tailor-sdk", "tailor-sdk", "login"]); +const npxOtherPackageSplitSpawned = spawn("npx", ["foo", "--package", "tailor-sdk", "tailor-sdk", "login"]); const npxPackageFlagSpawned = spawn("npx", ["-p", "tailor-sdk", "tailor-sdk", "login"]); const npxPackageEqualsSpawned = spawn("npx", ["--package=tailor-sdk", "tailor-sdk", "login"]); const npxPackageFlagDynamicSpawned = spawn("npx", ["-p", "tailor-sdk", "tailor-sdk", subcommand]); @@ -24,6 +27,7 @@ const npxPackageDynamicSpawned = spawn("npx", ["-p", pkg, "tailor-sdk", "login"] const pnpmDlxSpawned = spawn("pnpm", ["dlx", "tailor-sdk", "login"]); const pnpmDlxDynamicSpawned = spawn("pnpm", ["dlx", "tailor-sdk"]); const pnpmDlxOtherPackageSpawned = spawn("pnpm", ["dlx", "foo", "tailor-sdk", "login"]); +const pnpmDlxOtherPackageFlagSpawned = spawn("pnpm", ["dlx", "foo", "-p", "tailor-sdk", "tailor-sdk", "login"]); const pnpmDlxOptionSpawned = spawn("pnpm", ["--silent", "dlx", "tailor-sdk", "login"]); const pnpmDlxSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "dlx", "tailor-sdk", "login"]); const pnpmDlxRegistrySpawned = spawn("pnpm", ["--registry", registry, "dlx", "tailor-sdk", "login"]); @@ -33,6 +37,7 @@ const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor-sdk", "deploy"]); const pnpmExecDynamicSpawned = spawn("pnpm", ["exec", "tailor-sdk", subcommand]); const pnpmExecHelpSpawned = spawn("pnpm", ["exec", "tailor-sdk", "--help"]); +const pathQualifiedSpawned = spawn("./node_modules/.bin/tailor-sdk", ["deploy"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; const npxArgs = ["tailor-sdk", "login"]; spawn("npx", npxArgs); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 1093412f0..119e80403 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -24,6 +24,8 @@ const dynamicNpxRegistryCommand = `npx --registry ${registry} @tailor-platform/s const dynamicRunnerCommand = `${runner} tailor-sdk login`; const literalPlaceholderCommand = `prefix __TAILOR_SDK_TEMPLATE_EXPR_0__ tailor-sdk ${subcommand}`; const npxOtherPackage = "npx foo tailor-sdk login"; +const npxOtherPackageFlag = "npx foo -p tailor-sdk tailor-sdk login"; +const npxOtherPackageFlagEquals = "npx foo --package=tailor-sdk tailor-sdk login"; const dynamicBunxCommand = `bunx @tailor-platform/sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx @tailor-platform/sdk ${subcommand}`; const pnpmDlxWithOption = "pnpm --silent dlx @tailor-platform/sdk login"; @@ -43,6 +45,7 @@ const npxProfileValue = "npx @tailor-platform/sdk -p tailor-sdk login"; const pnpmDlxPackageFlag = "pnpm dlx --package @tailor-platform/sdk tailor login"; const pnpmDlxPackageFlagEquals = "pnpm dlx --package=@tailor-platform/sdk tailor login"; const yarnDlxPackageFlag = "yarn dlx --package @tailor-platform/sdk tailor login"; +const pnpmDlxOtherPackageFlag = "pnpm dlx foo -p tailor-sdk tailor-sdk login"; const npxOtherPackageQuoted = "npx foo \"tailor-sdk login\""; const npxOtherPackageSingleQuoted = "npx foo 'tailor-sdk login'"; const shellWrapped = "sh -c \"tailor deploy\""; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 8d5ebaae7..6a24beecc 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -24,6 +24,8 @@ const dynamicNpxRegistryCommand = `npx --registry ${registry} tailor-sdk login`; const dynamicRunnerCommand = `${runner} tailor-sdk login`; const literalPlaceholderCommand = `prefix __TAILOR_SDK_TEMPLATE_EXPR_0__ tailor-sdk ${subcommand}`; const npxOtherPackage = "npx foo tailor-sdk login"; +const npxOtherPackageFlag = "npx foo -p tailor-sdk tailor-sdk login"; +const npxOtherPackageFlagEquals = "npx foo --package=tailor-sdk tailor-sdk login"; const dynamicBunxCommand = `bunx tailor-sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx tailor-sdk ${subcommand}`; const pnpmDlxWithOption = "pnpm --silent dlx tailor-sdk login"; @@ -43,6 +45,7 @@ const npxProfileValue = "npx tailor-sdk -p tailor-sdk login"; const pnpmDlxPackageFlag = "pnpm dlx --package tailor-sdk tailor-sdk login"; const pnpmDlxPackageFlagEquals = "pnpm dlx --package=tailor-sdk tailor-sdk login"; const yarnDlxPackageFlag = "yarn dlx --package tailor-sdk tailor-sdk login"; +const pnpmDlxOtherPackageFlag = "pnpm dlx foo -p tailor-sdk tailor-sdk login"; const npxOtherPackageQuoted = "npx foo \"tailor-sdk login\""; const npxOtherPackageSingleQuoted = "npx foo 'tailor-sdk login'"; const shellWrapped = "sh -c \"tailor-sdk deploy\""; From 2af0de871809a8289d463bc106e25986ba32af07 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 23:07:12 +0900 Subject: [PATCH 327/618] fix(sdk-codemod): preserve template value arguments --- .../codemods/v2/rename-bin/scripts/transform.ts | 16 +++++++++++++--- .../rename-bin/tests/source-template/expected.ts | 2 ++ .../v2/rename-bin/tests/source-template/input.ts | 2 ++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index e049c15b5..241ea726b 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -338,6 +338,14 @@ function isAfterTemplatePlaceholder(source: string, offset: number): boolean { return tokens != null && tokens.some((token) => SOURCE_TEMPLATE_EXPR_PLACEHOLDER_RE.test(token)); } +function isTemplateSubstitutionCliValue(text: string, offset: number): boolean { + const tokens = sourceTokens(text.slice(0, offset).trimEnd()); + if (tokens == null || tokens.length === 0) return false; + const previous = tokens.at(-1)!; + if (!isTailorCliValueFlag(previous)) return false; + return tokens.slice(0, -1).some((token) => TAILOR_CLI_TOKEN_RE.test(token)); +} + function renameSourceCommandText(value: string): string { const protectedValue = protectSourceCliValueReferences(value); const withPackageFlagValues = protectedValue.source.replace( @@ -779,9 +787,11 @@ function pushTemplateStringEdit( ); substitutions.push({ placeholder, - text: transformTemplateSubstitutionText( - source.slice(childRange.start.index, childRange.end.index), - ), + text: isTemplateSubstitutionCliValue(text, childStart) + ? source.slice(childRange.start.index, childRange.end.index) + : transformTemplateSubstitutionText( + source.slice(childRange.start.index, childRange.end.index), + ), }); text = `${text.slice(0, childStart)}${placeholder}${text.slice(childEnd)}`; } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 119e80403..11ea20bdf 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -34,6 +34,8 @@ const dynamicPnpmDlxWithOptionValue = `pnpm --filter ${app} dlx @tailor-platform const yarnDlxWithOption = "yarn --quiet dlx @tailor-platform/sdk login"; const nestedCommand = `run ${"tailor deploy"}`; const nestedTailorCommand = `tailor deploy ${"tailor login"}`; +const nestedArgValue = `tailor --arg ${"tailor-sdk deploy"} deploy`; +const nestedInlineArgValue = `tailor --arg=${"tailor-sdk deploy"} deploy`; const npxPackageFlag = "npx -p @tailor-platform/sdk tailor login"; const npxPackageSplitOnly = "npx --package @tailor-platform/sdk"; const npxPackageSplitHelp = "npx -p @tailor-platform/sdk --help"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 6a24beecc..ffb6d4f8b 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -34,6 +34,8 @@ const dynamicPnpmDlxWithOptionValue = `pnpm --filter ${app} dlx tailor-sdk login const yarnDlxWithOption = "yarn --quiet dlx tailor-sdk login"; const nestedCommand = `run ${"tailor-sdk deploy"}`; const nestedTailorCommand = `tailor deploy ${"tailor-sdk login"}`; +const nestedArgValue = `tailor-sdk --arg ${"tailor-sdk deploy"} deploy`; +const nestedInlineArgValue = `tailor-sdk --arg=${"tailor-sdk deploy"} deploy`; const npxPackageFlag = "npx -p tailor-sdk tailor-sdk login"; const npxPackageSplitOnly = "npx --package tailor-sdk"; const npxPackageSplitHelp = "npx -p tailor-sdk --help"; From 3e712f24fe838688ef2e4793b1cb6586e7a44325 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 23:30:28 +0900 Subject: [PATCH 328/618] fix(sdk-codemod): guard source command rewrites --- .../v2/rename-bin/scripts/transform.ts | 194 +++++++++++++++--- .../tests/source-js-string/expected.js | 6 + .../tests/source-js-string/input.js | 6 + .../tests/source-template/expected.ts | 9 + .../rename-bin/tests/source-template/input.ts | 9 + packages/sdk-codemod/src/registry.ts | 4 + 6 files changed, 198 insertions(+), 30 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 241ea726b..397ce9e3f 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -55,7 +55,7 @@ const TAILOR_CLI_COMMANDS = [ ] as const; const TAILOR_CLI_COMMAND_PATTERN = `(?:${TAILOR_CLI_COMMANDS.join("|")})`; const TAILOR_CLI_VALUE_FLAG = - "(?:--env-file-if-exists|--env-file|--profile|--config|--workspace-id|--arg|--query|--file|-e|-p|-c|-w|-a|-q|-f)"; + "(?:--env-file-if-exists|--env-file|--profile|--config|--workspace-id|--arg|--query|--file|--name|--namespace|--dir|-e|-p|-c|-w|-a|-q|-f|-n)"; const TAILOR_CLI_VALUE_FLAGS = new Set([ "--env-file-if-exists", "--env-file", @@ -65,6 +65,9 @@ const TAILOR_CLI_VALUE_FLAGS = new Set([ "--arg", "--query", "--file", + "--name", + "--namespace", + "--dir", "-e", "-p", "-c", @@ -72,13 +75,14 @@ const TAILOR_CLI_VALUE_FLAGS = new Set([ "-a", "-q", "-f", + "-n", ]); const SOURCE_ESCAPED_QUOTED_VALUE = String.raw`\\"(?:\\\\.|[^"\\])*\\"|\\'(?:\\\\.|[^'\\])*\\'`; const SOURCE_CLI_ARG_VALUE = `(?:${SOURCE_ESCAPED_QUOTED_VALUE}|${SOURCE_ARG_VALUE})`; const SOURCE_COMMAND_GAP = `(?:\\s+--?[\\w-]+(?:=${SOURCE_CLI_ARG_VALUE})?(?:\\s+${SOURCE_CLI_ARG_VALUE})?)*`; const SOURCE_RUNNER_OPTION_GAP = `(?:\\s+(?:-\\w+|--\\w[\\w-]*)(?:=${SOURCE_ARG_VALUE})?(?:\\s+(?!tailor-sdk(?![\\w-])|-)${SOURCE_ARG_VALUE})?)*`; -const SOURCE_CLI_VALUE_REFERENCE_RE = new RegExp( - `(${TAILOR_CLI_VALUE_FLAG}(?:=|\\s+))(${SOURCE_CLI_ARG_VALUE})`, +const SOURCE_OPTION_VALUE_REFERENCE_RE = new RegExp( + `((?:--[\\w-]+|-\\w)(?:=|\\s+))(${SOURCE_CLI_ARG_VALUE})`, "g", ); const SOURCE_TEMPLATE_EXPR_PLACEHOLDER = "__TAILOR_SDK_TEMPLATE_EXPR_\\d+_\\d+__"; @@ -93,9 +97,8 @@ const SOURCE_PKG_RUNNER_RE = new RegExp( `\\b(${PACKAGE_RUNNER_COMMAND}${SOURCE_RUNNER_OPTION_GAP})\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})`, "g", ); -const SOURCE_RUNNER_OPTION_NO_PACKAGE_FLAG_GAP = `(?:\\s+(?!(?:-p|--package)(?:=|\\s|$))(?:-\\w+|--\\w[\\w-]*)(?:=${SOURCE_ARG_VALUE})?(?:\\s+(?!tailor-sdk(?![\\w-])|-)${SOURCE_ARG_VALUE})?)*`; const SOURCE_PACKAGE_FLAG_VALUE_RE = new RegExp( - `\\b(${PACKAGE_RUNNER_COMMAND}${SOURCE_RUNNER_OPTION_NO_PACKAGE_FLAG_GAP}\\s+(?:-p|--package)(?:=|\\s+))tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})`, + `((?:-p|--package)(?:=|\\s+))tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?`, "g", ); const SOURCE_PACKAGE_FLAG_BINARY_RE = new RegExp( @@ -139,6 +142,7 @@ const NPX_PACKAGE_FLAG_CONTEXT_RE = new RegExp( `(?:^|[;&|]\\s*)npx(?:\\s+(?:${NPX_OPTION_WITH_VALUE}\\s+${SOURCE_ARG_VALUE}|-\\w+|--\\w[\\w-]*(?:=${SOURCE_ARG_VALUE})?))*\\s*$`, ); const SOURCE_TOKEN_RE = new RegExp(SOURCE_ARG_VALUE, "g"); +const CLI_RENAME_LEGACY_RE = /(? { if (!arg.includes("tailor-sdk")) return match; - if (!isAfterTailorCliToken(value, offset)) return match; - if (prefix.startsWith("-p") && NPX_PACKAGE_FLAG_CONTEXT_RE.test(value.slice(0, offset))) { + const flag = sourceOptionFlag(prefix); + if ( + !isAfterTailorCliToken(value, offset) && + !isPackageRunnerOptionValue(value, offset, flag) + ) { + return match; + } + if (isPackageFlag(flag) && NPX_PACKAGE_FLAG_CONTEXT_RE.test(value.slice(0, offset))) { return match; } const placeholder = `__TAILOR_SDK_SOURCE_VALUE_${protectedValues.length}__`; @@ -195,6 +205,14 @@ function restoreSourceCliValueReferences(value: string, protectedValues: string[ return restored; } +function sourceOptionFlag(prefix: string): string { + return prefix.trim().replace(/[=\s].*$/, ""); +} + +function isPackageFlag(value: string): boolean { + return value === "-p" || value === "--package"; +} + function sourceTokens(value: string): string[] | null { const tokens: string[] = []; SOURCE_TOKEN_RE.lastIndex = 0; @@ -238,6 +256,11 @@ function skipsRunnerOptionValue(token: string): boolean { return RUNNER_OPTION_VALUE_FLAGS.has(token.split("=", 1)[0]!) && !token.includes("="); } +function isPotentialValueFlag(value: string): boolean { + const flag = value.split("=", 1)[0]!; + return /^--[\w-]+$/.test(flag) || /^-\w$/.test(flag); +} + function packageRunnerPackageStartTokenIndex(tokens: readonly string[]): number | null { const executable = tokens[0]; if (executable === "npx" || executable === "bunx") { @@ -259,14 +282,16 @@ function packageRunnerPackageStartTokenIndex(tokens: readonly string[]): number return null; } -function hasPackageBeforePackageFlag( +function hasPositionalPackageBeforeSourcePackageFlag( tokens: readonly string[], start: number, - end: number, ): boolean { - for (let index = start; index < end; index += 1) { + for (let index = start; index < tokens.length; index += 1) { const token = tokens[index]!; - if (SOURCE_PACKAGE_FLAG_RE.test(token)) return false; + if (SOURCE_PACKAGE_FLAG_RE.test(token)) { + if (!token.includes("=")) index += 1; + continue; + } if (token.startsWith("-")) { if (skipsRunnerOptionValue(token)) index += 1; continue; @@ -290,11 +315,61 @@ function firstRunnerPackageToken(tokens: string[]): string | null { return null; } -function sourceHasPackageBeforePackageFlag(source: string): boolean { +function isPackageRunnerOptionValue(source: string, offset: number, flag: string): boolean { + if (!RUNNER_OPTION_VALUE_FLAGS.has(flag)) return false; + const segmentStart = Math.max( + source.lastIndexOf(";", offset - 1), + source.lastIndexOf("&", offset - 1), + source.lastIndexOf("|", offset - 1), + ); + const tokens = sourceTokens(source.slice(segmentStart + 1, offset).trim()); + const executable = tokens?.[0]; + return ( + executable === "npx" || executable === "bunx" || executable === "pnpm" || executable === "yarn" + ); +} + +function isPackageFlagValueInPackageRunner(source: string, offset: number): boolean { + const segmentStart = Math.max( + source.lastIndexOf(";", offset - 1), + source.lastIndexOf("&", offset - 1), + source.lastIndexOf("|", offset - 1), + ); + const tokens = sourceTokens(source.slice(segmentStart + 1, offset).trim()); + if (tokens == null) return false; + const start = packageRunnerPackageStartTokenIndex(tokens); + return start != null && !hasPositionalPackageBeforeSourcePackageFlag(tokens, start); +} + +function sourcePackageFlagsAllowBinaryRewrite(source: string): boolean { const tokens = sourceTokens(source.trim()); if (tokens == null) return false; const start = packageRunnerPackageStartTokenIndex(tokens); - return start != null && hasPackageBeforePackageFlag(tokens, start, tokens.length); + if (start == null) return false; + + let hasPackageFlag = false; + let hasTailorPackageFlag = false; + for (let index = start; index < tokens.length; index += 1) { + const token = tokens[index]!; + if (SOURCE_PACKAGE_FLAG_RE.test(token)) { + hasPackageFlag = true; + const value = token.includes("=") ? token.slice(token.indexOf("=") + 1) : tokens[index + 1]; + if ( + value != null && + (TAILOR_CLI_TOKEN_RE.test(value) || SOURCE_TEMPLATE_EXPR_PLACEHOLDER_RE.test(value)) + ) { + hasTailorPackageFlag = true; + } + if (!token.includes("=")) index += 1; + continue; + } + if (token.startsWith("-")) { + if (skipsRunnerOptionValue(token)) index += 1; + continue; + } + return false; + } + return hasPackageFlag && hasTailorPackageFlag; } function isAfterOtherPackageRunner(source: string, offset: number): boolean { @@ -346,12 +421,26 @@ function isTemplateSubstitutionCliValue(text: string, offset: number): boolean { return tokens.slice(0, -1).some((token) => TAILOR_CLI_TOKEN_RE.test(token)); } +function needsCliRenameMigration(value: string): boolean { + return value.includes("tailor-sdk") && CLI_RENAME_LEGACY_RE.test(value); +} + function renameSourceCommandText(value: string): string { + if (needsCliRenameMigration(value)) return value; + const protectedValue = protectSourceCliValueReferences(value); const withPackageFlagValues = protectedValue.source.replace( SOURCE_PACKAGE_FLAG_VALUE_RE, - (_match, prefix: string, version?: string) => - version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}@tailor-platform/sdk`, + ( + match: string, + prefix: string, + version: string | undefined, + offset: number, + source: string, + ) => { + if (!isPackageFlagValueInPackageRunner(source, offset)) return match; + return version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}@tailor-platform/sdk`; + }, ); const withPackageRunners = withPackageFlagValues.replace( SOURCE_PKG_RUNNER_RE, @@ -367,7 +456,7 @@ function renameSourceCommandText(value: string): string { (match: string, prefix: string, version?: string) => { if (!/\s(?:-p|--package)(?:=|\s)/.test(prefix)) return match; if (/(?:^|\s)(?:-p|--package)\s+$/.test(prefix)) return match; - if (sourceHasPackageBeforePackageFlag(prefix)) return match; + if (!sourcePackageFlagsAllowBinaryRewrite(prefix)) return match; return version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}tailor`; }, ); @@ -476,7 +565,7 @@ function firstNonOptionIndex(elements: SgNode[], start: number, source: string): } function isTailorCliValueFlag(value: string): boolean { - return TAILOR_CLI_VALUE_FLAGS.has(value.split("=", 1)[0]!); + return TAILOR_CLI_VALUE_FLAGS.has(value.split("=", 1)[0]!) || isPotentialValueFlag(value); } function packageRunnerPackageStartIndex( @@ -712,14 +801,56 @@ function isCliValueArgument(node: SgNode, source: string): boolean { if (index < 0) return false; if (!isTailorCliArgumentArray(parent, index, source)) return false; const text = sourceStringContent(node, source) ?? sourceStringRawContent(node, source); - if (text != null && isTailorCliValueFlag(text) && text.includes("=")) return true; + if ( + text != null && + text.includes("tailor-sdk") && + isTailorCliValueFlag(text) && + text.includes("=") + ) { + return true; + } if (index === 0) return false; const previousValue = sourceStringContent(elements[index - 1]!, source); return ( - previousValue != null && isTailorCliValueFlag(previousValue) && !previousValue.includes("=") + text != null && + text.includes("tailor-sdk") && + previousValue != null && + isTailorCliValueFlag(previousValue) && + !previousValue.includes("=") ); } +function sourceStringValue(node: SgNode, source: string): string | null { + return sourceStringContent(node, source) ?? sourceStringRawContent(node, source); +} + +function collectSourceStringValues(node: SgNode, source: string, values: string[]): void { + const value = sourceStringValue(node, source); + if (value != null) { + values.push(value); + return; + } + for (const child of node.children()) { + collectSourceStringValues(child, source, values); + } +} + +function cliInvocationContextNode(node: SgNode): SgNode | null { + const parent = node.parent(); + if (parent?.kind() === "array" && parent.parent()?.kind() === "arguments") { + return parent.parent()!; + } + return parent?.kind() === "arguments" ? parent : null; +} + +function contextNeedsCliRenameMigration(node: SgNode, source: string): boolean { + const context = cliInvocationContextNode(node); + if (context == null) return false; + const values: string[] = []; + collectSourceStringValues(context, source, values); + return values.some((value) => CLI_RENAME_LEGACY_RE.test(value)); +} + function pushSourceStringEdit( edits: Array<[number, number, string]>, source: string, @@ -730,16 +861,19 @@ function pushSourceStringEdit( const end = range.end.index - 1; const text = source.slice(start, end); const packageFlagReplacement = sourcePackageFlagReplacement(node, source); - const replacement = isCliValueArgument(node, source) - ? text - : packageFlagReplacement != null - ? packageFlagReplacement.replacement - : TAILOR_SDK_TOKEN_RE.test(text) && isPackageRunnerPackageArgument(node, source) - ? renamePackageName(text) - : (TAILOR_SDK_TOKEN_RE.test(text) || TAILOR_SDK_PATH_RE.test(text)) && - isCliBinaryArgument(node, source) - ? renameBinary(text) - : renameSourceCommandText(text); + const replacement = + contextNeedsCliRenameMigration(node, source) && text.includes("tailor-sdk") + ? text + : isCliValueArgument(node, source) + ? text + : packageFlagReplacement != null + ? packageFlagReplacement.replacement + : TAILOR_SDK_TOKEN_RE.test(text) && isPackageRunnerPackageArgument(node, source) + ? renamePackageName(text) + : (TAILOR_SDK_TOKEN_RE.test(text) || TAILOR_SDK_PATH_RE.test(text)) && + isCliBinaryArgument(node, source) + ? renameBinary(text) + : renameSourceCommandText(text); if (replacement !== text) { edits.push([start, end, replacement]); } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 9f94dcb7d..6a260f23d 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -4,7 +4,11 @@ const argSpawned = spawn("tailor", ["--arg", "tailor-sdk deploy", "deploy"]); const inlineArgSpawned = spawn("tailor", ["--arg=tailor-sdk deploy", "deploy"]); const templateArgSpawned = spawn("tailor", ["--arg", `tailor-sdk ${cmd}`, "deploy"]); const inlineTemplateArgSpawned = spawn("tailor", [`--arg=tailor-sdk ${cmd}`, "deploy"]); +const nameValueSpawned = spawn("tailor", ["tailordb", "migration", "generate", "--name", "tailor-sdk deploy"]); +const directNameValueSpawned = spawn("tailor", ["tailordb", "migration", "generate", "--name", "tailor-sdk deploy"]); const shellSpawned = spawn("sh", ["-c", "tailor deploy"]); +const cliRenameCommandSpawned = spawn("tailor-sdk", ["crash-report", "list"]); +const cliRenameFlagSpawned = spawn("tailor-sdk", ["login", "--machineuser"]); const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "dev", "login"]); @@ -19,6 +23,8 @@ const npxOtherPackageFlagSpawned = spawn("npx", ["foo", "-p", "tailor-sdk", "tai const npxOtherPackageEqualsSpawned = spawn("npx", ["foo", "--package=tailor-sdk", "tailor-sdk", "login"]); const npxOtherPackageSplitSpawned = spawn("npx", ["foo", "--package", "tailor-sdk", "tailor-sdk", "login"]); const npxPackageFlagSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "tailor", "login"]); +const npxMultiPackageFlagSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "-p", "dotenv-cli", "tailor", "login"]); +const npxMultiPackageFlagSecondSpawned = spawn("npx", ["-p", "dotenv-cli", "-p", "@tailor-platform/sdk", "tailor", "login"]); const npxPackageEqualsSpawned = spawn("npx", ["--package=@tailor-platform/sdk", "tailor", "login"]); const npxPackageFlagDynamicSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "tailor", subcommand]); const npxPackageEqualsDynamicSpawned = spawn("npx", ["--package=@tailor-platform/sdk", "tailor", subcommand]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 105571904..fca5eb51c 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -4,7 +4,11 @@ const argSpawned = spawn("tailor-sdk", ["--arg", "tailor-sdk deploy", "deploy"]) const inlineArgSpawned = spawn("tailor-sdk", ["--arg=tailor-sdk deploy", "deploy"]); const templateArgSpawned = spawn("tailor-sdk", ["--arg", `tailor-sdk ${cmd}`, "deploy"]); const inlineTemplateArgSpawned = spawn("tailor-sdk", [`--arg=tailor-sdk ${cmd}`, "deploy"]); +const nameValueSpawned = spawn("tailor", ["tailordb", "migration", "generate", "--name", "tailor-sdk deploy"]); +const directNameValueSpawned = spawn("tailor-sdk", ["tailordb", "migration", "generate", "--name", "tailor-sdk deploy"]); const shellSpawned = spawn("sh", ["-c", "tailor-sdk deploy"]); +const cliRenameCommandSpawned = spawn("tailor-sdk", ["crash-report", "list"]); +const cliRenameFlagSpawned = spawn("tailor-sdk", ["login", "--machineuser"]); const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["tailor-sdk", "--profile", "dev", "login"]); @@ -19,6 +23,8 @@ const npxOtherPackageFlagSpawned = spawn("npx", ["foo", "-p", "tailor-sdk", "tai const npxOtherPackageEqualsSpawned = spawn("npx", ["foo", "--package=tailor-sdk", "tailor-sdk", "login"]); const npxOtherPackageSplitSpawned = spawn("npx", ["foo", "--package", "tailor-sdk", "tailor-sdk", "login"]); const npxPackageFlagSpawned = spawn("npx", ["-p", "tailor-sdk", "tailor-sdk", "login"]); +const npxMultiPackageFlagSpawned = spawn("npx", ["-p", "tailor-sdk", "-p", "dotenv-cli", "tailor-sdk", "login"]); +const npxMultiPackageFlagSecondSpawned = spawn("npx", ["-p", "dotenv-cli", "-p", "tailor-sdk", "tailor-sdk", "login"]); const npxPackageEqualsSpawned = spawn("npx", ["--package=tailor-sdk", "tailor-sdk", "login"]); const npxPackageFlagDynamicSpawned = spawn("npx", ["-p", "tailor-sdk", "tailor-sdk", subcommand]); const npxPackageEqualsDynamicSpawned = spawn("npx", ["--package=tailor-sdk", "tailor-sdk", subcommand]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 11ea20bdf..fbccd5e4d 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -3,9 +3,15 @@ const setup = `pnpm tailor staticwebsite deploy --name ${siteName}`; const deploy = "tailor deploy"; const envFileDeploy = "tailor --env-file .env deploy"; const profileValue = "tailor --profile tailor-sdk deploy"; +const nameValue = "tailor tailordb migration generate --name \"tailor-sdk deploy\""; +const directNameValue = "tailor tailordb migration generate --name \"tailor-sdk deploy\""; +const namespaceValue = "tailor tailordb truncate --namespace tailor-sdk"; +const dirValue = "tailor staticwebsite deploy --name site --dir tailor-sdk"; const help = "tailor --help"; const npxVersion = "npx @tailor-platform/sdk --version"; const generated = "Run tailor generate after changes"; +const cliRenameCommand = "tailor-sdk crash-report list"; +const cliRenameFlag = "tailor-sdk login --machineuser"; const dynamicCommand = `tailor ${subcommand}`; const dynamicCommandWithTrailingFlag = `tailor ${subcommand} --json`; const dynamicCommandBeforeSeparator = `tailor ${subcommand} && echo done`; @@ -37,11 +43,14 @@ const nestedTailorCommand = `tailor deploy ${"tailor login"}`; const nestedArgValue = `tailor --arg ${"tailor-sdk deploy"} deploy`; const nestedInlineArgValue = `tailor --arg=${"tailor-sdk deploy"} deploy`; const npxPackageFlag = "npx -p @tailor-platform/sdk tailor login"; +const npxMultiPackageFlag = "npx -p @tailor-platform/sdk -p dotenv-cli tailor login"; +const npxMultiPackageFlagSecond = "npx -p dotenv-cli -p @tailor-platform/sdk tailor login"; const npxPackageSplitOnly = "npx --package @tailor-platform/sdk"; const npxPackageSplitHelp = "npx -p @tailor-platform/sdk --help"; const npxPackageFlagEquals = "npx --package=@tailor-platform/sdk tailor login"; const npxPackageFlagDynamic = `npx -p @tailor-platform/sdk tailor ${subcommand}`; const npxPackageEqualsDynamic = `npx --package=@tailor-platform/sdk tailor ${subcommand}`; +const npxRegistryValue = "npx --registry tailor-sdk @tailor-platform/sdk login"; const npxRegistryPackageFlag = "npx --registry https://registry.npmjs.org -p @tailor-platform/sdk tailor login"; const npxProfileValue = "npx @tailor-platform/sdk -p tailor-sdk login"; const pnpmDlxPackageFlag = "pnpm dlx --package @tailor-platform/sdk tailor login"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index ffb6d4f8b..5858d9e71 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -3,9 +3,15 @@ const setup = `pnpm tailor-sdk staticwebsite deploy --name ${siteName}`; const deploy = "tailor-sdk deploy"; const envFileDeploy = "tailor-sdk --env-file .env deploy"; const profileValue = "tailor-sdk --profile tailor-sdk deploy"; +const nameValue = "tailor tailordb migration generate --name \"tailor-sdk deploy\""; +const directNameValue = "tailor-sdk tailordb migration generate --name \"tailor-sdk deploy\""; +const namespaceValue = "tailor tailordb truncate --namespace tailor-sdk"; +const dirValue = "tailor staticwebsite deploy --name site --dir tailor-sdk"; const help = "tailor-sdk --help"; const npxVersion = "npx tailor-sdk --version"; const generated = "Run tailor-sdk generate after changes"; +const cliRenameCommand = "tailor-sdk crash-report list"; +const cliRenameFlag = "tailor-sdk login --machineuser"; const dynamicCommand = `tailor-sdk ${subcommand}`; const dynamicCommandWithTrailingFlag = `tailor-sdk ${subcommand} --json`; const dynamicCommandBeforeSeparator = `tailor-sdk ${subcommand} && echo done`; @@ -37,11 +43,14 @@ const nestedTailorCommand = `tailor deploy ${"tailor-sdk login"}`; const nestedArgValue = `tailor-sdk --arg ${"tailor-sdk deploy"} deploy`; const nestedInlineArgValue = `tailor-sdk --arg=${"tailor-sdk deploy"} deploy`; const npxPackageFlag = "npx -p tailor-sdk tailor-sdk login"; +const npxMultiPackageFlag = "npx -p tailor-sdk -p dotenv-cli tailor-sdk login"; +const npxMultiPackageFlagSecond = "npx -p dotenv-cli -p tailor-sdk tailor-sdk login"; const npxPackageSplitOnly = "npx --package tailor-sdk"; const npxPackageSplitHelp = "npx -p tailor-sdk --help"; const npxPackageFlagEquals = "npx --package=tailor-sdk tailor-sdk login"; const npxPackageFlagDynamic = `npx -p tailor-sdk tailor-sdk ${subcommand}`; const npxPackageEqualsDynamic = `npx --package=tailor-sdk tailor-sdk ${subcommand}`; +const npxRegistryValue = "npx --registry tailor-sdk tailor-sdk login"; const npxRegistryPackageFlag = "npx --registry https://registry.npmjs.org -p tailor-sdk tailor-sdk login"; const npxProfileValue = "npx tailor-sdk -p tailor-sdk login"; const pnpmDlxPackageFlag = "pnpm dlx --package tailor-sdk tailor-sdk login"; diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index bdea53db6..38859a948 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -13,6 +13,9 @@ const RENAME_BIN_SOURCE_VALUE_FLAGS = [ "--arg", "--query", "--file", + "--name", + "--namespace", + "--dir", "-e", "-p", "-c", @@ -20,6 +23,7 @@ const RENAME_BIN_SOURCE_VALUE_FLAGS = [ "-a", "-q", "-f", + "-n", ]; const RENAME_BIN_SOURCE_COMMANDS = [ "api", From 100611ab93464c74e650d6d4c006307c1815b5c3 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 27 Jun 2026 23:51:26 +0900 Subject: [PATCH 329/618] fix(sdk-codemod): handle quoted runner packages --- .../v2/rename-bin/scripts/transform.ts | 202 +++++++++++++++++- .../tests/source-js-string/expected.js | 1 + .../tests/source-js-string/input.js | 1 + .../tests/source-template/expected.ts | 5 + .../rename-bin/tests/source-template/input.ts | 5 + packages/sdk-codemod/src/registry.ts | 2 +- packages/sdk-codemod/src/runner.test.ts | 54 +++++ packages/sdk-codemod/src/runner.ts | 4 + 8 files changed, 265 insertions(+), 9 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 397ce9e3f..ee9fdc8ab 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -101,6 +101,8 @@ const SOURCE_PACKAGE_FLAG_VALUE_RE = new RegExp( `((?:-p|--package)(?:=|\\s+))tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?`, "g", ); +const SOURCE_PACKAGE_FLAG_EQUALS_QUOTED_VALUE_RE = + /((?:-p|--package)=)(\\"|\\'|"|')tailor-sdk(?![\w-])(@[^\s'"`;|&)]+)?(\\"|\\'|"|')/g; const SOURCE_PACKAGE_FLAG_BINARY_RE = new RegExp( `\\b(${PACKAGE_RUNNER_COMMAND}(?:(?!\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})\\s+${SOURCE_ARG_VALUE})*\\s+)tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})`, "g", @@ -141,8 +143,9 @@ const NPX_OPTION_WITH_VALUE = "(?:--registry|--cache|--userconfig|--prefix)"; const NPX_PACKAGE_FLAG_CONTEXT_RE = new RegExp( `(?:^|[;&|]\\s*)npx(?:\\s+(?:${NPX_OPTION_WITH_VALUE}\\s+${SOURCE_ARG_VALUE}|-\\w+|--\\w[\\w-]*(?:=${SOURCE_ARG_VALUE})?))*\\s*$`, ); -const SOURCE_TOKEN_RE = new RegExp(SOURCE_ARG_VALUE, "g"); +const SOURCE_TOKEN_RE = new RegExp(SOURCE_CLI_ARG_VALUE, "g"); const CLI_RENAME_LEGACY_RE = /(?, + tokens: Array<{ start: number; end: number }>, +): string { + let updated = value; + for (const [index, replacement] of [...replacements.entries()].toSorted(([a], [b]) => b - a)) { + const token = tokens[index]; + if (token == null) continue; + updated = `${updated.slice(0, token.start)}${replacement}${updated.slice(token.end)}`; + } + return updated; +} + function protectSourceCliValueReferences(value: string): { source: string; protectedValues: string[]; @@ -180,10 +217,11 @@ function protectSourceCliValueReferences(value: string): { (match: string, prefix: string, arg: string, offset: number) => { if (!arg.includes("tailor-sdk")) return match; const flag = sourceOptionFlag(prefix); - if ( - !isAfterTailorCliToken(value, offset) && - !isPackageRunnerOptionValue(value, offset, flag) - ) { + const afterPackageRunner = isAfterPackageRunnerPrefix(value, offset); + if (afterPackageRunner && !isPackageRunnerOptionValue(value, offset, flag)) { + return match; + } + if (!afterPackageRunner && !isAfterTailorCliToken(value, offset)) { return match; } if (isPackageFlag(flag) && NPX_PACKAGE_FLAG_CONTEXT_RE.test(value.slice(0, offset))) { @@ -214,15 +252,48 @@ function isPackageFlag(value: string): boolean { } function sourceTokens(value: string): string[] | null { + const tokens = sourceTokenSpans(value); + return tokens == null ? null : tokens.map((token) => token.value); +} + +function sourceTokenSpans(value: string): Array<{ + start: number; + end: number; + text: string; + value: string; +}> | null { const tokens: string[] = []; SOURCE_TOKEN_RE.lastIndex = 0; let lastEnd = 0; for (const match of value.matchAll(SOURCE_TOKEN_RE)) { if (value.slice(lastEnd, match.index).trim() !== "") return null; - tokens.push(match[0]); + tokens.push(sourceTokenValue(match[0])); lastEnd = (match.index ?? 0) + match[0].length; } - return value.slice(lastEnd).trim() === "" ? tokens : null; + if (value.slice(lastEnd).trim() !== "") return null; + + return [...value.matchAll(new RegExp(SOURCE_CLI_ARG_VALUE, "g"))].map((match) => ({ + start: match.index ?? 0, + end: (match.index ?? 0) + match[0].length, + text: match[0], + value: sourceTokenValue(match[0]), + })); +} + +function sourceTokenValue(token: string): string { + if (token.startsWith('\\"') && token.endsWith('\\"')) return token.slice(2, -2); + if (token.startsWith("\\'") && token.endsWith("\\'")) return token.slice(2, -2); + if (token.startsWith('"') && token.endsWith('"')) return token.slice(1, -1); + if (token.startsWith("'") && token.endsWith("'")) return token.slice(1, -1); + return token; +} + +function replaceSourceTokenValue(token: string, replacement: string): string { + if (token.startsWith('\\"') && token.endsWith('\\"')) return `\\"${replacement}\\"`; + if (token.startsWith("\\'") && token.endsWith("\\'")) return `\\'${replacement}\\'`; + if (token.startsWith('"') && token.endsWith('"')) return `"${replacement}"`; + if (token.startsWith("'") && token.endsWith("'")) return `'${replacement}'`; + return replacement; } function activeQuoteStart(source: string, start: number, offset: number): number | null { @@ -282,6 +353,68 @@ function packageRunnerPackageStartTokenIndex(tokens: readonly string[]): number return null; } +function renameSourcePackageRunnerTokens(value: string): string { + const spans = sourceTokenSpans(value); + if (spans == null) return value; + const tokens = spans.map((span) => span.value); + const start = packageRunnerPackageStartTokenIndex(tokens); + if (start == null) return value; + + const replacements = new Map(); + let hasPackageFlag = false; + let hasTailorPackageFlag = false; + + for (let index = start; index < tokens.length; index += 1) { + const token = tokens[index]!; + if (SOURCE_PACKAGE_FLAG_RE.test(token)) { + hasPackageFlag = true; + if (token.includes("=")) { + const [flag, rawValue = ""] = spans[index]!.text.split(/=(.*)/s, 2); + const packageReplacement = renameSourcePackageToken(rawValue); + if (packageReplacement != null) { + replacements.set(index, `${flag}=${packageReplacement}`); + hasTailorPackageFlag = true; + } else if (isTailorPackageValue(sourceTokenValue(rawValue))) { + hasTailorPackageFlag = true; + } + } else { + const valueIndex = index + 1; + const value = spans[valueIndex]; + if (value != null) { + const packageReplacement = renameSourcePackageToken(value.text); + if (packageReplacement != null) { + replacements.set(valueIndex, packageReplacement); + hasTailorPackageFlag = true; + } else if (isTailorPackageValue(value.value)) { + hasTailorPackageFlag = true; + } + } + index += 1; + } + continue; + } + + if (token.startsWith("-")) { + if (skipsRunnerOptionValue(token)) index += 1; + continue; + } + + if (!hasPackageFlag) { + const packageReplacement = renameSourcePackageToken(spans[index]!.text); + if (packageReplacement != null) replacements.set(index, packageReplacement); + break; + } + + if (hasTailorPackageFlag) { + const binaryReplacement = renameSourceBinaryToken(spans[index]!.text); + if (binaryReplacement != null) replacements.set(index, binaryReplacement); + } + break; + } + + return replacements.size === 0 ? value : replaceSourceSpans(value, replacements, spans); +} + function hasPositionalPackageBeforeSourcePackageFlag( tokens: readonly string[], start: number, @@ -329,6 +462,19 @@ function isPackageRunnerOptionValue(source: string, offset: number, flag: string ); } +function isAfterPackageRunnerPrefix(source: string, offset: number): boolean { + const segmentStart = Math.max( + source.lastIndexOf(";", offset - 1), + source.lastIndexOf("&", offset - 1), + source.lastIndexOf("|", offset - 1), + ); + const tokens = sourceTokens(source.slice(segmentStart + 1, offset).trim()); + const executable = tokens?.[0]; + return ( + executable === "npx" || executable === "bunx" || executable === "pnpm" || executable === "yarn" + ); +} + function isPackageFlagValueInPackageRunner(source: string, offset: number): boolean { const segmentStart = Math.max( source.lastIndexOf(";", offset - 1), @@ -429,7 +575,24 @@ function renameSourceCommandText(value: string): string { if (needsCliRenameMigration(value)) return value; const protectedValue = protectSourceCliValueReferences(value); - const withPackageFlagValues = protectedValue.source.replace( + const withQuotedPackageFlagValues = protectedValue.source.replace( + SOURCE_PACKAGE_FLAG_EQUALS_QUOTED_VALUE_RE, + ( + match: string, + prefix: string, + openQuote: string, + version: string | undefined, + closeQuote: string, + offset: number, + source: string, + ) => { + if (!isPackageFlagValueInPackageRunner(source, offset)) return match; + const packageName = version ? `@tailor-platform/sdk${version}` : "@tailor-platform/sdk"; + return `${prefix}${openQuote}${packageName}${closeQuote}`; + }, + ); + const withTokenPackageRunners = renameSourcePackageRunnerTokens(withQuotedPackageFlagValues); + const withPackageFlagValues = withTokenPackageRunners.replace( SOURCE_PACKAGE_FLAG_VALUE_RE, ( match: string, @@ -729,6 +892,8 @@ function isPackageRunnerCommandBinaryArgument(node: SgNode, source: string): boo if (start == null || !hasPackageFlagBeforeArrayPackage(elements, index, source, start)) { return false; } + const commandIndex = packageRunnerCommandIndex(elements, start, source); + if (commandIndex !== index) return false; const text = sourceStringContent(node, source); return ( @@ -738,6 +903,27 @@ function isPackageRunnerCommandBinaryArgument(node: SgNode, source: string): boo ); } +function packageRunnerCommandIndex( + elements: SgNode[], + start: number, + source: string, +): number | null { + for (let index = start; index < elements.length; index += 1) { + const value = sourceStringContent(elements[index]!, source); + if (value == null) return null; + if (SOURCE_PACKAGE_FLAG_RE.test(value)) { + if (!value.includes("=")) index += 1; + continue; + } + if (value.startsWith("-")) { + if (skipsRunnerOptionValue(value)) index += 1; + continue; + } + return index; + } + return null; +} + function isPackageManagerExecBinaryArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() !== "array") return false; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 6a260f23d..79e1f9958 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -22,6 +22,7 @@ const npxOtherPackageSpawned = spawn("npx", ["foo", "tailor-sdk", "login"]); const npxOtherPackageFlagSpawned = spawn("npx", ["foo", "-p", "tailor-sdk", "tailor-sdk", "login"]); const npxOtherPackageEqualsSpawned = spawn("npx", ["foo", "--package=tailor-sdk", "tailor-sdk", "login"]); const npxOtherPackageSplitSpawned = spawn("npx", ["foo", "--package", "tailor-sdk", "tailor-sdk", "login"]); +const npxToolValueSpawned = spawn("npx", ["-p", "some-tool", "tool", "--name", "tailor-sdk", "deploy"]); const npxPackageFlagSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "tailor", "login"]); const npxMultiPackageFlagSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "-p", "dotenv-cli", "tailor", "login"]); const npxMultiPackageFlagSecondSpawned = spawn("npx", ["-p", "dotenv-cli", "-p", "@tailor-platform/sdk", "tailor", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index fca5eb51c..4ebbbe56c 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -22,6 +22,7 @@ const npxOtherPackageSpawned = spawn("npx", ["foo", "tailor-sdk", "login"]); const npxOtherPackageFlagSpawned = spawn("npx", ["foo", "-p", "tailor-sdk", "tailor-sdk", "login"]); const npxOtherPackageEqualsSpawned = spawn("npx", ["foo", "--package=tailor-sdk", "tailor-sdk", "login"]); const npxOtherPackageSplitSpawned = spawn("npx", ["foo", "--package", "tailor-sdk", "tailor-sdk", "login"]); +const npxToolValueSpawned = spawn("npx", ["-p", "some-tool", "tool", "--name", "tailor-sdk", "deploy"]); const npxPackageFlagSpawned = spawn("npx", ["-p", "tailor-sdk", "tailor-sdk", "login"]); const npxMultiPackageFlagSpawned = spawn("npx", ["-p", "tailor-sdk", "-p", "dotenv-cli", "tailor-sdk", "login"]); const npxMultiPackageFlagSecondSpawned = spawn("npx", ["-p", "dotenv-cli", "-p", "tailor-sdk", "tailor-sdk", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index fbccd5e4d..762f7134a 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -32,6 +32,7 @@ const literalPlaceholderCommand = `prefix __TAILOR_SDK_TEMPLATE_EXPR_0__ tailor- const npxOtherPackage = "npx foo tailor-sdk login"; const npxOtherPackageFlag = "npx foo -p tailor-sdk tailor-sdk login"; const npxOtherPackageFlagEquals = "npx foo --package=tailor-sdk tailor-sdk login"; +const npxQuotedPackage = "npx \"@tailor-platform/sdk\" login"; const dynamicBunxCommand = `bunx @tailor-platform/sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx @tailor-platform/sdk ${subcommand}`; const pnpmDlxWithOption = "pnpm --silent dlx @tailor-platform/sdk login"; @@ -45,9 +46,13 @@ const nestedInlineArgValue = `tailor --arg=${"tailor-sdk deploy"} deploy`; const npxPackageFlag = "npx -p @tailor-platform/sdk tailor login"; const npxMultiPackageFlag = "npx -p @tailor-platform/sdk -p dotenv-cli tailor login"; const npxMultiPackageFlagSecond = "npx -p dotenv-cli -p @tailor-platform/sdk tailor login"; +const npxPackageFlagWithRunnerOption = "npx --package @tailor-platform/sdk --yes tailor login"; const npxPackageSplitOnly = "npx --package @tailor-platform/sdk"; const npxPackageSplitHelp = "npx -p @tailor-platform/sdk --help"; const npxPackageFlagEquals = "npx --package=@tailor-platform/sdk tailor login"; +const npxPackageSingleQuoted = "npx --package '@tailor-platform/sdk' tailor login"; +const npxPackageDoubleQuoted = "npx --package \"@tailor-platform/sdk\" tailor login"; +const npxPackageEqualsDoubleQuoted = "npx --package=\"@tailor-platform/sdk\" tailor login"; const npxPackageFlagDynamic = `npx -p @tailor-platform/sdk tailor ${subcommand}`; const npxPackageEqualsDynamic = `npx --package=@tailor-platform/sdk tailor ${subcommand}`; const npxRegistryValue = "npx --registry tailor-sdk @tailor-platform/sdk login"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 5858d9e71..e7211e601 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -32,6 +32,7 @@ const literalPlaceholderCommand = `prefix __TAILOR_SDK_TEMPLATE_EXPR_0__ tailor- const npxOtherPackage = "npx foo tailor-sdk login"; const npxOtherPackageFlag = "npx foo -p tailor-sdk tailor-sdk login"; const npxOtherPackageFlagEquals = "npx foo --package=tailor-sdk tailor-sdk login"; +const npxQuotedPackage = "npx \"tailor-sdk\" login"; const dynamicBunxCommand = `bunx tailor-sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx tailor-sdk ${subcommand}`; const pnpmDlxWithOption = "pnpm --silent dlx tailor-sdk login"; @@ -45,9 +46,13 @@ const nestedInlineArgValue = `tailor-sdk --arg=${"tailor-sdk deploy"} deploy`; const npxPackageFlag = "npx -p tailor-sdk tailor-sdk login"; const npxMultiPackageFlag = "npx -p tailor-sdk -p dotenv-cli tailor-sdk login"; const npxMultiPackageFlagSecond = "npx -p dotenv-cli -p tailor-sdk tailor-sdk login"; +const npxPackageFlagWithRunnerOption = "npx --package tailor-sdk --yes tailor-sdk login"; const npxPackageSplitOnly = "npx --package tailor-sdk"; const npxPackageSplitHelp = "npx -p tailor-sdk --help"; const npxPackageFlagEquals = "npx --package=tailor-sdk tailor-sdk login"; +const npxPackageSingleQuoted = "npx --package 'tailor-sdk' tailor-sdk login"; +const npxPackageDoubleQuoted = "npx --package \"tailor-sdk\" tailor-sdk login"; +const npxPackageEqualsDoubleQuoted = "npx --package=\"tailor-sdk\" tailor-sdk login"; const npxPackageFlagDynamic = `npx -p tailor-sdk tailor-sdk ${subcommand}`; const npxPackageEqualsDynamic = `npx --package=tailor-sdk tailor-sdk ${subcommand}`; const npxRegistryValue = "npx --registry tailor-sdk tailor-sdk login"; diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 38859a948..15082e47b 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -73,7 +73,7 @@ const RENAME_BIN_SOURCE_LEGACY_PATTERN = new RegExp( "(? { expect(result.warnings).toEqual([]); }); + test("keeps split Tailor option values out of rename-bin residual warnings", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "commands.ts"), + 'spawn("tailor", ["tailordb", "migration", "generate", "--name", "tailor-sdk deploy"]);', + "utf-8", + ); + const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: renameBin?.sourceStringLegacyPatterns, + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toEqual([]); + }); + + test("warns for dynamic template rename-bin residuals", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "commands.ts"), + "const command = `${runner} tailor-sdk ${subcommand}`;", + "utf-8", + ); + const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: renameBin?.sourceStringLegacyPatterns, + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toHaveLength(1); + expect(result.warnings[0]).toContain("commands.ts: contains"); + expect(result.warnings[0]).toContain("rule: test/rename-bin"); + }); + test("keeps source string residual checks in non-Tailor option values", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 5456f1c55..ac7b18d2b 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -70,6 +70,9 @@ const SOURCE_VALUE_FLAGS = new Set([ "--arg", "--query", "--file", + "--name", + "--namespace", + "--dir", "-e", "-p", "-c", @@ -77,6 +80,7 @@ const SOURCE_VALUE_FLAGS = new Set([ "-a", "-q", "-f", + "-n", ]); const SOURCE_CLI_BINARY_RE = /^(?:tailor|tailor-sdk(?:@[^\s'"`;|&)]+)?|@tailor-platform\/sdk(?:@[^\s'"`;|&)]+)?)$/; From 33a351860e81db751b02d325ea2224cb71efadac Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 00:05:13 +0900 Subject: [PATCH 330/618] fix(sdk-codemod): keep array runner commands moving --- .../v2/rename-bin/scripts/transform.ts | 53 ++++--------------- .../tests/source-js-string/expected.js | 6 ++- .../tests/source-js-string/input.js | 2 + 3 files changed, 16 insertions(+), 45 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index ee9fdc8ab..af6057b23 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -1006,37 +1006,6 @@ function isCliValueArgument(node: SgNode, source: string): boolean { ); } -function sourceStringValue(node: SgNode, source: string): string | null { - return sourceStringContent(node, source) ?? sourceStringRawContent(node, source); -} - -function collectSourceStringValues(node: SgNode, source: string, values: string[]): void { - const value = sourceStringValue(node, source); - if (value != null) { - values.push(value); - return; - } - for (const child of node.children()) { - collectSourceStringValues(child, source, values); - } -} - -function cliInvocationContextNode(node: SgNode): SgNode | null { - const parent = node.parent(); - if (parent?.kind() === "array" && parent.parent()?.kind() === "arguments") { - return parent.parent()!; - } - return parent?.kind() === "arguments" ? parent : null; -} - -function contextNeedsCliRenameMigration(node: SgNode, source: string): boolean { - const context = cliInvocationContextNode(node); - if (context == null) return false; - const values: string[] = []; - collectSourceStringValues(context, source, values); - return values.some((value) => CLI_RENAME_LEGACY_RE.test(value)); -} - function pushSourceStringEdit( edits: Array<[number, number, string]>, source: string, @@ -1048,18 +1017,16 @@ function pushSourceStringEdit( const text = source.slice(start, end); const packageFlagReplacement = sourcePackageFlagReplacement(node, source); const replacement = - contextNeedsCliRenameMigration(node, source) && text.includes("tailor-sdk") - ? text - : isCliValueArgument(node, source) - ? text - : packageFlagReplacement != null - ? packageFlagReplacement.replacement - : TAILOR_SDK_TOKEN_RE.test(text) && isPackageRunnerPackageArgument(node, source) - ? renamePackageName(text) - : (TAILOR_SDK_TOKEN_RE.test(text) || TAILOR_SDK_PATH_RE.test(text)) && - isCliBinaryArgument(node, source) - ? renameBinary(text) - : renameSourceCommandText(text); + packageFlagReplacement != null + ? packageFlagReplacement.replacement + : TAILOR_SDK_TOKEN_RE.test(text) && isPackageRunnerPackageArgument(node, source) + ? renamePackageName(text) + : (TAILOR_SDK_TOKEN_RE.test(text) || TAILOR_SDK_PATH_RE.test(text)) && + isCliBinaryArgument(node, source) + ? renameBinary(text) + : isCliValueArgument(node, source) + ? text + : renameSourceCommandText(text); if (replacement !== text) { edits.push([start, end, replacement]); } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 79e1f9958..7b5b64db2 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -7,8 +7,8 @@ const inlineTemplateArgSpawned = spawn("tailor", [`--arg=tailor-sdk ${cmd}`, "de const nameValueSpawned = spawn("tailor", ["tailordb", "migration", "generate", "--name", "tailor-sdk deploy"]); const directNameValueSpawned = spawn("tailor", ["tailordb", "migration", "generate", "--name", "tailor-sdk deploy"]); const shellSpawned = spawn("sh", ["-c", "tailor deploy"]); -const cliRenameCommandSpawned = spawn("tailor-sdk", ["crash-report", "list"]); -const cliRenameFlagSpawned = spawn("tailor-sdk", ["login", "--machineuser"]); +const cliRenameCommandSpawned = spawn("tailor", ["crash-report", "list"]); +const cliRenameFlagSpawned = spawn("tailor", ["login", "--machineuser"]); const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "dev", "login"]); @@ -24,6 +24,8 @@ const npxOtherPackageEqualsSpawned = spawn("npx", ["foo", "--package=tailor-sdk" const npxOtherPackageSplitSpawned = spawn("npx", ["foo", "--package", "tailor-sdk", "tailor-sdk", "login"]); const npxToolValueSpawned = spawn("npx", ["-p", "some-tool", "tool", "--name", "tailor-sdk", "deploy"]); const npxPackageFlagSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "tailor", "login"]); +const npxPackageFlagWithRunnerOptionSpawned = spawn("npx", ["--package", "@tailor-platform/sdk", "--yes", "tailor", "login"]); +const npxMigratedPackageFlagWithRunnerOptionSpawned = spawn("npx", ["--package", "@tailor-platform/sdk", "--yes", "tailor", "login"]); const npxMultiPackageFlagSpawned = spawn("npx", ["-p", "@tailor-platform/sdk", "-p", "dotenv-cli", "tailor", "login"]); const npxMultiPackageFlagSecondSpawned = spawn("npx", ["-p", "dotenv-cli", "-p", "@tailor-platform/sdk", "tailor", "login"]); const npxPackageEqualsSpawned = spawn("npx", ["--package=@tailor-platform/sdk", "tailor", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 4ebbbe56c..70272daa1 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -24,6 +24,8 @@ const npxOtherPackageEqualsSpawned = spawn("npx", ["foo", "--package=tailor-sdk" const npxOtherPackageSplitSpawned = spawn("npx", ["foo", "--package", "tailor-sdk", "tailor-sdk", "login"]); const npxToolValueSpawned = spawn("npx", ["-p", "some-tool", "tool", "--name", "tailor-sdk", "deploy"]); const npxPackageFlagSpawned = spawn("npx", ["-p", "tailor-sdk", "tailor-sdk", "login"]); +const npxPackageFlagWithRunnerOptionSpawned = spawn("npx", ["--package", "tailor-sdk", "--yes", "tailor-sdk", "login"]); +const npxMigratedPackageFlagWithRunnerOptionSpawned = spawn("npx", ["--package", "@tailor-platform/sdk", "--yes", "tailor-sdk", "login"]); const npxMultiPackageFlagSpawned = spawn("npx", ["-p", "tailor-sdk", "-p", "dotenv-cli", "tailor-sdk", "login"]); const npxMultiPackageFlagSecondSpawned = spawn("npx", ["-p", "dotenv-cli", "-p", "tailor-sdk", "tailor-sdk", "login"]); const npxPackageEqualsSpawned = spawn("npx", ["--package=tailor-sdk", "tailor-sdk", "login"]); From 7eb94ada7ed04b226b7d0bc7944352591a83c7d3 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 00:21:34 +0900 Subject: [PATCH 331/618] fix(sdk-codemod): warn on split legacy argv --- .../v2/rename-bin/scripts/transform.ts | 64 +++++++++++++++++-- .../tests/source-js-string/expected.js | 8 ++- .../tests/source-js-string/input.js | 2 + packages/sdk-codemod/src/registry.test.ts | 1 + packages/sdk-codemod/src/runner.test.ts | 32 ++++++++++ packages/sdk-codemod/src/runner.ts | 33 ++++++++++ 6 files changed, 133 insertions(+), 7 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index af6057b23..4da7027de 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -144,7 +144,7 @@ const NPX_PACKAGE_FLAG_CONTEXT_RE = new RegExp( `(?:^|[;&|]\\s*)npx(?:\\s+(?:${NPX_OPTION_WITH_VALUE}\\s+${SOURCE_ARG_VALUE}|-\\w+|--\\w[\\w-]*(?:=${SOURCE_ARG_VALUE})?))*\\s*$`, ); const SOURCE_TOKEN_RE = new RegExp(SOURCE_CLI_ARG_VALUE, "g"); -const CLI_RENAME_LEGACY_RE = /(? { expect(renameBin?.sourceStringLegacyPatterns).toHaveLength(1); const sourceStringPattern = renameBin?.sourceStringLegacyPatterns?.[0] as RegExp; expect(sourceStringPattern.test("tailor-sdk deploy")).toBe(true); + expect(sourceStringPattern.test("tailor-sdk apply")).toBe(true); expect(sourceStringPattern.test("tailor --profile tailor-sdk deploy")).toBe(false); expect(sourceStringPattern.test('tailor --arg "tailor-sdk deploy" deploy')).toBe(false); const matches = picomatch(renameBin?.filePatterns ?? [], { dot: true }); diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index bbe6f44cd..54bbc00a4 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -598,6 +598,38 @@ describe("runCodemods", () => { expect(result.warnings).toEqual([]); }); + test("warns for split argv rename-bin residuals", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "commands.ts"), + [ + 'spawn("tailor-sdk", ["apply"]);', + 'spawn("tailor-sdk", ["crash-report", "list"]);', + 'spawn("npx", ["-p", "@tailor-platform/sdk", "tailor-sdk", "crash-report"]);', + ].join("\n"), + "utf-8", + ); + const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: renameBin?.sourceStringLegacyPatterns, + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toHaveLength(1); + expect(result.warnings[0]).toContain("commands.ts: contains"); + expect(result.warnings[0]).toContain("rule: test/rename-bin"); + }); + test("warns for dynamic template rename-bin residuals", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index ac7b18d2b..b5ec8e376 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -184,6 +184,14 @@ function sourceStringContentForResidualMatching(relative: string, content: strin const fragments: string[] = []; const visit = (node: SgNode): void => { + if (node.kind() === "arguments") { + const value = sourceArgumentsCommandContent(node, content); + if (value != null) fragments.push(value); + } + if (node.kind() === "array") { + const value = sourceArrayCommandContent(node, content); + if (value != null) fragments.push(value); + } if (node.kind() === "string") { if (isSourceValueArgument(node, content)) return; const value = sourceStringNodeContent(node, content); @@ -203,6 +211,31 @@ function sourceStringContentForResidualMatching(relative: string, content: strin return fragments.join(SOURCE_STRING_FRAGMENT_SEPARATOR); } +function sourceArgumentsCommandContent(node: SgNode, source: string): string | null { + const args = sourceArrayElements(node); + const executable = args[0] == null ? null : sourceStringNodeContent(args[0]!, source); + const argv = args[1]; + if (executable == null || argv?.kind() !== "array") return null; + + const values = sourceArrayCommandValues(argv, source); + return values.length === 0 ? null : [executable, ...values].join(" "); +} + +function sourceArrayCommandContent(node: SgNode, source: string): string | null { + const values = sourceArrayCommandValues(node, source); + return values.length < 2 ? null : values.join(" "); +} + +function sourceArrayCommandValues(node: SgNode, source: string): string[] { + const values: string[] = []; + for (const element of sourceArrayElements(node)) { + if (isSourceValueArgument(element, source)) continue; + const value = sourceStringNodeContent(element, source); + if (value != null) values.push(value); + } + return values; +} + function isSyntaxOnlyNode(node: SgNode): boolean { const kind = node.kind(); return ( From f599db8f53790c1a4849107fa4f55efa8eb89ff1 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 00:37:21 +0900 Subject: [PATCH 332/618] fix(sdk-codemod): warn on source command residuals --- packages/sdk-codemod/src/registry.test.ts | 15 +++--- packages/sdk-codemod/src/registry.ts | 15 +++++- packages/sdk-codemod/src/runner.test.ts | 58 +++++++++++++++++++++++ packages/sdk-codemod/src/runner.ts | 4 ++ 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 714a285d9..2332d1256 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -63,12 +63,15 @@ describe("getApplicableCodemods", () => { ); expect(renameBin?.filePatterns).toEqual(expect.arrayContaining([sourcePattern])); - expect(renameBin?.sourceStringLegacyPatterns).toHaveLength(1); - const sourceStringPattern = renameBin?.sourceStringLegacyPatterns?.[0] as RegExp; - expect(sourceStringPattern.test("tailor-sdk deploy")).toBe(true); - expect(sourceStringPattern.test("tailor-sdk apply")).toBe(true); - expect(sourceStringPattern.test("tailor --profile tailor-sdk deploy")).toBe(false); - expect(sourceStringPattern.test('tailor --arg "tailor-sdk deploy" deploy')).toBe(false); + expect(renameBin?.sourceStringLegacyPatterns).toHaveLength(2); + const sourceStringPatterns = renameBin?.sourceStringLegacyPatterns as RegExp[]; + const matchesSourceStringPattern = (value: string) => + sourceStringPatterns.some((pattern) => pattern.test(value)); + expect(matchesSourceStringPattern("tailor-sdk deploy")).toBe(true); + expect(matchesSourceStringPattern("tailor-sdk apply")).toBe(true); + expect(matchesSourceStringPattern('sh -c "tailor-sdk apply"')).toBe(true); + expect(matchesSourceStringPattern("tailor --profile tailor-sdk deploy")).toBe(false); + expect(matchesSourceStringPattern('tailor --arg "tailor-sdk deploy" deploy')).toBe(false); const matches = picomatch(renameBin?.filePatterns ?? [], { dot: true }); expect(matches("packages/app/frontend/e2e/global-setup.ts")).toBe(true); expect(matches("tailor.d.ts")).toBe(true); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 15082e47b..ca92872e7 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -66,6 +66,7 @@ const RENAME_BIN_SOURCE_VALUE_GUARDS = RENAME_BIN_SOURCE_VALUE_FLAGS.flatMap((fl const escaped = escapeRegExp(flag); return [`(? { expect(result.warnings[0]).toContain("rule: test/rename-bin"); }); + test("warns for source comment and JSX rename-bin residuals", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "commands.tsx"), + ["// tailor-sdk apply", "const docs = tailor-sdk crash-report list;"].join( + "\n", + ), + "utf-8", + ); + const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.tsx"], [], { + sourceStringLegacyPatterns: renameBin?.sourceStringLegacyPatterns, + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toHaveLength(1); + expect(result.warnings[0]).toContain("commands.tsx: contains"); + expect(result.warnings[0]).toContain("rule: test/rename-bin"); + }); + + test("warns for quoted shell rename-bin residuals", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "commands.ts"), + "const command = 'sh -c \"tailor-sdk apply\"';", + "utf-8", + ); + const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: renameBin?.sourceStringLegacyPatterns, + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toHaveLength(1); + expect(result.warnings[0]).toContain("commands.ts: contains"); + expect(result.warnings[0]).toContain("rule: test/rename-bin"); + }); + test("warns for dynamic template rename-bin residuals", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index b5ec8e376..9b7134d95 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -184,6 +184,10 @@ function sourceStringContentForResidualMatching(relative: string, content: strin const fragments: string[] = []; const visit = (node: SgNode): void => { + if (node.kind() === "comment" || node.kind() === "jsx_text") { + fragments.push(node.text()); + return; + } if (node.kind() === "arguments") { const value = sourceArgumentsCommandContent(node, content); if (value != null) fragments.push(value); From 1a28efc74fa3d7a3c4a61c14b5bdc04ae34474cd Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 00:58:58 +0900 Subject: [PATCH 333/618] fix(sdk-codemod): handle npm exec package flags --- .../v2/rename-bin/scripts/transform.ts | 54 ++++++++++++++++++- .../tests/source-js-string/expected.js | 4 ++ .../tests/source-js-string/input.js | 4 ++ packages/sdk-codemod/src/runner.test.ts | 27 ++++++++++ packages/sdk-codemod/src/runner.ts | 12 ++++- 5 files changed, 98 insertions(+), 3 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 4da7027de..c3581bd7d 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -127,6 +127,7 @@ const CLI_ARGUMENT_CALLEE_RE = /(?:^|\.)(?:spawn|spawnSync|execFile|execFileSync const SOURCE_EXEC_PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn"]); const SOURCE_PACKAGE_RUNNERS = new Set(["bunx", "npx"]); const SOURCE_DLX_PACKAGE_RUNNERS = new Set(["pnpm", "yarn"]); +const SOURCE_NPM_EXEC_PACKAGE_RUNNERS = new Set(["npm"]); const PACKAGE_MANAGER_OPTION_VALUE_FLAGS = new Set([ "--registry", "--cache", @@ -336,6 +337,19 @@ function packageRunnerPackageStartTokenIndex(tokens: readonly string[]): number const executable = tokens[0]; if (executable === "npx" || executable === "bunx") { return 1; + } else if (executable === "npm") { + let index = 1; + for (; index < tokens.length; index += 1) { + const token = tokens[index]!; + if (token === "exec") { + return index + 1; + } + if (token.startsWith("-")) { + if (skipsRunnerOptionValue(token)) index += 1; + continue; + } + return null; + } } else if (executable === "pnpm" || executable === "yarn") { let index = 1; for (; index < tokens.length; index += 1) { @@ -568,7 +582,33 @@ function isTemplateSubstitutionCliValue(text: string, offset: number): boolean { } function needsCliRenameMigration(value: string): boolean { - return value.includes("tailor-sdk") && CLI_RENAME_LEGACY_RE.test(value); + if (!value.includes("tailor-sdk")) return false; + const tokens = sourceTokens(value); + if (tokens == null) return CLI_RENAME_LEGACY_RE.test(value); + for (let index = 0; index < tokens.length; index += 1) { + if (!TAILOR_SDK_TOKEN_RE.test(tokens[index]!)) continue; + if (tokensAfterCliBinaryNeedRename(tokens, index + 1)) return true; + } + return false; +} + +function tokensAfterCliBinaryNeedRename(tokens: readonly string[], start: number): boolean { + let commandSeen = false; + for (let index = start; index < tokens.length; index += 1) { + const token = tokens[index]!; + if (token === "--machineuser" || token.startsWith("--machineuser=")) return true; + if (token.startsWith("-")) { + if (TAILOR_CLI_VALUE_FLAGS.has(token.split("=", 1)[0]!) && !token.includes("=")) { + index += 1; + } + continue; + } + if (!commandSeen) { + if (token === "apply" || token === "crash-report") return true; + commandSeen = true; + } + } + return false; } function renameSourceCommandText(value: string): string { @@ -737,6 +777,13 @@ function packageRunnerPackageStartIndex( source: string, ): number | null { if (SOURCE_PACKAGE_RUNNERS.has(executable)) return 0; + if (SOURCE_NPM_EXEC_PACKAGE_RUNNERS.has(executable)) { + const execIndex = firstNonOptionIndex(elements, 0, source); + if (execIndex == null || sourceStringContent(elements[execIndex]!, source) !== "exec") { + return null; + } + return execIndex + 1; + } if (!SOURCE_DLX_PACKAGE_RUNNERS.has(executable)) return null; const dlxIndex = firstNonOptionIndex(elements, 0, source); if (dlxIndex == null || sourceStringContent(elements[dlxIndex]!, source) !== "dlx") { @@ -875,6 +922,11 @@ function isPackageRunnerArrayArgument(node: SgNode, source: string): boolean { if (executable == null) return false; if (executable === "bunx" || executable === "npx") return true; + if (executable === "npm") { + const elements = sourceArrayElements(parent); + const execIndex = firstNonOptionIndex(elements, 0, source); + return execIndex != null && sourceStringContent(elements[execIndex]!, source) === "exec"; + } if (executable !== "pnpm" && executable !== "yarn") return false; const elements = sourceArrayElements(parent); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index cc2d3472c..49bb8bc52 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -1,4 +1,5 @@ const script = "tailor deploy"; +const proseApply = "Run tailor deploy to apply changes"; const spawned = spawn("tailor", ["deploy"]); const argSpawned = spawn("tailor", ["--arg", "tailor-sdk deploy", "deploy"]); const inlineArgSpawned = spawn("tailor", ["--arg=tailor-sdk deploy", "deploy"]); @@ -48,6 +49,9 @@ const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor", "deploy"]); const pnpmExecDynamicSpawned = spawn("pnpm", ["exec", "tailor", subcommand]); const pnpmExecHelpSpawned = spawn("pnpm", ["exec", "tailor", "--help"]); +const npmExecSpawned = spawn("npm", ["exec", "@tailor-platform/sdk", "login"]); +const npmExecPackageFlagSpawned = spawn("npm", ["exec", "--package", "@tailor-platform/sdk", "tailor", "login"]); +const npmExecPackageEqualsSpawned = spawn("npm", ["exec", "--package=@tailor-platform/sdk", "tailor", "login"]); const pathQualifiedSpawned = spawn("./node_modules/.bin/tailor", ["deploy"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; const npxArgs = ["tailor-sdk", "login"]; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 8533ded9d..195fc0cd1 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -1,4 +1,5 @@ const script = "tailor-sdk deploy"; +const proseApply = "Run tailor-sdk deploy to apply changes"; const spawned = spawn("tailor-sdk", ["deploy"]); const argSpawned = spawn("tailor-sdk", ["--arg", "tailor-sdk deploy", "deploy"]); const inlineArgSpawned = spawn("tailor-sdk", ["--arg=tailor-sdk deploy", "deploy"]); @@ -48,6 +49,9 @@ const pnpmBinarySpawned = spawn("pnpm", ["tailor-sdk", "deploy"]); const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor-sdk", "deploy"]); const pnpmExecDynamicSpawned = spawn("pnpm", ["exec", "tailor-sdk", subcommand]); const pnpmExecHelpSpawned = spawn("pnpm", ["exec", "tailor-sdk", "--help"]); +const npmExecSpawned = spawn("npm", ["exec", "tailor-sdk", "login"]); +const npmExecPackageFlagSpawned = spawn("npm", ["exec", "--package", "tailor-sdk", "tailor-sdk", "login"]); +const npmExecPackageEqualsSpawned = spawn("npm", ["exec", "--package=tailor-sdk", "tailor-sdk", "login"]); const pathQualifiedSpawned = spawn("./node_modules/.bin/tailor-sdk", ["deploy"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; const npxArgs = ["tailor-sdk", "login"]; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index ac69c3287..ae119805d 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -743,6 +743,33 @@ describe("runCodemods", () => { ]); }); + test("keeps non-rename-bin source string residual checks in Tailor option values", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "env.ts"), + 'spawn("tailor", ["--arg", "LOG_LEVEL=debug", "deploy"]);', + "utf-8", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/env", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: ["LOG_LEVEL"], + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toEqual([ + "env.ts: contains LOG_LEVEL but was not migrated automatically (rule: test/env). Manual migration may be needed.", + ]); + }); + test("flags files matching a suspicious pattern for LLM review", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 9b7134d95..8ff879bbd 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -197,13 +197,13 @@ function sourceStringContentForResidualMatching(relative: string, content: strin if (value != null) fragments.push(value); } if (node.kind() === "string") { - if (isSourceValueArgument(node, content)) return; + if (isSourceTailorSdkValueArgument(node, content)) return; const value = sourceStringNodeContent(node, content); if (value != null) fragments.push(value); return; } if (node.kind() === "string_fragment") { - if (isSourceValueArgument(node, content)) return; + if (isSourceTailorSdkValueArgument(node, content)) return; fragments.push(node.text()); return; } @@ -240,6 +240,14 @@ function sourceArrayCommandValues(node: SgNode, source: string): string[] { return values; } +function isSourceTailorSdkValueArgument(fragment: SgNode, source: string): boolean { + const text = + fragment.kind() === "string_fragment" + ? fragment.text() + : sourceStringNodeContent(fragment, source); + return text != null && text.includes("tailor-sdk") && isSourceValueArgument(fragment, source); +} + function isSyntaxOnlyNode(node: SgNode): boolean { const kind = node.kind(); return ( From 1772e818e9ae53e8e0a476f759e368ce12dc553a Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 01:25:38 +0900 Subject: [PATCH 334/618] fix(sdk-codemod): avoid unsafe source binary rewrites --- .../v2/rename-bin/scripts/transform.ts | 82 ++++++++++++++++--- .../tests/source-js-string/expected.js | 2 + .../tests/source-js-string/input.js | 2 + .../tests/source-template/expected.ts | 6 ++ .../rename-bin/tests/source-template/input.ts | 6 ++ 5 files changed, 88 insertions(+), 10 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index c3581bd7d..896c38b69 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -194,6 +194,21 @@ function isTailorPackageValue(value: string): boolean { ); } +function sourcePathBasename(value: string): string { + return value.split(/[\\/]/).at(-1) ?? value; +} + +function isTailorSdkBinaryTokenValue(value: string): boolean { + return TAILOR_SDK_TOKEN_RE.test(sourcePathBasename(value)); +} + +function isTailorCliTokenValue(value: string): boolean { + const basename = sourcePathBasename(value); + return ( + TAILOR_CLI_TOKEN_RE.test(value) || basename === "tailor" || TAILOR_SDK_TOKEN_RE.test(basename) + ); +} + function replaceSourceSpans( value: string, replacements: Map, @@ -484,9 +499,8 @@ function isAfterPackageRunnerPrefix(source: string, offset: number): boolean { ); const tokens = sourceTokens(source.slice(segmentStart + 1, offset).trim()); const executable = tokens?.[0]; - return ( - executable === "npx" || executable === "bunx" || executable === "pnpm" || executable === "yarn" - ); + if (executable === "npx" || executable === "bunx") return true; + return (executable === "pnpm" || executable === "yarn") && tokens?.includes("dlx") === true; } function isPackageFlagValueInPackageRunner(source: string, offset: number): boolean { @@ -559,7 +573,7 @@ function isAfterTailorCliToken(source: string, offset: number): boolean { ); const segment = source.slice(segmentStart + 1, offset).trim(); const tokens = sourceTokens(segment); - return tokens != null && tokens.some((token) => TAILOR_CLI_TOKEN_RE.test(token)); + return tokens != null && tokens.some((token) => isTailorCliTokenValue(token)); } function isAfterTemplatePlaceholder(source: string, offset: number): boolean { @@ -578,7 +592,7 @@ function isTemplateSubstitutionCliValue(text: string, offset: number): boolean { if (tokens == null || tokens.length === 0) return false; const previous = tokens.at(-1)!; if (!isTailorCliValueFlag(previous)) return false; - return tokens.slice(0, -1).some((token) => TAILOR_CLI_TOKEN_RE.test(token)); + return tokens.slice(0, -1).some((token) => isTailorCliTokenValue(token)); } function needsCliRenameMigration(value: string): boolean { @@ -586,12 +600,37 @@ function needsCliRenameMigration(value: string): boolean { const tokens = sourceTokens(value); if (tokens == null) return CLI_RENAME_LEGACY_RE.test(value); for (let index = 0; index < tokens.length; index += 1) { - if (!TAILOR_SDK_TOKEN_RE.test(tokens[index]!)) continue; + const token = tokens[index]!; + if (isTailorCliValueToken(tokens, index)) continue; + if (token !== value && token.includes("tailor-sdk") && needsCliRenameMigration(token)) { + return true; + } + if (!isTailorSdkBinaryTokenValue(token)) continue; if (tokensAfterCliBinaryNeedRename(tokens, index + 1)) return true; } return false; } +function isTailorCliValueToken(tokens: readonly string[], index: number): boolean { + const token = tokens[index]!; + const flag = token.split("=", 1)[0]!; + if ( + token.includes("=") && + TAILOR_CLI_VALUE_FLAGS.has(flag) && + tokens.slice(0, index).some((value) => isTailorCliTokenValue(value)) + ) { + return true; + } + + const previous = tokens[index - 1]; + return ( + previous != null && + TAILOR_CLI_VALUE_FLAGS.has(previous.split("=", 1)[0]!) && + !previous.includes("=") && + tokens.slice(0, index - 1).some((value) => isTailorCliTokenValue(value)) + ); +} + function tokensAfterCliBinaryNeedRename(tokens: readonly string[], start: number): boolean { let commandSeen = false; for (let index = start; index < tokens.length; index += 1) { @@ -1060,12 +1099,15 @@ function isCliBinaryArgument(node: SgNode, source: string): boolean { function arrayHasCliRenameLegacyArgs(elements: SgNode[], start: number, source: string): boolean { for (let index = start; index < elements.length; index += 1) { - const value = sourceStringContent(elements[index]!, source); + const value = + sourceStringContent(elements[index]!, source) ?? + sourceStringRawContent(elements[index]!, source); if (value == null) continue; if (TAILOR_CLI_VALUE_FLAGS.has(value.split("=", 1)[0]!) && !value.includes("=")) { index += 1; continue; } + if (CLI_RENAME_LEGACY_RE.test(value)) return true; if (value === "apply" || value === "crash-report") return true; if (value === "--machineuser" || value.startsWith("--machineuser=")) return true; } @@ -1077,13 +1119,13 @@ function isTailorCliArgumentArray(arrayNode: SgNode, index: number, source: stri if (argumentsNode?.kind() === "arguments") { const callArgs = sourceArrayElements(argumentsNode); const executable = callArgs[0] == null ? null : sourceStringContent(callArgs[0]!, source); - if (executable != null && TAILOR_CLI_TOKEN_RE.test(executable)) return true; + if (executable != null && isTailorCliTokenValue(executable)) return true; } const elements = sourceArrayElements(arrayNode); return elements.slice(0, index).some((element) => { const value = sourceStringContent(element, source); - return value != null && TAILOR_CLI_TOKEN_RE.test(value); + return value != null && isTailorCliTokenValue(value); }); } @@ -1156,6 +1198,24 @@ function templateSubstitutionPlaceholder( } } +function templateSubstitutionsNeedCliRenameMigration( + text: string, + substitutions: ReadonlyArray<{ placeholder: string; text: string }>, +): boolean { + let restored = text; + for (const substitution of substitutions) { + restored = restored.replaceAll( + substitution.placeholder, + templateSubstitutionMigrationText(substitution.text), + ); + } + return needsCliRenameMigration(restored); +} + +function templateSubstitutionMigrationText(value: string): string { + return value.startsWith("${") && value.endsWith("}") ? value.slice(2, -1).trim() : value; +} + function pushTemplateStringEdit( edits: Array<[number, number, string]>, source: string, @@ -1191,7 +1251,9 @@ function pushTemplateStringEdit( text = `${text.slice(0, childStart)}${placeholder}${text.slice(childEnd)}`; } - let replacement = renameSourceCommandText(text); + let replacement = templateSubstitutionsNeedCliRenameMigration(text, substitutions) + ? text + : renameSourceCommandText(text); for (const substitution of substitutions) { replacement = replacement.replaceAll(substitution.placeholder, substitution.text); } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 49bb8bc52..ef89e23a3 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -11,6 +11,7 @@ const shellSpawned = spawn("sh", ["-c", "tailor deploy"]); const applySpawned = spawn("tailor-sdk", ["apply"]); const cliRenameCommandSpawned = spawn("tailor-sdk", ["crash-report", "list"]); const cliRenameFlagSpawned = spawn("tailor-sdk", ["login", "--machineuser"]); +const dynamicCliRenameCommandSpawned = spawn("tailor-sdk", [`${"apply"}`]); const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "dev", "login"]); @@ -53,6 +54,7 @@ const npmExecSpawned = spawn("npm", ["exec", "@tailor-platform/sdk", "login"]); const npmExecPackageFlagSpawned = spawn("npm", ["exec", "--package", "@tailor-platform/sdk", "tailor", "login"]); const npmExecPackageEqualsSpawned = spawn("npm", ["exec", "--package=@tailor-platform/sdk", "tailor", "login"]); const pathQualifiedSpawned = spawn("./node_modules/.bin/tailor", ["deploy"]); +const pathQualifiedArgSpawned = spawn("./node_modules/.bin/tailor", ["--arg", "tailor-sdk deploy", "deploy"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; const npxArgs = ["tailor-sdk", "login"]; spawn("npx", npxArgs); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 195fc0cd1..a6c2a1ed5 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -11,6 +11,7 @@ const shellSpawned = spawn("sh", ["-c", "tailor-sdk deploy"]); const applySpawned = spawn("tailor-sdk", ["apply"]); const cliRenameCommandSpawned = spawn("tailor-sdk", ["crash-report", "list"]); const cliRenameFlagSpawned = spawn("tailor-sdk", ["login", "--machineuser"]); +const dynamicCliRenameCommandSpawned = spawn("tailor-sdk", [`${"apply"}`]); const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["tailor-sdk", "--profile", "dev", "login"]); @@ -53,6 +54,7 @@ const npmExecSpawned = spawn("npm", ["exec", "tailor-sdk", "login"]); const npmExecPackageFlagSpawned = spawn("npm", ["exec", "--package", "tailor-sdk", "tailor-sdk", "login"]); const npmExecPackageEqualsSpawned = spawn("npm", ["exec", "--package=tailor-sdk", "tailor-sdk", "login"]); const pathQualifiedSpawned = spawn("./node_modules/.bin/tailor-sdk", ["deploy"]); +const pathQualifiedArgSpawned = spawn("./node_modules/.bin/tailor-sdk", ["--arg", "tailor-sdk deploy", "deploy"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; const npxArgs = ["tailor-sdk", "login"]; spawn("npx", npxArgs); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 762f7134a..761fd340b 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -3,16 +3,22 @@ const setup = `pnpm tailor staticwebsite deploy --name ${siteName}`; const deploy = "tailor deploy"; const envFileDeploy = "tailor --env-file .env deploy"; const profileValue = "tailor --profile tailor-sdk deploy"; +const pnpmExecProfileValue = "pnpm exec tailor --profile tailor-sdk deploy"; +const yarnExecProfileValue = "yarn exec tailor --profile tailor-sdk deploy"; const nameValue = "tailor tailordb migration generate --name \"tailor-sdk deploy\""; const directNameValue = "tailor tailordb migration generate --name \"tailor-sdk deploy\""; const namespaceValue = "tailor tailordb truncate --namespace tailor-sdk"; const dirValue = "tailor staticwebsite deploy --name site --dir tailor-sdk"; +const pathQualifiedArgValue = "./node_modules/.bin/tailor --arg \"tailor-sdk deploy\" deploy"; +const pathQualifiedCliRenameCommand = "./node_modules/.bin/tailor-sdk apply"; const help = "tailor --help"; const npxVersion = "npx @tailor-platform/sdk --version"; const generated = "Run tailor generate after changes"; const cliRenameCommand = "tailor-sdk crash-report list"; const cliRenameFlag = "tailor-sdk login --machineuser"; +const shellWrappedCliRenameCommand = "sh -c \"tailor-sdk apply\""; const dynamicCommand = `tailor ${subcommand}`; +const dynamicCliRenameCommand = `tailor-sdk ${"apply"}`; const dynamicCommandWithTrailingFlag = `tailor ${subcommand} --json`; const dynamicCommandBeforeSeparator = `tailor ${subcommand} && echo done`; const dynamicPnpmCommand = `pnpm tailor ${subcommand}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index e7211e601..ca7d059ce 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -3,16 +3,22 @@ const setup = `pnpm tailor-sdk staticwebsite deploy --name ${siteName}`; const deploy = "tailor-sdk deploy"; const envFileDeploy = "tailor-sdk --env-file .env deploy"; const profileValue = "tailor-sdk --profile tailor-sdk deploy"; +const pnpmExecProfileValue = "pnpm exec tailor-sdk --profile tailor-sdk deploy"; +const yarnExecProfileValue = "yarn exec tailor-sdk --profile tailor-sdk deploy"; const nameValue = "tailor tailordb migration generate --name \"tailor-sdk deploy\""; const directNameValue = "tailor-sdk tailordb migration generate --name \"tailor-sdk deploy\""; const namespaceValue = "tailor tailordb truncate --namespace tailor-sdk"; const dirValue = "tailor staticwebsite deploy --name site --dir tailor-sdk"; +const pathQualifiedArgValue = "./node_modules/.bin/tailor-sdk --arg \"tailor-sdk deploy\" deploy"; +const pathQualifiedCliRenameCommand = "./node_modules/.bin/tailor-sdk apply"; const help = "tailor-sdk --help"; const npxVersion = "npx tailor-sdk --version"; const generated = "Run tailor-sdk generate after changes"; const cliRenameCommand = "tailor-sdk crash-report list"; const cliRenameFlag = "tailor-sdk login --machineuser"; +const shellWrappedCliRenameCommand = "sh -c \"tailor-sdk apply\""; const dynamicCommand = `tailor-sdk ${subcommand}`; +const dynamicCliRenameCommand = `tailor-sdk ${"apply"}`; const dynamicCommandWithTrailingFlag = `tailor-sdk ${subcommand} --json`; const dynamicCommandBeforeSeparator = `tailor-sdk ${subcommand} && echo done`; const dynamicPnpmCommand = `pnpm tailor-sdk ${subcommand}`; From 899af08255cb0b0eb200319c647dda03f9be097c Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 01:45:53 +0900 Subject: [PATCH 335/618] fix(sdk-codemod): scope source residual warnings --- .../v2/rename-bin/scripts/transform.ts | 4 +- packages/sdk-codemod/src/migration-doc.ts | 3 +- packages/sdk-codemod/src/registry.test.ts | 1 + packages/sdk-codemod/src/registry.ts | 4 ++ packages/sdk-codemod/src/runner.test.ts | 32 ++++++++++++- packages/sdk-codemod/src/runner.ts | 47 +++++++++++++++++-- packages/sdk-codemod/src/types.ts | 7 +++ 7 files changed, 88 insertions(+), 10 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 896c38b69..599fc1c7e 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -1103,8 +1103,8 @@ function arrayHasCliRenameLegacyArgs(elements: SgNode[], start: number, source: sourceStringContent(elements[index]!, source) ?? sourceStringRawContent(elements[index]!, source); if (value == null) continue; - if (TAILOR_CLI_VALUE_FLAGS.has(value.split("=", 1)[0]!) && !value.includes("=")) { - index += 1; + if (TAILOR_CLI_VALUE_FLAGS.has(value.split("=", 1)[0]!)) { + if (!value.includes("=")) index += 1; continue; } if (CLI_RENAME_LEGACY_RE.test(value)) return true; diff --git a/packages/sdk-codemod/src/migration-doc.ts b/packages/sdk-codemod/src/migration-doc.ts index 8c3cd7d9a..418bc8eb2 100644 --- a/packages/sdk-codemod/src/migration-doc.ts +++ b/packages/sdk-codemod/src/migration-doc.ts @@ -7,7 +7,7 @@ export type AutomationLevel = "Automatic" | "Partially automatic" | "Manual"; * - `Automatic`: a transform fully covers it, with no residual to flag. * - `Partially automatic`: a transform covers the common cases but flags * residuals (via `legacyPatterns`/`sourceStringLegacyPatterns`/ - * `suspiciousPatterns`/`prompt`) to finish. + * `sourceTextLegacyPatterns`/`suspiciousPatterns`/`prompt`) to finish. * - `Manual`: no transform; the change is migrated by hand (optionally guided * by a `prompt`). Whether a person or an LLM does it does not matter here. * @param codemod - The codemod registry entry @@ -18,6 +18,7 @@ export function automationLevel(codemod: CodemodPackage): AutomationLevel { const flagsResidual = (codemod.legacyPatterns?.length ?? 0) > 0 || (codemod.sourceStringLegacyPatterns?.length ?? 0) > 0 || + (codemod.sourceTextLegacyPatterns?.length ?? 0) > 0 || (codemod.suspiciousPatterns?.length ?? 0) > 0 || codemod.prompt != null; return flagsResidual ? "Partially automatic" : "Automatic"; diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 2332d1256..64e8bc649 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -64,6 +64,7 @@ describe("getApplicableCodemods", () => { expect(renameBin?.filePatterns).toEqual(expect.arrayContaining([sourcePattern])); expect(renameBin?.sourceStringLegacyPatterns).toHaveLength(2); + expect(renameBin?.sourceTextLegacyPatterns).toHaveLength(2); const sourceStringPatterns = renameBin?.sourceStringLegacyPatterns as RegExp[]; const matchesSourceStringPattern = (value: string) => sourceStringPatterns.some((pattern) => pattern.test(value)); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index ca92872e7..cf9e55ebd 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -620,6 +620,10 @@ export const allCodemods: CodemodPackage[] = [ RENAME_BIN_SOURCE_LEGACY_PATTERN, RENAME_BIN_QUOTED_SOURCE_LEGACY_PATTERN, ], + sourceTextLegacyPatterns: [ + RENAME_BIN_SOURCE_LEGACY_PATTERN, + RENAME_BIN_QUOTED_SOURCE_LEGACY_PATTERN, + ], examples: [ { lang: "sh", diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index ae119805d..38a70ab80 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -27,7 +27,10 @@ function makeCodemod( scriptPath?: string, filePatterns?: string[], legacyPatterns?: Array, - extra?: Pick, + extra?: Pick< + CodemodPackage, + "sourceStringLegacyPatterns" | "sourceTextLegacyPatterns" | "suspiciousPatterns" | "prompt" + >, ): CodemodPackage { return { id, @@ -546,6 +549,31 @@ describe("runCodemods", () => { expect(result.warnings).toEqual([]); }); + test("keeps generic source string residual checks out of comments", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "env.ts"), + "// PLATFORM_URL is documented here\nconst value = 1;", + "utf-8", + ); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/env", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: ["PLATFORM_URL"], + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toEqual([]); + }); + test("keeps escaped quoted Tailor values out of rename-bin residual warnings", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); tmpDir = dir; @@ -646,7 +674,7 @@ describe("runCodemods", () => { [ { codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.tsx"], [], { - sourceStringLegacyPatterns: renameBin?.sourceStringLegacyPatterns, + sourceTextLegacyPatterns: renameBin?.sourceTextLegacyPatterns, }), scriptPath: partialTransformPath, }, diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 8ff879bbd..e9d6ecb05 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -162,6 +162,7 @@ interface LoadedTransform { matches: (relativePath: string) => boolean; legacyPatterns: CodemodPatternGroup[]; sourceStringLegacyPatterns: CodemodPatternGroup[]; + sourceTextLegacyPatterns: CodemodPatternGroup[]; suspiciousPatterns: CodemodPatternGroup[]; prompt?: string; } @@ -184,10 +185,6 @@ function sourceStringContentForResidualMatching(relative: string, content: strin const fragments: string[] = []; const visit = (node: SgNode): void => { - if (node.kind() === "comment" || node.kind() === "jsx_text") { - fragments.push(node.text()); - return; - } if (node.kind() === "arguments") { const value = sourceArgumentsCommandContent(node, content); if (value != null) fragments.push(value); @@ -215,6 +212,31 @@ function sourceStringContentForResidualMatching(relative: string, content: strin return fragments.join(SOURCE_STRING_FRAGMENT_SEPARATOR); } +function sourceTextContentForResidualMatching(relative: string, content: string): string | null { + const ext = path.extname(relative).toLowerCase(); + if (!SOURCE_EXTENSIONS.has(ext)) return null; + + let root: SgNode; + try { + root = parse(sourceLang(relative), content).root(); + } catch { + return null; + } + + const fragments: string[] = []; + const visit = (node: SgNode): void => { + if (node.kind() === "comment" || node.kind() === "jsx_text") { + fragments.push(node.text()); + return; + } + for (const child of node.children()) { + visit(child); + } + }; + visit(root); + return fragments.join(SOURCE_STRING_FRAGMENT_SEPARATOR); +} + function sourceArgumentsCommandContent(node: SgNode, source: string): string | null { const args = sourceArrayElements(node); const executable = args[0] == null ? null : sourceStringNodeContent(args[0]!, source); @@ -409,6 +431,7 @@ function legacyPatternWarnings( relative: string, content: string, sourceStringContent: string | null, + sourceTextContent: string | null, transforms: LoadedTransform[], ): string[] { return transforms.flatMap((lt) => { @@ -423,6 +446,12 @@ function legacyPatternWarnings( if (label != null) found.add(label); } } + if (sourceTextContent != null) { + for (const pattern of lt.sourceTextLegacyPatterns) { + const label = matchResidualPattern(sourceTextContent, pattern); + if (label != null) found.add(label); + } + } if (found.size === 0) return []; return [ `${relative}: contains ${Array.from(found).join(", ")} but was not migrated automatically (rule: ${lt.id}). Manual migration may be needed.`, @@ -456,6 +485,7 @@ export async function runCodemods( matches: picomatch(patterns, { dot: true }), legacyPatterns: codemod.legacyPatterns ?? [], sourceStringLegacyPatterns: codemod.sourceStringLegacyPatterns ?? [], + sourceTextLegacyPatterns: codemod.sourceTextLegacyPatterns ?? [], suspiciousPatterns: codemod.suspiciousPatterns ?? [], prompt: codemod.prompt, }); @@ -504,8 +534,15 @@ export async function runCodemods( const residualContent = contentForResidualMatching(relative, current); const sourceStringContent = sourceStringContentForResidualMatching(relative, current); + const sourceTextContent = sourceTextContentForResidualMatching(relative, current); warnings.push( - ...legacyPatternWarnings(relative, residualContent, sourceStringContent, matchedTransforms), + ...legacyPatternWarnings( + relative, + residualContent, + sourceStringContent, + sourceTextContent, + matchedTransforms, + ), ); for (const lt of matchedTransforms) { diff --git a/packages/sdk-codemod/src/types.ts b/packages/sdk-codemod/src/types.ts index 34965ebd6..651b34ba6 100644 --- a/packages/sdk-codemod/src/types.ts +++ b/packages/sdk-codemod/src/types.ts @@ -55,6 +55,13 @@ export interface CodemodPackage { * manual follow-up warning. */ sourceStringLegacyPatterns?: CodemodPatternGroup[]; + /** + * Patterns to detect only inside comments and JSX text of source files after + * a transform runs. Use this for source text that is intentionally masked + * from generic residual matching, but still contains user-facing command + * examples that need a manual follow-up warning. + */ + sourceTextLegacyPatterns?: CodemodPatternGroup[]; /** * Patterns that, when present in a file's post-transform content, mark it * as a candidate for LLM-assisted review. Use this for migrations the From 0f68461de5dc2076c4f9a903ba34d411bf9ffd9f Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 02:30:20 +0900 Subject: [PATCH 336/618] fix(sdk-codemod): tighten source command detection --- .../v2/rename-bin/scripts/transform.ts | 40 ++++++++----------- .../tests/source-template/expected.ts | 2 + .../rename-bin/tests/source-template/input.ts | 2 + packages/sdk-codemod/src/registry.test.ts | 2 + packages/sdk-codemod/src/registry.ts | 4 +- packages/sdk-codemod/src/runner.test.ts | 28 ++++++++++++- 6 files changed, 52 insertions(+), 26 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 599fc1c7e..84aa745de 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -3,7 +3,21 @@ import * as path from "pathe"; import type { SgNode } from "@ast-grep/napi"; const SOURCE_ARG_VALUE = `(?:[^\\s'"\`;|&]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`; -const PACKAGE_RUNNER_COMMAND = `(?:npx|bunx|(?:pnpm|yarn)(?:\\s+(?:-\\w+|--\\w[\\w-]*)(?:=${SOURCE_ARG_VALUE})?(?:\\s+(?!dlx\\b|-)${SOURCE_ARG_VALUE})?)*\\s+dlx)`; +const RUNNER_OPTION_VALUE_FLAG_LIST = [ + "--registry", + "--cache", + "--userconfig", + "--prefix", + "--filter", + "-F", + "--dir", + "-C", + "--cwd", +] as const; +const RUNNER_OPTION_VALUE_FLAG_PATTERN = `(?:${RUNNER_OPTION_VALUE_FLAG_LIST.join("|")})`; +const PACKAGE_RUNNER_BOOLEAN_OPTION = `(?!(?:${RUNNER_OPTION_VALUE_FLAG_PATTERN})(?:=|\\s|$))(?:-\\w+|--\\w[\\w-]*)(?:=${SOURCE_ARG_VALUE})?`; +const PACKAGE_RUNNER_OPTION = `(?:${RUNNER_OPTION_VALUE_FLAG_PATTERN}(?:=${SOURCE_ARG_VALUE}|\\s+${SOURCE_ARG_VALUE})|${PACKAGE_RUNNER_BOOLEAN_OPTION})`; +const PACKAGE_RUNNER_COMMAND = `(?:npx|bunx|(?:pnpm|yarn)(?:\\s+${PACKAGE_RUNNER_OPTION})*\\s+dlx)`; // Package-runner forms (`npx`, `pnpm dlx`, `yarn dlx`, `bunx`) resolve npm package // names, so `tailor-sdk@...` must become `@tailor-platform/sdk@...` — rewriting @@ -128,17 +142,7 @@ const SOURCE_EXEC_PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn"]); const SOURCE_PACKAGE_RUNNERS = new Set(["bunx", "npx"]); const SOURCE_DLX_PACKAGE_RUNNERS = new Set(["pnpm", "yarn"]); const SOURCE_NPM_EXEC_PACKAGE_RUNNERS = new Set(["npm"]); -const PACKAGE_MANAGER_OPTION_VALUE_FLAGS = new Set([ - "--registry", - "--cache", - "--userconfig", - "--prefix", - "--filter", - "-F", - "--dir", - "-C", - "--cwd", -]); +const PACKAGE_MANAGER_OPTION_VALUE_FLAGS = new Set(RUNNER_OPTION_VALUE_FLAG_LIST); const SOURCE_PACKAGE_FLAG_RE = /^(?:-p|--package)(?:=.*)?$/; const NPX_OPTION_WITH_VALUE = "(?:--registry|--cache|--userconfig|--prefix)"; const NPX_PACKAGE_FLAG_CONTEXT_RE = new RegExp( @@ -147,17 +151,7 @@ const NPX_PACKAGE_FLAG_CONTEXT_RE = new RegExp( const SOURCE_TOKEN_RE = new RegExp(SOURCE_CLI_ARG_VALUE, "g"); const CLI_RENAME_LEGACY_RE = /(? diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 761fd340b..48ff2eaac 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -43,6 +43,8 @@ const dynamicBunxCommand = `bunx @tailor-platform/sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx @tailor-platform/sdk ${subcommand}`; const pnpmDlxWithOption = "pnpm --silent dlx @tailor-platform/sdk login"; const pnpmDlxWithOptionValue = "pnpm --filter app dlx @tailor-platform/sdk login"; +const pnpmFilterNamedDlxCommand = "pnpm --filter dlx tailor deploy"; +const pnpmDlxAfterFilterNamedDlx = "pnpm --filter dlx dlx @tailor-platform/sdk login"; const dynamicPnpmDlxWithOptionValue = `pnpm --filter ${app} dlx @tailor-platform/sdk login`; const yarnDlxWithOption = "yarn --quiet dlx @tailor-platform/sdk login"; const nestedCommand = `run ${"tailor deploy"}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index ca7d059ce..50a9aeb20 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -43,6 +43,8 @@ const dynamicBunxCommand = `bunx tailor-sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx tailor-sdk ${subcommand}`; const pnpmDlxWithOption = "pnpm --silent dlx tailor-sdk login"; const pnpmDlxWithOptionValue = "pnpm --filter app dlx tailor-sdk login"; +const pnpmFilterNamedDlxCommand = "pnpm --filter dlx tailor-sdk deploy"; +const pnpmDlxAfterFilterNamedDlx = "pnpm --filter dlx dlx tailor-sdk login"; const dynamicPnpmDlxWithOptionValue = `pnpm --filter ${app} dlx tailor-sdk login`; const yarnDlxWithOption = "yarn --quiet dlx tailor-sdk login"; const nestedCommand = `run ${"tailor-sdk deploy"}`; diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 64e8bc649..91d4b8cbe 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -71,7 +71,9 @@ describe("getApplicableCodemods", () => { expect(matchesSourceStringPattern("tailor-sdk deploy")).toBe(true); expect(matchesSourceStringPattern("tailor-sdk apply")).toBe(true); expect(matchesSourceStringPattern('sh -c "tailor-sdk apply"')).toBe(true); + expect(matchesSourceStringPattern('bash -lc "tailor-sdk crash-report list"')).toBe(true); expect(matchesSourceStringPattern("tailor --profile tailor-sdk deploy")).toBe(false); + expect(matchesSourceStringPattern("tailor --name tailor-sdk deploy")).toBe(false); expect(matchesSourceStringPattern('tailor --arg "tailor-sdk deploy" deploy')).toBe(false); const matches = picomatch(renameBin?.filePatterns ?? [], { dot: true }); expect(matches("packages/app/frontend/e2e/global-setup.ts")).toBe(true); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index cf9e55ebd..09530677a 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -64,7 +64,7 @@ function escapeRegExp(value: string): string { const RENAME_BIN_SOURCE_VALUE_GUARDS = RENAME_BIN_SOURCE_VALUE_FLAGS.flatMap((flag) => { const escaped = escapeRegExp(flag); - return [`(? { tmpDir = dir; await fs.promises.writeFile( path.join(dir, "commands.ts"), - "const command = 'sh -c \"tailor-sdk apply\"';", + "const command = 'bash -lc \"tailor-sdk crash-report list\"';", "utf-8", ); const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); @@ -716,6 +716,32 @@ describe("runCodemods", () => { expect(result.warnings[0]).toContain("rule: test/rename-bin"); }); + test("keeps multiple-spaced Tailor option values out of rename-bin residual warnings", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "commands.ts"), + 'const command = "tailor --name tailor-sdk deploy";', + "utf-8", + ); + const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: renameBin?.sourceStringLegacyPatterns, + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toEqual([]); + }); + test("warns for dynamic template rename-bin residuals", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); tmpDir = dir; From 9c8c71bfe762e03587224bbe629d48104ca7d2b3 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 02:49:48 +0900 Subject: [PATCH 337/618] fix(sdk-codemod): preserve ambiguous source commands --- .../v2/rename-bin/scripts/transform.ts | 32 ++++++++++--------- .../tests/source-js-string/expected.js | 3 ++ .../tests/source-js-string/input.js | 3 ++ .../tests/source-template/expected.ts | 1 + .../rename-bin/tests/source-template/input.ts | 1 + packages/sdk-codemod/src/runner.test.ts | 28 ++++++++++++++++ 6 files changed, 53 insertions(+), 15 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 84aa745de..1c0bcbb5c 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -134,9 +134,11 @@ const SOURCE_DYNAMIC_OPTION_TAILOR_SDK_RE = new RegExp( "g", ); const TAILOR_SDK_TOKEN_RE = /^tailor-sdk(@[^\s'"`;|&)]+)?$/; -const TAILOR_SDK_PATH_RE = /(?:^|[\\/])tailor-sdk(?![\w-])(@[^\s'"`;|&)]+)?/; +const TAILOR_SDK_COMMAND_TOKEN_RE = /^tailor-sdk(@[^\s'"`;|&)]+)?(?:\.(?:cmd|ps1|exe))?$/; +const TAILOR_COMMAND_TOKEN_RE = /^tailor(?:\.(?:cmd|ps1|exe))?$/; +const TAILOR_SDK_PATH_RE = /(?:^|[\\/])tailor-sdk(?:\.(?:cmd|ps1|exe))?$/; const TAILOR_CLI_TOKEN_RE = - /^(?:tailor|tailor-sdk(?:@[^\s'"`;|&)]+)?|@tailor-platform\/sdk(?:@[^\s'"`;|&)]+)?)$/; + /^(?:tailor(?:\.(?:cmd|ps1|exe))?|tailor-sdk(?:@[^\s'"`;|&)]+)?(?:\.(?:cmd|ps1|exe))?|@tailor-platform\/sdk(?:@[^\s'"`;|&)]+)?)$/; const CLI_ARGUMENT_CALLEE_RE = /(?:^|\.)(?:spawn|spawnSync|execFile|execFileSync|execa|execaSync)$/; const SOURCE_EXEC_PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn"]); const SOURCE_PACKAGE_RUNNERS = new Set(["bunx", "npx"]); @@ -176,16 +178,17 @@ function renameSourcePackageToken(token: string): string | null { function renameSourceBinaryToken(token: string): string | null { const value = sourceTokenValue(token); - if (!TAILOR_SDK_TOKEN_RE.test(value)) return null; - return replaceSourceTokenValue(token, value.includes("@") ? renamePackageName(value) : "tailor"); + if (!TAILOR_SDK_COMMAND_TOKEN_RE.test(value)) return null; + return replaceSourceTokenValue( + token, + value.includes("@") && !/\.(?:cmd|ps1|exe)$/.test(value) + ? renamePackageName(value) + : renameBinary(value), + ); } function isTailorPackageValue(value: string): boolean { - return ( - TAILOR_SDK_TOKEN_RE.test(value) || - TAILOR_PLATFORM_SDK_TOKEN_RE.test(value) || - SOURCE_TEMPLATE_EXPR_PLACEHOLDER_RE.test(value) - ); + return TAILOR_SDK_TOKEN_RE.test(value) || TAILOR_PLATFORM_SDK_TOKEN_RE.test(value); } function sourcePathBasename(value: string): string { @@ -193,13 +196,15 @@ function sourcePathBasename(value: string): string { } function isTailorSdkBinaryTokenValue(value: string): boolean { - return TAILOR_SDK_TOKEN_RE.test(sourcePathBasename(value)); + return TAILOR_SDK_COMMAND_TOKEN_RE.test(sourcePathBasename(value)); } function isTailorCliTokenValue(value: string): boolean { const basename = sourcePathBasename(value); return ( - TAILOR_CLI_TOKEN_RE.test(value) || basename === "tailor" || TAILOR_SDK_TOKEN_RE.test(basename) + TAILOR_CLI_TOKEN_RE.test(value) || + TAILOR_COMMAND_TOKEN_RE.test(basename) || + TAILOR_SDK_COMMAND_TOKEN_RE.test(basename) ); } @@ -522,10 +527,7 @@ function sourcePackageFlagsAllowBinaryRewrite(source: string): boolean { if (SOURCE_PACKAGE_FLAG_RE.test(token)) { hasPackageFlag = true; const value = token.includes("=") ? token.slice(token.indexOf("=") + 1) : tokens[index + 1]; - if ( - value != null && - (TAILOR_CLI_TOKEN_RE.test(value) || SOURCE_TEMPLATE_EXPR_PLACEHOLDER_RE.test(value)) - ) { + if (value != null && isTailorPackageValue(value)) { hasTailorPackageFlag = true; } if (!token.includes("=")) index += 1; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index ef89e23a3..a05ac08c3 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -55,6 +55,9 @@ const npmExecPackageFlagSpawned = spawn("npm", ["exec", "--package", "@tailor-pl const npmExecPackageEqualsSpawned = spawn("npm", ["exec", "--package=@tailor-platform/sdk", "tailor", "login"]); const pathQualifiedSpawned = spawn("./node_modules/.bin/tailor", ["deploy"]); const pathQualifiedArgSpawned = spawn("./node_modules/.bin/tailor", ["--arg", "tailor-sdk deploy", "deploy"]); +const packageDirectoryPathSpawned = spawn("./node_modules/tailor-sdk/bin/cli.js", ["deploy"]); +const windowsShimArgSpawned = spawn("tailor.cmd", ["--arg", "tailor-sdk deploy", "deploy"]); +const pathQualifiedWindowsShimArgSpawned = spawn("./node_modules/.bin/tailor.cmd", ["--arg", "tailor-sdk deploy", "deploy"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; const npxArgs = ["tailor-sdk", "login"]; spawn("npx", npxArgs); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index a6c2a1ed5..34bb8ad06 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -55,6 +55,9 @@ const npmExecPackageFlagSpawned = spawn("npm", ["exec", "--package", "tailor-sdk const npmExecPackageEqualsSpawned = spawn("npm", ["exec", "--package=tailor-sdk", "tailor-sdk", "login"]); const pathQualifiedSpawned = spawn("./node_modules/.bin/tailor-sdk", ["deploy"]); const pathQualifiedArgSpawned = spawn("./node_modules/.bin/tailor-sdk", ["--arg", "tailor-sdk deploy", "deploy"]); +const packageDirectoryPathSpawned = spawn("./node_modules/tailor-sdk/bin/cli.js", ["deploy"]); +const windowsShimArgSpawned = spawn("tailor-sdk.cmd", ["--arg", "tailor-sdk deploy", "deploy"]); +const pathQualifiedWindowsShimArgSpawned = spawn("./node_modules/.bin/tailor-sdk.cmd", ["--arg", "tailor-sdk deploy", "deploy"]); const arrayCommand = ["tailor-sdk", "--profile", "dev", "deploy"]; const npxArgs = ["tailor-sdk", "login"]; spawn("npx", npxArgs); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 48ff2eaac..e3cf11350 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -61,6 +61,7 @@ const npxPackageFlagEquals = "npx --package=@tailor-platform/sdk tailor login"; const npxPackageSingleQuoted = "npx --package '@tailor-platform/sdk' tailor login"; const npxPackageDoubleQuoted = "npx --package \"@tailor-platform/sdk\" tailor login"; const npxPackageEqualsDoubleQuoted = "npx --package=\"@tailor-platform/sdk\" tailor login"; +const npxDynamicPackageFlag = `npx -p ${pkg} tailor-sdk login`; const npxPackageFlagDynamic = `npx -p @tailor-platform/sdk tailor ${subcommand}`; const npxPackageEqualsDynamic = `npx --package=@tailor-platform/sdk tailor ${subcommand}`; const npxRegistryValue = "npx --registry tailor-sdk @tailor-platform/sdk login"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 50a9aeb20..f0f2bde4d 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -61,6 +61,7 @@ const npxPackageFlagEquals = "npx --package=tailor-sdk tailor-sdk login"; const npxPackageSingleQuoted = "npx --package 'tailor-sdk' tailor-sdk login"; const npxPackageDoubleQuoted = "npx --package \"tailor-sdk\" tailor-sdk login"; const npxPackageEqualsDoubleQuoted = "npx --package=\"tailor-sdk\" tailor-sdk login"; +const npxDynamicPackageFlag = `npx -p ${pkg} tailor-sdk login`; const npxPackageFlagDynamic = `npx -p tailor-sdk tailor-sdk ${subcommand}`; const npxPackageEqualsDynamic = `npx --package=tailor-sdk tailor-sdk ${subcommand}`; const npxRegistryValue = "npx --registry tailor-sdk tailor-sdk login"; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index d5546f9ac..3b2dd35ab 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -770,6 +770,34 @@ describe("runCodemods", () => { expect(result.warnings[0]).toContain("rule: test/rename-bin"); }); + test("warns for dynamic package flag rename-bin residuals", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "commands.ts"), + "const command = `npx -p ${pkg} tailor-sdk login`;", + "utf-8", + ); + const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: renameBin?.sourceStringLegacyPatterns, + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toHaveLength(1); + expect(result.warnings[0]).toContain("commands.ts: contains"); + expect(result.warnings[0]).toContain("rule: test/rename-bin"); + }); + test("keeps source string residual checks in non-Tailor option values", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); tmpDir = dir; From 305fe769c046619a77998ae2c948b53914cd4273 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 03:02:34 +0900 Subject: [PATCH 338/618] chore: enforce explicit zod object policies via lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three oxlint rules — eslint-plugin-zod's `prefer-strict-object` / `prefer-loose-object` and a local `tailor-zod/require-object-policy-comment` plugin — so every `z.object()` is annotated with `strictObject` / `looseObject` or a previous-line `// strip` / `// catchall` policy comment. Claude-Session: https://claude.ai/code/session_013h9pf2cYvej4rTdwFqwtHT --- example/.oxlintrc.json | 8 +- example/package.json | 1 + llm-challenge/.oxlintrc.json | 8 +- llm-challenge/package.json | 1 + package.json | 1 + packages/create-sdk/.oxlintrc.json | 8 +- packages/create-sdk/package.json | 1 + packages/sdk-codemod/.oxlintrc.json | 8 +- packages/sdk-codemod/package.json | 1 + packages/sdk/.oxlintrc.json | 11 +- packages/sdk/package.json | 1 + pnpm-lock.yaml | 576 +++++++++++++++++++++++++++ scripts/oxlint/tailor-zod-plugin.cjs | 72 ++++ 13 files changed, 687 insertions(+), 10 deletions(-) create mode 100644 scripts/oxlint/tailor-zod-plugin.cjs diff --git a/example/.oxlintrc.json b/example/.oxlintrc.json index b956ae6f6..e59dc5750 100644 --- a/example/.oxlintrc.json +++ b/example/.oxlintrc.json @@ -61,7 +61,10 @@ "typescript/prefer-includes": "error", "typescript/prefer-nullish-coalescing": "error", "typescript/prefer-regexp-exec": "error", - "typescript/prefer-string-starts-ends-with": "error" + "typescript/prefer-string-starts-ends-with": "error", + "tailor-zod/require-object-policy-comment": "error", + "zod/prefer-loose-object": "error", + "zod/prefer-strict-object": "error" }, "overrides": [ { @@ -102,5 +105,6 @@ "import/no-cycle": "error" } } - ] + ], + "jsPlugins": ["eslint-plugin-zod", "../scripts/oxlint/tailor-zod-plugin.cjs"] } diff --git a/example/package.json b/example/package.json index b0874430a..2ae3cfc94 100644 --- a/example/package.json +++ b/example/package.json @@ -34,6 +34,7 @@ "@connectrpc/connect-node": "2.1.2", "@types/node": "24.13.2", "@typescript/native-preview": "7.0.0-dev.20260620.1", + "eslint-plugin-zod": "4.7.0", "graphql": "17.0.1", "graphql-request": "7.4.0", "multiline-ts": "4.0.1", diff --git a/llm-challenge/.oxlintrc.json b/llm-challenge/.oxlintrc.json index c3ba83401..065b8506d 100644 --- a/llm-challenge/.oxlintrc.json +++ b/llm-challenge/.oxlintrc.json @@ -2,6 +2,10 @@ "$schema": "./node_modules/oxlint/configuration_schema.json", "rules": { "unicorn/no-array-reverse": "error", - "unicorn/no-array-sort": "error" - } + "unicorn/no-array-sort": "error", + "tailor-zod/require-object-policy-comment": "error", + "zod/prefer-loose-object": "error", + "zod/prefer-strict-object": "error" + }, + "jsPlugins": ["eslint-plugin-zod", "../scripts/oxlint/tailor-zod-plugin.cjs"] } diff --git a/llm-challenge/package.json b/llm-challenge/package.json index 40c02c117..d578c3db2 100644 --- a/llm-challenge/package.json +++ b/llm-challenge/package.json @@ -11,6 +11,7 @@ }, "devDependencies": { "@types/node": "24.13.2", + "eslint-plugin-zod": "4.7.0", "oxlint": "1.70.0", "oxlint-tsgolint": "0.23.0", "tsx": "4.22.4", diff --git a/package.json b/package.json index 24e2b09f2..c711591ae 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@changesets/cli": "3.0.0-next.5", "@tailor-platform/sdk": "workspace:^", "@types/node": "24.13.2", + "eslint-plugin-zod": "4.7.0", "knip": "6.17.1", "lefthook": "2.1.9", "oxfmt": "0.55.0", diff --git a/packages/create-sdk/.oxlintrc.json b/packages/create-sdk/.oxlintrc.json index fdce5f9cf..b39d54bbd 100644 --- a/packages/create-sdk/.oxlintrc.json +++ b/packages/create-sdk/.oxlintrc.json @@ -63,7 +63,10 @@ "typescript/prefer-includes": "error", "typescript/prefer-nullish-coalescing": "error", "typescript/prefer-regexp-exec": "error", - "typescript/prefer-string-starts-ends-with": "error" + "typescript/prefer-string-starts-ends-with": "error", + "tailor-zod/require-object-policy-comment": "error", + "zod/prefer-loose-object": "error", + "zod/prefer-strict-object": "error" }, "overrides": [ { @@ -156,5 +159,6 @@ "node": true } } - ] + ], + "jsPlugins": ["eslint-plugin-zod", "../../scripts/oxlint/tailor-zod-plugin.cjs"] } diff --git a/packages/create-sdk/package.json b/packages/create-sdk/package.json index 2d5f69890..0b3913d64 100644 --- a/packages/create-sdk/package.json +++ b/packages/create-sdk/package.json @@ -35,6 +35,7 @@ }, "devDependencies": { "@types/node": "24.13.2", + "eslint-plugin-zod": "4.7.0", "oxlint": "1.70.0", "oxlint-tsgolint": "0.23.0", "tsdown": "0.22.3", diff --git a/packages/sdk-codemod/.oxlintrc.json b/packages/sdk-codemod/.oxlintrc.json index a9488014a..09c7307e2 100644 --- a/packages/sdk-codemod/.oxlintrc.json +++ b/packages/sdk-codemod/.oxlintrc.json @@ -28,7 +28,10 @@ "typescript/no-namespace": "error", "typescript/no-require-imports": "error", "typescript/no-unnecessary-type-constraint": "error", - "typescript/no-unsafe-function-type": "error" + "typescript/no-unsafe-function-type": "error", + "tailor-zod/require-object-policy-comment": "error", + "zod/prefer-loose-object": "error", + "zod/prefer-strict-object": "error" }, "overrides": [ { @@ -56,5 +59,6 @@ "prefer-spread": "error" } } - ] + ], + "jsPlugins": ["eslint-plugin-zod", "../../scripts/oxlint/tailor-zod-plugin.cjs"] } diff --git a/packages/sdk-codemod/package.json b/packages/sdk-codemod/package.json index 49b8eae35..73dc128be 100644 --- a/packages/sdk-codemod/package.json +++ b/packages/sdk-codemod/package.json @@ -40,6 +40,7 @@ "@types/node": "24.13.2", "@types/picomatch": "4.0.3", "@types/semver": "7.7.1", + "eslint-plugin-zod": "4.7.0", "oxlint": "1.70.0", "tsdown": "0.22.3", "typescript": "6.0.3", diff --git a/packages/sdk/.oxlintrc.json b/packages/sdk/.oxlintrc.json index 4d4c18071..670e52388 100644 --- a/packages/sdk/.oxlintrc.json +++ b/packages/sdk/.oxlintrc.json @@ -89,7 +89,10 @@ "typescript/no-unnecessary-type-constraint": "error", "typescript/no-unsafe-function-type": "error", "local/no-deprecated-type-matcher": "error", - "local/require-param-strict": "error" + "local/require-param-strict": "error", + "tailor-zod/require-object-policy-comment": "error", + "zod/prefer-loose-object": "error", + "zod/prefer-strict-object": "error" }, "overrides": [ { @@ -466,5 +469,9 @@ "options": { "typeAware": true }, - "jsPlugins": ["./oxlint-plugins/index.js"] + "jsPlugins": [ + "./oxlint-plugins/index.js", + "eslint-plugin-zod", + "../../scripts/oxlint/tailor-zod-plugin.cjs" + ] } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 650c9d3bf..2963ffae0 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -223,6 +223,7 @@ "@types/semver": "7.7.1", "@typescript/native-preview": "7.0.0-dev.20260620.1", "@vitest/coverage-v8": "4.1.9", + "eslint-plugin-zod": "4.7.0", "oxfmt": "0.55.0", "oxlint": "1.70.0", "oxlint-tsgolint": "0.23.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98cec9b2c..cd07da978 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,9 @@ importers: '@types/node': specifier: 24.13.2 version: 24.13.2 + eslint-plugin-zod: + specifier: 4.7.0 + version: 4.7.0(eslint@10.5.0(jiti@2.7.0))(oxlint@1.70.0(oxlint-tsgolint@0.23.0))(typescript@6.0.3)(zod@4.4.3) knip: specifier: 6.17.1 version: 6.17.1 @@ -73,6 +76,9 @@ importers: '@typescript/native-preview': specifier: 7.0.0-dev.20260620.1 version: 7.0.0-dev.20260620.1 + eslint-plugin-zod: + specifier: 4.7.0 + version: 4.7.0(eslint@10.5.0(jiti@2.7.0))(oxlint@1.70.0(oxlint-tsgolint@0.23.0))(typescript@6.0.3)(zod@4.4.3) graphql: specifier: 17.0.1 version: 17.0.1 @@ -106,6 +112,9 @@ importers: '@types/node': specifier: 24.13.2 version: 24.13.2 + eslint-plugin-zod: + specifier: 4.7.0 + version: 4.7.0(eslint@10.5.0(jiti@2.7.0))(oxlint@1.70.0(oxlint-tsgolint@0.23.0))(typescript@6.0.3)(zod@4.4.3) oxlint: specifier: 1.70.0 version: 1.70.0(oxlint-tsgolint@0.23.0) @@ -146,6 +155,9 @@ importers: '@types/node': specifier: 24.13.2 version: 24.13.2 + eslint-plugin-zod: + specifier: 4.7.0 + version: 4.7.0(eslint@10.5.0(jiti@2.7.0))(oxlint@1.70.0(oxlint-tsgolint@0.23.0))(typescript@6.0.3)(zod@4.4.3) oxlint: specifier: 1.70.0 version: 1.70.0(oxlint-tsgolint@0.23.0) @@ -552,6 +564,9 @@ importers: '@vitest/coverage-v8': specifier: 4.1.9 version: 4.1.9(vitest@4.1.9) + eslint-plugin-zod: + specifier: 4.7.0 + version: 4.7.0(eslint@10.5.0(jiti@2.7.0))(oxlint@1.70.0(oxlint-tsgolint@0.23.0))(typescript@6.0.3)(zod@4.4.3) oxfmt: specifier: 0.55.0 version: 0.55.0 @@ -616,6 +631,9 @@ importers: '@types/semver': specifier: 7.7.1 version: 7.7.1 + eslint-plugin-zod: + specifier: 4.7.0 + version: 4.7.0(eslint@10.5.0(jiti@2.7.0))(oxlint@1.70.0(oxlint-tsgolint@0.23.0))(typescript@6.0.3)(zod@4.4.3) oxlint: specifier: 1.70.0 version: 1.70.0(oxlint-tsgolint@0.23.0) @@ -1045,11 +1063,65 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint-zod/utils@2.3.0': + resolution: {integrity: sha512-6XEQLA5lDOOLnX05bgzDT3DyxlCGgLDFX3q0wYSSUSyVJj5WD+8wdlIAFY8kjatSDHWJQ+qFI/BdiDh9o50nBQ==} + engines: {node: ^20 || ^22 || >=24} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@graphql-typed-document-node/core@3.2.0': resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@inquirer/ansi@2.0.7': resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} @@ -2266,12 +2338,18 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/jsesc@2.5.1': resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/madge@5.0.3': resolution: {integrity: sha512-NlQJd0qRAoyu+pawTDhLxkW940QT2dqASfwd2g/xEZu2F4Xjwa7TVRSPdbmZwUF1ygvAh0/nepeN7JjwEuOXCA==} @@ -2293,26 +2371,63 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.62.0': + resolution: {integrity: sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.62.0': + resolution: {integrity: sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.61.0': resolution: {integrity: sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/tsconfig-utils@8.62.0': + resolution: {integrity: sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/types@8.61.0': resolution: {integrity: sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.62.0': + resolution: {integrity: sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.61.0': resolution: {integrity: sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/typescript-estree@8.62.0': + resolution: {integrity: sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.62.0': + resolution: {integrity: sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@8.61.0': resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.62.0': + resolution: {integrity: sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260620.1': resolution: {integrity: sha512-1qwEjLW1JCRkDYrS8OyWdfXoL9bNHV28kP3ouQxytZmNpghWSMKZutsxDjVVbnlsbydBdYqqZMttPuTUlm3y0A==} engines: {node: '>=16.20.0'} @@ -2416,6 +2531,19 @@ packages: '@vue/shared@3.5.38': resolution: {integrity: sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} @@ -2587,6 +2715,9 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + default-browser-id@5.0.1: resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} engines: {node: '>=18'} @@ -2707,20 +2838,69 @@ packages: engines: {node: '>=18'} hasBin: true + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} engines: {node: '>=6.0'} hasBin: true + eslint-plugin-zod@4.7.0: + resolution: {integrity: sha512-unziYvD8FGxhfL6rYuC3Cjdx/K3yMWSTv6t5uB9KEgzMajDXuQcp67/WDxdUiEFhGY1LlXJs03xQc+Pg6+vqrw==} + engines: {node: ^20 || ^22 || >=24} + peerDependencies: + eslint: ^9 || ^10 + oxlint: ^1.59.0 + zod: ^4 + peerDependenciesMeta: + eslint: + optional: true + oxlint: + optional: true + zod: + optional: true + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@5.0.1: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@10.5.0: + resolution: {integrity: sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} @@ -2749,6 +2929,12 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-string-truncated-width@3.0.3: resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} @@ -2777,6 +2963,10 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + filing-cabinet@5.5.1: resolution: {integrity: sha512-PzLBTChlVPn6LnNxF0KWs+XqPziVh3Sfmz/3TXOymHxu6a9yhrDcQn7YwgpcRM6mqhR2WHVGPR8RU4fmcF1IVA==} engines: {node: '>=18'} @@ -2786,6 +2976,17 @@ packages: resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} engines: {node: '>=18'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + formatly@0.3.0: resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} engines: {node: '>=18.3.0'} @@ -2817,6 +3018,10 @@ packages: resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} engines: {node: '>=20.20.0'} + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -2871,6 +3076,10 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -2878,6 +3087,10 @@ packages: resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==} engines: {node: ^22.18.0 || >=24.0.0} + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + inflection@3.0.2: resolution: {integrity: sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==} engines: {node: '>=18.0.0'} @@ -2897,10 +3110,18 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + is-in-ssh@1.0.0: resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} engines: {node: '>=20'} @@ -2980,14 +3201,26 @@ packages: engines: {node: '>=6'} hasBin: true + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + knip@6.17.1: resolution: {integrity: sha512-HcQsZSQ4Ymhuay4BVzJtM5pFZNDSomYYqcNCZOSITPQh9g18a09DqziWAxSt2G+BH9wGlG+0ZjWpEnaFlnKseQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3054,6 +3287,10 @@ packages: resolution: {integrity: sha512-bwDaIOViTktE8kJLf9jP0p+H2/RDTlFFlc43Am2YgUsX22hI6Sq4RbzsrecwzY5y+MHTipOH7WsmWSEniePHWQ==} hasBin: true + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -3128,6 +3365,10 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lodash.truncate@4.4.2: resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} @@ -3215,6 +3456,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + nearley@2.20.1: resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} hasBin: true @@ -3239,6 +3483,10 @@ packages: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -3284,10 +3532,18 @@ packages: vite-plus: optional: true + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + p-limit@7.3.0: resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} engines: {node: '>=20'} + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} @@ -3302,6 +3558,10 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -3374,6 +3634,10 @@ packages: engines: {node: '>=18'} hasBin: true + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + pretty-ms@7.0.1: resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} engines: {node: '>=10'} @@ -3387,6 +3651,10 @@ packages: engines: {node: '>=18'} hasBin: true + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} @@ -3701,6 +3969,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type-fest@5.7.0: resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==} engines: {node: '>=20'} @@ -3733,6 +4005,9 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -3844,6 +4119,10 @@ packages: wonka@6.3.6: resolution: {integrity: sha512-MXH+6mDHAZ2GuMpgKS055FR6v0xVP3XwquxIMYXgiW+FejHQlMGlvVRZT4qMCxR+bEo/FCtIdKxwej9WV3YQag==} + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wsl-utils@0.3.1: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} @@ -3857,6 +4136,10 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + yocto-queue@1.2.2: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} @@ -4224,10 +4507,65 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@10.5.0(jiti@2.7.0))': + dependencies: + eslint: 10.5.0(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint-zod/utils@2.3.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + esquery: 1.7.0 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + '@graphql-typed-document-node/core@3.2.0(graphql@17.0.1)': dependencies: graphql: 17.0.1 + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@inquirer/ansi@2.0.7': {} '@inquirer/checkbox@5.2.1(@types/node@24.13.2)': @@ -5006,10 +5344,14 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/esrecurse@4.3.1': {} + '@types/estree@1.0.9': {} '@types/jsesc@2.5.1': {} + '@types/json-schema@7.0.15': {} + '@types/madge@5.0.3': dependencies: '@types/node': 24.13.2 @@ -5033,12 +5375,32 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.62.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.62.0(typescript@6.0.3) + '@typescript-eslint/types': 8.62.0 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.62.0': + dependencies: + '@typescript-eslint/types': 8.62.0 + '@typescript-eslint/visitor-keys': 8.62.0 + '@typescript-eslint/tsconfig-utils@8.61.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.62.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + '@typescript-eslint/types@8.61.0': {} + '@typescript-eslint/types@8.62.0': {} + '@typescript-eslint/typescript-estree@8.61.0(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.61.0(typescript@5.9.3) @@ -5054,11 +5416,42 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.62.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.62.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.62.0(typescript@6.0.3) + '@typescript-eslint/types': 8.62.0 + '@typescript-eslint/visitor-keys': 8.62.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.62.0 + '@typescript-eslint/types': 8.62.0 + '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) + eslint: 10.5.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.61.0': dependencies: '@typescript-eslint/types': 8.61.0 eslint-visitor-keys: 5.0.1 + '@typescript-eslint/visitor-keys@8.62.0': + dependencies: + '@typescript-eslint/types': 8.62.0 + eslint-visitor-keys: 5.0.1 + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260620.1': optional: true @@ -5184,6 +5577,19 @@ snapshots: '@vue/shared@3.5.38': {} + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 @@ -5317,6 +5723,8 @@ snapshots: deep-extend@0.6.0: {} + deep-is@0.1.4: {} + default-browser-id@5.0.1: {} default-browser@5.5.0: @@ -5456,6 +5864,8 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + escape-string-regexp@4.0.0: {} + escodegen@2.1.0: dependencies: esprima: 4.0.1 @@ -5464,10 +5874,82 @@ snapshots: optionalDependencies: source-map: 0.6.1 + eslint-plugin-zod@4.7.0(eslint@10.5.0(jiti@2.7.0))(oxlint@1.70.0(oxlint-tsgolint@0.23.0))(typescript@6.0.3)(zod@4.4.3): + dependencies: + '@eslint-zod/utils': 2.3.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + optionalDependencies: + eslint: 10.5.0(jiti@2.7.0) + oxlint: 1.70.0(oxlint-tsgolint@0.23.0) + zod: 4.4.3 + transitivePeerDependencies: + - supports-color + - typescript + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + eslint-visitor-keys@5.0.1: {} + eslint@10.5.0(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 5.0.1 + esprima@4.0.1: {} + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + estraverse@5.3.0: {} estree-walker@2.0.2: {} @@ -5499,6 +5981,10 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fast-string-truncated-width@3.0.3: {} fast-string-width@3.0.2: @@ -5523,6 +6009,10 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + filing-cabinet@5.5.1: dependencies: app-module-path: 2.2.0 @@ -5539,6 +6029,18 @@ snapshots: find-up-simple@1.0.1: {} + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + formatly@0.3.0: dependencies: fd-package-json: 2.0.0 @@ -5568,6 +6070,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + glob@13.0.6: dependencies: minimatch: 10.2.5 @@ -5609,10 +6115,14 @@ snapshots: ieee754@1.2.1: {} + ignore@5.3.2: {} + import-meta-resolve@4.2.0: {} import-without-cache@0.4.0: {} + imurmurhash@0.1.4: {} + inflection@3.0.2: {} inherits@2.0.4: {} @@ -5625,8 +6135,14 @@ snapshots: is-docker@3.0.0: {} + is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + is-in-ssh@1.0.0: {} is-inside-container@1.0.0: @@ -5680,10 +6196,20 @@ snapshots: jsesc@3.1.0: {} + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + knip@6.17.1: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -5750,6 +6276,11 @@ snapshots: lefthook-windows-arm64: 2.1.9 lefthook-windows-x64: 2.1.9 + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lightningcss-android-arm64@1.32.0: optional: true @@ -5799,6 +6330,10 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lodash.truncate@4.4.2: {} log-symbols@4.1.0: @@ -5880,6 +6415,8 @@ snapshots: nanoid@3.3.12: {} + natural-compare@1.4.0: {} + nearley@2.20.1: dependencies: commander: 2.20.3 @@ -5911,6 +6448,15 @@ snapshots: powershell-utils: 0.1.0 wsl-utils: 0.3.1 + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + ora@5.4.1: dependencies: bl: 4.1.0 @@ -6051,10 +6597,18 @@ snapshots: '@oxlint/binding-win32-x64-msvc': 1.70.0 oxlint-tsgolint: 0.23.0 + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + p-limit@7.3.0: dependencies: yocto-queue: 1.2.2 + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + package-manager-detector@1.6.0: {} parse-ms@2.1.0: {} @@ -6063,6 +6617,8 @@ snapshots: path-browserify@1.0.1: {} + path-exists@4.0.0: {} + path-key@3.1.1: {} path-key@4.0.0: {} @@ -6138,6 +6694,8 @@ snapshots: transitivePeerDependencies: - supports-color + prelude-ls@1.2.1: {} + pretty-ms@7.0.1: dependencies: parse-ms: 2.1.0 @@ -6153,6 +6711,8 @@ snapshots: picocolors: 1.1.1 sade: 1.8.1 + punycode@2.3.1: {} + quansync@1.0.0: {} quote-unquote@1.0.0: {} @@ -6397,6 +6957,10 @@ snapshots: dependencies: typescript: 5.9.3 + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + ts-cron-validator@1.1.5: {} ts-graphviz@2.1.6: @@ -6453,6 +7017,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type-fest@5.7.0: dependencies: tagged-tag: 1.0.0 @@ -6474,6 +7042,10 @@ snapshots: unicorn-magic@0.3.0: {} + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + util-deprecate@1.0.2: {} vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): @@ -6539,6 +7111,8 @@ snapshots: wonka@6.3.6: {} + word-wrap@1.2.5: {} + wsl-utils@0.3.1: dependencies: is-wsl: 3.1.1 @@ -6548,6 +7122,8 @@ snapshots: yaml@2.9.0: {} + yocto-queue@0.1.0: {} + yocto-queue@1.2.2: {} yoctocolors@2.1.2: {} diff --git a/scripts/oxlint/tailor-zod-plugin.cjs b/scripts/oxlint/tailor-zod-plugin.cjs new file mode 100644 index 000000000..83e187f37 --- /dev/null +++ b/scripts/oxlint/tailor-zod-plugin.cjs @@ -0,0 +1,72 @@ +"use strict"; + +const OBJECT_POLICY_COMMENT_PATTERN = /\b(catchall|strip)\b/i; + +function getPropertyName(property) { + if (property == null) { + return undefined; + } + + if (property.type === "Identifier") { + return property.name; + } + + if (property.type === "Literal") { + return property.value; + } + + return undefined; +} + +function isZObjectCall(node) { + return ( + node.callee.type === "MemberExpression" && + node.callee.object.type === "Identifier" && + node.callee.object.name === "z" && + getPropertyName(node.callee.property) === "object" + ); +} + +function hasObjectPolicyComment(sourceCode, node) { + const previousLine = sourceCode.getText().split(/\r?\n/)[node.loc.start.line - 2] ?? ""; + const trimmedPreviousLine = previousLine.trimStart(); + return ( + (trimmedPreviousLine.startsWith("//") || trimmedPreviousLine.startsWith("/*")) && + OBJECT_POLICY_COMMENT_PATTERN.test(trimmedPreviousLine) + ); +} + +module.exports = { + meta: { + name: "tailor-zod", + }, + rules: { + "require-object-policy-comment": { + meta: { + type: "problem", + docs: { + description: "Require an unknown-key policy comment for z.object().", + }, + schema: [], + messages: { + missingObjectPolicyComment: + 'Add a previous-line comment containing "strip" or "catchall" for z.object(), or use z.strictObject() / z.looseObject().', + }, + }, + create(context) { + const sourceCode = context.sourceCode ?? context.getSourceCode(); + + return { + CallExpression(node) { + if (isZObjectCall(node) && !hasObjectPolicyComment(sourceCode, node)) { + context.report({ + node, + messageId: "missingObjectPolicyComment", + }); + } + }, + }; + }, + }, + }, +}; From 7b8515d2cd0f75b34d180866778dfd0925156370 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 03:03:54 +0900 Subject: [PATCH 339/618] chore: annotate or strict-ify every zod object schema Apply the new lint rules across all packages: rewrite call sites that chain `.strict()` / `.passthrough()` / `.loose()` to `z.strictObject()` / `z.looseObject()`, and prepend a `// strip unknown keys` policy comment to the remaining bare `z.object()` calls. Claude-Session: https://claude.ai/code/session_013h9pf2cYvej4rTdwFqwtHT --- packages/create-sdk/src/index.ts | 1 + packages/sdk-codemod/src/index.ts | 34 ++++---- packages/sdk/src/cli/cache/types.ts | 3 + packages/sdk/src/cli/commands/api/index.ts | 54 ++++++------ packages/sdk/src/cli/commands/api/inspect.ts | 18 ++-- packages/sdk/src/cli/commands/api/list.ts | 2 +- .../cli/commands/authconnection/authorize.ts | 36 ++++---- .../src/cli/commands/authconnection/delete.ts | 12 ++- .../src/cli/commands/authconnection/list.ts | 2 +- .../src/cli/commands/authconnection/open.ts | 2 +- .../src/cli/commands/authconnection/revoke.ts | 12 ++- .../sdk/src/cli/commands/crashreport/list.ts | 8 +- .../sdk/src/cli/commands/crashreport/send.ts | 16 ++-- packages/sdk/src/cli/commands/deploy/index.ts | 42 +++++----- .../src/cli/commands/deploy/secrets-state.ts | 1 + packages/sdk/src/cli/commands/executor/get.ts | 10 +-- .../sdk/src/cli/commands/executor/jobs.ts | 82 +++++++++---------- .../sdk/src/cli/commands/executor/list.ts | 10 +-- .../sdk/src/cli/commands/executor/trigger.ts | 68 ++++++++------- .../sdk/src/cli/commands/executor/webhook.ts | 10 +-- packages/sdk/src/cli/commands/function/get.ts | 17 ++-- .../sdk/src/cli/commands/function/list.ts | 11 ++- .../sdk/src/cli/commands/function/logs.ts | 18 ++-- .../sdk/src/cli/commands/function/test-run.ts | 1 + .../sdk/src/cli/commands/generate/index.ts | 22 +++-- packages/sdk/src/cli/commands/init.ts | 22 +++-- packages/sdk/src/cli/commands/login.ts | 5 +- packages/sdk/src/cli/commands/logout.ts | 2 +- .../sdk/src/cli/commands/machineuser/list.ts | 10 +-- .../sdk/src/cli/commands/machineuser/token.ts | 18 ++-- .../sdk/src/cli/commands/oauth2client/get.ts | 16 ++-- .../sdk/src/cli/commands/oauth2client/list.ts | 10 +-- packages/sdk/src/cli/commands/open.ts | 8 +- .../commands/organization/folder/create.ts | 23 +++--- .../commands/organization/folder/delete.ts | 13 ++- .../cli/commands/organization/folder/get.ts | 11 ++- .../cli/commands/organization/folder/list.ts | 17 ++-- .../commands/organization/folder/update.ts | 19 ++--- .../sdk/src/cli/commands/organization/get.ts | 9 +- .../sdk/src/cli/commands/organization/list.ts | 14 ++-- .../sdk/src/cli/commands/organization/tree.ts | 24 +++--- .../src/cli/commands/organization/update.ts | 17 ++-- .../sdk/src/cli/commands/profile/create.ts | 56 ++++++------- .../sdk/src/cli/commands/profile/delete.ts | 14 ++-- packages/sdk/src/cli/commands/profile/list.ts | 2 +- .../sdk/src/cli/commands/profile/update.ts | 56 ++++++------- packages/sdk/src/cli/commands/remove.ts | 10 +-- .../sdk/src/cli/commands/secret/create.ts | 12 ++- .../sdk/src/cli/commands/secret/delete.ts | 12 ++- packages/sdk/src/cli/commands/secret/list.ts | 12 ++- .../sdk/src/cli/commands/secret/update.ts | 12 ++- .../src/cli/commands/secret/vault/create.ts | 10 +-- .../src/cli/commands/secret/vault/delete.ts | 12 ++- .../sdk/src/cli/commands/secret/vault/list.ts | 10 +-- packages/sdk/src/cli/commands/setup/index.ts | 80 +++++++++--------- packages/sdk/src/cli/commands/show.ts | 8 +- .../sdk/src/cli/commands/skills/install.ts | 22 +++-- .../src/cli/commands/staticwebsite/deploy.ts | 26 +++--- .../cli/commands/staticwebsite/domain/get.ts | 16 ++-- .../cli/commands/staticwebsite/domain/list.ts | 16 ++-- .../sdk/src/cli/commands/staticwebsite/get.ts | 16 ++-- .../src/cli/commands/staticwebsite/list.ts | 10 +-- .../src/cli/commands/tailordb/erd/deploy.ts | 18 ++-- .../src/cli/commands/tailordb/erd/export.ts | 29 +++---- .../src/cli/commands/tailordb/erd/serve.ts | 28 +++---- .../cli/commands/tailordb/migrate/generate.ts | 24 +++--- .../cli/commands/tailordb/migrate/script.ts | 24 +++--- .../src/cli/commands/tailordb/migrate/set.ts | 26 +++--- .../cli/commands/tailordb/migrate/status.ts | 16 ++-- .../src/cli/commands/tailordb/migrate/sync.ts | 27 +++--- .../sdk/src/cli/commands/tailordb/truncate.ts | 34 ++++---- .../sdk/src/cli/commands/upgrade/index.ts | 28 +++---- packages/sdk/src/cli/commands/user/current.ts | 2 +- packages/sdk/src/cli/commands/user/list.ts | 2 +- .../sdk/src/cli/commands/user/pat/create.ts | 22 +++-- .../sdk/src/cli/commands/user/pat/delete.ts | 14 ++-- .../sdk/src/cli/commands/user/pat/list.ts | 2 +- .../sdk/src/cli/commands/user/pat/update.ts | 22 +++-- packages/sdk/src/cli/commands/user/switch.ts | 14 ++-- .../src/cli/commands/workflow/executions.ts | 60 +++++++------- packages/sdk/src/cli/commands/workflow/get.ts | 10 +-- .../sdk/src/cli/commands/workflow/list.ts | 10 +-- .../sdk/src/cli/commands/workflow/resume.ts | 18 ++-- .../sdk/src/cli/commands/workflow/start.ts | 30 ++++--- .../sdk/src/cli/commands/workflow/wait.ts | 18 ++-- .../src/cli/commands/workspace/app/health.ts | 17 ++-- .../src/cli/commands/workspace/app/list.ts | 11 ++- .../sdk/src/cli/commands/workspace/create.ts | 75 +++++++++-------- .../sdk/src/cli/commands/workspace/delete.ts | 17 ++-- .../sdk/src/cli/commands/workspace/get.ts | 9 +- .../sdk/src/cli/commands/workspace/list.ts | 8 +- .../sdk/src/cli/commands/workspace/restore.ts | 17 ++-- .../src/cli/commands/workspace/user/invite.ts | 23 +++--- .../src/cli/commands/workspace/user/list.ts | 11 ++- .../src/cli/commands/workspace/user/remove.ts | 17 ++-- .../src/cli/commands/workspace/user/update.ts | 23 +++--- packages/sdk/src/cli/docs.test.ts | 1 + packages/sdk/src/cli/index.ts | 1 + packages/sdk/src/cli/query/index.ts | 6 +- packages/sdk/src/cli/shared/client.ts | 2 + packages/sdk/src/cli/shared/context.ts | 10 +++ packages/sdk/src/parser/app-config/schema.ts | 1 + .../sdk/src/parser/plugin-config/schema.ts | 3 +- .../src/parser/service/aigateway/schema.ts | 1 + .../parser/service/auth-connection/schema.ts | 2 + .../sdk/src/parser/service/auth/schema.ts | 20 +++++ .../sdk/src/parser/service/executor/schema.ts | 62 +++++++------- .../sdk/src/parser/service/field/schema.ts | 4 + packages/sdk/src/parser/service/idp/schema.ts | 12 ++- .../sdk/src/parser/service/resolver/schema.ts | 22 +++-- .../sdk/src/parser/service/secrets/schema.ts | 2 + .../parser/service/staticwebsite/schema.ts | 1 + .../sdk/src/parser/service/tailordb/schema.ts | 23 +++++- .../sdk/src/parser/service/workflow/schema.ts | 4 + 114 files changed, 979 insertions(+), 1046 deletions(-) diff --git a/packages/create-sdk/src/index.ts b/packages/create-sdk/src/index.ts index 6e9fbacde..53d9e73cb 100644 --- a/packages/create-sdk/src/index.ts +++ b/packages/create-sdk/src/index.ts @@ -15,6 +15,7 @@ const main = async () => { const cmd = defineCommand({ name: packageJson.name ?? "create-sdk", description: packageJson.description, + // strip unknown keys args: z.object({ name: arg(z.string().optional(), { positional: true, diff --git a/packages/sdk-codemod/src/index.ts b/packages/sdk-codemod/src/index.ts index 183a06c5a..1bb812122 100644 --- a/packages/sdk-codemod/src/index.ts +++ b/packages/sdk-codemod/src/index.ts @@ -25,7 +25,7 @@ interface RuleSummary { const listCommand = defineCommand({ name: "list", description: "List the available codemod rules (id, name, kind, version range).", - args: z.object({}).strict(), + args: z.strictObject({}), run: () => { const rules: RuleSummary[] = allCodemods.map((codemod) => ({ id: codemod.id, @@ -85,23 +85,21 @@ human-readable form, so \`stdout\` stays pure JSON for piping.`, desc: "Preview the changes and any LLM-review prompts without writing files", }, ], - args: z - .object({ - from: arg(z.string(), { - description: "Source SDK version (the version before upgrade)", - }), - to: arg(z.string(), { - description: "Target SDK version (the version after upgrade)", - }), - target: arg(z.string().default("."), { - description: "Project directory to transform", - }), - "dry-run": arg(z.boolean().default(false), { - alias: "d", - description: "Preview changes without modifying files", - }), - }) - .strict(), + args: z.strictObject({ + from: arg(z.string(), { + description: "Source SDK version (the version before upgrade)", + }), + to: arg(z.string(), { + description: "Target SDK version (the version after upgrade)", + }), + target: arg(z.string().default("."), { + description: "Project directory to transform", + }), + "dry-run": arg(z.boolean().default(false), { + alias: "d", + description: "Preview changes without modifying files", + }), + }), run: async (args) => { const targetPath = path.resolve(args.target); const dryRun = args["dry-run"]; diff --git a/packages/sdk/src/cli/cache/types.ts b/packages/sdk/src/cli/cache/types.ts index 2ecf7e603..4ff752b68 100644 --- a/packages/sdk/src/cli/cache/types.ts +++ b/packages/sdk/src/cli/cache/types.ts @@ -1,10 +1,12 @@ import { z } from "zod"; +// strip unknown keys const cacheOutputFileSchema = z.object({ outputPath: z.string(), contentHash: z.string(), }); +// strip unknown keys const cacheEntrySchema = z.object({ kind: z.literal("bundle"), inputHash: z.string(), @@ -13,6 +15,7 @@ const cacheEntrySchema = z.object({ createdAt: z.string(), }); +// strip unknown keys const cacheManifestSchema = z.object({ version: z.literal(1), sdkVersion: z.string(), diff --git a/packages/sdk/src/cli/commands/api/index.ts b/packages/sdk/src/cli/commands/api/index.ts index 4ddf57565..53fe2035a 100644 --- a/packages/sdk/src/cli/commands/api/index.ts +++ b/packages/sdk/src/cli/commands/api/index.ts @@ -216,36 +216,34 @@ Use \`--field key=value\` (repeatable) to set request body fields without writin list: listCommand, inspect: inspectCommand, }, - args: z - .object({ - ...workspaceArgs, - ...configArg, - body: arg(z.string().default("{}"), { - alias: "b", - description: "Request body as JSON.", - }), - field: arg(fieldArg.array().optional(), { - alias: "f", - description: - "Set a body field as `key=value` (repeatable; dotted keys nest). Overrides --body.", - completion: { - custom: { - expand: { - dependsOn: ["endpoint"], - enumerate: ({ endpoint }) => - enumerateAllFieldCompletions(extractMethodName(endpoint ?? "")), - }, + args: z.strictObject({ + ...workspaceArgs, + ...configArg, + body: arg(z.string().default("{}"), { + alias: "b", + description: "Request body as JSON.", + }), + field: arg(fieldArg.array().optional(), { + alias: "f", + description: + "Set a body field as `key=value` (repeatable; dotted keys nest). Overrides --body.", + completion: { + custom: { + expand: { + dependsOn: ["endpoint"], + enumerate: ({ endpoint }) => + enumerateAllFieldCompletions(extractMethodName(endpoint ?? "")), }, }, - }), - endpoint: arg(z.string(), { - positional: true, - description: - "API endpoint to call (e.g., 'GetApplication' or 'tailor.v1.OperatorService/GetApplication').", - completion: { custom: { choices: listMethodChoices() } }, - }), - }) - .strict(), + }, + }), + endpoint: arg(z.string(), { + positional: true, + description: + "API endpoint to call (e.g., 'GetApplication' or 'tailor.v1.OperatorService/GetApplication').", + completion: { custom: { choices: listMethodChoices() } }, + }), + }), run: async (args) => { // Direct API calls can target any OperatorService method, including // Create/Update/Delete. Block all of them under a readonly profile rather diff --git a/packages/sdk/src/cli/commands/api/inspect.ts b/packages/sdk/src/cli/commands/api/inspect.ts index 921be6070..76ebd3aea 100644 --- a/packages/sdk/src/cli/commands/api/inspect.ts +++ b/packages/sdk/src/cli/commands/api/inspect.ts @@ -18,16 +18,14 @@ export const inspectCommand = defineAppCommand({ desc: "Inspect a deeply nested input with `(oneof config)` annotations.", }, ], - args: z - .object({ - endpoint: arg(z.string(), { - positional: true, - description: - "API endpoint to inspect (e.g., 'GetApplication' or 'tailor.v1.OperatorService/GetApplication').", - completion: { custom: { choices: listMethodNames() } }, - }), - }) - .strict(), + args: z.strictObject({ + endpoint: arg(z.string(), { + positional: true, + description: + "API endpoint to inspect (e.g., 'GetApplication' or 'tailor.v1.OperatorService/GetApplication').", + completion: { custom: { choices: listMethodNames() } }, + }), + }), run: (args) => { const methodName = extractMethodName(args.endpoint); const method = getMethodDescriptor(methodName); diff --git a/packages/sdk/src/cli/commands/api/list.ts b/packages/sdk/src/cli/commands/api/list.ts index c4cc5c464..e8f370e30 100644 --- a/packages/sdk/src/cli/commands/api/list.ts +++ b/packages/sdk/src/cli/commands/api/list.ts @@ -8,7 +8,7 @@ export const listCommand = defineAppCommand({ description: "List all invocable OperatorService methods.", notes: "Only single-request (non-streaming) methods are listed, because the CLI issues a single JSON request and reads one JSON response.", - args: z.object({}).strict(), + args: z.strictObject({}), run: () => { const names = listMethodNames(); if (logger.jsonMode) { diff --git a/packages/sdk/src/cli/commands/authconnection/authorize.ts b/packages/sdk/src/cli/commands/authconnection/authorize.ts index a018b448c..c296091e1 100644 --- a/packages/sdk/src/cli/commands/authconnection/authorize.ts +++ b/packages/sdk/src/cli/commands/authconnection/authorize.ts @@ -38,27 +38,21 @@ function randomState() { export const authorizeAuthConnectionCommand = defineAppCommand({ name: "authorize", description: "Authorize an auth connection via OAuth2 flow.", - args: z - .object({ - ...workspaceArgs, - ...connectionNameArgs, - scopes: z - .string() - .optional() - .default(defaultScopes) - .describe("OAuth2 scopes to request (comma-separated)"), - port: z.coerce - .number() - .optional() - .default(defaultPort) - .describe("Local callback server port"), - "no-browser": z - .boolean() - .optional() - .default(false) - .describe("Don't open browser automatically"), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...connectionNameArgs, + scopes: z + .string() + .optional() + .default(defaultScopes) + .describe("OAuth2 scopes to request (comma-separated)"), + port: z.coerce.number().optional().default(defaultPort).describe("Local callback server port"), + "no-browser": z + .boolean() + .optional() + .default(false) + .describe("Don't open browser automatically"), + }), run: async (args) => { await assertWritable({ profile: args.profile }); const accessToken = await loadAccessToken({ diff --git a/packages/sdk/src/cli/commands/authconnection/delete.ts b/packages/sdk/src/cli/commands/authconnection/delete.ts index 67887408b..94245b842 100644 --- a/packages/sdk/src/cli/commands/authconnection/delete.ts +++ b/packages/sdk/src/cli/commands/authconnection/delete.ts @@ -12,13 +12,11 @@ import { connectionNameArgs } from "./args"; export const deleteAuthConnectionCommand = defineAppCommand({ name: "delete", description: "Delete an auth connection entirely.", - args: z - .object({ - ...workspaceArgs, - ...connectionNameArgs, - ...confirmationArgs, - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...connectionNameArgs, + ...confirmationArgs, + }), run: async (args) => { await assertWritable({ profile: args.profile }); const accessToken = await loadAccessToken({ diff --git a/packages/sdk/src/cli/commands/authconnection/list.ts b/packages/sdk/src/cli/commands/authconnection/list.ts index d260e2ae4..8495a6768 100644 --- a/packages/sdk/src/cli/commands/authconnection/list.ts +++ b/packages/sdk/src/cli/commands/authconnection/list.ts @@ -36,7 +36,7 @@ function connectionInfo(connection: AuthConnection): ConnectionInfo { export const listAuthConnectionCommand = defineAppCommand({ name: "list", description: "List all auth connections.", - args: z.object({ ...workspaceArgs, ...paginationArgs() }).strict(), + args: z.strictObject({ ...workspaceArgs, ...paginationArgs() }), run: async (args) => { const accessToken = await loadAccessToken({ profile: args.profile, diff --git a/packages/sdk/src/cli/commands/authconnection/open.ts b/packages/sdk/src/cli/commands/authconnection/open.ts index aabfed2d3..bc76adc85 100644 --- a/packages/sdk/src/cli/commands/authconnection/open.ts +++ b/packages/sdk/src/cli/commands/authconnection/open.ts @@ -10,7 +10,7 @@ const consoleBaseUrl = "https://console.tailor.tech"; export const openAuthConnectionCommand = defineAppCommand({ name: "open", description: "Open the auth connections page in the Tailor Platform Console.", - args: z.object({ ...workspaceArgs }).strict(), + args: z.strictObject({ ...workspaceArgs }), run: async (args) => { const workspaceId = await loadWorkspaceId({ workspaceId: args["workspace-id"], diff --git a/packages/sdk/src/cli/commands/authconnection/revoke.ts b/packages/sdk/src/cli/commands/authconnection/revoke.ts index 7d8486791..70213a6bd 100644 --- a/packages/sdk/src/cli/commands/authconnection/revoke.ts +++ b/packages/sdk/src/cli/commands/authconnection/revoke.ts @@ -15,13 +15,11 @@ export const revokeAuthConnectionCommand = defineAppCommand({ "Revoke an auth connection's tokens (keeps the connection; use 'delete' to remove it).", notes: "Revoke invalidates the connection's active session and tokens but keeps the connection and its stored credentials, so it can be re-authorized later. Use `delete` to remove the connection entirely.", - args: z - .object({ - ...workspaceArgs, - ...connectionNameArgs, - ...confirmationArgs, - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...connectionNameArgs, + ...confirmationArgs, + }), run: async (args) => { await assertWritable({ profile: args.profile }); const accessToken = await loadAccessToken({ diff --git a/packages/sdk/src/cli/commands/crashreport/list.ts b/packages/sdk/src/cli/commands/crashreport/list.ts index 3d7d0c03b..6d307efcb 100644 --- a/packages/sdk/src/cli/commands/crashreport/list.ts +++ b/packages/sdk/src/cli/commands/crashreport/list.ts @@ -26,11 +26,9 @@ function formatCrashReportFiles(files: string[], localDir: string) { export const listCommand = defineAppCommand({ name: "list", description: "List local crash report files.", - args: z - .object({ - ...paginationArgs(), - }) - .strict(), + args: z.strictObject({ + ...paginationArgs(), + }), run: async (args) => { const config = parseCrashReportConfig(); const jsonOutput = logger.jsonMode; diff --git a/packages/sdk/src/cli/commands/crashreport/send.ts b/packages/sdk/src/cli/commands/crashreport/send.ts index 44a08b112..18c1240ec 100644 --- a/packages/sdk/src/cli/commands/crashreport/send.ts +++ b/packages/sdk/src/cli/commands/crashreport/send.ts @@ -11,15 +11,13 @@ import type { CrashReport } from "#/cli/crashreport/report"; export const sendCommand = defineAppCommand({ name: "send", description: "Submit a crash report to help improve the SDK.", - args: z - .object({ - file: arg(z.string(), { - description: "Path to the crash report file", - required: true, - completion: { type: "file", extensions: ["log"] }, - }), - }) - .strict(), + args: z.strictObject({ + file: arg(z.string(), { + description: "Path to the crash report file", + required: true, + completion: { type: "file", extensions: ["log"] }, + }), + }), run: async (args) => { let content: string; try { diff --git a/packages/sdk/src/cli/commands/deploy/index.ts b/packages/sdk/src/cli/commands/deploy/index.ts index 212401612..24f80da0b 100644 --- a/packages/sdk/src/cli/commands/deploy/index.ts +++ b/packages/sdk/src/cli/commands/deploy/index.ts @@ -8,28 +8,26 @@ import { assertWritable } from "#/cli/shared/readonly-guard"; export const deployCommand = defineAppCommand({ name: "deploy", description: "Deploy your application by applying the Tailor configuration.", - args: z - .object({ - ...deploymentArgs, - ...confirmationArgs, - "dry-run": arg(z.boolean().optional(), { - alias: "d", - description: "Run the command without making any changes", - }), - "no-schema-check": arg(z.boolean().optional(), { - description: "Skip schema diff check against migration snapshots", - }), - "no-validate": arg(z.boolean().optional(), { - description: "Skip client-side validation against platform resource constraints", - }), - "no-cache": arg(z.boolean().optional(), { - description: "Disable bundle caching for this run", - }), - "clean-cache": arg(z.boolean().optional(), { - description: "Clean the bundle cache before building", - }), - }) - .strict(), + args: z.strictObject({ + ...deploymentArgs, + ...confirmationArgs, + "dry-run": arg(z.boolean().optional(), { + alias: "d", + description: "Run the command without making any changes", + }), + "no-schema-check": arg(z.boolean().optional(), { + description: "Skip schema diff check against migration snapshots", + }), + "no-validate": arg(z.boolean().optional(), { + description: "Skip client-side validation against platform resource constraints", + }), + "no-cache": arg(z.boolean().optional(), { + description: "Disable bundle caching for this run", + }), + "clean-cache": arg(z.boolean().optional(), { + description: "Clean the bundle cache before building", + }), + }), run: async (args) => { await assertWritable({ profile: args.profile }); const { initTelemetry } = await import("#/cli/telemetry/index"); diff --git a/packages/sdk/src/cli/commands/deploy/secrets-state.ts b/packages/sdk/src/cli/commands/deploy/secrets-state.ts index c1d18481d..d75db8883 100644 --- a/packages/sdk/src/cli/commands/deploy/secrets-state.ts +++ b/packages/sdk/src/cli/commands/deploy/secrets-state.ts @@ -4,6 +4,7 @@ import * as path from "pathe"; import { z } from "zod"; import { getDistDir } from "#/cli/shared/dist-dir"; +// strip unknown keys const SecretsStateSchema = z.object({ vaults: z.record(z.string(), z.record(z.string(), z.string())), connections: z.record(z.string(), z.string()).optional(), diff --git a/packages/sdk/src/cli/commands/executor/get.ts b/packages/sdk/src/cli/commands/executor/get.ts index 4cfddb9ac..f41894210 100644 --- a/packages/sdk/src/cli/commands/executor/get.ts +++ b/packages/sdk/src/cli/commands/executor/get.ts @@ -93,12 +93,10 @@ export async function getExecutor( export const getCommand = defineAppCommand({ name: "get", description: "Get executor details", - args: z - .object({ - ...workspaceArgs, - ...nameArgs, - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...nameArgs, + }), run: async (args) => { const executor = await getExecutor({ name: args.name, diff --git a/packages/sdk/src/cli/commands/executor/jobs.ts b/packages/sdk/src/cli/commands/executor/jobs.ts index ca75add48..bc33af85c 100644 --- a/packages/sdk/src/cli/commands/executor/jobs.ts +++ b/packages/sdk/src/cli/commands/executor/jobs.ts @@ -739,48 +739,46 @@ export const jobsCommand = defineAppCommand({ desc: "Wait for job with logs", }, ], - args: z - .object({ - ...workspaceArgs, - "executor-name": arg(z.string(), { - positional: true, - description: "Executor name", - }), - "job-id": arg(z.string().optional(), { - positional: true, - description: "Job ID (if provided, shows job details)", - }), - status: arg(z.string().optional(), { - alias: "s", - description: - "Filter by status (PENDING, RUNNING, SUCCESS, FAILED, CANCELED) (list mode only)", - }), - attempts: arg(z.boolean().default(false), { - description: "Show job attempts (only with job ID) (detail mode only)", - }), - wait: arg(z.boolean().default(false), { - alias: "W", - description: - "Wait for job completion and downstream execution (workflow/function) if applicable (detail mode only)", - }), - interval: arg(durationArg.default("3s"), { - alias: "i", - description: "Polling interval when using --wait (e.g., '3s', '500ms', '1m')", - }), - timeout: arg(durationArg.default("5m"), { - alias: "t", - description: "Maximum time to wait when using --wait (e.g., '30s', '5m')", - }), - ...pagedLogArgs, - limit: arg(nonNegativeIntArg.default(50), { - description: "Maximum number of jobs to list (0: unlimited, default: 50) (list mode only)", - }), - logs: arg(z.boolean().default(false), { - alias: "l", - description: "Display function execution logs after completion (requires --wait)", - }), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + "executor-name": arg(z.string(), { + positional: true, + description: "Executor name", + }), + "job-id": arg(z.string().optional(), { + positional: true, + description: "Job ID (if provided, shows job details)", + }), + status: arg(z.string().optional(), { + alias: "s", + description: + "Filter by status (PENDING, RUNNING, SUCCESS, FAILED, CANCELED) (list mode only)", + }), + attempts: arg(z.boolean().default(false), { + description: "Show job attempts (only with job ID) (detail mode only)", + }), + wait: arg(z.boolean().default(false), { + alias: "W", + description: + "Wait for job completion and downstream execution (workflow/function) if applicable (detail mode only)", + }), + interval: arg(durationArg.default("3s"), { + alias: "i", + description: "Polling interval when using --wait (e.g., '3s', '500ms', '1m')", + }), + timeout: arg(durationArg.default("5m"), { + alias: "t", + description: "Maximum time to wait when using --wait (e.g., '30s', '5m')", + }), + ...pagedLogArgs, + limit: arg(nonNegativeIntArg.default(50), { + description: "Maximum number of jobs to list (0: unlimited, default: 50) (list mode only)", + }), + logs: arg(z.boolean().default(false), { + alias: "l", + description: "Display function execution logs after completion (requires --wait)", + }), + }), run: async (args) => { const jsonOutput = logger.jsonMode || args.json; if (args.jobId) { diff --git a/packages/sdk/src/cli/commands/executor/list.ts b/packages/sdk/src/cli/commands/executor/list.ts index 3a7ba1537..b6d96f985 100644 --- a/packages/sdk/src/cli/commands/executor/list.ts +++ b/packages/sdk/src/cli/commands/executor/list.ts @@ -48,12 +48,10 @@ export async function listExecutors(options?: ListExecutorsOptions): Promise { const jsonOutput = logger.jsonMode; const executors = await listExecutors({ diff --git a/packages/sdk/src/cli/commands/executor/trigger.ts b/packages/sdk/src/cli/commands/executor/trigger.ts index ec36913f3..deecd3aa2 100644 --- a/packages/sdk/src/cli/commands/executor/trigger.ts +++ b/packages/sdk/src/cli/commands/executor/trigger.ts @@ -184,41 +184,39 @@ The \`--logs\` option displays logs from the downstream execution when available { cmd: "my-executor -W", desc: "Trigger and wait for completion" }, { cmd: "my-executor -W -l", desc: "Trigger, wait, and show logs" }, ], - args: z - .object({ - ...workspaceArgs, - "executor-name": arg(z.string(), { - positional: true, - description: "Executor name", - }), - data: arg(jsonDataArg.optional(), { - alias: "d", - description: "Request body (JSON string)", - }), - header: arg(headerArg.array().optional(), { - alias: "H", - overrideBuiltinAlias: true, - description: "Request header (format: 'Key: Value', can be specified multiple times)", - }), - wait: arg(z.boolean().default(false), { - alias: "W", - description: - "Wait for job completion and downstream execution (workflow/function) if applicable", - }), - interval: arg(durationArg.default("3s"), { - alias: "i", - description: "Polling interval when using --wait (e.g., '3s', '500ms', '1m')", - }), - timeout: arg(durationArg.default("5m"), { - alias: "t", - description: "Maximum time to wait when using --wait (e.g., '30s', '5m')", - }), - logs: arg(z.boolean().default(false), { - alias: "l", - description: "Display function execution logs after completion (requires --wait)", - }), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + "executor-name": arg(z.string(), { + positional: true, + description: "Executor name", + }), + data: arg(jsonDataArg.optional(), { + alias: "d", + description: "Request body (JSON string)", + }), + header: arg(headerArg.array().optional(), { + alias: "H", + overrideBuiltinAlias: true, + description: "Request header (format: 'Key: Value', can be specified multiple times)", + }), + wait: arg(z.boolean().default(false), { + alias: "W", + description: + "Wait for job completion and downstream execution (workflow/function) if applicable", + }), + interval: arg(durationArg.default("3s"), { + alias: "i", + description: "Polling interval when using --wait (e.g., '3s', '500ms', '1m')", + }), + timeout: arg(durationArg.default("5m"), { + alias: "t", + description: "Maximum time to wait when using --wait (e.g., '30s', '5m')", + }), + logs: arg(z.boolean().default(false), { + alias: "l", + description: "Display function execution logs after completion (requires --wait)", + }), + }), run: async (args) => { const jsonOutput = logger.jsonMode || args.json; await assertWritable({ profile: args.profile }); diff --git a/packages/sdk/src/cli/commands/executor/webhook.ts b/packages/sdk/src/cli/commands/executor/webhook.ts index c65fb42e8..0fcf0e627 100644 --- a/packages/sdk/src/cli/commands/executor/webhook.ts +++ b/packages/sdk/src/cli/commands/executor/webhook.ts @@ -60,12 +60,10 @@ export async function listWebhookExecutors( const listWebhookCommand = defineAppCommand({ name: "list", description: "List executors with incoming webhook triggers", - args: z - .object({ - ...workspaceArgs, - ...paginationArgs(), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...paginationArgs(), + }), run: async (args) => { const jsonOutput = logger.jsonMode; const executors = await listWebhookExecutors({ diff --git a/packages/sdk/src/cli/commands/function/get.ts b/packages/sdk/src/cli/commands/function/get.ts index ae9675313..8a24114e9 100644 --- a/packages/sdk/src/cli/commands/function/get.ts +++ b/packages/sdk/src/cli/commands/function/get.ts @@ -10,6 +10,7 @@ import { logger } from "#/cli/shared/logger"; import { assertDefined } from "#/utils/assert"; import { functionRegistryInfo, type FunctionRegistryInfo } from "./transform"; +// strip unknown keys const getFunctionRegistryOptionsSchema = z.object({ workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), profile: z.string().optional(), @@ -71,15 +72,13 @@ export async function getFunctionRegistry( export const getCommand = defineAppCommand({ name: "get", description: "Get a function registry by name", - args: z - .object({ - ...workspaceArgs, - name: arg(z.string(), { - description: "Function name", - alias: "n", - }), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + name: arg(z.string(), { + description: "Function name", + alias: "n", + }), + }), run: async (args) => { const fn = await getFunctionRegistry({ workspaceId: args["workspace-id"], diff --git a/packages/sdk/src/cli/commands/function/list.ts b/packages/sdk/src/cli/commands/function/list.ts index a6a2896b6..789b50ba9 100644 --- a/packages/sdk/src/cli/commands/function/list.ts +++ b/packages/sdk/src/cli/commands/function/list.ts @@ -9,6 +9,7 @@ import { logger } from "#/cli/shared/logger"; import { assertDefined } from "#/utils/assert"; import { functionRegistryInfo, type FunctionRegistryInfo } from "./transform"; +// strip unknown keys const listFunctionRegistriesOptionsSchema = z.object({ workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), profile: z.string().optional(), @@ -77,12 +78,10 @@ export async function listFunctionRegistries( export const listCommand = defineAppCommand({ name: "list", description: "List function registries in a workspace", - args: z - .object({ - ...workspaceArgs, - ...paginationArgs(), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...paginationArgs(), + }), run: async (args) => { const jsonOutput = logger.jsonMode; const registries = await listFunctionRegistries({ diff --git a/packages/sdk/src/cli/commands/function/logs.ts b/packages/sdk/src/cli/commands/function/logs.ts index 66a2705e6..bacf904e3 100644 --- a/packages/sdk/src/cli/commands/function/logs.ts +++ b/packages/sdk/src/cli/commands/function/logs.ts @@ -295,16 +295,14 @@ Stack traces are mapped only when the execution includes a content hash for the desc: "Get execution details as JSON", }, ], - args: z - .object({ - ...workspaceArgs, - ...pagedLogArgs, - "execution-id": arg(z.string().optional(), { - positional: true, - description: "Execution ID (if provided, shows details with logs)", - }), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...pagedLogArgs, + "execution-id": arg(z.string().optional(), { + positional: true, + description: "Execution ID (if provided, shows details with logs)", + }), + }), run: async (args) => { const accessToken = await loadAccessToken({ profile: args.profile, diff --git a/packages/sdk/src/cli/commands/function/test-run.ts b/packages/sdk/src/cli/commands/function/test-run.ts index 8c1201271..3cc8d875a 100644 --- a/packages/sdk/src/cli/commands/function/test-run.ts +++ b/packages/sdk/src/cli/commands/function/test-run.ts @@ -27,6 +27,7 @@ import type { Jsonifiable } from "type-fest"; export const testRunCommand = defineAppCommand({ name: "test-run", description: "Run a function on the Tailor Platform server without deploying.", + // strip unknown keys args: z.object({ ...workspaceArgs, file: arg(z.string(), { diff --git a/packages/sdk/src/cli/commands/generate/index.ts b/packages/sdk/src/cli/commands/generate/index.ts index a3a44ecdb..358a711af 100644 --- a/packages/sdk/src/cli/commands/generate/index.ts +++ b/packages/sdk/src/cli/commands/generate/index.ts @@ -6,18 +6,16 @@ import { defineAppCommand } from "#/cli/shared/command"; export const generateCommand = defineAppCommand({ name: "generate", description: "Generate files using Tailor configuration.", - args: z - .object({ - config: arg(z.string().default("tailor.config.ts"), { - alias: "c", - description: "Path to SDK config file", - }), - watch: arg(z.boolean().default(false), { - alias: "W", - description: "Watch for type/resolver changes and regenerate", - }), - }) - .strict(), + args: z.strictObject({ + config: arg(z.string().default("tailor.config.ts"), { + alias: "c", + description: "Path to SDK config file", + }), + watch: arg(z.boolean().default(false), { + alias: "W", + description: "Watch for type/resolver changes and regenerate", + }), + }), run: async (args) => { const { initTelemetry } = await import("#/cli/telemetry/index"); await initTelemetry(); diff --git a/packages/sdk/src/cli/commands/init.ts b/packages/sdk/src/cli/commands/init.ts index b5c8edd68..ac278d535 100644 --- a/packages/sdk/src/cli/commands/init.ts +++ b/packages/sdk/src/cli/commands/init.ts @@ -17,18 +17,16 @@ const detectPackageManager = () => { export const initCommand = defineAppCommand({ name: "init", description: "Initialize a new project using create-sdk.", - args: z - .object({ - name: arg(z.string().optional(), { - positional: true, - description: "Project name", - }), - template: arg(z.string().optional(), { - alias: "t", - description: "Template name", - }), - }) - .strict(), + args: z.strictObject({ + name: arg(z.string().optional(), { + positional: true, + description: "Project name", + }), + template: arg(z.string().optional(), { + alias: "t", + description: "Template name", + }), + }), run: async (args) => { const packageJson = await readPackageJson(); const version = diff --git a/packages/sdk/src/cli/commands/login.ts b/packages/sdk/src/cli/commands/login.ts index 9a644819f..6f9a78de9 100644 --- a/packages/sdk/src/cli/commands/login.ts +++ b/packages/sdk/src/cli/commands/login.ts @@ -136,9 +136,9 @@ export const loginCommand = defineAppCommand({ name: "login", description: "Login to Tailor Platform.", args: z.xor([ - z.object({}).strict().describe("User Login"), + z.strictObject({}).describe("User Login"), z - .object({ + .strictObject({ "machine-user": arg(z.literal(true), { description: "Login as a platform machine user.", required: true, @@ -153,7 +153,6 @@ export const loginCommand = defineAppCommand({ env: "TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET", }), }) - .strict() .describe("Machine User Login"), ]), run: async (args) => { diff --git a/packages/sdk/src/cli/commands/logout.ts b/packages/sdk/src/cli/commands/logout.ts index 5df996246..bd3c2e85f 100644 --- a/packages/sdk/src/cli/commands/logout.ts +++ b/packages/sdk/src/cli/commands/logout.ts @@ -12,7 +12,7 @@ import { logger } from "#/cli/shared/logger"; export const logoutCommand = defineAppCommand({ name: "logout", description: "Logout from Tailor Platform.", - args: z.object({}).strict(), + args: z.strictObject({}), run: async () => { const pfConfig = await readPlatformConfig(); const currentUser = pfConfig.current_user; diff --git a/packages/sdk/src/cli/commands/machineuser/list.ts b/packages/sdk/src/cli/commands/machineuser/list.ts index d1a4373be..df1ac09d5 100644 --- a/packages/sdk/src/cli/commands/machineuser/list.ts +++ b/packages/sdk/src/cli/commands/machineuser/list.ts @@ -93,12 +93,10 @@ export async function listMachineUsers( export const listCommand = defineAppCommand({ name: "list", description: "List all machine users in the application.", - args: z - .object({ - ...deploymentArgs, - ...paginationArgs(), - }) - .strict(), + args: z.strictObject({ + ...deploymentArgs, + ...paginationArgs(), + }), run: async (args) => { // Execute machineuser list logic const machineUsers = await listMachineUsers({ diff --git a/packages/sdk/src/cli/commands/machineuser/token.ts b/packages/sdk/src/cli/commands/machineuser/token.ts index 1b77bd50b..933587f17 100644 --- a/packages/sdk/src/cli/commands/machineuser/token.ts +++ b/packages/sdk/src/cli/commands/machineuser/token.ts @@ -84,16 +84,14 @@ export async function getMachineUserToken( export const tokenCommand = defineAppCommand({ name: "token", description: "Get an access token for a machine user.", - args: z - .object({ - ...deploymentArgs, - name: arg(z.string().optional(), { - positional: true, - description: "Machine user name. Falls back to the active profile's default machine user.", - env: "TAILOR_PLATFORM_MACHINE_USER_NAME", - }), - }) - .strict(), + args: z.strictObject({ + ...deploymentArgs, + name: arg(z.string().optional(), { + positional: true, + description: "Machine user name. Falls back to the active profile's default machine user.", + env: "TAILOR_PLATFORM_MACHINE_USER_NAME", + }), + }), run: async (args) => { // Execute machineuser token logic const token = await getMachineUserToken({ diff --git a/packages/sdk/src/cli/commands/oauth2client/get.ts b/packages/sdk/src/cli/commands/oauth2client/get.ts index 013bbb56c..e53e63aec 100644 --- a/packages/sdk/src/cli/commands/oauth2client/get.ts +++ b/packages/sdk/src/cli/commands/oauth2client/get.ts @@ -64,15 +64,13 @@ export async function getOAuth2Client( export const getCommand = defineAppCommand({ name: "get", description: "Get OAuth2 client credentials (including client secret).", - args: z - .object({ - ...deploymentArgs, - name: arg(z.string(), { - positional: true, - description: "OAuth2 client name", - }), - }) - .strict(), + args: z.strictObject({ + ...deploymentArgs, + name: arg(z.string(), { + positional: true, + description: "OAuth2 client name", + }), + }), run: async (args) => { const credentials = await getOAuth2Client({ name: args.name, diff --git a/packages/sdk/src/cli/commands/oauth2client/list.ts b/packages/sdk/src/cli/commands/oauth2client/list.ts index 3cab7deb9..a78e65dac 100644 --- a/packages/sdk/src/cli/commands/oauth2client/list.ts +++ b/packages/sdk/src/cli/commands/oauth2client/list.ts @@ -62,12 +62,10 @@ export async function listOAuth2Clients( export const listCommand = defineAppCommand({ name: "list", description: "List all OAuth2 clients in the application.", - args: z - .object({ - ...deploymentArgs, - ...paginationArgs(), - }) - .strict(), + args: z.strictObject({ + ...deploymentArgs, + ...paginationArgs(), + }), run: async (args) => { const oauth2Clients = await listOAuth2Clients({ workspaceId: args["workspace-id"], diff --git a/packages/sdk/src/cli/commands/open.ts b/packages/sdk/src/cli/commands/open.ts index ecf5a86a2..2c53eca5a 100644 --- a/packages/sdk/src/cli/commands/open.ts +++ b/packages/sdk/src/cli/commands/open.ts @@ -11,11 +11,9 @@ const consoleBaseUrl = "https://console.tailor.tech"; export const openCommand = defineAppCommand({ name: "open", description: "Open Tailor Platform Console.", - args: z - .object({ - ...deploymentArgs, - }) - .strict(), + args: z.strictObject({ + ...deploymentArgs, + }), run: async (args) => { const workspaceId = await loadWorkspaceId({ workspaceId: args["workspace-id"], diff --git a/packages/sdk/src/cli/commands/organization/folder/create.ts b/packages/sdk/src/cli/commands/organization/folder/create.ts index 1aa96544c..024081763 100644 --- a/packages/sdk/src/cli/commands/organization/folder/create.ts +++ b/packages/sdk/src/cli/commands/organization/folder/create.ts @@ -9,6 +9,7 @@ import { assertWritable } from "#/cli/shared/readonly-guard"; import { assertDefined } from "#/utils/assert"; import { folderInfo, type FolderInfo } from "../transform"; +// strip unknown keys const createFolderOptionsSchema = z.object({ organizationId: z.uuid({ message: "organization-id must be a valid UUID" }), parentFolderId: z.string().optional(), @@ -47,18 +48,16 @@ export async function createFolder(options: CreateFolderOptions): Promise { await assertWritable(); const folder = await createFolder({ diff --git a/packages/sdk/src/cli/commands/organization/folder/delete.ts b/packages/sdk/src/cli/commands/organization/folder/delete.ts index daf3eeddf..08cd3a63e 100644 --- a/packages/sdk/src/cli/commands/organization/folder/delete.ts +++ b/packages/sdk/src/cli/commands/organization/folder/delete.ts @@ -8,6 +8,7 @@ import { prompt } from "#/cli/shared/prompt"; import { assertWritable } from "#/cli/shared/readonly-guard"; import { assertDefined } from "#/utils/assert"; +// strip unknown keys const deleteFolderOptionsSchema = z.object({ organizationId: z.uuid({ message: "organization-id must be a valid UUID" }), folderId: z.uuid({ message: "folder-id must be a valid UUID" }), @@ -38,13 +39,11 @@ export async function deleteFolder(options: DeleteFolderOptions): Promise export const deleteCommand = defineAppCommand({ name: "delete", description: "Delete a folder from an organization.", - args: z - .object({ - ...organizationArgs, - ...folderArgs, - ...confirmationArgs, - }) - .strict(), + args: z.strictObject({ + ...organizationArgs, + ...folderArgs, + ...confirmationArgs, + }), run: async (args) => { await assertWritable(); const accessToken = await loadAccessToken(); diff --git a/packages/sdk/src/cli/commands/organization/folder/get.ts b/packages/sdk/src/cli/commands/organization/folder/get.ts index 86e9b2998..f813606c0 100644 --- a/packages/sdk/src/cli/commands/organization/folder/get.ts +++ b/packages/sdk/src/cli/commands/organization/folder/get.ts @@ -8,6 +8,7 @@ import { logger } from "#/cli/shared/logger"; import { assertDefined } from "#/utils/assert"; import { folderInfo, type FolderInfo } from "../transform"; +// strip unknown keys const getFolderOptionsSchema = z.object({ organizationId: z.uuid({ message: "organization-id must be a valid UUID" }), folderId: z.uuid({ message: "folder-id must be a valid UUID" }), @@ -44,12 +45,10 @@ export async function getFolder(options: GetFolderOptions): Promise export const getCommand = defineAppCommand({ name: "get", description: "Show detailed information about a folder.", - args: z - .object({ - ...organizationArgs, - ...folderArgs, - }) - .strict(), + args: z.strictObject({ + ...organizationArgs, + ...folderArgs, + }), run: async (args) => { const folder = await getFolder({ organizationId: args["organization-id"], diff --git a/packages/sdk/src/cli/commands/organization/folder/list.ts b/packages/sdk/src/cli/commands/organization/folder/list.ts index fe35ab345..59f9496aa 100644 --- a/packages/sdk/src/cli/commands/organization/folder/list.ts +++ b/packages/sdk/src/cli/commands/organization/folder/list.ts @@ -8,6 +8,7 @@ import { logger } from "#/cli/shared/logger"; import { assertDefined } from "#/utils/assert"; import { folderListInfo, type FolderListInfo } from "../transform"; +// strip unknown keys const listFoldersOptionsSchema = z.object({ organizationId: z.uuid({ message: "organization-id must be a valid UUID" }), parentFolderId: z.string().optional(), @@ -54,15 +55,13 @@ export async function listFolders(options: ListFoldersOptions): Promise { const folders = await listFolders({ organizationId: args["organization-id"], diff --git a/packages/sdk/src/cli/commands/organization/folder/update.ts b/packages/sdk/src/cli/commands/organization/folder/update.ts index ee6108550..e147fed60 100644 --- a/packages/sdk/src/cli/commands/organization/folder/update.ts +++ b/packages/sdk/src/cli/commands/organization/folder/update.ts @@ -9,6 +9,7 @@ import { assertWritable } from "#/cli/shared/readonly-guard"; import { assertDefined } from "#/utils/assert"; import { folderInfo, type FolderInfo } from "../transform"; +// strip unknown keys const updateFolderOptionsSchema = z.object({ organizationId: z.uuid({ message: "organization-id must be a valid UUID" }), folderId: z.uuid({ message: "folder-id must be a valid UUID" }), @@ -47,16 +48,14 @@ export async function updateFolder(options: UpdateFolderOptions): Promise { await assertWritable(); const folder = await updateFolder({ diff --git a/packages/sdk/src/cli/commands/organization/get.ts b/packages/sdk/src/cli/commands/organization/get.ts index b57062ea2..9ec49da59 100644 --- a/packages/sdk/src/cli/commands/organization/get.ts +++ b/packages/sdk/src/cli/commands/organization/get.ts @@ -8,6 +8,7 @@ import { logger } from "#/cli/shared/logger"; import { assertDefined } from "#/utils/assert"; import { organizationInfo, type OrganizationInfo } from "./transform"; +// strip unknown keys const getOrganizationOptionsSchema = z.object({ organizationId: z.uuid({ message: "organization-id must be a valid UUID" }), }); @@ -42,11 +43,9 @@ export async function getOrganization(options: GetOrganizationOptions): Promise< export const getCommand = defineAppCommand({ name: "get", description: "Show detailed information about an organization.", - args: z - .object({ - ...organizationArgs, - }) - .strict(), + args: z.strictObject({ + ...organizationArgs, + }), run: async (args) => { const organization = await getOrganization({ organizationId: args["organization-id"], diff --git a/packages/sdk/src/cli/commands/organization/list.ts b/packages/sdk/src/cli/commands/organization/list.ts index 5f780f90e..4ef3e3832 100644 --- a/packages/sdk/src/cli/commands/organization/list.ts +++ b/packages/sdk/src/cli/commands/organization/list.ts @@ -35,14 +35,12 @@ export async function listOrganizations( export const listCommand = defineAppCommand({ name: "list", description: "List organizations you belong to.", - args: z - .object({ - limit: arg(positiveIntArg.optional(), { - alias: "l", - description: "Maximum number of organizations to list", - }), - }) - .strict(), + args: z.strictObject({ + limit: arg(positiveIntArg.optional(), { + alias: "l", + description: "Maximum number of organizations to list", + }), + }), run: async (args) => { const organizations = await listOrganizations({ limit: args.limit }); logger.out(organizations); diff --git a/packages/sdk/src/cli/commands/organization/tree.ts b/packages/sdk/src/cli/commands/organization/tree.ts index 5c0f931b9..f6d13f658 100644 --- a/packages/sdk/src/cli/commands/organization/tree.ts +++ b/packages/sdk/src/cli/commands/organization/tree.ts @@ -162,19 +162,17 @@ export async function organizationTree( export const treeCommand = defineAppCommand({ name: "tree", description: "Display organization folder hierarchy as a tree.", - args: z - .object({ - "organization-id": arg(z.string().optional(), { - alias: "o", - description: "Organization ID (show all if omitted)", - env: "TAILOR_PLATFORM_ORGANIZATION_ID", - }), - depth: arg(positiveIntArg.optional(), { - alias: "d", - description: "Maximum folder depth to display", - }), - }) - .strict(), + args: z.strictObject({ + "organization-id": arg(z.string().optional(), { + alias: "o", + description: "Organization ID (show all if omitted)", + env: "TAILOR_PLATFORM_ORGANIZATION_ID", + }), + depth: arg(positiveIntArg.optional(), { + alias: "d", + description: "Maximum folder depth to display", + }), + }), run: async (args) => { const accessToken = await loadAccessToken(); const client = await initOperatorClient(accessToken); diff --git a/packages/sdk/src/cli/commands/organization/update.ts b/packages/sdk/src/cli/commands/organization/update.ts index b679313d9..7a9a66692 100644 --- a/packages/sdk/src/cli/commands/organization/update.ts +++ b/packages/sdk/src/cli/commands/organization/update.ts @@ -9,6 +9,7 @@ import { assertWritable } from "#/cli/shared/readonly-guard"; import { assertDefined } from "#/utils/assert"; import { organizationInfo, type OrganizationInfo } from "./transform"; +// strip unknown keys const updateOrganizationOptionsSchema = z.object({ organizationId: z.uuid({ message: "organization-id must be a valid UUID" }), name: z.string().min(1, "Name must not be empty"), @@ -47,15 +48,13 @@ export async function updateOrganization( export const updateCommand = defineAppCommand({ name: "update", description: "Update an organization's name.", - args: z - .object({ - ...organizationArgs, - name: arg(z.string(), { - alias: "n", - description: "New organization name", - }), - }) - .strict(), + args: z.strictObject({ + ...organizationArgs, + name: arg(z.string(), { + alias: "n", + description: "New organization name", + }), + }), run: async (args) => { await assertWritable(); const organization = await updateOrganization({ diff --git a/packages/sdk/src/cli/commands/profile/create.ts b/packages/sdk/src/cli/commands/profile/create.ts index db911e2c7..15eea5415 100644 --- a/packages/sdk/src/cli/commands/profile/create.ts +++ b/packages/sdk/src/cli/commands/profile/create.ts @@ -9,35 +9,33 @@ import type { ProfileInfo } from "./types"; export const createCommand = defineAppCommand({ name: "create", description: "Create a new profile.", - args: z - .object({ - name: arg(z.string(), { - positional: true, - description: "Profile name", - }), - user: arg(z.string(), { - alias: "u", - description: "User email address or machine user client ID", - }), - "workspace-id": arg(z.string(), { - alias: "w", - description: "Workspace ID", - }), - permission: arg(z.enum(["write", "read"]).default("write"), { - description: - "Profile permission. 'read' blocks all write commands while the profile is active.", - }), - "machine-user": arg(z.string().optional(), { - alias: "m", - description: - "Default machine user name for application-data commands (query, workflow start, function test-run, machineuser token).", - }), - "machine-user-override": arg(z.enum(["allow", "deny"]).optional(), { - description: - "Whether the command line or TAILOR_PLATFORM_MACHINE_USER_NAME may override the profile's machine user. 'deny' requires --machine-user.", - }), - }) - .strict(), + args: z.strictObject({ + name: arg(z.string(), { + positional: true, + description: "Profile name", + }), + user: arg(z.string(), { + alias: "u", + description: "User email address or machine user client ID", + }), + "workspace-id": arg(z.string(), { + alias: "w", + description: "Workspace ID", + }), + permission: arg(z.enum(["write", "read"]).default("write"), { + description: + "Profile permission. 'read' blocks all write commands while the profile is active.", + }), + "machine-user": arg(z.string().optional(), { + alias: "m", + description: + "Default machine user name for application-data commands (query, workflow start, function test-run, machineuser token).", + }), + "machine-user-override": arg(z.enum(["allow", "deny"]).optional(), { + description: + "Whether the command line or TAILOR_PLATFORM_MACHINE_USER_NAME may override the profile's machine user. 'deny' requires --machine-user.", + }), + }), run: async (args) => { if (args["machine-user-override"] === "deny" && !args["machine-user"]) { throw new Error("--machine-user-override deny requires --machine-user."); diff --git a/packages/sdk/src/cli/commands/profile/delete.ts b/packages/sdk/src/cli/commands/profile/delete.ts index 252786e9e..f7d70ea25 100644 --- a/packages/sdk/src/cli/commands/profile/delete.ts +++ b/packages/sdk/src/cli/commands/profile/delete.ts @@ -7,14 +7,12 @@ import { logger } from "#/cli/shared/logger"; export const deleteCommand = defineAppCommand({ name: "delete", description: "Delete a profile.", - args: z - .object({ - name: arg(z.string(), { - positional: true, - description: "Profile name", - }), - }) - .strict(), + args: z.strictObject({ + name: arg(z.string(), { + positional: true, + description: "Profile name", + }), + }), run: async (args) => { const config = await readPlatformConfig(); diff --git a/packages/sdk/src/cli/commands/profile/list.ts b/packages/sdk/src/cli/commands/profile/list.ts index 8924a8709..69dd46740 100644 --- a/packages/sdk/src/cli/commands/profile/list.ts +++ b/packages/sdk/src/cli/commands/profile/list.ts @@ -9,7 +9,7 @@ import type { ProfileInfo } from "./types"; export const listCommand = defineAppCommand({ name: "list", description: "List all profiles.", - args: z.object({}).strict(), + args: z.strictObject({}), run: async () => { const config = await readPlatformConfig(); const jsonOutput = logger.jsonMode; diff --git a/packages/sdk/src/cli/commands/profile/update.ts b/packages/sdk/src/cli/commands/profile/update.ts index bcd91dd0c..a0d50544e 100644 --- a/packages/sdk/src/cli/commands/profile/update.ts +++ b/packages/sdk/src/cli/commands/profile/update.ts @@ -9,35 +9,33 @@ import type { ProfileInfo } from "./types"; export const updateCommand = defineAppCommand({ name: "update", description: "Update profile properties.", - args: z - .object({ - name: arg(z.string(), { - positional: true, - description: "Profile name", - }), - user: arg(z.string().optional(), { - alias: "u", - description: "New user email address or machine user client ID", - }), - "workspace-id": arg(z.string().optional(), { - alias: "w", - description: "New workspace ID", - }), - permission: arg(z.enum(["write", "read"]).optional(), { - description: - "Profile permission. 'read' blocks all write commands; 'write' lifts the restriction.", - }), - "machine-user": arg(z.string().optional(), { - alias: "m", - description: - "Default machine user name for application-data commands (query, workflow start, function test-run, machineuser token). Pass an empty string to clear.", - }), - "machine-user-override": arg(z.enum(["allow", "deny"]).optional(), { - description: - "Whether the command line or TAILOR_PLATFORM_MACHINE_USER_NAME may override the profile's machine user. 'deny' requires --machine-user; 'allow' lifts the restriction.", - }), - }) - .strict(), + args: z.strictObject({ + name: arg(z.string(), { + positional: true, + description: "Profile name", + }), + user: arg(z.string().optional(), { + alias: "u", + description: "New user email address or machine user client ID", + }), + "workspace-id": arg(z.string().optional(), { + alias: "w", + description: "New workspace ID", + }), + permission: arg(z.enum(["write", "read"]).optional(), { + description: + "Profile permission. 'read' blocks all write commands; 'write' lifts the restriction.", + }), + "machine-user": arg(z.string().optional(), { + alias: "m", + description: + "Default machine user name for application-data commands (query, workflow start, function test-run, machineuser token). Pass an empty string to clear.", + }), + "machine-user-override": arg(z.enum(["allow", "deny"]).optional(), { + description: + "Whether the command line or TAILOR_PLATFORM_MACHINE_USER_NAME may override the profile's machine user. 'deny' requires --machine-user; 'allow' lifts the restriction.", + }), + }), run: async (args) => { const config = await readPlatformConfig(); diff --git a/packages/sdk/src/cli/commands/remove.ts b/packages/sdk/src/cli/commands/remove.ts index c5bb981f4..9a5658d8e 100644 --- a/packages/sdk/src/cli/commands/remove.ts +++ b/packages/sdk/src/cli/commands/remove.ts @@ -170,12 +170,10 @@ export async function remove(options?: RemoveOptions): Promise { export const removeCommand = defineAppCommand({ name: "remove", description: "Remove all resources managed by the application from the workspace.", - args: z - .object({ - ...deploymentArgs, - ...confirmationArgs, - }) - .strict(), + args: z.strictObject({ + ...deploymentArgs, + ...confirmationArgs, + }), run: async (args) => { await assertWritable({ profile: args.profile }); const { client, workspaceId, application, config } = await loadOptions({ diff --git a/packages/sdk/src/cli/commands/secret/create.ts b/packages/sdk/src/cli/commands/secret/create.ts index 4c09f810e..f72ea18e6 100644 --- a/packages/sdk/src/cli/commands/secret/create.ts +++ b/packages/sdk/src/cli/commands/secret/create.ts @@ -13,13 +13,11 @@ import { checkVaultManaged, releaseVaultOwnership } from "./check-vault-managed" export const createSecretCommand = defineAppCommand({ name: "create", description: "Create a secret in a vault.", - args: z - .object({ - ...workspaceArgs, - ...secretValueArgs, - ...confirmationArgs, - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...secretValueArgs, + ...confirmationArgs, + }), run: async (args) => { await assertWritable({ profile: args.profile }); const accessToken = await loadAccessToken({ diff --git a/packages/sdk/src/cli/commands/secret/delete.ts b/packages/sdk/src/cli/commands/secret/delete.ts index b69f3e6cc..86ccad56b 100644 --- a/packages/sdk/src/cli/commands/secret/delete.ts +++ b/packages/sdk/src/cli/commands/secret/delete.ts @@ -13,13 +13,11 @@ import { checkVaultManaged, releaseVaultOwnership } from "./check-vault-managed" export const deleteSecretCommand = defineAppCommand({ name: "delete", description: "Delete a secret in a vault.", - args: z - .object({ - ...workspaceArgs, - ...secretIdentifyArgs, - ...confirmationArgs, - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...secretIdentifyArgs, + ...confirmationArgs, + }), run: async (args) => { await assertWritable({ profile: args.profile }); const accessToken = await loadAccessToken({ diff --git a/packages/sdk/src/cli/commands/secret/list.ts b/packages/sdk/src/cli/commands/secret/list.ts index fa97d8c6c..9d9c1a401 100644 --- a/packages/sdk/src/cli/commands/secret/list.ts +++ b/packages/sdk/src/cli/commands/secret/list.ts @@ -67,13 +67,11 @@ async function secretList(options: SecretListOptions): Promise { export const listSecretCommand = defineAppCommand({ name: "list", description: "List all secrets in a vault.", - args: z - .object({ - ...workspaceArgs, - ...vaultArgs, - ...paginationArgs(), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...vaultArgs, + ...paginationArgs(), + }), run: async (args) => { try { const secrets = await secretList({ diff --git a/packages/sdk/src/cli/commands/secret/update.ts b/packages/sdk/src/cli/commands/secret/update.ts index 94c53441b..0a58e1ab5 100644 --- a/packages/sdk/src/cli/commands/secret/update.ts +++ b/packages/sdk/src/cli/commands/secret/update.ts @@ -13,13 +13,11 @@ import { checkVaultManaged, releaseVaultOwnership } from "./check-vault-managed" export const updateSecretCommand = defineAppCommand({ name: "update", description: "Update a secret in a vault.", - args: z - .object({ - ...workspaceArgs, - ...secretValueArgs, - ...confirmationArgs, - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...secretValueArgs, + ...confirmationArgs, + }), run: async (args) => { await assertWritable({ profile: args.profile }); const accessToken = await loadAccessToken({ diff --git a/packages/sdk/src/cli/commands/secret/vault/create.ts b/packages/sdk/src/cli/commands/secret/vault/create.ts index d5bd0a9b6..4cbba85e5 100644 --- a/packages/sdk/src/cli/commands/secret/vault/create.ts +++ b/packages/sdk/src/cli/commands/secret/vault/create.ts @@ -11,12 +11,10 @@ import { nameArgs } from "./args"; export const createCommand = defineAppCommand({ name: "create", description: "Create a new Secret Manager vault.", - args: z - .object({ - ...workspaceArgs, - ...nameArgs, - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...nameArgs, + }), run: async (args) => { await assertWritable({ profile: args.profile }); const accessToken = await loadAccessToken({ diff --git a/packages/sdk/src/cli/commands/secret/vault/delete.ts b/packages/sdk/src/cli/commands/secret/vault/delete.ts index 06554e52a..162ec2ef3 100644 --- a/packages/sdk/src/cli/commands/secret/vault/delete.ts +++ b/packages/sdk/src/cli/commands/secret/vault/delete.ts @@ -13,13 +13,11 @@ import { nameArgs } from "./args"; export const deleteCommand = defineAppCommand({ name: "delete", description: "Delete a Secret Manager vault.", - args: z - .object({ - ...workspaceArgs, - ...nameArgs, - ...confirmationArgs, - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...nameArgs, + ...confirmationArgs, + }), run: async (args) => { await assertWritable({ profile: args.profile }); const accessToken = await loadAccessToken({ diff --git a/packages/sdk/src/cli/commands/secret/vault/list.ts b/packages/sdk/src/cli/commands/secret/vault/list.ts index 84432301d..866aad478 100644 --- a/packages/sdk/src/cli/commands/secret/vault/list.ts +++ b/packages/sdk/src/cli/commands/secret/vault/list.ts @@ -63,12 +63,10 @@ async function vaultList(options?: VaultListOptions): Promise { export const listCommand = defineAppCommand({ name: "list", description: "List all Secret Manager vaults in the workspace.", - args: z - .object({ - ...workspaceArgs, - ...paginationArgs(), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...paginationArgs(), + }), run: async (args) => { const vaults = await vaultList({ workspaceId: args["workspace-id"], diff --git a/packages/sdk/src/cli/commands/setup/index.ts b/packages/sdk/src/cli/commands/setup/index.ts index 8a93d17be..ffb424d0b 100644 --- a/packages/sdk/src/cli/commands/setup/index.ts +++ b/packages/sdk/src/cli/commands/setup/index.ts @@ -7,7 +7,7 @@ import { setupGitHub } from "./generate"; const checkCommand = defineAppCommand({ name: "check", description: "Audit generated workflows for drift against the current config/repo (read-only).", - args: z.object({}).strict(), + args: z.strictObject({}), run: () => { checkGitHub({ outputDir: process.cwd() }); }, @@ -16,47 +16,43 @@ const checkCommand = defineAppCommand({ export const setupCommand = defineAppCommand({ name: "setup", description: "Generate a CI deploy workflow for your project. (beta)", - args: z - .object({ - provider: arg( - z - .enum(["github"], { message: "Only the 'github' provider is supported." }) - .default("github"), - { - alias: "p", - description: "CI provider to generate for (only 'github' is supported)", - }, - ), - "workspace-name": arg(z.string().min(1).optional(), { - alias: "n", - description: "Workspace name (defaults to the config 'name')", - }), - branch: arg(z.string().min(1).optional(), { - description: - "Branch target: deploy trigger branch (defaults to the detected default branch). " + - "Tag target: tag-reachability guard branch (no guard when omitted)", - }), - tag: arg(z.boolean().default(false), { - description: "Generate a tag target (deploy on tag push)", - }), - "tag-pattern": arg(z.string().min(1).optional(), { - description: "Tag glob to match (requires --tag; defaults to v*)", - }), - environment: arg(z.string().min(1).optional(), { - description: "GitHub Environment for the plan/deploy jobs (defaults to the workspace name)", - }), - "no-plan": arg(z.boolean().default(false), { - description: "Disable the plan job for a branch target (cannot be combined with --tag)", - }), - dir: arg(z.string().min(1).default("."), { - alias: "d", - description: "App directory (for monorepo setups)", - }), - force: arg(z.boolean().default(false), { - description: "Discard hand edits / take over unmanaged files and regenerate", - }), - }) - .strict(), + args: z.strictObject({ + provider: arg( + z.enum(["github"], { message: "Only the 'github' provider is supported." }).default("github"), + { + alias: "p", + description: "CI provider to generate for (only 'github' is supported)", + }, + ), + "workspace-name": arg(z.string().min(1).optional(), { + alias: "n", + description: "Workspace name (defaults to the config 'name')", + }), + branch: arg(z.string().min(1).optional(), { + description: + "Branch target: deploy trigger branch (defaults to the detected default branch). " + + "Tag target: tag-reachability guard branch (no guard when omitted)", + }), + tag: arg(z.boolean().default(false), { + description: "Generate a tag target (deploy on tag push)", + }), + "tag-pattern": arg(z.string().min(1).optional(), { + description: "Tag glob to match (requires --tag; defaults to v*)", + }), + environment: arg(z.string().min(1).optional(), { + description: "GitHub Environment for the plan/deploy jobs (defaults to the workspace name)", + }), + "no-plan": arg(z.boolean().default(false), { + description: "Disable the plan job for a branch target (cannot be combined with --tag)", + }), + dir: arg(z.string().min(1).default("."), { + alias: "d", + description: "App directory (for monorepo setups)", + }), + force: arg(z.boolean().default(false), { + description: "Discard hand edits / take over unmanaged files and regenerate", + }), + }), subCommands: { check: checkCommand, }, diff --git a/packages/sdk/src/cli/commands/show.ts b/packages/sdk/src/cli/commands/show.ts index a6d505875..ba9b09acb 100644 --- a/packages/sdk/src/cli/commands/show.ts +++ b/packages/sdk/src/cli/commands/show.ts @@ -101,11 +101,9 @@ const showWorkspaceNameTransformer = createWorkspaceNameTransformer( export const showCommand = defineAppCommand({ name: "show", description: "Show information about the deployed application.", - args: z - .object({ - ...deploymentArgs, - }) - .strict(), + args: z.strictObject({ + ...deploymentArgs, + }), run: async (args) => { // Execute show logic const appInfo = await show({ diff --git a/packages/sdk/src/cli/commands/skills/install.ts b/packages/sdk/src/cli/commands/skills/install.ts index 3a807db4c..1d546c80c 100644 --- a/packages/sdk/src/cli/commands/skills/install.ts +++ b/packages/sdk/src/cli/commands/skills/install.ts @@ -20,18 +20,16 @@ const DEFAULT_AGENT = "claude-code"; export const installCommand = defineAppCommand({ name: "install", description: "Install the tailor agent skill from the installed SDK package.", - args: z - .object({ - agent: arg(z.string().default(DEFAULT_AGENT), { - alias: "a", - description: `vercel/skills agent name (e.g. ${DEFAULT_AGENT}, codex). Defaults to ${DEFAULT_AGENT}.`, - }), - yes: arg(z.boolean().default(false), { - alias: "y", - description: "Auto-approve prompts.", - }), - }) - .strict(), + args: z.strictObject({ + agent: arg(z.string().default(DEFAULT_AGENT), { + alias: "a", + description: `vercel/skills agent name (e.g. ${DEFAULT_AGENT}, codex). Defaults to ${DEFAULT_AGENT}.`, + }), + yes: arg(z.boolean().default(false), { + alias: "y", + description: "Auto-approve prompts.", + }), + }), run: async (args) => { const exitCode = await runSkillsInstaller({ source: await resolveBundledSkillsDir(), diff --git a/packages/sdk/src/cli/commands/staticwebsite/deploy.ts b/packages/sdk/src/cli/commands/staticwebsite/deploy.ts index 0514a9aa0..c737d85a6 100644 --- a/packages/sdk/src/cli/commands/staticwebsite/deploy.ts +++ b/packages/sdk/src/cli/commands/staticwebsite/deploy.ts @@ -229,20 +229,18 @@ export function logSkippedFiles(skippedFiles: string[]) { export const deployCommand = defineAppCommand({ name: "deploy", description: "Deploy a static website from a local build directory.", - args: z - .object({ - ...workspaceArgs, - name: arg(z.string(), { - alias: "n", - description: "Static website name", - }), - dir: arg(z.string(), { - alias: "d", - description: "Path to the static website files", - completion: { type: "directory" }, - }), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + name: arg(z.string(), { + alias: "n", + description: "Static website name", + }), + dir: arg(z.string(), { + alias: "d", + description: "Path to the static website files", + completion: { type: "directory" }, + }), + }), run: async (args) => { await assertWritable({ profile: args.profile }); logger.info(`Deploying static website "${args.name}" from directory: ${args.dir}`); diff --git a/packages/sdk/src/cli/commands/staticwebsite/domain/get.ts b/packages/sdk/src/cli/commands/staticwebsite/domain/get.ts index c03c83293..3ad530bec 100644 --- a/packages/sdk/src/cli/commands/staticwebsite/domain/get.ts +++ b/packages/sdk/src/cli/commands/staticwebsite/domain/get.ts @@ -11,15 +11,13 @@ import { statusLabels } from "./status"; export const domainGetCommand = defineAppCommand({ name: "get", description: "Get details of a custom domain.", - args: z - .object({ - ...workspaceArgs, - domain: arg(z.string(), { - positional: true, - description: "Custom domain name", - }), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + domain: arg(z.string(), { + positional: true, + description: "Custom domain name", + }), + }), run: async (args) => { const accessToken = await loadAccessToken({ profile: args.profile, diff --git a/packages/sdk/src/cli/commands/staticwebsite/domain/list.ts b/packages/sdk/src/cli/commands/staticwebsite/domain/list.ts index d9a6cfbed..fe05ba0f5 100644 --- a/packages/sdk/src/cli/commands/staticwebsite/domain/list.ts +++ b/packages/sdk/src/cli/commands/staticwebsite/domain/list.ts @@ -11,15 +11,13 @@ import { statusLabels } from "./status"; export const domainListCommand = defineAppCommand({ name: "list", description: "List custom domains for a static website.", - args: z - .object({ - ...workspaceArgs, - name: arg(z.string(), { - positional: true, - description: "Static website name", - }), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + name: arg(z.string(), { + positional: true, + description: "Static website name", + }), + }), run: async (args) => { const accessToken = await loadAccessToken({ profile: args.profile, diff --git a/packages/sdk/src/cli/commands/staticwebsite/get.ts b/packages/sdk/src/cli/commands/staticwebsite/get.ts index 537ccc547..0d451d34c 100644 --- a/packages/sdk/src/cli/commands/staticwebsite/get.ts +++ b/packages/sdk/src/cli/commands/staticwebsite/get.ts @@ -10,15 +10,13 @@ import { logger } from "#/cli/shared/logger"; export const getCommand = defineAppCommand({ name: "get", description: "Get details of a specific static website.", - args: z - .object({ - ...workspaceArgs, - name: arg(z.string(), { - positional: true, - description: "Static website name", - }), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + name: arg(z.string(), { + positional: true, + description: "Static website name", + }), + }), run: async (args) => { const accessToken = await loadAccessToken({ profile: args.profile, diff --git a/packages/sdk/src/cli/commands/staticwebsite/list.ts b/packages/sdk/src/cli/commands/staticwebsite/list.ts index 0fe4d56f0..b22df94f9 100644 --- a/packages/sdk/src/cli/commands/staticwebsite/list.ts +++ b/packages/sdk/src/cli/commands/staticwebsite/list.ts @@ -63,12 +63,10 @@ async function listStaticWebsites( export const listCommand = defineAppCommand({ name: "list", description: "List all static websites in a workspace.", - args: z - .object({ - ...workspaceArgs, - ...paginationArgs(), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...paginationArgs(), + }), run: async (args) => { const jsonOutput = logger.jsonMode; const websites = await listStaticWebsites({ diff --git a/packages/sdk/src/cli/commands/tailordb/erd/deploy.ts b/packages/sdk/src/cli/commands/tailordb/erd/deploy.ts index fc4261e38..9853bb23d 100644 --- a/packages/sdk/src/cli/commands/tailordb/erd/deploy.ts +++ b/packages/sdk/src/cli/commands/tailordb/erd/deploy.ts @@ -11,16 +11,14 @@ import { initErdDeployContext } from "./utils"; export const erdDeployCommand = defineAppCommand({ name: "deploy", description: "Deploy ERD static website for TailorDB namespace(s).", - args: z - .object({ - ...deploymentArgs, - namespace: arg(z.string().optional(), { - alias: "n", - description: - "TailorDB namespace name (optional - deploys all namespaces with erdSite if omitted)", - }), - }) - .strict(), + args: z.strictObject({ + ...deploymentArgs, + namespace: arg(z.string().optional(), { + alias: "n", + description: + "TailorDB namespace name (optional - deploys all namespaces with erdSite if omitted)", + }), + }), run: async (args) => { await assertWritable({ profile: args.profile }); const { client, workspaceId } = await initErdDeployContext(args); diff --git a/packages/sdk/src/cli/commands/tailordb/erd/export.ts b/packages/sdk/src/cli/commands/tailordb/erd/export.ts index 798d12377..7278ff84c 100644 --- a/packages/sdk/src/cli/commands/tailordb/erd/export.ts +++ b/packages/sdk/src/cli/commands/tailordb/erd/export.ts @@ -177,22 +177,19 @@ export function prepareErdBuildsFromContext( export const erdExportCommand = defineAppCommand({ name: "export", description: "Export TailorDB ERD static viewer from local TailorDB schema.", - args: z - .object({ - ...configArg, - namespace: arg(z.string().optional(), { - alias: "n", - description: - "TailorDB namespace name (optional if only one namespace is defined in config)", - }), - output: arg(z.string().default(DEFAULT_ERD_BASE_DIR), { - alias: "o", - description: - "Output directory path for TailorDB ERD viewer files (writes to `//dist`)", - completion: { type: "directory" }, - }), - }) - .strict(), + args: z.strictObject({ + ...configArg, + namespace: arg(z.string().optional(), { + alias: "n", + description: "TailorDB namespace name (optional if only one namespace is defined in config)", + }), + output: arg(z.string().default(DEFAULT_ERD_BASE_DIR), { + alias: "o", + description: + "Output directory path for TailorDB ERD viewer files (writes to `//dist`)", + completion: { type: "directory" }, + }), + }), run: async (args) => { initErdCommand(); diff --git a/packages/sdk/src/cli/commands/tailordb/erd/serve.ts b/packages/sdk/src/cli/commands/tailordb/erd/serve.ts index c452f7e81..7694b32c3 100644 --- a/packages/sdk/src/cli/commands/tailordb/erd/serve.ts +++ b/packages/sdk/src/cli/commands/tailordb/erd/serve.ts @@ -427,21 +427,19 @@ async function waitForShutdown(server: http.Server, watcher: FSWatcher): Promise export const erdServeCommand = defineAppCommand({ name: "serve", description: "Generate and serve TailorDB ERD locally with watch reload. (beta)", - args: z - .object({ - ...configArg, - namespace: arg(z.string().optional(), { - alias: "n", - description: "TailorDB namespace name (uses first namespace in config if not specified)", - }), - port: arg(z.coerce.number().int().min(0).max(65535).default(0), { - description: "Local server port (0 selects a free port)", - }), - open: arg(z.boolean().default(false), { - description: "Open the ERD viewer in the default browser", - }), - }) - .strict(), + args: z.strictObject({ + ...configArg, + namespace: arg(z.string().optional(), { + alias: "n", + description: "TailorDB namespace name (uses first namespace in config if not specified)", + }), + port: arg(z.coerce.number().int().min(0).max(65535).default(0), { + description: "Local server port (0 selects a free port)", + }), + open: arg(z.boolean().default(false), { + description: "Open the ERD viewer in the default browser", + }), + }), run: async (args) => { initErdCommand(); diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/generate.ts b/packages/sdk/src/cli/commands/tailordb/migrate/generate.ts index 54a7aed38..789efdf18 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/generate.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/generate.ts @@ -342,19 +342,17 @@ export const generateCommand = defineAppCommand({ name: "generate", description: "Generate migration files by detecting schema differences between current local types and the previous migration snapshot.", - args: z - .object({ - ...confirmationArgs, - ...configArg, - name: arg(z.string().optional(), { - alias: "n", - description: "Optional description for the migration", - }), - init: arg(z.boolean().default(false), { - description: "Delete existing migrations and start fresh", - }), - }) - .strict(), + args: z.strictObject({ + ...confirmationArgs, + ...configArg, + name: arg(z.string().optional(), { + alias: "n", + description: "Optional description for the migration", + }), + init: arg(z.boolean().default(false), { + description: "Delete existing migrations and start fresh", + }), + }), run: async (args) => { await generate({ configPath: args.config, diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/script.ts b/packages/sdk/src/cli/commands/tailordb/migrate/script.ts index 82ac46020..12b974e8d 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/script.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/script.ts @@ -136,19 +136,17 @@ function resolveTargetNamespace( export const scriptCommand = defineAppCommand({ name: "script", description: "Add a migration script (migrate.ts) template to an existing migration directory.", - args: z - .object({ - ...configArg, - number: arg(z.string(), { - positional: true, - description: "Migration number to add a script to (e.g., 0001 or 1)", - }), - namespace: arg(z.string().optional(), { - alias: "n", - description: "Target TailorDB namespace (required if multiple namespaces exist)", - }), - }) - .strict(), + args: z.strictObject({ + ...configArg, + number: arg(z.string(), { + positional: true, + description: "Migration number to add a script to (e.g., 0001 or 1)", + }), + namespace: arg(z.string().optional(), { + alias: "n", + description: "Target TailorDB namespace (required if multiple namespaces exist)", + }), + }), run: async (args) => { await script({ configPath: args.config, diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/set.ts b/packages/sdk/src/cli/commands/tailordb/migrate/set.ts index 3538881ee..9596ab8f0 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/set.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/set.ts @@ -154,20 +154,18 @@ async function set(options: SetOptions): Promise { export const setCommand = defineAppCommand({ name: "set", description: "Set migration checkpoint to a specific number.", - args: z - .object({ - ...deploymentArgs, - ...confirmationArgs, - number: arg(z.string(), { - positional: true, - description: "Migration number to set (e.g., 0001 or 1)", - }), - namespace: arg(z.string().optional(), { - alias: "n", - description: "Target TailorDB namespace (required if multiple namespaces exist)", - }), - }) - .strict(), + args: z.strictObject({ + ...deploymentArgs, + ...confirmationArgs, + number: arg(z.string(), { + positional: true, + description: "Migration number to set (e.g., 0001 or 1)", + }), + namespace: arg(z.string().optional(), { + alias: "n", + description: "Target TailorDB namespace (required if multiple namespaces exist)", + }), + }), run: async (args) => { await assertWritable({ profile: args.profile }); await set({ diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/status.ts b/packages/sdk/src/cli/commands/tailordb/migrate/status.ts index b615f24fa..7677f1877 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/status.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/status.ts @@ -163,15 +163,13 @@ export const statusCommand = defineAppCommand({ name: "status", description: "Show the current migration status for TailorDB namespaces, including applied and pending migrations.", - args: z - .object({ - ...deploymentArgs, - namespace: arg(z.string().optional(), { - alias: "n", - description: "Target TailorDB namespace (shows all namespaces if not specified)", - }), - }) - .strict(), + args: z.strictObject({ + ...deploymentArgs, + namespace: arg(z.string().optional(), { + alias: "n", + description: "Target TailorDB namespace (shows all namespaces if not specified)", + }), + }), run: async (args) => { await status({ configPath: args.config, diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/sync.ts b/packages/sdk/src/cli/commands/tailordb/migrate/sync.ts index 0275bc08a..4a0de9e43 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/sync.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/sync.ts @@ -509,21 +509,18 @@ export const syncCommand = defineAppCommand({ name: "sync", description: "Sync remote TailorDB schema to a specific migration snapshot (recovery from --no-schema-check drift).", - args: z - .object({ - ...deploymentArgs, - ...confirmationArgs, - number: arg(z.string(), { - positional: true, - description: - "Migration number to sync to (e.g., 0001 or 1; 0 targets the baseline snapshot)", - }), - namespace: arg(z.string().optional(), { - alias: "n", - description: "Target TailorDB namespace (required if multiple namespaces exist)", - }), - }) - .strict(), + args: z.strictObject({ + ...deploymentArgs, + ...confirmationArgs, + number: arg(z.string(), { + positional: true, + description: "Migration number to sync to (e.g., 0001 or 1; 0 targets the baseline snapshot)", + }), + namespace: arg(z.string().optional(), { + alias: "n", + description: "Target TailorDB namespace (required if multiple namespaces exist)", + }), + }), run: async (args) => { await assertWritable({ profile: args.profile }); await sync({ diff --git a/packages/sdk/src/cli/commands/tailordb/truncate.ts b/packages/sdk/src/cli/commands/tailordb/truncate.ts index 8633c3a04..cf495d4a4 100644 --- a/packages/sdk/src/cli/commands/tailordb/truncate.ts +++ b/packages/sdk/src/cli/commands/tailordb/truncate.ts @@ -207,24 +207,22 @@ async function $truncate(options: InternalTruncateOptions = {}): Promise { export const truncateCommand = defineAppCommand({ name: "truncate", description: "Truncate (delete all records from) TailorDB tables.", - args: z - .object({ - ...deploymentArgs, - ...confirmationArgs, - types: arg(z.string().array().optional(), { - positional: true, - description: "Type names to truncate", - }), - all: arg(z.boolean().default(false), { - alias: "a", - description: "Truncate all tables in all owned namespaces (excludes external namespaces)", - }), - namespace: arg(z.string().optional(), { - alias: "n", - description: "Truncate all tables in specified namespace", - }), - }) - .strict(), + args: z.strictObject({ + ...deploymentArgs, + ...confirmationArgs, + types: arg(z.string().array().optional(), { + positional: true, + description: "Type names to truncate", + }), + all: arg(z.boolean().default(false), { + alias: "a", + description: "Truncate all tables in all owned namespaces (excludes external namespaces)", + }), + namespace: arg(z.string().optional(), { + alias: "n", + description: "Truncate all tables in specified namespace", + }), + }), run: async (args) => { await assertWritable({ profile: args.profile }); const types = args.types && args.types.length > 0 ? args.types : undefined; diff --git a/packages/sdk/src/cli/commands/upgrade/index.ts b/packages/sdk/src/cli/commands/upgrade/index.ts index 2b4db9e6b..d222dc20b 100644 --- a/packages/sdk/src/cli/commands/upgrade/index.ts +++ b/packages/sdk/src/cli/commands/upgrade/index.ts @@ -6,21 +6,19 @@ import { defineAppCommand } from "#/cli/shared/command"; export const upgradeCommand = defineAppCommand({ name: "upgrade", description: "Run codemods to upgrade your project to a newer SDK version.", - args: z - .object({ - from: arg(z.string(), { - description: "SDK version before the upgrade (e.g., 1.33.0)", - }), - "dry-run": arg(z.boolean().default(false), { - alias: "d", - description: "Preview changes without modifying files", - }), - path: arg(z.string().default("."), { - description: "Project directory to upgrade", - completion: { type: "directory" }, - }), - }) - .strict(), + args: z.strictObject({ + from: arg(z.string(), { + description: "SDK version before the upgrade (e.g., 1.33.0)", + }), + "dry-run": arg(z.boolean().default(false), { + alias: "d", + description: "Preview changes without modifying files", + }), + path: arg(z.string().default("."), { + description: "Project directory to upgrade", + completion: { type: "directory" }, + }), + }), run: async (args) => { const { initTelemetry } = await import("#/cli/telemetry/index"); await initTelemetry(); diff --git a/packages/sdk/src/cli/commands/user/current.ts b/packages/sdk/src/cli/commands/user/current.ts index 897b69304..3ab2d97d4 100644 --- a/packages/sdk/src/cli/commands/user/current.ts +++ b/packages/sdk/src/cli/commands/user/current.ts @@ -7,7 +7,7 @@ import ml from "#/utils/multiline"; export const currentCommand = defineAppCommand({ name: "current", description: "Show current user.", - args: z.object({}).strict(), + args: z.strictObject({}), run: async () => { const config = await readPlatformConfig(); const jsonOutput = logger.jsonMode; diff --git a/packages/sdk/src/cli/commands/user/list.ts b/packages/sdk/src/cli/commands/user/list.ts index 61e056c16..2d130548d 100644 --- a/packages/sdk/src/cli/commands/user/list.ts +++ b/packages/sdk/src/cli/commands/user/list.ts @@ -7,7 +7,7 @@ import ml from "#/utils/multiline"; export const listCommand = defineAppCommand({ name: "list", description: "List all users.", - args: z.object({}).strict(), + args: z.strictObject({}), run: async () => { const config = await readPlatformConfig(); const jsonOutput = logger.jsonMode; diff --git a/packages/sdk/src/cli/commands/user/pat/create.ts b/packages/sdk/src/cli/commands/user/pat/create.ts index 9d254436e..77453f3aa 100644 --- a/packages/sdk/src/cli/commands/user/pat/create.ts +++ b/packages/sdk/src/cli/commands/user/pat/create.ts @@ -10,18 +10,16 @@ import { getScopesFromWriteFlag, printCreatedToken } from "./transform"; export const createCommand = defineAppCommand({ name: "create", description: "Create a new personal access token.", - args: z - .object({ - name: arg(z.string(), { - positional: true, - description: "Token name", - }), - write: arg(z.boolean().default(false), { - alias: "W", - description: "Grant write permission (default: read-only)", - }), - }) - .strict(), + args: z.strictObject({ + name: arg(z.string(), { + positional: true, + description: "Token name", + }), + write: arg(z.boolean().default(false), { + alias: "W", + description: "Grant write permission (default: read-only)", + }), + }), run: async (args) => { await assertWritable(); const config = await readPlatformConfig(); diff --git a/packages/sdk/src/cli/commands/user/pat/delete.ts b/packages/sdk/src/cli/commands/user/pat/delete.ts index e8463787e..bce335aea 100644 --- a/packages/sdk/src/cli/commands/user/pat/delete.ts +++ b/packages/sdk/src/cli/commands/user/pat/delete.ts @@ -10,14 +10,12 @@ import ml from "#/utils/multiline"; export const deleteCommand = defineAppCommand({ name: "delete", description: "Delete a personal access token.", - args: z - .object({ - name: arg(z.string(), { - positional: true, - description: "Token name", - }), - }) - .strict(), + args: z.strictObject({ + name: arg(z.string(), { + positional: true, + description: "Token name", + }), + }), run: async (args) => { await assertWritable(); const config = await readPlatformConfig(); diff --git a/packages/sdk/src/cli/commands/user/pat/list.ts b/packages/sdk/src/cli/commands/user/pat/list.ts index df565532d..08591a1e0 100644 --- a/packages/sdk/src/cli/commands/user/pat/list.ts +++ b/packages/sdk/src/cli/commands/user/pat/list.ts @@ -10,7 +10,7 @@ import { transformPersonalAccessToken, type PersonalAccessTokenInfo } from "./tr export const listCommand = defineAppCommand({ name: "list", description: "List all personal access tokens.", - args: z.object({ ...paginationArgs() }).strict(), + args: z.strictObject({ ...paginationArgs() }), run: async (args) => { const jsonOutput = logger.jsonMode; const config = await readPlatformConfig(); diff --git a/packages/sdk/src/cli/commands/user/pat/update.ts b/packages/sdk/src/cli/commands/user/pat/update.ts index fc92ac01e..eaff3d73e 100644 --- a/packages/sdk/src/cli/commands/user/pat/update.ts +++ b/packages/sdk/src/cli/commands/user/pat/update.ts @@ -10,18 +10,16 @@ import { getScopesFromWriteFlag, printCreatedToken } from "./transform"; export const updateCommand = defineAppCommand({ name: "update", description: "Update a personal access token (delete and recreate).", - args: z - .object({ - name: arg(z.string(), { - positional: true, - description: "Token name", - }), - write: arg(z.boolean().default(false), { - alias: "W", - description: "Grant write permission (if not specified, keeps read-only)", - }), - }) - .strict(), + args: z.strictObject({ + name: arg(z.string(), { + positional: true, + description: "Token name", + }), + write: arg(z.boolean().default(false), { + alias: "W", + description: "Grant write permission (if not specified, keeps read-only)", + }), + }), run: async (args) => { await assertWritable(); const config = await readPlatformConfig(); diff --git a/packages/sdk/src/cli/commands/user/switch.ts b/packages/sdk/src/cli/commands/user/switch.ts index 53449da0a..e99bf1bc3 100644 --- a/packages/sdk/src/cli/commands/user/switch.ts +++ b/packages/sdk/src/cli/commands/user/switch.ts @@ -8,14 +8,12 @@ import ml from "#/utils/multiline"; export const switchCommand = defineAppCommand({ name: "switch", description: "Set current user.", - args: z - .object({ - user: arg(z.string(), { - positional: true, - description: "User email address or machine user client ID", - }), - }) - .strict(), + args: z.strictObject({ + user: arg(z.string(), { + positional: true, + description: "User email address or machine user client ID", + }), + }), run: async (args) => { const config = await readPlatformConfig(); diff --git a/packages/sdk/src/cli/commands/workflow/executions.ts b/packages/sdk/src/cli/commands/workflow/executions.ts index 7b7066991..b2d64944a 100644 --- a/packages/sdk/src/cli/commands/workflow/executions.ts +++ b/packages/sdk/src/cli/commands/workflow/executions.ts @@ -354,37 +354,35 @@ export function printExecutionWithLogs(execution: WorkflowExecutionDetailInfo): export const executionsCommand = defineAppCommand({ name: "executions", description: "List or get workflow executions.", - args: z - .object({ - ...workspaceArgs, - ...pagedLogArgs, - "execution-id": arg(z.string().optional(), { - positional: true, - description: "Execution ID (if provided, shows details)", - }), - "workflow-name": arg( - z - .string() - .regex( - /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/, - "Must be 3-63 lowercase alphanumeric characters or hyphens, starting and ending with alphanumeric", - ) - .optional(), - { - alias: "n", - description: "Filter by workflow name (list mode only)", - }, - ), - status: arg(z.string().optional(), { - alias: "s", - description: "Filter by status (list mode only)", - }), - ...waitArgs, - logs: arg(z.boolean().default(false), { - description: "Display job execution logs (detail mode only)", - }), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...pagedLogArgs, + "execution-id": arg(z.string().optional(), { + positional: true, + description: "Execution ID (if provided, shows details)", + }), + "workflow-name": arg( + z + .string() + .regex( + /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/, + "Must be 3-63 lowercase alphanumeric characters or hyphens, starting and ending with alphanumeric", + ) + .optional(), + { + alias: "n", + description: "Filter by workflow name (list mode only)", + }, + ), + status: arg(z.string().optional(), { + alias: "s", + description: "Filter by status (list mode only)", + }), + ...waitArgs, + logs: arg(z.boolean().default(false), { + description: "Display job execution logs (detail mode only)", + }), + }), run: async (args) => { const jsonOutput = logger.jsonMode || args.json; if (args.executionId) { diff --git a/packages/sdk/src/cli/commands/workflow/get.ts b/packages/sdk/src/cli/commands/workflow/get.ts index 082529452..19d0969b0 100644 --- a/packages/sdk/src/cli/commands/workflow/get.ts +++ b/packages/sdk/src/cli/commands/workflow/get.ts @@ -88,12 +88,10 @@ export async function getWorkflow( export const getCommand = defineAppCommand({ name: "get", description: "Get workflow details.", - args: z - .object({ - ...workspaceArgs, - ...nameArgs, - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...nameArgs, + }), run: async (args) => { const workflow = await getWorkflow({ name: args.name, diff --git a/packages/sdk/src/cli/commands/workflow/list.ts b/packages/sdk/src/cli/commands/workflow/list.ts index 854984e37..50e86ef6b 100644 --- a/packages/sdk/src/cli/commands/workflow/list.ts +++ b/packages/sdk/src/cli/commands/workflow/list.ts @@ -48,12 +48,10 @@ export async function listWorkflows(options?: ListWorkflowsOptions): Promise { const jsonOutput = logger.jsonMode; const workflows = await listWorkflows({ diff --git a/packages/sdk/src/cli/commands/workflow/resume.ts b/packages/sdk/src/cli/commands/workflow/resume.ts index faa0b5205..96548a1fc 100644 --- a/packages/sdk/src/cli/commands/workflow/resume.ts +++ b/packages/sdk/src/cli/commands/workflow/resume.ts @@ -77,16 +77,14 @@ export async function resumeWorkflow( export const resumeCommand = defineAppCommand({ name: "resume", description: "Resume a failed or pending workflow execution.", - args: z - .object({ - ...workspaceArgs, - "execution-id": arg(z.string(), { - positional: true, - description: "Failed execution ID", - }), - ...waitArgs, - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + "execution-id": arg(z.string(), { + positional: true, + description: "Failed execution ID", + }), + ...waitArgs, + }), run: async (args) => { const jsonOutput = logger.jsonMode || args.json; const { executionId, wait } = await resumeWorkflow({ diff --git a/packages/sdk/src/cli/commands/workflow/start.ts b/packages/sdk/src/cli/commands/workflow/start.ts index 891b08a64..87c72f0a4 100644 --- a/packages/sdk/src/cli/commands/workflow/start.ts +++ b/packages/sdk/src/cli/commands/workflow/start.ts @@ -250,22 +250,20 @@ export async function startWorkflow( export const startCommand = defineAppCommand({ name: "start", description: "Start a workflow execution.", - args: z - .object({ - ...deploymentArgs, - ...nameArgs, - "machine-user": arg(z.string().optional(), { - alias: "m", - description: "Machine user name. Falls back to the active profile's default machine user.", - env: "TAILOR_PLATFORM_MACHINE_USER_NAME", - }), - arg: arg(z.string().optional(), { - alias: "a", - description: "Workflow argument (JSON string)", - }), - ...waitArgs, - }) - .strict(), + args: z.strictObject({ + ...deploymentArgs, + ...nameArgs, + "machine-user": arg(z.string().optional(), { + alias: "m", + description: "Machine user name. Falls back to the active profile's default machine user.", + env: "TAILOR_PLATFORM_MACHINE_USER_NAME", + }), + arg: arg(z.string().optional(), { + alias: "a", + description: "Workflow argument (JSON string)", + }), + ...waitArgs, + }), run: async (args) => { const { executionId, wait } = await startWorkflowByName({ name: args.name, diff --git a/packages/sdk/src/cli/commands/workflow/wait.ts b/packages/sdk/src/cli/commands/workflow/wait.ts index c2f19d224..5f244927d 100644 --- a/packages/sdk/src/cli/commands/workflow/wait.ts +++ b/packages/sdk/src/cli/commands/workflow/wait.ts @@ -71,16 +71,14 @@ export const waitCommand = defineAppCommand({ desc: "Wait for success, failure, or suspension", }, ], - args: z - .object({ - ...workspaceArgs, - "execution-id": arg(z.string(), { - positional: true, - description: "Execution ID", - }), - ...workflowWaitControlArgs, - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + "execution-id": arg(z.string(), { + positional: true, + description: "Execution ID", + }), + ...workflowWaitControlArgs, + }), run: async (args) => { const jsonOutput = logger.jsonMode || args.json; const result = await waitWorkflowExecution({ diff --git a/packages/sdk/src/cli/commands/workspace/app/health.ts b/packages/sdk/src/cli/commands/workspace/app/health.ts index 807190d6d..c84b75b86 100644 --- a/packages/sdk/src/cli/commands/workspace/app/health.ts +++ b/packages/sdk/src/cli/commands/workspace/app/health.ts @@ -9,6 +9,7 @@ import { logger } from "#/cli/shared/logger"; import { assertDefined } from "#/utils/assert"; import { appHealthInfo, type AppHealthInfo } from "./transform"; +// strip unknown keys const healthOptionsSchema = z.object({ workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), profile: z.string().optional(), @@ -56,15 +57,13 @@ export async function getAppHealth(options: HealthOptions): Promise { const health = await getAppHealth({ workspaceId: args["workspace-id"], diff --git a/packages/sdk/src/cli/commands/workspace/app/list.ts b/packages/sdk/src/cli/commands/workspace/app/list.ts index 45f0ef57a..f747ac5dd 100644 --- a/packages/sdk/src/cli/commands/workspace/app/list.ts +++ b/packages/sdk/src/cli/commands/workspace/app/list.ts @@ -8,6 +8,7 @@ import { logger } from "#/cli/shared/logger"; import { assertDefined } from "#/utils/assert"; import { appInfo, type AppInfo } from "./transform"; +// strip unknown keys const listAppsOptionsSchema = z.object({ workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), profile: z.string().optional(), @@ -66,12 +67,10 @@ export async function listApps(options: ListAppsOptions): Promise { export const listCommand = defineAppCommand({ name: "list", description: "List applications in a workspace", - args: z - .object({ - ...workspaceArgs, - ...paginationArgs(), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...paginationArgs(), + }), run: async (args) => { const jsonOutput = logger.jsonMode; const apps = await listApps({ diff --git a/packages/sdk/src/cli/commands/workspace/create.ts b/packages/sdk/src/cli/commands/workspace/create.ts index 4ed2116bc..2938a7d82 100644 --- a/packages/sdk/src/cli/commands/workspace/create.ts +++ b/packages/sdk/src/cli/commands/workspace/create.ts @@ -19,6 +19,7 @@ import type { ProfileInfo } from "../profile"; * - name: 3-63 chars, lowercase alphanumeric and hyphens, cannot start/end with hyphen * - organizationId, folderId: optional UUIDs */ +// strip unknown keys const createWorkspaceOptionsSchema = z.object({ name: z .string() @@ -80,44 +81,42 @@ export async function createWorkspace(options: CreateWorkspaceOptions): Promise< export const createCommand = defineAppCommand({ name: "create", description: "Create a new Tailor Platform workspace.", - args: z - .object({ - name: arg(z.string(), { - alias: "n", - description: "Workspace name", - }), - region: arg(z.string(), { - alias: "r", - description: "Workspace region (us-west, asia-northeast)", - }), - "delete-protection": arg(z.boolean().default(false), { - alias: "d", - description: "Enable delete protection", - }), - "organization-id": arg(z.string().optional(), { - alias: "o", - description: "Organization ID to workspace associate with", - env: "TAILOR_PLATFORM_ORGANIZATION_ID", - }), - "folder-id": arg(z.string().optional(), { - alias: "f", - description: "Folder ID to workspace associate with", - env: "TAILOR_PLATFORM_FOLDER_ID", - }), - "profile-name": arg(z.string().optional(), { - alias: "p", - description: "Profile name to create", - }), - "profile-user": arg(z.string().optional(), { - description: - "User email address or machine user client ID for the profile (defaults to current user)", - }), - permission: arg(z.enum(["write", "read"]).default("write"), { - description: - "Profile permission (requires --profile-name). 'read' blocks all write commands while the profile is active.", - }), - }) - .strict(), + args: z.strictObject({ + name: arg(z.string(), { + alias: "n", + description: "Workspace name", + }), + region: arg(z.string(), { + alias: "r", + description: "Workspace region (us-west, asia-northeast)", + }), + "delete-protection": arg(z.boolean().default(false), { + alias: "d", + description: "Enable delete protection", + }), + "organization-id": arg(z.string().optional(), { + alias: "o", + description: "Organization ID to workspace associate with", + env: "TAILOR_PLATFORM_ORGANIZATION_ID", + }), + "folder-id": arg(z.string().optional(), { + alias: "f", + description: "Folder ID to workspace associate with", + env: "TAILOR_PLATFORM_FOLDER_ID", + }), + "profile-name": arg(z.string().optional(), { + alias: "p", + description: "Profile name to create", + }), + "profile-user": arg(z.string().optional(), { + description: + "User email address or machine user client ID for the profile (defaults to current user)", + }), + permission: arg(z.enum(["write", "read"]).default("write"), { + description: + "Profile permission (requires --profile-name). 'read' blocks all write commands while the profile is active.", + }), + }), run: async (args) => { // This command does not expose `--profile`, so the guard resolves the // active profile from `TAILOR_PLATFORM_PROFILE` only. diff --git a/packages/sdk/src/cli/commands/workspace/delete.ts b/packages/sdk/src/cli/commands/workspace/delete.ts index 22f09b820..24048fc43 100644 --- a/packages/sdk/src/cli/commands/workspace/delete.ts +++ b/packages/sdk/src/cli/commands/workspace/delete.ts @@ -10,6 +10,7 @@ import { assertWritable } from "#/cli/shared/readonly-guard"; import { assertDefined } from "#/utils/assert"; import { resolveWorkspaceFolderName, workspaceDisplayName } from "./transform"; +// strip unknown keys const deleteWorkspaceOptionsSchema = z.object({ workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }), }); @@ -50,15 +51,13 @@ export async function deleteWorkspace(options: DeleteWorkspaceOptions): Promise< export const deleteCommand = defineAppCommand({ name: "delete", description: "Delete a Tailor Platform workspace.", - args: z - .object({ - "workspace-id": arg(z.string(), { - alias: "w", - description: "Workspace ID", - }), - ...confirmationArgs, - }) - .strict(), + args: z.strictObject({ + "workspace-id": arg(z.string(), { + alias: "w", + description: "Workspace ID", + }), + ...confirmationArgs, + }), run: async (args) => { await assertWritable(); // Load and validate options diff --git a/packages/sdk/src/cli/commands/workspace/get.ts b/packages/sdk/src/cli/commands/workspace/get.ts index 31ce0c6f0..7224f1a9f 100644 --- a/packages/sdk/src/cli/commands/workspace/get.ts +++ b/packages/sdk/src/cli/commands/workspace/get.ts @@ -12,6 +12,7 @@ import { type WorkspaceDetails, } from "./transform"; +// strip unknown keys const getWorkspaceOptionsSchema = z.object({ workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), profile: z.string().optional(), @@ -60,11 +61,9 @@ export async function getWorkspace(options: GetWorkspaceOptions): Promise { const workspace = await getWorkspace({ workspaceId: args["workspace-id"], diff --git a/packages/sdk/src/cli/commands/workspace/list.ts b/packages/sdk/src/cli/commands/workspace/list.ts index 91d27277d..6c5d9992f 100644 --- a/packages/sdk/src/cli/commands/workspace/list.ts +++ b/packages/sdk/src/cli/commands/workspace/list.ts @@ -43,11 +43,9 @@ export async function listWorkspaces(options?: ListWorkspacesOptions): Promise { const workspaces = await listWorkspaces({ order: args.order, diff --git a/packages/sdk/src/cli/commands/workspace/restore.ts b/packages/sdk/src/cli/commands/workspace/restore.ts index 7efff4a27..321433790 100644 --- a/packages/sdk/src/cli/commands/workspace/restore.ts +++ b/packages/sdk/src/cli/commands/workspace/restore.ts @@ -9,6 +9,7 @@ import { prompt } from "#/cli/shared/prompt"; import { assertWritable } from "#/cli/shared/readonly-guard"; import { assertDefined } from "#/utils/assert"; +// strip unknown keys const restoreWorkspaceOptionsSchema = z.object({ workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }), }); @@ -46,15 +47,13 @@ export async function restoreWorkspace(options: RestoreWorkspaceOptions): Promis export const restoreCommand = defineAppCommand({ name: "restore", description: "Restore a deleted workspace", - args: z - .object({ - "workspace-id": arg(z.string(), { - alias: "w", - description: "Workspace ID", - }), - ...confirmationArgs, - }) - .strict(), + args: z.strictObject({ + "workspace-id": arg(z.string(), { + alias: "w", + description: "Workspace ID", + }), + ...confirmationArgs, + }), run: async (args) => { await assertWritable(); const { client, workspaceId } = await loadOptions({ diff --git a/packages/sdk/src/cli/commands/workspace/user/invite.ts b/packages/sdk/src/cli/commands/workspace/user/invite.ts index e13e3cf08..c6c0bdc26 100644 --- a/packages/sdk/src/cli/commands/workspace/user/invite.ts +++ b/packages/sdk/src/cli/commands/workspace/user/invite.ts @@ -9,6 +9,7 @@ import { assertWritable } from "#/cli/shared/readonly-guard"; import { assertDefined } from "#/utils/assert"; import { stringToRole, validRoles } from "./transform"; +// strip unknown keys const inviteUserOptionsSchema = z.object({ workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), profile: z.string().optional(), @@ -57,18 +58,16 @@ export async function inviteUser(options: InviteUserOptions): Promise { export const inviteCommand = defineAppCommand({ name: "invite", description: "Invite a user to a workspace", - args: z - .object({ - ...workspaceArgs, - email: arg(z.email(), { - description: "Email address of the user to invite", - }), - role: arg(z.enum(validRoles), { - description: `Role to assign (${validRoles.join(", ")})`, - alias: "r", - }), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + email: arg(z.email(), { + description: "Email address of the user to invite", + }), + role: arg(z.enum(validRoles), { + description: `Role to assign (${validRoles.join(", ")})`, + alias: "r", + }), + }), run: async (args) => { await assertWritable({ profile: args.profile }); await inviteUser({ diff --git a/packages/sdk/src/cli/commands/workspace/user/list.ts b/packages/sdk/src/cli/commands/workspace/user/list.ts index ea28f2d3c..559e1b2b2 100644 --- a/packages/sdk/src/cli/commands/workspace/user/list.ts +++ b/packages/sdk/src/cli/commands/workspace/user/list.ts @@ -7,6 +7,7 @@ import { logger } from "#/cli/shared/logger"; import { assertDefined } from "#/utils/assert"; import { userInfo, type UserInfo } from "./transform"; +// strip unknown keys const listUsersOptionsSchema = z.object({ workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), profile: z.string().optional(), @@ -65,12 +66,10 @@ export async function listUsers(options: ListUsersOptions): Promise export const listCommand = defineAppCommand({ name: "list", description: "List users in a workspace", - args: z - .object({ - ...workspaceArgs, - ...paginationArgs(), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + ...paginationArgs(), + }), run: async (args) => { const users = await listUsers({ workspaceId: args["workspace-id"], diff --git a/packages/sdk/src/cli/commands/workspace/user/remove.ts b/packages/sdk/src/cli/commands/workspace/user/remove.ts index 3de9e9a19..a5f260f7d 100644 --- a/packages/sdk/src/cli/commands/workspace/user/remove.ts +++ b/packages/sdk/src/cli/commands/workspace/user/remove.ts @@ -9,6 +9,7 @@ import { prompt } from "#/cli/shared/prompt"; import { assertWritable } from "#/cli/shared/readonly-guard"; import { assertDefined } from "#/utils/assert"; +// strip unknown keys const removeUserOptionsSchema = z.object({ workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), profile: z.string().optional(), @@ -54,15 +55,13 @@ export async function removeUser(options: RemoveUserOptions): Promise { export const removeCommand = defineAppCommand({ name: "remove", description: "Remove a user from a workspace", - args: z - .object({ - ...workspaceArgs, - email: arg(z.email(), { - description: "Email address of the user to remove", - }), - ...confirmationArgs, - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + email: arg(z.email(), { + description: "Email address of the user to remove", + }), + ...confirmationArgs, + }), run: async (args) => { await assertWritable({ profile: args.profile }); if (!args.yes) { diff --git a/packages/sdk/src/cli/commands/workspace/user/update.ts b/packages/sdk/src/cli/commands/workspace/user/update.ts index f3d98a12d..d185e4524 100644 --- a/packages/sdk/src/cli/commands/workspace/user/update.ts +++ b/packages/sdk/src/cli/commands/workspace/user/update.ts @@ -9,6 +9,7 @@ import { assertWritable } from "#/cli/shared/readonly-guard"; import { assertDefined } from "#/utils/assert"; import { stringToRole, validRoles } from "./transform"; +// strip unknown keys const updateUserOptionsSchema = z.object({ workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), profile: z.string().optional(), @@ -57,18 +58,16 @@ export async function updateUser(options: UpdateUserOptions): Promise { export const updateCommand = defineAppCommand({ name: "update", description: "Update a user's role in a workspace", - args: z - .object({ - ...workspaceArgs, - email: arg(z.email(), { - description: "Email address of the user to update", - }), - role: arg(z.enum(validRoles), { - description: `New role to assign (${validRoles.join(", ")})`, - alias: "r", - }), - }) - .strict(), + args: z.strictObject({ + ...workspaceArgs, + email: arg(z.email(), { + description: "Email address of the user to update", + }), + role: arg(z.enum(validRoles), { + description: `New role to assign (${validRoles.join(", ")})`, + alias: "r", + }), + }), run: async (args) => { await assertWritable({ profile: args.profile }); await updateUser({ diff --git a/packages/sdk/src/cli/docs.test.ts b/packages/sdk/src/cli/docs.test.ts index 23c244404..f163778bd 100644 --- a/packages/sdk/src/cli/docs.test.ts +++ b/packages/sdk/src/cli/docs.test.ts @@ -114,6 +114,7 @@ describe("CLI Documentation", () => { command: mainCommand, templates, targetCommands, + // strip unknown keys globalArgs: z.object(commonArgs), formatter: mdFormatter, }); diff --git a/packages/sdk/src/cli/index.ts b/packages/sdk/src/cli/index.ts index da64c5304..1f428949d 100644 --- a/packages/sdk/src/cli/index.ts +++ b/packages/sdk/src/cli/index.ts @@ -87,6 +87,7 @@ export const mainCommand = withCompletionCommand( runMain(mainCommand, { version: packageJson.version, + // strip unknown keys globalArgs: z.object(commonArgs), displayErrors: false, cleanup: async ({ error }) => { diff --git a/packages/sdk/src/cli/query/index.ts b/packages/sdk/src/cli/query/index.ts index bb16016ca..bb5a788f2 100644 --- a/packages/sdk/src/cli/query/index.ts +++ b/packages/sdk/src/cli/query/index.ts @@ -41,6 +41,7 @@ import type { Application } from "@tailor-platform/tailor-proto/application_reso export type { QueryEngine } from "./types"; const queryEngineSchema = z.enum(queryEngines); +// strip unknown keys const queryBaseOptionsSchema = z.object({ workspaceId: z.string().optional(), profile: z.string().optional(), @@ -746,7 +747,7 @@ export const queryCommand = defineAppCommand({ name: "query", description: "Run SQL/GraphQL query.", args: z - .object({ + .strictObject({ ...deploymentArgs, engine: arg(queryEngineSchema, { description: "Query engine (sql or gql)", @@ -797,8 +798,7 @@ export const queryCommand = defineAppCommand({ message: "Pass only one of --edit, -q/--query, or -f/--file.", }); } - }) - .strict(), + }), run: async (args) => { const mode = await resolveQueryCommandInput({ query: args.query, diff --git a/packages/sdk/src/cli/shared/client.ts b/packages/sdk/src/cli/shared/client.ts index b6ca1a2f7..27d766499 100644 --- a/packages/sdk/src/cli/shared/client.ts +++ b/packages/sdk/src/cli/shared/client.ts @@ -508,6 +508,7 @@ export async function fetchUserInfo(accessToken: string) { } const rawJson = await resp.json(); + // strip unknown keys const schema = z.object({ sub: z.string(), email: z.string(), @@ -626,6 +627,7 @@ export async function fetchMachineUserToken(url: string, clientId: string, clien } const rawJson = await resp.json(); + // strip unknown keys const schema = z.object({ token_type: z.string(), access_token: z.string(), diff --git a/packages/sdk/src/cli/shared/context.ts b/packages/sdk/src/cli/shared/context.ts index 8224042e9..0f84d3300 100644 --- a/packages/sdk/src/cli/shared/context.ts +++ b/packages/sdk/src/cli/shared/context.ts @@ -20,6 +20,7 @@ import { deleteKeyringTokens, } from "./token-store"; +// strip unknown keys const pfProfileSchema = z.object({ user: z.string(), workspace_id: z.string(), @@ -28,17 +29,20 @@ const pfProfileSchema = z.object({ machine_user_override: z.enum(["allow", "deny"]).optional(), }); +// strip unknown keys const pfUserSchemaV1 = z.object({ access_token: z.string(), refresh_token: z.string().optional(), token_expires_at: z.string(), }); +// strip unknown keys const pfUserKeyringSchema = z.object({ storage: z.literal("keyring"), token_expires_at: z.string(), }); +// strip unknown keys const pfUserFileSchema = z.object({ storage: z.literal("file"), token_expires_at: z.string(), @@ -58,6 +62,7 @@ const pfUserFileSchemaV3 = pfUserFileSchema.extend({ const pfUserSchemaV3 = z.discriminatedUnion("storage", [pfUserKeyringSchemaV3, pfUserFileSchemaV3]); +// strip unknown keys const pfConfigSchemaV1 = z.object({ version: z.literal(1), users: z.partialRecord(z.string(), pfUserSchemaV1), @@ -78,6 +83,7 @@ const semverSchema = z.templateLiteral([ z.number().int(), ]); +// strip unknown keys const pfConfigSchemaV2 = z.object({ version: z.literal(V2_CONFIG_VERSION), min_sdk_version: semverSchema, @@ -88,6 +94,7 @@ const pfConfigSchemaV2 = z.object({ current_user: z.string().nullable(), }); +// strip unknown keys const pfConfigSchemaV3 = z.object({ version: z.literal(LATEST_CONFIG_VERSION), min_sdk_version: semverSchema, @@ -333,6 +340,7 @@ export function writePlatformConfig(config: PfConfig | PfConfigV2 | PfConfigV1) writeSecretFile(configPath, stringifyYAML(diskConfig)); } +// strip unknown keys const tcContextConfigSchema = z.object({ username: z.string().optional(), controlplaneaccesstoken: z.string().optional(), @@ -341,8 +349,10 @@ const tcContextConfigSchema = z.object({ workspaceid: z.string().optional(), }); +// strip unknown keys const tcConfigSchema = z .object({ + // strip unknown keys global: z .object({ context: z.string().optional(), diff --git a/packages/sdk/src/parser/app-config/schema.ts b/packages/sdk/src/parser/app-config/schema.ts index 193a90e59..7d2de5f5c 100644 --- a/packages/sdk/src/parser/app-config/schema.ts +++ b/packages/sdk/src/parser/app-config/schema.ts @@ -22,6 +22,7 @@ const logLevelSchema = z * label-compatible prefix is added at the metadata boundary, so user-facing * configs only need to carry a UUID. */ +// strip unknown keys export const AppConfigSchema = z.object({ id: z.uuid({ message: "'id' must be a UUID." }).optional(), name: z.string().min(1, { message: "'name' must be a non-empty string." }), diff --git a/packages/sdk/src/parser/plugin-config/schema.ts b/packages/sdk/src/parser/plugin-config/schema.ts index bf1f19de7..cdae7f1b2 100644 --- a/packages/sdk/src/parser/plugin-config/schema.ts +++ b/packages/sdk/src/parser/plugin-config/schema.ts @@ -5,7 +5,7 @@ import type { Plugin } from "#/plugin/types"; // Custom plugin schema (object form) // Using passthrough() to preserve additional properties on Plugin instances export const PluginConfigSchema = z - .object({ + .looseObject({ id: z.string(), description: z.string(), importPath: z.string().optional(), @@ -19,7 +19,6 @@ export const PluginConfigSchema = z onResolverReady: functionSchema.optional(), onExecutorReady: functionSchema.optional(), }) - .passthrough() .refine( (p) => { // importPath is required when plugin has definition-time hooks diff --git a/packages/sdk/src/parser/service/aigateway/schema.ts b/packages/sdk/src/parser/service/aigateway/schema.ts index 14c5d74a7..01c84985f 100644 --- a/packages/sdk/src/parser/service/aigateway/schema.ts +++ b/packages/sdk/src/parser/service/aigateway/schema.ts @@ -3,6 +3,7 @@ import { z } from "zod"; const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/; const AUTH_NAMESPACE_PATTERN = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/; +// strip unknown keys export const AIGatewaySchema = z .object({ name: z diff --git a/packages/sdk/src/parser/service/auth-connection/schema.ts b/packages/sdk/src/parser/service/auth-connection/schema.ts index c99092b48..6a09d8167 100644 --- a/packages/sdk/src/parser/service/auth-connection/schema.ts +++ b/packages/sdk/src/parser/service/auth-connection/schema.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +// strip unknown keys export const AuthConnectionOAuth2ConfigSchema = z.object({ providerUrl: z.string().describe("OAuth2 provider URL"), issuerUrl: z.string().describe("OAuth2 issuer URL"), @@ -9,6 +10,7 @@ export const AuthConnectionOAuth2ConfigSchema = z.object({ tokenUrl: z.string().optional().describe("OAuth2 token endpoint override"), }); +// strip unknown keys export const AuthConnectionConfigSchema = z .object({ type: z.literal("oauth2").describe("Connection type"), diff --git a/packages/sdk/src/parser/service/auth/schema.ts b/packages/sdk/src/parser/service/auth/schema.ts index f506b2299..3305490be 100644 --- a/packages/sdk/src/parser/service/auth/schema.ts +++ b/packages/sdk/src/parser/service/auth/schema.ts @@ -3,6 +3,7 @@ import { AuthConnectionConfigSchema } from "#/parser/service/auth-connection/ind import { TailorFieldSchema } from "#/parser/service/field/schema"; import type { ValueOperand } from "#/configure/services/auth/types"; +// strip unknown keys export const AuthInvokerObjectSchema = z.object({ namespace: z.string().describe("Auth namespace"), machineUserName: z.string().describe("Machine user name for authentication"), @@ -13,11 +14,13 @@ export const AuthInvokerSchema = z.union([ AuthInvokerObjectSchema, ]); +// strip unknown keys const secretValueSchema = z.object({ vaultName: z.string().describe("Vault name containing the secret"), secretKey: z.string().describe("Key of the secret in the vault"), }); +// strip unknown keys export const OIDCSchema = z.object({ name: z.string().describe("Identity provider name"), kind: z.literal("OIDC"), @@ -28,6 +31,7 @@ export const OIDCSchema = z.object({ usernameClaim: z.string().optional().describe("JWT claim to use as username"), }); +// strip unknown keys export const SAMLSchema = z .object({ name: z.string().describe("Identity provider name"), @@ -52,6 +56,7 @@ export const SAMLSchema = z return hasMetadata !== hasRaw; }, "Provide either metadataURL or rawMetadata"); +// strip unknown keys export const IDTokenSchema = z.object({ name: z.string().describe("Identity provider name"), kind: z.literal("IDToken"), @@ -61,6 +66,7 @@ export const IDTokenSchema = z.object({ usernameClaim: z.string().optional().describe("JWT claim to use as username"), }); +// strip unknown keys export const BuiltinIdPSchema = z.object({ name: z.string().describe("Identity provider name"), kind: z.literal("BuiltInIdP"), @@ -79,6 +85,7 @@ export const OAuth2ClientGrantTypeSchema = z .union([z.literal("authorization_code"), z.literal("refresh_token")]) .describe("OAuth2 grant type"); +// strip unknown keys export const OAuth2ClientSchema = z .object({ description: z.string().optional().describe("Client description"), @@ -126,6 +133,7 @@ export const OAuth2ClientSchema = z path: ["requireDpop"], }); +// strip unknown keys export const SCIMAuthorizationSchema = z.object({ type: z.union([z.literal("oauth2"), z.literal("bearer")]).describe("SCIM authorization type"), bearerSecret: secretValueSchema @@ -143,6 +151,7 @@ export const SCIMAttributeTypeSchema = z ]) .describe("SCIM attribute data type"); +// strip unknown keys export const SCIMAttributeSchema = z.object({ type: SCIMAttributeTypeSchema.describe("Attribute data type"), name: z.string().describe("Attribute name"), @@ -163,16 +172,19 @@ export const SCIMAttributeSchema = z.object({ }, }); +// strip unknown keys const SCIMSchemaSchema = z.object({ name: z.string().describe("SCIM schema name"), attributes: z.array(SCIMAttributeSchema).describe("Schema attributes"), }); +// strip unknown keys export const SCIMAttributeMappingSchema = z.object({ tailorDBField: z.string().describe("TailorDB field name to map to"), scimPath: z.string().describe("SCIM attribute path"), }); +// strip unknown keys export const SCIMResourceSchema = z.object({ name: z.string().describe("SCIM resource name"), tailorDBNamespace: z.string().describe("TailorDB namespace for the resource"), @@ -181,21 +193,25 @@ export const SCIMResourceSchema = z.object({ attributeMapping: z.array(SCIMAttributeMappingSchema).describe("Attribute mapping configuration"), }); +// strip unknown keys export const SCIMSchema = z.object({ machineUserName: z.string().describe("Machine user name for SCIM operations"), authorization: SCIMAuthorizationSchema.describe("SCIM authorization configuration"), resources: z.array(SCIMResourceSchema).describe("SCIM resource definitions"), }); +// strip unknown keys export const TenantProviderSchema = z.object({ namespace: z.string().describe("TailorDB namespace for the tenant type"), type: z.string().describe("TailorDB type name for tenants"), signatureField: z.string().describe("Field used as the tenant signature"), }); +// strip unknown keys const UserProfileSchema = z.object({ namespace: z.string().optional().describe("TailorDB namespace where the user type is defined"), // FIXME: improve TailorDBInstance schema validation + // strip unknown keys type: z.object({ name: z.string(), fields: z.any(), @@ -221,18 +237,22 @@ const ValueOperandSchema: z.ZodType = z.union([ z.array(z.boolean()), ]); +// strip unknown keys const MachineUserSchema = z.object({ attributes: z.record(z.string(), ValueOperandSchema).optional(), attributeList: z.array(z.uuid()).optional(), }); +// strip unknown keys const BeforeLoginHookSchema = z.object({ handler: z.function(), invoker: z.string(), }); +// strip unknown keys const AuthConfigBaseSchema = z.object({ name: z.string().describe("Auth service name"), + // strip unknown keys hooks: z .object({ beforeLogin: BeforeLoginHookSchema.optional().describe("Before login auth hook"), diff --git a/packages/sdk/src/parser/service/executor/schema.ts b/packages/sdk/src/parser/service/executor/schema.ts index f07513562..153c7263e 100644 --- a/packages/sdk/src/parser/service/executor/schema.ts +++ b/packages/sdk/src/parser/service/executor/schema.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { AuthInvokerSchema } from "../auth"; import { functionSchema } from "../common"; +// strip unknown keys export const TailorDBTriggerSchema = z.object({ kind: z.literal("tailordb").describe("TailorDB record event trigger"), events: z @@ -19,12 +20,14 @@ export const TailorDBTriggerSchema = z.object({ condition: functionSchema.optional().describe("Condition function to filter events"), }); +// strip unknown keys export const ResolverExecutedTriggerSchema = z.object({ kind: z.literal("resolverExecuted"), resolverName: z.string().describe("Name of the resolver to trigger on"), condition: functionSchema.optional().describe("Condition function to filter events"), }); +// strip unknown keys export const ScheduleTriggerSchema = z.object({ kind: z.literal("schedule"), cron: z.string().describe("CRON expression for the schedule"), @@ -35,16 +38,19 @@ export const ScheduleTriggerSchema = z.object({ .describe("Timezone for the CRON schedule (default: UTC)"), }); +// strip unknown keys export const IncomingWebhookTriggerResponseSchema = z.object({ body: functionSchema.optional().describe("Function returning the response body"), statusCode: z.number().int().optional().describe("HTTP status code for the response"), }); +// strip unknown keys export const IncomingWebhookTriggerSchema = z.object({ kind: z.literal("incomingWebhook"), response: IncomingWebhookTriggerResponseSchema.optional().describe("Response configuration"), }); +// strip unknown keys export const IdpUserTriggerSchema = z.object({ kind: z.literal("idpUser").describe("IdP user event trigger"), events: z @@ -60,6 +66,7 @@ export const IdpUserTriggerSchema = z.object({ ), }); +// strip unknown keys export const AuthAccessTokenTriggerSchema = z.object({ kind: z.literal("authAccessToken").describe("Auth access token event trigger"), events: z @@ -84,30 +91,30 @@ export const TriggerSchema = z.discriminatedUnion("kind", [ AuthAccessTokenTriggerSchema, ]); -export const FunctionOperationSchema = z - .object({ - kind: z.enum(["function", "jobFunction"]), - body: functionSchema.describe("Function implementation"), - invoker: AuthInvokerSchema.optional().describe("Invoker for the function execution"), - }) - .strict(); +export const FunctionOperationSchema = z.strictObject({ + kind: z.enum(["function", "jobFunction"]), + body: functionSchema.describe("Function implementation"), + invoker: AuthInvokerSchema.optional().describe("Invoker for the function execution"), +}); -export const GqlOperationSchema = z - .object({ - kind: z.literal("graphql"), - appName: z.string().optional().describe("Target application name for the GraphQL query"), - query: z.preprocess((val) => String(val), z.string().describe("GraphQL query string")), - variables: functionSchema.optional().describe("Function to compute GraphQL variables"), - invoker: AuthInvokerSchema.optional().describe("Invoker for the GraphQL execution"), - }) - .strict(); +export const GqlOperationSchema = z.strictObject({ + kind: z.literal("graphql"), + appName: z.string().optional().describe("Target application name for the GraphQL query"), + query: z.preprocess((val) => String(val), z.string().describe("GraphQL query string")), + variables: functionSchema.optional().describe("Function to compute GraphQL variables"), + invoker: AuthInvokerSchema.optional().describe("Invoker for the GraphQL execution"), +}); +// strip unknown keys export const WebhookOperationSchema = z.object({ kind: z.literal("webhook"), url: functionSchema.describe("Function returning the webhook URL"), requestBody: functionSchema.optional().describe("Function to compute the request body"), headers: z - .record(z.string(), z.union([z.string(), z.object({ vault: z.string(), key: z.string() })])) + .record( + z.string(), + z.union([z.string(), z.strictObject({ vault: z.string(), key: z.string() })]), + ) .optional() .describe("HTTP headers for the webhook request"), }); @@ -127,17 +134,15 @@ export const WorkflowOperationSchema = z.preprocess( const { workflow, ...rest } = val as { workflow: { name: string } }; return { ...rest, workflowName: workflow.name }; }, - z - .object({ - kind: z.literal("workflow"), - workflowName: z.string().describe("Name of the workflow to execute"), - args: z - .union([z.record(z.string(), z.unknown()), functionSchema]) - .optional() - .describe("Arguments to pass to the workflow"), - invoker: AuthInvokerSchema.optional().describe("Invoker for the workflow execution"), - }) - .strict(), + z.strictObject({ + kind: z.literal("workflow"), + workflowName: z.string().describe("Name of the workflow to execute"), + args: z + .union([z.record(z.string(), z.unknown()), functionSchema]) + .optional() + .describe("Arguments to pass to the workflow"), + invoker: AuthInvokerSchema.optional().describe("Invoker for the workflow execution"), + }), ); const OperationSchema = z.union([ @@ -147,6 +152,7 @@ const OperationSchema = z.union([ WorkflowOperationSchema, ]); +// strip unknown keys export const ExecutorSchema = z.object({ name: z.string().describe("Executor name"), description: z.string().optional().describe("Executor description"), diff --git a/packages/sdk/src/parser/service/field/schema.ts b/packages/sdk/src/parser/service/field/schema.ts index 8686c02ae..f0014d5ea 100644 --- a/packages/sdk/src/parser/service/field/schema.ts +++ b/packages/sdk/src/parser/service/field/schema.ts @@ -15,16 +15,19 @@ const TailorFieldTypeSchema = z.enum([ "nested", ]); +// strip unknown keys const AllowedValueSchema = z.object({ value: z.string().describe("The allowed value"), description: z.string().optional().describe("Description of the allowed value"), }); +// strip unknown keys const FieldMetadataSchema = z.object({ required: z.boolean().optional().describe("Whether the field is required"), array: z.boolean().optional().describe("Whether the field is an array"), description: z.string().optional().describe("Field description"), allowedValues: z.array(AllowedValueSchema).optional().describe("Allowed values for enum fields"), + // strip unknown keys hooks: z .object({ create: functionSchema.optional().describe("Hook function called on creation"), @@ -35,6 +38,7 @@ const FieldMetadataSchema = z.object({ typeName: z.string().optional().describe("Type name for nested or enum fields"), }); +// strip unknown keys export const TailorFieldSchema = z.object({ type: TailorFieldTypeSchema.describe("Field data type"), metadata: FieldMetadataSchema.describe("Field metadata configuration"), diff --git a/packages/sdk/src/parser/service/idp/schema.ts b/packages/sdk/src/parser/service/idp/schema.ts index ab9570e8a..f79cba6ab 100644 --- a/packages/sdk/src/parser/service/idp/schema.ts +++ b/packages/sdk/src/parser/service/idp/schema.ts @@ -36,6 +36,7 @@ function normalizeIdPGqlOperations( export const IdPGqlOperationsSchema = z .union([ z.literal("query"), + // strip unknown keys z.object({ create: z.boolean().optional().describe("Enable _createUser mutation (default: true)"), update: z.boolean().optional().describe("Enable _updateUser mutation (default: true)"), @@ -63,6 +64,7 @@ export const IdPLangSchema = z.enum(["en", "ja"]).describe("IdP UI language"); const allowedReturnOriginPattern = /^(https?:\/\/[a-zA-Z0-9.-]+(:[0-9]+)?|[a-z0-9][a-z0-9-]{1,61}[a-z0-9]:url)$/; +// strip unknown keys export const IdPUserAuthPolicySchema = z .object({ useNonEmailIdentifier: z @@ -229,6 +231,7 @@ const emailFieldSchema = z .max(200, "must be 200 characters or less") .regex(/^[^\r\n]*$/, "must not contain newline characters"); +// strip unknown keys export const IdPEmailConfigSchema = z .object({ fromName: emailFieldSchema.optional().describe("Default sender display name for emails"), @@ -243,9 +246,13 @@ const IdPPermissionOperandSchema = z.union([ z.boolean(), z.array(z.string()).readonly(), z.array(z.boolean()).readonly(), + // strip unknown keys z.object({ user: z.string() }), + // strip unknown keys z.object({ idpUser: z.enum(["id", "name", "disabled"]) }), + // strip unknown keys z.object({ oldIdpUser: z.enum(["id", "name", "disabled"]) }), + // strip unknown keys z.object({ newIdpUser: z.enum(["id", "name", "disabled"]) }), ]); @@ -257,6 +264,7 @@ const IdPPermissionConditionSchema = z const IdPActionPermissionSchema = z.union([ // Object format: { conditions, description?, permit? } + // strip unknown keys z.object({ conditions: z.union([ IdPPermissionConditionSchema, @@ -291,6 +299,7 @@ const IdPActionPermissionSchema = z.union([ .readonly(), ]); +// strip unknown keys export const IdPPermissionSchema = z .object({ create: z.array(IdPActionPermissionSchema).readonly(), @@ -302,11 +311,12 @@ export const IdPPermissionSchema = z }) .describe("Per-operation permission policies for IdP users"); +// strip unknown keys export const IdPSchema = z .object({ name: z.string().describe("IdP service name"), authorization: z - .union([z.literal("insecure"), z.literal("loggedIn"), z.object({ cel: z.string() })]) + .union([z.literal("insecure"), z.literal("loggedIn"), z.strictObject({ cel: z.string() })]) .optional() .describe("Authorization mode for IdP API access"), clients: z.array(z.string()).describe("OAuth2 client names that can use this IdP"), diff --git a/packages/sdk/src/parser/service/resolver/schema.ts b/packages/sdk/src/parser/service/resolver/schema.ts index a6ac53a9e..d6a6ad217 100644 --- a/packages/sdk/src/parser/service/resolver/schema.ts +++ b/packages/sdk/src/parser/service/resolver/schema.ts @@ -7,15 +7,13 @@ export const QueryTypeSchema = z .union([z.literal("query"), z.literal("mutation")]) .describe("GraphQL operation type"); -export const ResolverSchema = z - .object({ - operation: QueryTypeSchema.describe("GraphQL operation type (query or mutation)"), - name: z.string().describe("Resolver name"), - description: z.string().optional().describe("Resolver description"), - input: z.record(z.string(), TailorFieldSchema).optional().describe("Input field definitions"), - body: functionSchema.describe("Resolver implementation function"), - output: TailorFieldSchema.describe("Output field definition"), - publishEvents: z.boolean().optional().describe("Enable publishing events from this resolver"), - invoker: AuthInvokerSchema.optional().describe("Machine user to execute this resolver as"), - }) - .strict(); +export const ResolverSchema = z.strictObject({ + operation: QueryTypeSchema.describe("GraphQL operation type (query or mutation)"), + name: z.string().describe("Resolver name"), + description: z.string().optional().describe("Resolver description"), + input: z.record(z.string(), TailorFieldSchema).optional().describe("Input field definitions"), + body: functionSchema.describe("Resolver implementation function"), + output: TailorFieldSchema.describe("Output field definition"), + publishEvents: z.boolean().optional().describe("Enable publishing events from this resolver"), + invoker: AuthInvokerSchema.optional().describe("Machine user to execute this resolver as"), +}); diff --git a/packages/sdk/src/parser/service/secrets/schema.ts b/packages/sdk/src/parser/service/secrets/schema.ts index 06d0f4f62..a52aeecc5 100644 --- a/packages/sdk/src/parser/service/secrets/schema.ts +++ b/packages/sdk/src/parser/service/secrets/schema.ts @@ -4,8 +4,10 @@ const namePattern = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/; const nameSchema = z.string().regex(namePattern); const secretsVaultSchema = z.record(nameSchema, z.string().nullish()); +// strip unknown keys export const SecretsSchema = z.object({ vaults: z.record(nameSchema, secretsVaultSchema), + // strip unknown keys options: z.object({ ignoreNullishValues: z.boolean(), }), diff --git a/packages/sdk/src/parser/service/staticwebsite/schema.ts b/packages/sdk/src/parser/service/staticwebsite/schema.ts index ee53f0de9..2ac7ce5b2 100644 --- a/packages/sdk/src/parser/service/staticwebsite/schema.ts +++ b/packages/sdk/src/parser/service/staticwebsite/schema.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +// strip unknown keys export const StaticWebsiteSchema = z .object({ name: z.string().describe("Static website name"), diff --git a/packages/sdk/src/parser/service/tailordb/schema.ts b/packages/sdk/src/parser/service/tailordb/schema.ts index f14db9bd4..cdcbe4664 100644 --- a/packages/sdk/src/parser/service/tailordb/schema.ts +++ b/packages/sdk/src/parser/service/tailordb/schema.ts @@ -25,6 +25,7 @@ function normalizeGqlOperations( export const GqlOperationsSchema = z .union([ z.literal("query"), + // strip unknown keys z.object({ create: z.boolean().optional().describe("Enable create mutation (default: true)"), update: z.boolean().optional().describe("Enable update mutation (default: true)"), @@ -54,11 +55,13 @@ const TailorFieldTypeSchema = z.enum([ "nested", ]); +// strip unknown keys const AllowedValueSchema = z.object({ value: z.string(), description: z.string().optional(), }); +// strip unknown keys export const DBFieldMetadataSchema = z.object({ required: z.boolean().optional().describe("Whether the field is required"), array: z.boolean().optional().describe("Whether the field is an array"), @@ -74,6 +77,7 @@ export const DBFieldMetadataSchema = z.object({ foreignKey: z.boolean().optional().describe("Whether the field is a foreign key"), foreignKeyType: z.string().optional().describe("Target type name for foreign key relations"), foreignKeyField: z.string().optional().describe("Target field name for foreign key relations"), + // strip unknown keys hooks: z .object({ create: functionSchema.optional().describe("Hook function called on record creation"), @@ -85,6 +89,7 @@ export const DBFieldMetadataSchema = z.object({ .array(z.union([functionSchema, z.tuple([functionSchema, z.string()])])) .optional() .describe("Validation functions for the field"), + // strip unknown keys serial: z .object({ start: z.number().describe("Starting value for the serial sequence"), @@ -104,8 +109,10 @@ export const DBFieldMetadataSchema = z.object({ const RelationTypeSchema = z.enum(relationTypesKeys); +// strip unknown keys export const RawRelationConfigSchema = z.object({ type: RelationTypeSchema.describe("Relation cardinality type"), + // strip unknown keys toward: z.object({ type: z.string().describe("Target type name, or 'self' for self-relations"), as: z.string().optional().describe("Custom forward relation name"), @@ -115,6 +122,7 @@ export const RawRelationConfigSchema = z.object({ }); const TailorDBFieldSchema: z.ZodType = z.lazy(() => + // strip unknown keys z.object({ type: TailorFieldTypeSchema, fields: z.record(z.string(), TailorDBFieldSchema).optional(), @@ -127,6 +135,7 @@ const TailorDBFieldSchema: z.ZodType = z.lazy(() => * Schema for TailorDB type settings. * Normalizes gqlOperations from alias ("query") to object format. */ +// strip unknown keys export const TailorDBTypeSettingsSchema = z.object({ pluralForm: z.string().optional().describe("Custom plural form of the type name for GraphQL"), aggregation: z.boolean().optional().describe("Enable aggregation queries for this type"), @@ -147,7 +156,7 @@ export const GQL_PERMISSION_INVALID_OPERAND_MESSAGE = const GqlPermissionOperandSchema = z.union( [ - z.object({ user: z.string() }).strict(), + z.strictObject({ user: z.string() }), z.string(), z.boolean(), z.array(z.string()), @@ -169,8 +178,11 @@ const GqlPermissionOperandSchema = z.union( const RecordPermissionOperandSchema = z.union([ GqlPermissionOperandSchema, + // strip unknown keys z.object({ record: z.string() }), + // strip unknown keys z.object({ oldRecord: z.string() }), + // strip unknown keys z.object({ newRecord: z.string() }), ]); @@ -186,6 +198,7 @@ const GqlPermissionConditionSchema = z const ActionPermissionSchema = z.union([ // Object format: { conditions, description?, permit? } + // strip unknown keys z.object({ conditions: z.union([ RecordPermissionConditionSchema, @@ -229,6 +242,7 @@ const GqlPermissionActionSchema = z.enum([ "bulkUpsert", ]); +// strip unknown keys const GqlPermissionPolicySchema = z.object({ conditions: z.array(GqlPermissionConditionSchema).readonly(), actions: z.union([z.literal("all"), z.array(GqlPermissionActionSchema).readonly()]), @@ -236,7 +250,9 @@ const GqlPermissionPolicySchema = z.object({ description: z.string().optional(), }); +// strip unknown keys export const RawPermissionsSchema = z.object({ + // strip unknown keys record: z .object({ create: z.array(ActionPermissionSchema).readonly(), @@ -248,9 +264,11 @@ export const RawPermissionsSchema = z.object({ gql: z.array(GqlPermissionPolicySchema).readonly().optional(), }); +// strip unknown keys export const TailorDBTypeSchema = z.object({ name: z.string(), fields: z.record(z.string(), TailorDBFieldSchema), + // strip unknown keys metadata: z.object({ name: z.string(), description: z.string().optional(), @@ -260,6 +278,7 @@ export const TailorDBTypeSchema = z.object({ indexes: z .record( z.string(), + // strip unknown keys z.object({ fields: z.array(z.string()), unique: z.boolean().optional(), @@ -269,6 +288,7 @@ export const TailorDBTypeSchema = z.object({ }), }); +// strip unknown keys const TailorDBMigrationConfigSchema = z.object({ directory: z.string().describe("Directory containing migration files"), machineUser: z.string().optional().describe("Machine user name for migration execution"), @@ -278,6 +298,7 @@ const TailorDBMigrationConfigSchema = z.object({ * Schema for TailorDB service configuration. * Normalizes gqlOperations from alias ("query") to object format. */ +// strip unknown keys export const TailorDBServiceConfigSchema = z.object({ files: z.array(z.string()).describe("Glob patterns for TailorDB type definition files"), ignores: z.array(z.string()).optional().describe("Glob patterns to exclude from type discovery"), diff --git a/packages/sdk/src/parser/service/workflow/schema.ts b/packages/sdk/src/parser/service/workflow/schema.ts index 3953bca7c..f470fe345 100644 --- a/packages/sdk/src/parser/service/workflow/schema.ts +++ b/packages/sdk/src/parser/service/workflow/schema.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { functionSchema } from "../common"; +// strip unknown keys export const WorkflowJobSchema = z.object({ name: z.string().describe("Job name (must be unique across the project)"), trigger: functionSchema.describe("Trigger function that initiates the job"), @@ -31,6 +32,7 @@ const durationSchema = (maxSeconds: number) => message: `Duration must be at most ${maxSeconds} seconds`, }); +// strip unknown keys export const RetryPolicySchema = z .object({ maxRetries: z.number().int().min(1).max(10).describe("Maximum number of retries (1-10)"), @@ -51,6 +53,7 @@ export const RetryPolicySchema = z path: ["initialBackoff"], }); +// strip unknown keys export const ConcurrencyPolicySchema = z.object({ maxConcurrentExecutions: z .number() @@ -60,6 +63,7 @@ export const ConcurrencyPolicySchema = z.object({ .describe("Maximum number of concurrent executions (1-1000)"), }); +// strip unknown keys export const WorkflowSchema = z.object({ name: z.string().describe("Workflow name"), mainJob: WorkflowJobSchema.describe("Main job that starts the workflow"), From b9045a0d1fc3ac111f3edbc659880add17716786 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 03:06:54 +0900 Subject: [PATCH 340/618] fix(sdk-codemod): handle shim source commands --- .../v2/rename-bin/scripts/transform.ts | 49 ++++++++++++++----- .../tests/source-js-string/expected.js | 2 + .../tests/source-js-string/input.js | 2 + .../tests/source-template/expected.ts | 3 ++ .../rename-bin/tests/source-template/input.ts | 3 ++ packages/sdk-codemod/src/registry.test.ts | 1 + packages/sdk-codemod/src/registry.ts | 6 ++- 7 files changed, 52 insertions(+), 14 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 1c0bcbb5c..9bffa670e 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -91,6 +91,7 @@ const TAILOR_CLI_VALUE_FLAGS = new Set([ "-f", "-n", ]); +const TAILOR_CLI_COMMAND_VALUE_FLAGS = new Set(["--value", "-v"]); const SOURCE_ESCAPED_QUOTED_VALUE = String.raw`\\"(?:\\\\.|[^"\\])*\\"|\\'(?:\\\\.|[^'\\])*\\'`; const SOURCE_CLI_ARG_VALUE = `(?:${SOURCE_ESCAPED_QUOTED_VALUE}|${SOURCE_ARG_VALUE})`; const SOURCE_COMMAND_GAP = `(?:\\s+--?[\\w-]+(?:=${SOURCE_CLI_ARG_VALUE})?(?:\\s+${SOURCE_CLI_ARG_VALUE})?)*`; @@ -107,6 +108,7 @@ const SOURCE_DIRECT_INVOCATION_LOOKAHEAD = `(?:${SOURCE_COMMAND_GAP}\\s+${TAILOR const SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD = `(?:${SOURCE_DIRECT_INVOCATION_LOOKAHEAD}|${SOURCE_TEMPLATE_DYNAMIC_ARGS}|\\s*$)`; const SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD = `(?:${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD}|\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})`; const SOURCE_DYNAMIC_OPTION_VALUE_LOOKAHEAD = `(?=\\s+${TAILOR_CLI_VALUE_FLAG}(?:=|\\s+)\\s*$)`; +const SOURCE_TAILOR_SDK_COMMAND = `tailor-sdk(?:(\\.(?:cmd|ps1|exe))|(@[^\\s'"\`;|&)]+))?(?![\\w-])`; const SOURCE_PKG_RUNNER_RE = new RegExp( `\\b(${PACKAGE_RUNNER_COMMAND}${SOURCE_RUNNER_OPTION_GAP})\\s+tailor-sdk(?![\\w-])(@[^\\s'"\`;|&)]+)?(?=${SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD})`, "g", @@ -122,15 +124,15 @@ const SOURCE_PACKAGE_FLAG_BINARY_RE = new RegExp( "g", ); const SOURCE_TAILOR_SDK_RE = new RegExp( - `(? { + ( + match: string, + shim: string | undefined, + version: string | undefined, + offset: number, + source: string, + ) => { if (isAfterOtherPackageRunner(source, offset)) return match; if (isAfterTemplatePlaceholder(source, offset)) return match; + if (shim != null) return `tailor${shim}`; return version ? `@tailor-platform/sdk${version}` : "tailor"; }, ); const withDynamicCommands = withCommands.replace( SOURCE_DYNAMIC_TAILOR_SDK_RE, - (_match, prefix: string, version?: string) => - version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}tailor`, + (_match, prefix: string, shim?: string, version?: string) => + shim != null + ? `${prefix}tailor${shim}` + : version + ? `${prefix}@tailor-platform/sdk${version}` + : `${prefix}tailor`, ); const updated = withDynamicCommands.replace( SOURCE_DYNAMIC_OPTION_TAILOR_SDK_RE, - (_match, prefix: string, version?: string) => - version ? `${prefix}@tailor-platform/sdk${version}` : `${prefix}tailor`, + (_match, prefix: string, shim?: string, version?: string) => + shim != null + ? `${prefix}tailor${shim}` + : version + ? `${prefix}@tailor-platform/sdk${version}` + : `${prefix}tailor`, ); return restoreSourceCliValueReferences(updated, protectedValue.protectedValues); } @@ -1094,18 +1111,26 @@ function isCliBinaryArgument(node: SgNode, source: string): boolean { } function arrayHasCliRenameLegacyArgs(elements: SgNode[], start: number, source: string): boolean { + let commandSeen = false; for (let index = start; index < elements.length; index += 1) { const value = sourceStringContent(elements[index]!, source) ?? sourceStringRawContent(elements[index]!, source); if (value == null) continue; - if (TAILOR_CLI_VALUE_FLAGS.has(value.split("=", 1)[0]!)) { + if (value === "--machineuser" || value.startsWith("--machineuser=")) return true; + if ( + TAILOR_CLI_VALUE_FLAGS.has(value.split("=", 1)[0]!) || + (commandSeen && TAILOR_CLI_COMMAND_VALUE_FLAGS.has(value.split("=", 1)[0]!)) + ) { if (!value.includes("=")) index += 1; continue; } - if (CLI_RENAME_LEGACY_RE.test(value)) return true; - if (value === "apply" || value === "crash-report") return true; - if (value === "--machineuser" || value.startsWith("--machineuser=")) return true; + if (value.startsWith("-")) continue; + if (!commandSeen) { + if (CLI_RENAME_LEGACY_RE.test(value)) return true; + if (value === "apply" || value === "crash-report") return true; + commandSeen = true; + } } return false; } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index a05ac08c3..2df9ce1d9 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -11,6 +11,8 @@ const shellSpawned = spawn("sh", ["-c", "tailor deploy"]); const applySpawned = spawn("tailor-sdk", ["apply"]); const cliRenameCommandSpawned = spawn("tailor-sdk", ["crash-report", "list"]); const cliRenameFlagSpawned = spawn("tailor-sdk", ["login", "--machineuser"]); +const secretValueApplySpawned = spawn("tailor", ["secret", "create", "--value", "apply"]); +const secretShortValueApplySpawned = spawn("tailor", ["secret", "create", "-v", "apply"]); const dynamicCliRenameCommandSpawned = spawn("tailor-sdk", [`${"apply"}`]); const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 34bb8ad06..7cb5b8a5e 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -11,6 +11,8 @@ const shellSpawned = spawn("sh", ["-c", "tailor-sdk deploy"]); const applySpawned = spawn("tailor-sdk", ["apply"]); const cliRenameCommandSpawned = spawn("tailor-sdk", ["crash-report", "list"]); const cliRenameFlagSpawned = spawn("tailor-sdk", ["login", "--machineuser"]); +const secretValueApplySpawned = spawn("tailor-sdk", ["secret", "create", "--value", "apply"]); +const secretShortValueApplySpawned = spawn("tailor-sdk", ["secret", "create", "-v", "apply"]); const dynamicCliRenameCommandSpawned = spawn("tailor-sdk", [`${"apply"}`]); const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index e3cf11350..1983fd5f8 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -12,9 +12,12 @@ const dirValue = "tailor staticwebsite deploy --name site --dir tailor-sdk"; const pathQualifiedArgValue = "./node_modules/.bin/tailor --arg \"tailor-sdk deploy\" deploy"; const pathQualifiedCliRenameCommand = "./node_modules/.bin/tailor-sdk apply"; const help = "tailor --help"; +const windowsShimDeploy = "tailor.cmd deploy"; +const pathQualifiedWindowsShimDeploy = "./node_modules/.bin/tailor.ps1 deploy"; const npxVersion = "npx @tailor-platform/sdk --version"; const generated = "Run tailor generate after changes"; const cliRenameCommand = "tailor-sdk crash-report list"; +const windowsShimCliRenameCommand = "tailor-sdk.cmd crash-report list"; const cliRenameFlag = "tailor-sdk login --machineuser"; const shellWrappedCliRenameCommand = "sh -c \"tailor-sdk apply\""; const dynamicCommand = `tailor ${subcommand}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index f0f2bde4d..99108e869 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -12,9 +12,12 @@ const dirValue = "tailor staticwebsite deploy --name site --dir tailor-sdk"; const pathQualifiedArgValue = "./node_modules/.bin/tailor-sdk --arg \"tailor-sdk deploy\" deploy"; const pathQualifiedCliRenameCommand = "./node_modules/.bin/tailor-sdk apply"; const help = "tailor-sdk --help"; +const windowsShimDeploy = "tailor-sdk.cmd deploy"; +const pathQualifiedWindowsShimDeploy = "./node_modules/.bin/tailor-sdk.ps1 deploy"; const npxVersion = "npx tailor-sdk --version"; const generated = "Run tailor-sdk generate after changes"; const cliRenameCommand = "tailor-sdk crash-report list"; +const windowsShimCliRenameCommand = "tailor-sdk.cmd crash-report list"; const cliRenameFlag = "tailor-sdk login --machineuser"; const shellWrappedCliRenameCommand = "sh -c \"tailor-sdk apply\""; const dynamicCommand = `tailor-sdk ${subcommand}`; diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 91d4b8cbe..8023d66c6 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -72,6 +72,7 @@ describe("getApplicableCodemods", () => { expect(matchesSourceStringPattern("tailor-sdk apply")).toBe(true); expect(matchesSourceStringPattern('sh -c "tailor-sdk apply"')).toBe(true); expect(matchesSourceStringPattern('bash -lc "tailor-sdk crash-report list"')).toBe(true); + expect(matchesSourceStringPattern("tailor-sdk.cmd crash-report list")).toBe(true); expect(matchesSourceStringPattern("tailor --profile tailor-sdk deploy")).toBe(false); expect(matchesSourceStringPattern("tailor --name tailor-sdk deploy")).toBe(false); expect(matchesSourceStringPattern('tailor --arg "tailor-sdk deploy" deploy')).toBe(false); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 09530677a..faabb0b7b 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -67,20 +67,22 @@ const RENAME_BIN_SOURCE_VALUE_GUARDS = RENAME_BIN_SOURCE_VALUE_FLAGS.flatMap((fl return [`(? Date: Sun, 28 Jun 2026 03:24:06 +0900 Subject: [PATCH 341/618] fix(sdk-codemod): guard package flag source values --- .../v2/rename-bin/scripts/transform.ts | 10 ++++++- .../tests/source-template/expected.ts | 2 ++ .../rename-bin/tests/source-template/input.ts | 2 ++ packages/sdk-codemod/src/runner.test.ts | 29 +++++++++++++++++++ packages/sdk-codemod/src/runner.ts | 2 +- 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 9bffa670e..f2e273ca6 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -469,6 +469,11 @@ function firstRunnerPackageToken(tokens: string[]): string | null { if (start == null) return null; for (let index = start; index < tokens.length; index += 1) { const token = tokens[index]!; + if (SOURCE_PACKAGE_FLAG_RE.test(token)) { + return token.includes("=") + ? sourceTokenValue(token.slice(token.indexOf("=") + 1)) + : (tokens[index + 1] ?? null); + } if (token.startsWith("-")) { if (skipsRunnerOptionValue(token)) index += 1; continue; @@ -528,7 +533,10 @@ function sourcePackageFlagsAllowBinaryRewrite(source: string): boolean { const token = tokens[index]!; if (SOURCE_PACKAGE_FLAG_RE.test(token)) { hasPackageFlag = true; - const value = token.includes("=") ? token.slice(token.indexOf("=") + 1) : tokens[index + 1]; + const rawValue = token.includes("=") + ? token.slice(token.indexOf("=") + 1) + : tokens[index + 1]; + const value = rawValue == null ? null : sourceTokenValue(rawValue); if (value != null && isTailorPackageValue(value)) { hasTailorPackageFlag = true; } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 1983fd5f8..b505b5401 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -65,6 +65,8 @@ const npxPackageSingleQuoted = "npx --package '@tailor-platform/sdk' tailor logi const npxPackageDoubleQuoted = "npx --package \"@tailor-platform/sdk\" tailor login"; const npxPackageEqualsDoubleQuoted = "npx --package=\"@tailor-platform/sdk\" tailor login"; const npxDynamicPackageFlag = `npx -p ${pkg} tailor-sdk login`; +const npxDynamicPackageFlagEquals = `npx --package=${pkg} tailor-sdk login`; +const npxOtherPackageFlagEqualsSource = "npx --package=dotenv-cli tailor-sdk login"; const npxPackageFlagDynamic = `npx -p @tailor-platform/sdk tailor ${subcommand}`; const npxPackageEqualsDynamic = `npx --package=@tailor-platform/sdk tailor ${subcommand}`; const npxRegistryValue = "npx --registry tailor-sdk @tailor-platform/sdk login"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 99108e869..6716aef90 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -65,6 +65,8 @@ const npxPackageSingleQuoted = "npx --package 'tailor-sdk' tailor-sdk login"; const npxPackageDoubleQuoted = "npx --package \"tailor-sdk\" tailor-sdk login"; const npxPackageEqualsDoubleQuoted = "npx --package=\"tailor-sdk\" tailor-sdk login"; const npxDynamicPackageFlag = `npx -p ${pkg} tailor-sdk login`; +const npxDynamicPackageFlagEquals = `npx --package=${pkg} tailor-sdk login`; +const npxOtherPackageFlagEqualsSource = "npx --package=dotenv-cli tailor-sdk login"; const npxPackageFlagDynamic = `npx -p tailor-sdk tailor-sdk ${subcommand}`; const npxPackageEqualsDynamic = `npx --package=tailor-sdk tailor-sdk ${subcommand}`; const npxRegistryValue = "npx --registry tailor-sdk tailor-sdk login"; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 3b2dd35ab..7ebd80bdf 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -626,6 +626,35 @@ describe("runCodemods", () => { expect(result.warnings).toEqual([]); }); + test("keeps shim and path Tailor option values out of rename-bin residual warnings", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "commands.ts"), + [ + 'spawn("tailor.cmd", ["--arg", "tailor-sdk deploy", "deploy"]);', + 'spawn("./node_modules/.bin/tailor", ["--arg", "tailor-sdk deploy", "deploy"]);', + ].join("\n"), + "utf-8", + ); + const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: renameBin?.sourceStringLegacyPatterns, + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toEqual([]); + }); + test("warns for split argv rename-bin residuals", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index e9d6ecb05..bf6031b67 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -83,7 +83,7 @@ const SOURCE_VALUE_FLAGS = new Set([ "-n", ]); const SOURCE_CLI_BINARY_RE = - /^(?:tailor|tailor-sdk(?:@[^\s'"`;|&)]+)?|@tailor-platform\/sdk(?:@[^\s'"`;|&)]+)?)$/; + /^(?:(?:.*[\\/])?tailor(?:\.(?:cmd|ps1|exe))?|(?:.*[\\/])?tailor-sdk(?:@[^\s'"`;|&)]+)?(?:\.(?:cmd|ps1|exe))?|@tailor-platform\/sdk(?:@[^\s'"`;|&)]+)?)$/; function shouldSkipDirectory(name: string): boolean { return EXCLUDE_DIRS.has(name) || (name.startsWith(".") && !ALLOWED_DOT_DIRS.has(name)); From b52444625cef5cc35c68acec88cb115cad924f0c Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 03:43:58 +0900 Subject: [PATCH 342/618] fix(sdk-codemod): preserve literal replacements --- .../codemods/v2/rename-bin/scripts/transform.ts | 7 +++---- .../v2/rename-bin/tests/source-template/expected.ts | 1 + .../codemods/v2/rename-bin/tests/source-template/input.ts | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index f2e273ca6..7f814159a 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -255,7 +255,7 @@ function protectSourceCliValueReferences(value: string): { function restoreSourceCliValueReferences(value: string, protectedValues: string[]): string { let restored = value; for (const [index, protectedValue] of protectedValues.entries()) { - restored = restored.replaceAll(`__TAILOR_SDK_SOURCE_VALUE_${index}__`, protectedValue); + restored = restored.replaceAll(`__TAILOR_SDK_SOURCE_VALUE_${index}__`, () => protectedValue); } return restored; } @@ -1233,8 +1233,7 @@ function templateSubstitutionsNeedCliRenameMigration( ): boolean { let restored = text; for (const substitution of substitutions) { - restored = restored.replaceAll( - substitution.placeholder, + restored = restored.replaceAll(substitution.placeholder, () => templateSubstitutionMigrationText(substitution.text), ); } @@ -1284,7 +1283,7 @@ function pushTemplateStringEdit( ? text : renameSourceCommandText(text); for (const substitution of substitutions) { - replacement = replacement.replaceAll(substitution.placeholder, substitution.text); + replacement = replacement.replaceAll(substitution.placeholder, () => substitution.text); } if (replacement !== source.slice(start, end)) { edits.push([start, end, replacement]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index b505b5401..f0396c7ae 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -80,6 +80,7 @@ const npxOtherPackageQuoted = "npx foo \"tailor-sdk login\""; const npxOtherPackageSingleQuoted = "npx foo 'tailor-sdk login'"; const shellWrapped = "sh -c \"tailor deploy\""; const escapedArg = "tailor --arg \"tailor-sdk deploy\" deploy"; +const dollarArg = "tailor --arg \"$& tailor-sdk deploy\" deploy"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; const mixedPackageAndCommand = "Install tailor-sdk before running tailor deploy"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 6716aef90..28f59e426 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -80,6 +80,7 @@ const npxOtherPackageQuoted = "npx foo \"tailor-sdk login\""; const npxOtherPackageSingleQuoted = "npx foo 'tailor-sdk login'"; const shellWrapped = "sh -c \"tailor-sdk deploy\""; const escapedArg = "tailor-sdk --arg \"tailor-sdk deploy\" deploy"; +const dollarArg = "tailor-sdk --arg \"$& tailor-sdk deploy\" deploy"; const packageName = "tailor-sdk"; const packageMessage = "package tailor-sdk is installed"; const mixedPackageAndCommand = "Install tailor-sdk before running tailor-sdk deploy"; From 68d0764fc3eb3b1d6aee6bf64585baf6db6d8361 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 04:05:52 +0900 Subject: [PATCH 343/618] fix(sdk-codemod): warn on variable source binaries --- packages/sdk-codemod/src/runner.test.ts | 28 ++++++ packages/sdk-codemod/src/runner.ts | 117 +++++++++++++++++++----- 2 files changed, 124 insertions(+), 21 deletions(-) diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 7ebd80bdf..6449af065 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -687,6 +687,34 @@ describe("runCodemods", () => { expect(result.warnings[0]).toContain("rule: test/rename-bin"); }); + test("warns for variable-backed rename-bin source residuals", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "commands.ts"), + ['const bin = "tailor-sdk";', 'spawn(bin, ["deploy"]);'].join("\n"), + "utf-8", + ); + const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: renameBin?.sourceStringLegacyPatterns, + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toHaveLength(1); + expect(result.warnings[0]).toContain("commands.ts: contains"); + expect(result.warnings[0]).toContain("rule: test/rename-bin"); + }); + test("warns for source comment and JSX rename-bin residuals", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index bf6031b67..986f325fb 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -183,24 +183,25 @@ function sourceStringContentForResidualMatching(relative: string, content: strin return null; } + const sourceStringVariables = collectSourceStringVariables(root, content); const fragments: string[] = []; const visit = (node: SgNode): void => { if (node.kind() === "arguments") { - const value = sourceArgumentsCommandContent(node, content); + const value = sourceArgumentsCommandContent(node, content, sourceStringVariables); if (value != null) fragments.push(value); } if (node.kind() === "array") { - const value = sourceArrayCommandContent(node, content); + const value = sourceArrayCommandContent(node, content, sourceStringVariables); if (value != null) fragments.push(value); } if (node.kind() === "string") { - if (isSourceTailorSdkValueArgument(node, content)) return; + if (isSourceTailorSdkValueArgument(node, content, sourceStringVariables)) return; const value = sourceStringNodeContent(node, content); if (value != null) fragments.push(value); return; } if (node.kind() === "string_fragment") { - if (isSourceTailorSdkValueArgument(node, content)) return; + if (isSourceTailorSdkValueArgument(node, content, sourceStringVariables)) return; fragments.push(node.text()); return; } @@ -212,6 +213,37 @@ function sourceStringContentForResidualMatching(relative: string, content: strin return fragments.join(SOURCE_STRING_FRAGMENT_SEPARATOR); } +function collectSourceStringVariables(root: SgNode, source: string): ReadonlyMap { + const values = new Map(); + const visit = (node: SgNode): void => { + if (node.kind() === "variable_declarator" && isConstVariableDeclarator(node)) { + const children = node.children(); + const identifier = children.find((child) => child.kind() === "identifier"); + const initializer = children.findLast( + (child) => sourceStringNodeContent(child, source) != null, + ); + if (identifier != null && initializer != null) { + const value = sourceStringNodeContent(initializer, source); + if (value != null) values.set(identifier.text(), value); + } + } + for (const child of node.children()) { + visit(child); + } + }; + visit(root); + return values; +} + +function isConstVariableDeclarator(node: SgNode): boolean { + return ( + node + .parent() + ?.children() + .some((child) => child.kind() === "const") ?? false + ); +} + function sourceTextContentForResidualMatching(relative: string, content: string): string | null { const ext = path.extname(relative).toLowerCase(); if (!SOURCE_EXTENSIONS.has(ext)) return null; @@ -237,37 +269,58 @@ function sourceTextContentForResidualMatching(relative: string, content: string) return fragments.join(SOURCE_STRING_FRAGMENT_SEPARATOR); } -function sourceArgumentsCommandContent(node: SgNode, source: string): string | null { +function sourceArgumentsCommandContent( + node: SgNode, + source: string, + sourceStringVariables: ReadonlyMap, +): string | null { const args = sourceArrayElements(node); - const executable = args[0] == null ? null : sourceStringNodeContent(args[0]!, source); + const executable = + args[0] == null ? null : sourceStringLikeNodeContent(args[0]!, source, sourceStringVariables); const argv = args[1]; if (executable == null || argv?.kind() !== "array") return null; - const values = sourceArrayCommandValues(argv, source); + const values = sourceArrayCommandValues(argv, source, sourceStringVariables); return values.length === 0 ? null : [executable, ...values].join(" "); } -function sourceArrayCommandContent(node: SgNode, source: string): string | null { - const values = sourceArrayCommandValues(node, source); +function sourceArrayCommandContent( + node: SgNode, + source: string, + sourceStringVariables: ReadonlyMap, +): string | null { + const values = sourceArrayCommandValues(node, source, sourceStringVariables); return values.length < 2 ? null : values.join(" "); } -function sourceArrayCommandValues(node: SgNode, source: string): string[] { +function sourceArrayCommandValues( + node: SgNode, + source: string, + sourceStringVariables: ReadonlyMap, +): string[] { const values: string[] = []; for (const element of sourceArrayElements(node)) { - if (isSourceValueArgument(element, source)) continue; - const value = sourceStringNodeContent(element, source); + if (isSourceValueArgument(element, source, sourceStringVariables)) continue; + const value = sourceStringLikeNodeContent(element, source, sourceStringVariables); if (value != null) values.push(value); } return values; } -function isSourceTailorSdkValueArgument(fragment: SgNode, source: string): boolean { +function isSourceTailorSdkValueArgument( + fragment: SgNode, + source: string, + sourceStringVariables: ReadonlyMap, +): boolean { const text = fragment.kind() === "string_fragment" ? fragment.text() - : sourceStringNodeContent(fragment, source); - return text != null && text.includes("tailor-sdk") && isSourceValueArgument(fragment, source); + : sourceStringLikeNodeContent(fragment, source, sourceStringVariables); + return ( + text != null && + text.includes("tailor-sdk") && + isSourceValueArgument(fragment, source, sourceStringVariables) + ); } function isSyntaxOnlyNode(node: SgNode): boolean { @@ -291,6 +344,16 @@ function nodeRangeKey(node: SgNode): string { return `${range.start.index}:${range.end.index}`; } +function sourceStringLikeNodeContent( + node: SgNode, + source: string, + sourceStringVariables: ReadonlyMap, +): string | null { + const directValue = sourceStringNodeContent(node, source); + if (directValue != null) return directValue; + return node.kind() === "identifier" ? (sourceStringVariables.get(node.text()) ?? null) : null; +} + function sourceStringNodeContent(node: SgNode, source: string): string | null { const kind = node.kind(); if (kind !== "string" && kind !== "template_string") return null; @@ -304,7 +367,11 @@ function sourceStringNodeContent(node: SgNode, source: string): string | null { return source.slice(range.start.index + 1, range.end.index - 1); } -function isSourceValueArgument(fragment: SgNode, source: string): boolean { +function isSourceValueArgument( + fragment: SgNode, + source: string, + sourceStringVariables: ReadonlyMap, +): boolean { const stringNode = fragment.kind() === "string_fragment" ? fragment.parent() : fragment; if (stringNode == null) return false; const parent = stringNode.parent(); @@ -313,9 +380,9 @@ function isSourceValueArgument(fragment: SgNode, source: string): boolean { const elements = sourceArrayElements(parent); const index = elements.findIndex((element) => nodeRangeKey(element) === nodeRangeKey(stringNode)); if (index <= 0) return false; - if (!isTailorCliArgumentArray(parent, index, source)) return false; + if (!isTailorCliArgumentArray(parent, index, source, sourceStringVariables)) return false; - const previous = sourceStringNodeContent(elements[index - 1]!, source); + const previous = sourceStringLikeNodeContent(elements[index - 1]!, source, sourceStringVariables); return ( previous != null && SOURCE_VALUE_FLAGS.has(previous.split("=", 1)[0]!) && @@ -323,17 +390,25 @@ function isSourceValueArgument(fragment: SgNode, source: string): boolean { ); } -function isTailorCliArgumentArray(arrayNode: SgNode, index: number, source: string): boolean { +function isTailorCliArgumentArray( + arrayNode: SgNode, + index: number, + source: string, + sourceStringVariables: ReadonlyMap, +): boolean { const argumentsNode = arrayNode.parent(); if (argumentsNode?.kind() === "arguments") { const callArgs = sourceArrayElements(argumentsNode); - const executable = callArgs[0] == null ? null : sourceStringNodeContent(callArgs[0]!, source); + const executable = + callArgs[0] == null + ? null + : sourceStringLikeNodeContent(callArgs[0]!, source, sourceStringVariables); if (executable != null && SOURCE_CLI_BINARY_RE.test(executable)) return true; } const elements = sourceArrayElements(arrayNode); return elements.slice(0, index).some((element) => { - const value = sourceStringNodeContent(element, source); + const value = sourceStringLikeNodeContent(element, source, sourceStringVariables); return value != null && SOURCE_CLI_BINARY_RE.test(value); }); } From c8c3e70615d40346cb9e7c5a2457e145990a4675 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 04:17:29 +0900 Subject: [PATCH 344/618] fix(sdk-codemod): preserve npm workspace exec runners --- .../codemods/v2/rename-bin/scripts/transform.ts | 2 ++ .../v2/rename-bin/tests/source-js-string/expected.js | 8 ++++++++ .../v2/rename-bin/tests/source-js-string/input.js | 8 ++++++++ .../v2/rename-bin/tests/source-template/expected.ts | 2 ++ .../codemods/v2/rename-bin/tests/source-template/input.ts | 2 ++ 5 files changed, 22 insertions(+) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 7f814159a..6a02ca503 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -8,7 +8,9 @@ const RUNNER_OPTION_VALUE_FLAG_LIST = [ "--cache", "--userconfig", "--prefix", + "--workspace", "--filter", + "-w", "-F", "--dir", "-C", diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 2df9ce1d9..0c442c691 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -53,6 +53,14 @@ const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor", "deploy"]); const pnpmExecDynamicSpawned = spawn("pnpm", ["exec", "tailor", subcommand]); const pnpmExecHelpSpawned = spawn("pnpm", ["exec", "tailor", "--help"]); const npmExecSpawned = spawn("npm", ["exec", "@tailor-platform/sdk", "login"]); +const npmExecWorkspaceSpawned = spawn("npm", ["-w", "app", "exec", "@tailor-platform/sdk", "login"]); +const npmExecLongWorkspaceSpawned = spawn("npm", [ + "--workspace", + "app", + "exec", + "@tailor-platform/sdk", + "login", +]); const npmExecPackageFlagSpawned = spawn("npm", ["exec", "--package", "@tailor-platform/sdk", "tailor", "login"]); const npmExecPackageEqualsSpawned = spawn("npm", ["exec", "--package=@tailor-platform/sdk", "tailor", "login"]); const pathQualifiedSpawned = spawn("./node_modules/.bin/tailor", ["deploy"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 7cb5b8a5e..5d5be705a 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -53,6 +53,14 @@ const pnpmExecSpawned = spawn("pnpm", ["exec", "tailor-sdk", "deploy"]); const pnpmExecDynamicSpawned = spawn("pnpm", ["exec", "tailor-sdk", subcommand]); const pnpmExecHelpSpawned = spawn("pnpm", ["exec", "tailor-sdk", "--help"]); const npmExecSpawned = spawn("npm", ["exec", "tailor-sdk", "login"]); +const npmExecWorkspaceSpawned = spawn("npm", ["-w", "app", "exec", "tailor-sdk", "login"]); +const npmExecLongWorkspaceSpawned = spawn("npm", [ + "--workspace", + "app", + "exec", + "tailor-sdk", + "login", +]); const npmExecPackageFlagSpawned = spawn("npm", ["exec", "--package", "tailor-sdk", "tailor-sdk", "login"]); const npmExecPackageEqualsSpawned = spawn("npm", ["exec", "--package=tailor-sdk", "tailor-sdk", "login"]); const pathQualifiedSpawned = spawn("./node_modules/.bin/tailor-sdk", ["deploy"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index f0396c7ae..badb02965 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -15,6 +15,8 @@ const help = "tailor --help"; const windowsShimDeploy = "tailor.cmd deploy"; const pathQualifiedWindowsShimDeploy = "./node_modules/.bin/tailor.ps1 deploy"; const npxVersion = "npx @tailor-platform/sdk --version"; +const npmExecWorkspace = "npm -w app exec @tailor-platform/sdk login"; +const npmExecLongWorkspace = "npm --workspace app exec @tailor-platform/sdk login"; const generated = "Run tailor generate after changes"; const cliRenameCommand = "tailor-sdk crash-report list"; const windowsShimCliRenameCommand = "tailor-sdk.cmd crash-report list"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 28f59e426..5449b9835 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -15,6 +15,8 @@ const help = "tailor-sdk --help"; const windowsShimDeploy = "tailor-sdk.cmd deploy"; const pathQualifiedWindowsShimDeploy = "./node_modules/.bin/tailor-sdk.ps1 deploy"; const npxVersion = "npx tailor-sdk --version"; +const npmExecWorkspace = "npm -w app exec tailor-sdk login"; +const npmExecLongWorkspace = "npm --workspace app exec tailor-sdk login"; const generated = "Run tailor-sdk generate after changes"; const cliRenameCommand = "tailor-sdk crash-report list"; const windowsShimCliRenameCommand = "tailor-sdk.cmd crash-report list"; From b78959700580c0d2a1db84995d3e300d135c01d0 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 04:40:30 +0900 Subject: [PATCH 345/618] fix(sdk-codemod): keep source argv aliases safe --- .../v2/rename-bin/scripts/transform.ts | 190 +++++++++++++----- .../tests/source-js-string/expected.js | 5 + .../tests/source-js-string/input.js | 5 + .../tests/source-template/expected.ts | 1 + .../rename-bin/tests/source-template/input.ts | 1 + 5 files changed, 157 insertions(+), 45 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 6a02ca503..29278862f 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -8,9 +8,7 @@ const RUNNER_OPTION_VALUE_FLAG_LIST = [ "--cache", "--userconfig", "--prefix", - "--workspace", "--filter", - "-w", "-F", "--dir", "-C", @@ -148,7 +146,7 @@ const SOURCE_EXEC_PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn"]); const SOURCE_PACKAGE_RUNNERS = new Set(["bunx", "npx"]); const SOURCE_DLX_PACKAGE_RUNNERS = new Set(["pnpm", "yarn"]); const SOURCE_NPM_EXEC_PACKAGE_RUNNERS = new Set(["npm"]); -const PACKAGE_MANAGER_OPTION_VALUE_FLAGS = new Set(RUNNER_OPTION_VALUE_FLAG_LIST); +const NPM_OPTION_VALUE_FLAGS = new Set(["--workspace", "-w"]); const SOURCE_PACKAGE_FLAG_RE = /^(?:-p|--package)(?:=.*)?$/; const NPX_OPTION_WITH_VALUE = "(?:--registry|--cache|--userconfig|--prefix)"; const NPX_PACKAGE_FLAG_CONTEXT_RE = new RegExp( @@ -342,8 +340,13 @@ function activeQuoteStart(source: string, start: number, offset: number): number return quote?.start ?? null; } -function skipsRunnerOptionValue(token: string): boolean { - return RUNNER_OPTION_VALUE_FLAGS.has(token.split("=", 1)[0]!) && !token.includes("="); +function skipsRunnerOptionValue(token: string, executable?: string): boolean { + const flag = token.split("=", 1)[0]!; + return ( + (RUNNER_OPTION_VALUE_FLAGS.has(flag) || + (executable === "npm" && NPM_OPTION_VALUE_FLAGS.has(flag))) && + !token.includes("=") + ); } function isPotentialValueFlag(value: string): boolean { @@ -363,7 +366,7 @@ function packageRunnerPackageStartTokenIndex(tokens: readonly string[]): number return index + 1; } if (token.startsWith("-")) { - if (skipsRunnerOptionValue(token)) index += 1; + if (skipsRunnerOptionValue(token, executable)) index += 1; continue; } return null; @@ -376,7 +379,7 @@ function packageRunnerPackageStartTokenIndex(tokens: readonly string[]): number return index + 1; } if (token.startsWith("-")) { - if (skipsRunnerOptionValue(token)) index += 1; + if (skipsRunnerOptionValue(token, executable)) index += 1; continue; } return null; @@ -427,7 +430,7 @@ function renameSourcePackageRunnerTokens(value: string): string { } if (token.startsWith("-")) { - if (skipsRunnerOptionValue(token)) index += 1; + if (skipsRunnerOptionValue(token, tokens[0])) index += 1; continue; } @@ -458,7 +461,7 @@ function hasPositionalPackageBeforeSourcePackageFlag( continue; } if (token.startsWith("-")) { - if (skipsRunnerOptionValue(token)) index += 1; + if (skipsRunnerOptionValue(token, tokens[0])) index += 1; continue; } return true; @@ -477,7 +480,7 @@ function firstRunnerPackageToken(tokens: string[]): string | null { : (tokens[index + 1] ?? null); } if (token.startsWith("-")) { - if (skipsRunnerOptionValue(token)) index += 1; + if (skipsRunnerOptionValue(token, tokens[0])) index += 1; continue; } return token; @@ -546,7 +549,7 @@ function sourcePackageFlagsAllowBinaryRewrite(source: string): boolean { continue; } if (token.startsWith("-")) { - if (skipsRunnerOptionValue(token)) index += 1; + if (skipsRunnerOptionValue(token, tokens[0])) index += 1; continue; } return false; @@ -789,6 +792,62 @@ function sourceStringRawContent(node: SgNode, source: string): string | null { return source.slice(range.start.index + 1, range.end.index - 1); } +function collectSourceStringVariables(root: SgNode, source: string): ReadonlyMap { + const values = new Map(); + const ambiguous = new Set(); + const visit = (node: SgNode): void => { + if (node.kind() === "variable_declarator" && isConstVariableDeclarator(node)) { + const children = node.children(); + const identifier = children.find((child) => child.kind() === "identifier"); + const initializer = children.findLast((child) => sourceStringContent(child, source) != null); + const value = initializer == null ? null : sourceStringContent(initializer, source); + if (identifier != null && value != null) { + rememberSourceStringVariable(values, ambiguous, identifier.text(), value); + } + } + for (const child of node.children()) { + visit(child); + } + }; + visit(root); + return values; +} + +function isConstVariableDeclarator(node: SgNode): boolean { + return ( + node + .parent() + ?.children() + .some((child) => child.kind() === "const") ?? false + ); +} + +function rememberSourceStringVariable( + values: Map, + ambiguous: Set, + name: string, + value: string, +): void { + if (ambiguous.has(name)) return; + if (values.has(name)) { + values.delete(name); + ambiguous.add(name); + return; + } + values.set(name, value); +} + +function sourceStaticStringContent( + node: SgNode, + source: string, + sourceStringVariables: ReadonlyMap, +): string | null { + return ( + sourceStringContent(node, source) ?? + (node.kind() === "identifier" ? (sourceStringVariables.get(node.text()) ?? null) : null) + ); +} + function isSyntaxOnlyNode(node: SgNode): boolean { const kind = node.kind(); return ( @@ -817,12 +876,17 @@ function callExpressionCalleeText(argumentsNode: SgNode): string | null { return callee?.text() ?? null; } -function firstNonOptionIndex(elements: SgNode[], start: number, source: string): number | null { +function firstNonOptionIndex( + elements: SgNode[], + start: number, + source: string, + executable: string, +): number | null { for (let index = start; index < elements.length; index += 1) { const value = sourceStringContent(elements[index]!, source); if (value == null) return null; if (!value.startsWith("-")) return index; - if (PACKAGE_MANAGER_OPTION_VALUE_FLAGS.has(value.split("=", 1)[0]!) && !value.includes("=")) { + if (skipsRunnerOptionValue(value, executable)) { index += 1; } } @@ -840,14 +904,14 @@ function packageRunnerPackageStartIndex( ): number | null { if (SOURCE_PACKAGE_RUNNERS.has(executable)) return 0; if (SOURCE_NPM_EXEC_PACKAGE_RUNNERS.has(executable)) { - const execIndex = firstNonOptionIndex(elements, 0, source); + const execIndex = firstNonOptionIndex(elements, 0, source, executable); if (execIndex == null || sourceStringContent(elements[execIndex]!, source) !== "exec") { return null; } return execIndex + 1; } if (!SOURCE_DLX_PACKAGE_RUNNERS.has(executable)) return null; - const dlxIndex = firstNonOptionIndex(elements, 0, source); + const dlxIndex = firstNonOptionIndex(elements, 0, source, executable); if (dlxIndex == null || sourceStringContent(elements[dlxIndex]!, source) !== "dlx") { return null; } @@ -859,13 +923,14 @@ function hasPackageFlagBeforeArrayPackage( index: number, source: string, start: number, + executable: string, ): boolean { for (let currentIndex = start; currentIndex < index; currentIndex += 1) { const value = sourceStringContent(elements[currentIndex]!, source); if (value == null) return false; if (SOURCE_PACKAGE_FLAG_RE.test(value)) return true; if (value.startsWith("-")) { - if (skipsRunnerOptionValue(value)) currentIndex += 1; + if (skipsRunnerOptionValue(value, executable)) currentIndex += 1; continue; } return false; @@ -884,6 +949,7 @@ function hasTailorPackageFlagBeforeArrayCommand( index: number, source: string, start: number, + executable: string, ): boolean { for (let currentIndex = start; currentIndex < index; currentIndex += 1) { const value = sourceStringContent(elements[currentIndex]!, source); @@ -899,7 +965,7 @@ function hasTailorPackageFlagBeforeArrayCommand( continue; } if (value.startsWith("-")) { - if (skipsRunnerOptionValue(value)) currentIndex += 1; + if (skipsRunnerOptionValue(value, executable)) currentIndex += 1; continue; } return false; @@ -912,13 +978,14 @@ function isSplitPackageFlagValue( index: number, source: string, start: number, + executable: string, ): boolean { const previous = elements[index - 1]; if (previous == null) return false; const previousValue = sourceStringContent(previous, source); return ( (previousValue === "-p" || previousValue === "--package") && - hasPackageFlagBeforeArrayPackage(elements, index, source, start) + hasPackageFlagBeforeArrayPackage(elements, index, source, start, executable) ); } @@ -943,7 +1010,9 @@ function sourcePackageFlagReplacement( const index = nodeIndex(elements, node); const start = packageRunnerPackageStartIndex(executable, elements, source); if (index < 0 || start == null) return null; - if (!hasPackageFlagBeforeArrayPackage(elements, index + 1, source, start)) return null; + if (!hasPackageFlagBeforeArrayPackage(elements, index + 1, source, start, executable)) { + return null; + } return { text, replacement: `${match.groups.prefix}${renamePackageName( @@ -952,12 +1021,17 @@ function sourcePackageFlagReplacement( }; } -function firstTailorPackageIndex(elements: SgNode[], start: number, source: string): number | null { +function firstTailorPackageIndex( + elements: SgNode[], + start: number, + source: string, + executable: string, +): number | null { for (let index = start; index < elements.length; index += 1) { const value = sourceStringContent(elements[index]!, source); if (value == null) return null; if (value.startsWith("-")) { - if (skipsRunnerOptionValue(value)) index += 1; + if (skipsRunnerOptionValue(value, executable)) index += 1; continue; } if (TAILOR_SDK_TOKEN_RE.test(value)) { @@ -986,13 +1060,13 @@ function isPackageRunnerArrayArgument(node: SgNode, source: string): boolean { if (executable === "bunx" || executable === "npx") return true; if (executable === "npm") { const elements = sourceArrayElements(parent); - const execIndex = firstNonOptionIndex(elements, 0, source); + const execIndex = firstNonOptionIndex(elements, 0, source, executable); return execIndex != null && sourceStringContent(elements[execIndex]!, source) === "exec"; } if (executable !== "pnpm" && executable !== "yarn") return false; const elements = sourceArrayElements(parent); - const dlxIndex = firstNonOptionIndex(elements, 0, source); + const dlxIndex = firstNonOptionIndex(elements, 0, source, executable); return dlxIndex != null && sourceStringContent(elements[dlxIndex]!, source) === "dlx"; } @@ -1014,14 +1088,18 @@ function isPackageRunnerPackageArgument(node: SgNode, source: string): boolean { const start = packageRunnerPackageStartIndex(executable, elements, source); if (start == null) return false; - if (hasPackageFlagBeforeArrayPackage(elements, index, source, start)) { - return isSplitPackageFlagValue(elements, index, source, start); + if (hasPackageFlagBeforeArrayPackage(elements, index, source, start, executable)) { + return isSplitPackageFlagValue(elements, index, source, start, executable); } - return firstTailorPackageIndex(elements, start, source) === index; + return firstTailorPackageIndex(elements, start, source, executable) === index; } -function isPackageRunnerCommandBinaryArgument(node: SgNode, source: string): boolean { +function isPackageRunnerCommandBinaryArgument( + node: SgNode, + source: string, + sourceStringVariables: ReadonlyMap, +): boolean { const parent = node.parent(); if (parent?.kind() !== "array" || !isPackageRunnerArrayArgument(node, source)) return false; @@ -1037,18 +1115,23 @@ function isPackageRunnerCommandBinaryArgument(node: SgNode, source: string): boo const executable = sourceStringContent(executableNode, source); if (executable == null) return false; const start = packageRunnerPackageStartIndex(executable, elements, source); - if (start == null || !hasTailorPackageFlagBeforeArrayCommand(elements, index, source, start)) { + if ( + start == null || + !hasTailorPackageFlagBeforeArrayCommand(elements, index, source, start, executable) + ) { + return false; + } + if (arrayHasCliRenameLegacyArgs(elements, index + 1, source, sourceStringVariables)) { return false; } - if (arrayHasCliRenameLegacyArgs(elements, index + 1, source)) return false; - const commandIndex = packageRunnerCommandIndex(elements, start, source); + const commandIndex = packageRunnerCommandIndex(elements, start, source, executable); if (commandIndex !== index) return false; const text = sourceStringContent(node, source); return ( text != null && TAILOR_SDK_TOKEN_RE.test(text) && - !isSplitPackageFlagValue(elements, index, source, start) + !isSplitPackageFlagValue(elements, index, source, start, executable) ); } @@ -1056,6 +1139,7 @@ function packageRunnerCommandIndex( elements: SgNode[], start: number, source: string, + executable: string, ): number | null { for (let index = start; index < elements.length; index += 1) { const value = sourceStringContent(elements[index]!, source); @@ -1065,7 +1149,7 @@ function packageRunnerCommandIndex( continue; } if (value.startsWith("-")) { - if (skipsRunnerOptionValue(value)) index += 1; + if (skipsRunnerOptionValue(value, executable)) index += 1; continue; } return index; @@ -1073,7 +1157,11 @@ function packageRunnerCommandIndex( return null; } -function isPackageManagerExecBinaryArgument(node: SgNode, source: string): boolean { +function isPackageManagerExecBinaryArgument( + node: SgNode, + source: string, + sourceStringVariables: ReadonlyMap, +): boolean { const parent = node.parent(); if (parent?.kind() !== "array") return false; @@ -1092,22 +1180,26 @@ function isPackageManagerExecBinaryArgument(node: SgNode, source: string): boole const index = nodeIndex(elements, node); if (index < 0) return false; - const execIndex = firstNonOptionIndex(elements, 0, source); + const execIndex = firstNonOptionIndex(elements, 0, source, executable); if (execIndex == null || sourceStringContent(elements[execIndex]!, source) !== "exec") { return false; } return ( - firstNonOptionIndex(elements, execIndex + 1, source) === index && - !arrayHasCliRenameLegacyArgs(elements, index + 1, source) + firstNonOptionIndex(elements, execIndex + 1, source, executable) === index && + !arrayHasCliRenameLegacyArgs(elements, index + 1, source, sourceStringVariables) ); } -function isCliBinaryArgument(node: SgNode, source: string): boolean { +function isCliBinaryArgument( + node: SgNode, + source: string, + sourceStringVariables: ReadonlyMap, +): boolean { const parent = node.parent(); if (parent?.kind() === "array") { - if (isPackageRunnerCommandBinaryArgument(node, source)) return true; + if (isPackageRunnerCommandBinaryArgument(node, source, sourceStringVariables)) return true; if (isPackageRunnerArrayArgument(node, source)) return false; - return isPackageManagerExecBinaryArgument(node, source); + return isPackageManagerExecBinaryArgument(node, source, sourceStringVariables); } if (parent?.kind() !== "arguments") return false; @@ -1116,15 +1208,21 @@ function isCliBinaryArgument(node: SgNode, source: string): boolean { if (args[0] == null || nodeRangeKey(args[0]) !== nodeRangeKey(node)) return false; const argv = args[1]; return ( - argv?.kind() !== "array" || !arrayHasCliRenameLegacyArgs(sourceArrayElements(argv), 0, source) + argv?.kind() !== "array" || + !arrayHasCliRenameLegacyArgs(sourceArrayElements(argv), 0, source, sourceStringVariables) ); } -function arrayHasCliRenameLegacyArgs(elements: SgNode[], start: number, source: string): boolean { +function arrayHasCliRenameLegacyArgs( + elements: SgNode[], + start: number, + source: string, + sourceStringVariables: ReadonlyMap, +): boolean { let commandSeen = false; for (let index = start; index < elements.length; index += 1) { const value = - sourceStringContent(elements[index]!, source) ?? + sourceStaticStringContent(elements[index]!, source, sourceStringVariables) ?? sourceStringRawContent(elements[index]!, source); if (value == null) continue; if (value === "--machineuser" || value.startsWith("--machineuser=")) return true; @@ -1191,6 +1289,7 @@ function pushSourceStringEdit( edits: Array<[number, number, string]>, source: string, node: SgNode, + sourceStringVariables: ReadonlyMap, ): void { const range = node.range(); const start = range.start.index + 1; @@ -1203,7 +1302,7 @@ function pushSourceStringEdit( : TAILOR_SDK_TOKEN_RE.test(text) && isPackageRunnerPackageArgument(node, source) ? renamePackageName(text) : (TAILOR_SDK_TOKEN_RE.test(text) || TAILOR_SDK_PATH_RE.test(text)) && - isCliBinaryArgument(node, source) + isCliBinaryArgument(node, source, sourceStringVariables) ? renameBinary(text) : isCliValueArgument(node, source) ? text @@ -1309,6 +1408,7 @@ function transformSourceFile(source: string, filePath: string): string | null { return null; } + const sourceStringVariables = collectSourceStringVariables(root, source); const edits: Array<[number, number, string]> = []; const visit = (node: SgNode): void => { const kind = node.kind(); @@ -1320,7 +1420,7 @@ function transformSourceFile(source: string, filePath: string): string | null { } if (kind === "string") { - pushSourceStringEdit(edits, source, node); + pushSourceStringEdit(edits, source, node, sourceStringVariables); return; } @@ -1329,7 +1429,7 @@ function transformSourceFile(source: string, filePath: string): string | null { .children() .some((child: SgNode) => child.kind() === "template_substitution"); if (!hasSubstitution) { - pushSourceStringEdit(edits, source, node); + pushSourceStringEdit(edits, source, node, sourceStringVariables); return; } if (isCliValueArgument(node, source)) return; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 0c442c691..28890e122 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -8,9 +8,13 @@ const inlineTemplateArgSpawned = spawn("tailor", [`--arg=tailor-sdk ${cmd}`, "de const nameValueSpawned = spawn("tailor", ["tailordb", "migration", "generate", "--name", "tailor-sdk deploy"]); const directNameValueSpawned = spawn("tailor", ["tailordb", "migration", "generate", "--name", "tailor-sdk deploy"]); const shellSpawned = spawn("sh", ["-c", "tailor deploy"]); +const legacyApplyArg = "apply"; +const legacyMachineUserArg = "--machineuser"; const applySpawned = spawn("tailor-sdk", ["apply"]); +const applyAliasSpawned = spawn("tailor-sdk", [legacyApplyArg]); const cliRenameCommandSpawned = spawn("tailor-sdk", ["crash-report", "list"]); const cliRenameFlagSpawned = spawn("tailor-sdk", ["login", "--machineuser"]); +const cliRenameFlagAliasSpawned = spawn("tailor-sdk", ["login", legacyMachineUserArg]); const secretValueApplySpawned = spawn("tailor", ["secret", "create", "--value", "apply"]); const secretShortValueApplySpawned = spawn("tailor", ["secret", "create", "-v", "apply"]); const dynamicCliRenameCommandSpawned = spawn("tailor-sdk", [`${"apply"}`]); @@ -45,6 +49,7 @@ const pnpmDlxOtherPackageSpawned = spawn("pnpm", ["dlx", "foo", "tailor-sdk", "l const pnpmDlxOtherPackageFlagSpawned = spawn("pnpm", ["dlx", "foo", "-p", "tailor-sdk", "tailor-sdk", "login"]); const pnpmDlxOptionSpawned = spawn("pnpm", ["--silent", "dlx", "@tailor-platform/sdk", "login"]); const pnpmDlxSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "dlx", "@tailor-platform/sdk", "login"]); +const pnpmDlxWorkspaceRootSpawned = spawn("pnpm", ["-w", "dlx", "@tailor-platform/sdk", "login"]); const pnpmDlxRegistrySpawned = spawn("pnpm", ["--registry", registry, "dlx", "@tailor-platform/sdk", "login"]); const pnpmExecSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "exec", "tailor", "deploy"]); const yarnDlxOptionSpawned = spawn("yarn", ["--quiet", "dlx", "@tailor-platform/sdk", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 5d5be705a..5096be463 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -8,9 +8,13 @@ const inlineTemplateArgSpawned = spawn("tailor-sdk", [`--arg=tailor-sdk ${cmd}`, const nameValueSpawned = spawn("tailor", ["tailordb", "migration", "generate", "--name", "tailor-sdk deploy"]); const directNameValueSpawned = spawn("tailor-sdk", ["tailordb", "migration", "generate", "--name", "tailor-sdk deploy"]); const shellSpawned = spawn("sh", ["-c", "tailor-sdk deploy"]); +const legacyApplyArg = "apply"; +const legacyMachineUserArg = "--machineuser"; const applySpawned = spawn("tailor-sdk", ["apply"]); +const applyAliasSpawned = spawn("tailor-sdk", [legacyApplyArg]); const cliRenameCommandSpawned = spawn("tailor-sdk", ["crash-report", "list"]); const cliRenameFlagSpawned = spawn("tailor-sdk", ["login", "--machineuser"]); +const cliRenameFlagAliasSpawned = spawn("tailor-sdk", ["login", legacyMachineUserArg]); const secretValueApplySpawned = spawn("tailor-sdk", ["secret", "create", "--value", "apply"]); const secretShortValueApplySpawned = spawn("tailor-sdk", ["secret", "create", "-v", "apply"]); const dynamicCliRenameCommandSpawned = spawn("tailor-sdk", [`${"apply"}`]); @@ -45,6 +49,7 @@ const pnpmDlxOtherPackageSpawned = spawn("pnpm", ["dlx", "foo", "tailor-sdk", "l const pnpmDlxOtherPackageFlagSpawned = spawn("pnpm", ["dlx", "foo", "-p", "tailor-sdk", "tailor-sdk", "login"]); const pnpmDlxOptionSpawned = spawn("pnpm", ["--silent", "dlx", "tailor-sdk", "login"]); const pnpmDlxSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "dlx", "tailor-sdk", "login"]); +const pnpmDlxWorkspaceRootSpawned = spawn("pnpm", ["-w", "dlx", "tailor-sdk", "login"]); const pnpmDlxRegistrySpawned = spawn("pnpm", ["--registry", registry, "dlx", "tailor-sdk", "login"]); const pnpmExecSplitOptionSpawned = spawn("pnpm", ["--filter", "app", "exec", "tailor-sdk", "deploy"]); const yarnDlxOptionSpawned = spawn("yarn", ["--quiet", "dlx", "tailor-sdk", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index badb02965..1175e5605 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -48,6 +48,7 @@ const dynamicBunxCommand = `bunx @tailor-platform/sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx @tailor-platform/sdk ${subcommand}`; const pnpmDlxWithOption = "pnpm --silent dlx @tailor-platform/sdk login"; const pnpmDlxWithOptionValue = "pnpm --filter app dlx @tailor-platform/sdk login"; +const pnpmWorkspaceRootDlx = "pnpm -w dlx @tailor-platform/sdk login"; const pnpmFilterNamedDlxCommand = "pnpm --filter dlx tailor deploy"; const pnpmDlxAfterFilterNamedDlx = "pnpm --filter dlx dlx @tailor-platform/sdk login"; const dynamicPnpmDlxWithOptionValue = `pnpm --filter ${app} dlx @tailor-platform/sdk login`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 5449b9835..534cd5acc 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -48,6 +48,7 @@ const dynamicBunxCommand = `bunx tailor-sdk ${subcommand}`; const dynamicDlxCommand = `pnpm dlx tailor-sdk ${subcommand}`; const pnpmDlxWithOption = "pnpm --silent dlx tailor-sdk login"; const pnpmDlxWithOptionValue = "pnpm --filter app dlx tailor-sdk login"; +const pnpmWorkspaceRootDlx = "pnpm -w dlx tailor-sdk login"; const pnpmFilterNamedDlxCommand = "pnpm --filter dlx tailor-sdk deploy"; const pnpmDlxAfterFilterNamedDlx = "pnpm --filter dlx dlx tailor-sdk login"; const dynamicPnpmDlxWithOptionValue = `pnpm --filter ${app} dlx tailor-sdk login`; From 45a0e0d48de646d2dd8a6483635254870c056946 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 04:58:44 +0900 Subject: [PATCH 346/618] fix(sdk-codemod): keep source runner aliases safe --- .../v2/rename-bin/scripts/transform.ts | 50 +++++++++++++++---- .../tests/source-js-string/expected.js | 10 ++++ .../tests/source-js-string/input.js | 10 ++++ .../tests/source-template/expected.ts | 4 ++ .../rename-bin/tests/source-template/input.ts | 4 ++ packages/sdk-codemod/src/registry.ts | 2 +- packages/sdk-codemod/src/runner.test.ts | 5 +- 7 files changed, 74 insertions(+), 11 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 29278862f..22d75daa7 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -95,7 +95,7 @@ const TAILOR_CLI_COMMAND_VALUE_FLAGS = new Set(["--value", "-v"]); const SOURCE_ESCAPED_QUOTED_VALUE = String.raw`\\"(?:\\\\.|[^"\\])*\\"|\\'(?:\\\\.|[^'\\])*\\'`; const SOURCE_CLI_ARG_VALUE = `(?:${SOURCE_ESCAPED_QUOTED_VALUE}|${SOURCE_ARG_VALUE})`; const SOURCE_COMMAND_GAP = `(?:\\s+--?[\\w-]+(?:=${SOURCE_CLI_ARG_VALUE})?(?:\\s+${SOURCE_CLI_ARG_VALUE})?)*`; -const SOURCE_RUNNER_OPTION_GAP = `(?:\\s+(?:-\\w+|--\\w[\\w-]*)(?:=${SOURCE_ARG_VALUE})?(?:\\s+(?!tailor-sdk(?![\\w-])|-)${SOURCE_ARG_VALUE})?)*`; +const SOURCE_RUNNER_OPTION_GAP = `(?:\\s+${PACKAGE_RUNNER_OPTION})*`; const SOURCE_OPTION_VALUE_REFERENCE_RE = new RegExp( `((?:--[\\w-]+|-\\w)(?:=|\\s+))(${SOURCE_CLI_ARG_VALUE})`, "g", @@ -799,8 +799,10 @@ function collectSourceStringVariables(root: SgNode, source: string): ReadonlyMap if (node.kind() === "variable_declarator" && isConstVariableDeclarator(node)) { const children = node.children(); const identifier = children.find((child) => child.kind() === "identifier"); - const initializer = children.findLast((child) => sourceStringContent(child, source) != null); - const value = initializer == null ? null : sourceStringContent(initializer, source); + const initializer = children.findLast( + (child) => sourceConstInitializerContent(child, source) != null, + ); + const value = initializer == null ? null : sourceConstInitializerContent(initializer, source); if (identifier != null && value != null) { rememberSourceStringVariable(values, ambiguous, identifier.text(), value); } @@ -813,6 +815,23 @@ function collectSourceStringVariables(root: SgNode, source: string): ReadonlyMap return values; } +function sourceConstInitializerContent(node: SgNode, source: string): string | null { + const directValue = sourceStringContent(node, source); + if (directValue != null) return directValue; + if ( + node.kind() !== "as_expression" && + node.kind() !== "satisfies_expression" && + node.kind() !== "parenthesized_expression" + ) { + return null; + } + for (const child of node.children()) { + const childValue = sourceConstInitializerContent(child, source); + if (childValue != null) return childValue; + } + return null; +} + function isConstVariableDeclarator(node: SgNode): boolean { return ( node @@ -829,7 +848,9 @@ function rememberSourceStringVariable( value: string, ): void { if (ambiguous.has(name)) return; - if (values.has(name)) { + const existingValue = values.get(name); + if (existingValue != null) { + if (existingValue === value) return; values.delete(name); ambiguous.add(name); return; @@ -1331,24 +1352,31 @@ function templateSubstitutionPlaceholder( function templateSubstitutionsNeedCliRenameMigration( text: string, substitutions: ReadonlyArray<{ placeholder: string; text: string }>, + sourceStringVariables: ReadonlyMap, ): boolean { let restored = text; for (const substitution of substitutions) { restored = restored.replaceAll(substitution.placeholder, () => - templateSubstitutionMigrationText(substitution.text), + templateSubstitutionMigrationText(substitution.text, sourceStringVariables), ); } return needsCliRenameMigration(restored); } -function templateSubstitutionMigrationText(value: string): string { - return value.startsWith("${") && value.endsWith("}") ? value.slice(2, -1).trim() : value; +function templateSubstitutionMigrationText( + value: string, + sourceStringVariables: ReadonlyMap, +): string { + if (!value.startsWith("${") || !value.endsWith("}")) return value; + const expression = value.slice(2, -1).trim(); + return sourceStringVariables.get(expression) ?? expression; } function pushTemplateStringEdit( edits: Array<[number, number, string]>, source: string, node: SgNode, + sourceStringVariables: ReadonlyMap, ): void { const range = node.range(); const start = range.start.index + 1; @@ -1380,7 +1408,11 @@ function pushTemplateStringEdit( text = `${text.slice(0, childStart)}${placeholder}${text.slice(childEnd)}`; } - let replacement = templateSubstitutionsNeedCliRenameMigration(text, substitutions) + let replacement = templateSubstitutionsNeedCliRenameMigration( + text, + substitutions, + sourceStringVariables, + ) ? text : renameSourceCommandText(text); for (const substitution of substitutions) { @@ -1433,7 +1465,7 @@ function transformSourceFile(source: string, filePath: string): string | null { return; } if (isCliValueArgument(node, source)) return; - pushTemplateStringEdit(edits, source, node); + pushTemplateStringEdit(edits, source, node, sourceStringVariables); return; } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 28890e122..2bf4daf55 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -10,14 +10,24 @@ const directNameValueSpawned = spawn("tailor", ["tailordb", "migration", "genera const shellSpawned = spawn("sh", ["-c", "tailor deploy"]); const legacyApplyArg = "apply"; const legacyMachineUserArg = "--machineuser"; +const legacyTemplateArg = "apply"; const applySpawned = spawn("tailor-sdk", ["apply"]); const applyAliasSpawned = spawn("tailor-sdk", [legacyApplyArg]); +function duplicateAliasOne() { + const duplicateLegacyArg = "apply"; + return spawn("tailor-sdk", [duplicateLegacyArg]); +} +function duplicateAliasTwo() { + const duplicateLegacyArg = "apply"; + return spawn("tailor-sdk", [duplicateLegacyArg]); +} const cliRenameCommandSpawned = spawn("tailor-sdk", ["crash-report", "list"]); const cliRenameFlagSpawned = spawn("tailor-sdk", ["login", "--machineuser"]); const cliRenameFlagAliasSpawned = spawn("tailor-sdk", ["login", legacyMachineUserArg]); const secretValueApplySpawned = spawn("tailor", ["secret", "create", "--value", "apply"]); const secretShortValueApplySpawned = spawn("tailor", ["secret", "create", "-v", "apply"]); const dynamicCliRenameCommandSpawned = spawn("tailor-sdk", [`${"apply"}`]); +const dynamicCliRenameAliasCommand = `tailor-sdk ${legacyTemplateArg}`; const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "dev", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 5096be463..de21848e9 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -10,14 +10,24 @@ const directNameValueSpawned = spawn("tailor-sdk", ["tailordb", "migration", "ge const shellSpawned = spawn("sh", ["-c", "tailor-sdk deploy"]); const legacyApplyArg = "apply"; const legacyMachineUserArg = "--machineuser"; +const legacyTemplateArg = "apply"; const applySpawned = spawn("tailor-sdk", ["apply"]); const applyAliasSpawned = spawn("tailor-sdk", [legacyApplyArg]); +function duplicateAliasOne() { + const duplicateLegacyArg = "apply"; + return spawn("tailor-sdk", [duplicateLegacyArg]); +} +function duplicateAliasTwo() { + const duplicateLegacyArg = "apply"; + return spawn("tailor-sdk", [duplicateLegacyArg]); +} const cliRenameCommandSpawned = spawn("tailor-sdk", ["crash-report", "list"]); const cliRenameFlagSpawned = spawn("tailor-sdk", ["login", "--machineuser"]); const cliRenameFlagAliasSpawned = spawn("tailor-sdk", ["login", legacyMachineUserArg]); const secretValueApplySpawned = spawn("tailor-sdk", ["secret", "create", "--value", "apply"]); const secretShortValueApplySpawned = spawn("tailor-sdk", ["secret", "create", "-v", "apply"]); const dynamicCliRenameCommandSpawned = spawn("tailor-sdk", [`${"apply"}`]); +const dynamicCliRenameAliasCommand = `tailor-sdk ${legacyTemplateArg}`; const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["tailor-sdk", "--profile", "dev", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 1175e5605..7ccee763c 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -19,6 +19,8 @@ const npmExecWorkspace = "npm -w app exec @tailor-platform/sdk login"; const npmExecLongWorkspace = "npm --workspace app exec @tailor-platform/sdk login"; const generated = "Run tailor generate after changes"; const cliRenameCommand = "tailor-sdk crash-report list"; +const legacyConstApplyArg = "apply" as const; +const applyConstAliasSpawned = spawn("tailor-sdk", [legacyConstApplyArg]); const windowsShimCliRenameCommand = "tailor-sdk.cmd crash-report list"; const cliRenameFlag = "tailor-sdk login --machineuser"; const shellWrappedCliRenameCommand = "sh -c \"tailor-sdk apply\""; @@ -41,6 +43,7 @@ const dynamicNpxRegistryCommand = `npx --registry ${registry} @tailor-platform/s const dynamicRunnerCommand = `${runner} tailor-sdk login`; const literalPlaceholderCommand = `prefix __TAILOR_SDK_TEMPLATE_EXPR_0__ tailor-sdk ${subcommand}`; const npxOtherPackage = "npx foo tailor-sdk login"; +const npxBooleanOtherPackage = "npx --yes foo tailor-sdk login"; const npxOtherPackageFlag = "npx foo -p tailor-sdk tailor-sdk login"; const npxOtherPackageFlagEquals = "npx foo --package=tailor-sdk tailor-sdk login"; const npxQuotedPackage = "npx \"@tailor-platform/sdk\" login"; @@ -51,6 +54,7 @@ const pnpmDlxWithOptionValue = "pnpm --filter app dlx @tailor-platform/sdk login const pnpmWorkspaceRootDlx = "pnpm -w dlx @tailor-platform/sdk login"; const pnpmFilterNamedDlxCommand = "pnpm --filter dlx tailor deploy"; const pnpmDlxAfterFilterNamedDlx = "pnpm --filter dlx dlx @tailor-platform/sdk login"; +const pnpmDlxBooleanOtherPackage = "pnpm dlx --yes foo tailor-sdk login"; const dynamicPnpmDlxWithOptionValue = `pnpm --filter ${app} dlx @tailor-platform/sdk login`; const yarnDlxWithOption = "yarn --quiet dlx @tailor-platform/sdk login"; const nestedCommand = `run ${"tailor deploy"}`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 534cd5acc..09007eb1a 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -19,6 +19,8 @@ const npmExecWorkspace = "npm -w app exec tailor-sdk login"; const npmExecLongWorkspace = "npm --workspace app exec tailor-sdk login"; const generated = "Run tailor-sdk generate after changes"; const cliRenameCommand = "tailor-sdk crash-report list"; +const legacyConstApplyArg = "apply" as const; +const applyConstAliasSpawned = spawn("tailor-sdk", [legacyConstApplyArg]); const windowsShimCliRenameCommand = "tailor-sdk.cmd crash-report list"; const cliRenameFlag = "tailor-sdk login --machineuser"; const shellWrappedCliRenameCommand = "sh -c \"tailor-sdk apply\""; @@ -41,6 +43,7 @@ const dynamicNpxRegistryCommand = `npx --registry ${registry} tailor-sdk login`; const dynamicRunnerCommand = `${runner} tailor-sdk login`; const literalPlaceholderCommand = `prefix __TAILOR_SDK_TEMPLATE_EXPR_0__ tailor-sdk ${subcommand}`; const npxOtherPackage = "npx foo tailor-sdk login"; +const npxBooleanOtherPackage = "npx --yes foo tailor-sdk login"; const npxOtherPackageFlag = "npx foo -p tailor-sdk tailor-sdk login"; const npxOtherPackageFlagEquals = "npx foo --package=tailor-sdk tailor-sdk login"; const npxQuotedPackage = "npx \"tailor-sdk\" login"; @@ -51,6 +54,7 @@ const pnpmDlxWithOptionValue = "pnpm --filter app dlx tailor-sdk login"; const pnpmWorkspaceRootDlx = "pnpm -w dlx tailor-sdk login"; const pnpmFilterNamedDlxCommand = "pnpm --filter dlx tailor-sdk deploy"; const pnpmDlxAfterFilterNamedDlx = "pnpm --filter dlx dlx tailor-sdk login"; +const pnpmDlxBooleanOtherPackage = "pnpm dlx --yes foo tailor-sdk login"; const dynamicPnpmDlxWithOptionValue = `pnpm --filter ${app} dlx tailor-sdk login`; const yarnDlxWithOption = "yarn --quiet dlx tailor-sdk login"; const nestedCommand = `run ${"tailor-sdk deploy"}`; diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index faabb0b7b..7d4a64b2a 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -81,7 +81,7 @@ const RENAME_BIN_SOURCE_LEGACY_PATTERN = new RegExp( ); const RENAME_BIN_QUOTED_SOURCE_LEGACY_PATTERN = new RegExp( [ - "(?:^|[\\s;&|])(?:sh|bash|zsh)\\s+-\\w*c\\w*\\s+\\\\?[\"']", + "(?:^|[\\s;&|\\x00])(?:sh|bash|zsh)\\s+-\\w*c\\w*\\s+\\\\?[\"']", RENAME_BIN_SOURCE_COMMAND_TOKEN, `(?=\\s*(?:$|${RENAME_BIN_SOURCE_COMMAND_OR_FLAG}\\b))`, ].join(""), diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 6449af065..fa71f7239 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -750,7 +750,10 @@ describe("runCodemods", () => { tmpDir = dir; await fs.promises.writeFile( path.join(dir, "commands.ts"), - "const command = 'bash -lc \"tailor-sdk crash-report list\"';", + [ + 'const note = "unrelated";', + "const command = 'bash -lc \"tailor-sdk crash-report list\"';", + ].join("\n"), "utf-8", ); const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); From 42339c3ac0f3f2c890f18dded95965b233de9333 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 05:17:22 +0900 Subject: [PATCH 347/618] fix(sdk-codemod): scope source alias rewrites --- .../v2/rename-bin/scripts/transform.ts | 144 +++++++++++++++--- .../tests/source-js-string/expected.js | 8 + .../tests/source-js-string/input.js | 8 + .../tests/source-template/expected.ts | 1 + .../rename-bin/tests/source-template/input.ts | 1 + 5 files changed, 145 insertions(+), 17 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 22d75daa7..f5bef6b6b 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -389,6 +389,43 @@ function packageRunnerPackageStartTokenIndex(tokens: readonly string[]): number } function renameSourcePackageRunnerTokens(value: string): string { + return transformSourceCommandSegments(value, renameSourcePackageRunnerTokenSegment); +} + +function transformSourceCommandSegments( + value: string, + transform: (segment: string) => string, +): string { + let result = ""; + let segmentStart = 0; + let quote: string | null = null; + for (let index = 0; index < value.length; index += 1) { + const char = value[index]!; + if (quote != null) { + if (char === "\\") { + index += 1; + } else if (char === quote) { + quote = null; + } + continue; + } + if (char === "\\" && (value[index + 1] === '"' || value[index + 1] === "'")) { + index += 1; + continue; + } + if (char === '"' || char === "'") { + quote = char; + continue; + } + if (char === ";" || char === "&" || char === "|") { + result += transform(value.slice(segmentStart, index)) + char; + segmentStart = index + 1; + } + } + return result + transform(value.slice(segmentStart)); +} + +function renameSourcePackageRunnerTokenSegment(value: string): string { const spans = sourceTokenSpans(value); if (spans == null) return value; const tokens = spans.map((span) => span.value); @@ -865,7 +902,73 @@ function sourceStaticStringContent( ): string | null { return ( sourceStringContent(node, source) ?? - (node.kind() === "identifier" ? (sourceStringVariables.get(node.text()) ?? null) : null) + (node.kind() === "identifier" + ? (sourceScopedStringVariableContent(node, source) ?? + sourceStringVariables.get(node.text()) ?? + null) + : null) + ); +} + +function sourceScopedStringVariableContent(identifier: SgNode, source: string): string | null { + const name = identifier.text(); + const before = identifier.range().start.index; + let current = identifier.parent(); + while (current != null) { + if (isSourceScopeNode(current)) { + const value = findSourceStringVariableInScope(current, name, before, source); + if (value != null) return value; + } + current = current.parent(); + } + return null; +} + +function findSourceStringVariableInScope( + scope: SgNode, + name: string, + before: number, + source: string, +): string | null { + let value: string | null = null; + const visit = (node: SgNode): void => { + if (node !== scope && isSourceScopeNode(node)) return; + if (node.kind() === "variable_declarator" && node.range().end.index < before) { + const declarationValue = sourceStringVariableDeclarationValue(node, name, source); + if (declarationValue != null) value = declarationValue; + return; + } + for (const child of node.children()) { + visit(child); + } + }; + visit(scope); + return value; +} + +function sourceStringVariableDeclarationValue( + node: SgNode, + name: string, + source: string, +): string | null { + if (!isConstVariableDeclarator(node)) return null; + const children = node.children(); + const identifier = children.find((child) => child.kind() === "identifier"); + if (identifier?.text() !== name) return null; + const initializer = children.findLast( + (child) => sourceConstInitializerContent(child, source) != null, + ); + return initializer == null ? null : sourceConstInitializerContent(initializer, source); +} + +function isSourceScopeNode(node: SgNode): boolean { + const kind = node.kind(); + return ( + kind === "program" || + kind === "statement_block" || + kind === "function_declaration" || + kind === "arrow_function" || + kind === "method_definition" ); } @@ -1351,22 +1454,26 @@ function templateSubstitutionPlaceholder( function templateSubstitutionsNeedCliRenameMigration( text: string, - substitutions: ReadonlyArray<{ placeholder: string; text: string }>, - sourceStringVariables: ReadonlyMap, + substitutions: ReadonlyArray<{ placeholder: string; migrationText: string }>, ): boolean { let restored = text; for (const substitution of substitutions) { - restored = restored.replaceAll(substitution.placeholder, () => - templateSubstitutionMigrationText(substitution.text, sourceStringVariables), - ); + restored = restored.replaceAll(substitution.placeholder, () => substitution.migrationText); } return needsCliRenameMigration(restored); } function templateSubstitutionMigrationText( + node: SgNode, + source: string, value: string, sourceStringVariables: ReadonlyMap, ): string { + const identifier = node.children().find((child) => child.kind() === "identifier"); + if (identifier != null && node.text() === `\${${identifier.text()}}`) { + const identifierValue = sourceStaticStringContent(identifier, source, sourceStringVariables); + if (identifierValue != null) return identifierValue; + } if (!value.startsWith("${") || !value.endsWith("}")) return value; const expression = value.slice(2, -1).trim(); return sourceStringVariables.get(expression) ?? expression; @@ -1382,7 +1489,7 @@ function pushTemplateStringEdit( const start = range.start.index + 1; const end = range.end.index - 1; let text = source.slice(start, end); - const substitutions: Array<{ placeholder: string; text: string }> = []; + const substitutions: Array<{ placeholder: string; text: string; migrationText: string }> = []; const usedPlaceholders = new Set(); const substitutionNodes = node @@ -1397,22 +1504,25 @@ function pushTemplateStringEdit( text, usedPlaceholders, ); + const substitutionText = isTemplateSubstitutionCliValue(text, childStart) + ? source.slice(childRange.start.index, childRange.end.index) + : transformTemplateSubstitutionText( + source.slice(childRange.start.index, childRange.end.index), + ); substitutions.push({ placeholder, - text: isTemplateSubstitutionCliValue(text, childStart) - ? source.slice(childRange.start.index, childRange.end.index) - : transformTemplateSubstitutionText( - source.slice(childRange.start.index, childRange.end.index), - ), + text: substitutionText, + migrationText: templateSubstitutionMigrationText( + child, + source, + substitutionText, + sourceStringVariables, + ), }); text = `${text.slice(0, childStart)}${placeholder}${text.slice(childEnd)}`; } - let replacement = templateSubstitutionsNeedCliRenameMigration( - text, - substitutions, - sourceStringVariables, - ) + let replacement = templateSubstitutionsNeedCliRenameMigration(text, substitutions) ? text : renameSourceCommandText(text); for (const substitution of substitutions) { diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 2bf4daf55..329294e09 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -21,6 +21,14 @@ function duplicateAliasTwo() { const duplicateLegacyArg = "apply"; return spawn("tailor-sdk", [duplicateLegacyArg]); } +function conflictingAliasLegacy() { + const scopedArg = "apply"; + return spawn("tailor-sdk", [scopedArg]); +} +function conflictingAliasDeploy() { + const scopedArg = "deploy"; + return spawn("tailor", [scopedArg]); +} const cliRenameCommandSpawned = spawn("tailor-sdk", ["crash-report", "list"]); const cliRenameFlagSpawned = spawn("tailor-sdk", ["login", "--machineuser"]); const cliRenameFlagAliasSpawned = spawn("tailor-sdk", ["login", legacyMachineUserArg]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index de21848e9..6ea8960a7 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -21,6 +21,14 @@ function duplicateAliasTwo() { const duplicateLegacyArg = "apply"; return spawn("tailor-sdk", [duplicateLegacyArg]); } +function conflictingAliasLegacy() { + const scopedArg = "apply"; + return spawn("tailor-sdk", [scopedArg]); +} +function conflictingAliasDeploy() { + const scopedArg = "deploy"; + return spawn("tailor-sdk", [scopedArg]); +} const cliRenameCommandSpawned = spawn("tailor-sdk", ["crash-report", "list"]); const cliRenameFlagSpawned = spawn("tailor-sdk", ["login", "--machineuser"]); const cliRenameFlagAliasSpawned = spawn("tailor-sdk", ["login", legacyMachineUserArg]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 7ccee763c..834daa4b9 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -71,6 +71,7 @@ const npxPackageFlagEquals = "npx --package=@tailor-platform/sdk tailor login"; const npxPackageSingleQuoted = "npx --package '@tailor-platform/sdk' tailor login"; const npxPackageDoubleQuoted = "npx --package \"@tailor-platform/sdk\" tailor login"; const npxPackageEqualsDoubleQuoted = "npx --package=\"@tailor-platform/sdk\" tailor login"; +const npxPackageDoubleQuotedBeforeAnd = "npx --package \"@tailor-platform/sdk\" tailor login && echo done"; const npxDynamicPackageFlag = `npx -p ${pkg} tailor-sdk login`; const npxDynamicPackageFlagEquals = `npx --package=${pkg} tailor-sdk login`; const npxOtherPackageFlagEqualsSource = "npx --package=dotenv-cli tailor-sdk login"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 09007eb1a..54695ba05 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -71,6 +71,7 @@ const npxPackageFlagEquals = "npx --package=tailor-sdk tailor-sdk login"; const npxPackageSingleQuoted = "npx --package 'tailor-sdk' tailor-sdk login"; const npxPackageDoubleQuoted = "npx --package \"tailor-sdk\" tailor-sdk login"; const npxPackageEqualsDoubleQuoted = "npx --package=\"tailor-sdk\" tailor-sdk login"; +const npxPackageDoubleQuotedBeforeAnd = "npx --package \"tailor-sdk\" tailor-sdk login && echo done"; const npxDynamicPackageFlag = `npx -p ${pkg} tailor-sdk login`; const npxDynamicPackageFlagEquals = `npx --package=${pkg} tailor-sdk login`; const npxOtherPackageFlagEqualsSource = "npx --package=dotenv-cli tailor-sdk login"; From a83c8cec64423c41fdf456f9aa31249674e82e5b Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 05:45:07 +0900 Subject: [PATCH 348/618] fix(sdk-codemod): preserve aliased cli values --- .../v2/rename-bin/scripts/transform.ts | 61 ++++++++++++++----- .../tests/source-js-string/expected.js | 4 ++ .../tests/source-js-string/input.js | 4 ++ packages/sdk-codemod/src/registry.test.ts | 5 +- packages/sdk-codemod/src/registry.ts | 9 +++ packages/sdk-codemod/src/runner.test.ts | 28 +++++++++ 6 files changed, 95 insertions(+), 16 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index f5bef6b6b..2486f0c17 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -1367,28 +1367,40 @@ function arrayHasCliRenameLegacyArgs( return false; } -function isTailorCliArgumentArray(arrayNode: SgNode, index: number, source: string): boolean { +function isTailorCliArgumentArray( + arrayNode: SgNode, + index: number, + source: string, + sourceStringVariables: ReadonlyMap, +): boolean { const argumentsNode = arrayNode.parent(); if (argumentsNode?.kind() === "arguments") { const callArgs = sourceArrayElements(argumentsNode); - const executable = callArgs[0] == null ? null : sourceStringContent(callArgs[0]!, source); + const executable = + callArgs[0] == null + ? null + : sourceStaticStringContent(callArgs[0]!, source, sourceStringVariables); if (executable != null && isTailorCliTokenValue(executable)) return true; } const elements = sourceArrayElements(arrayNode); return elements.slice(0, index).some((element) => { - const value = sourceStringContent(element, source); + const value = sourceStaticStringContent(element, source, sourceStringVariables); return value != null && isTailorCliTokenValue(value); }); } -function isCliValueArgument(node: SgNode, source: string): boolean { +function isCliValueArgument( + node: SgNode, + source: string, + sourceStringVariables: ReadonlyMap, +): boolean { const parent = node.parent(); if (parent?.kind() !== "array") return false; const elements = sourceArrayElements(parent); const index = nodeIndex(elements, node); if (index < 0) return false; - if (!isTailorCliArgumentArray(parent, index, source)) return false; + if (!isTailorCliArgumentArray(parent, index, source, sourceStringVariables)) return false; const text = sourceStringContent(node, source) ?? sourceStringRawContent(node, source); if ( text != null && @@ -1399,7 +1411,11 @@ function isCliValueArgument(node: SgNode, source: string): boolean { return true; } if (index === 0) return false; - const previousValue = sourceStringContent(elements[index - 1]!, source); + const previousValue = sourceStaticStringContent( + elements[index - 1]!, + source, + sourceStringVariables, + ); return ( text != null && text.includes("tailor-sdk") && @@ -1428,7 +1444,7 @@ function pushSourceStringEdit( : (TAILOR_SDK_TOKEN_RE.test(text) || TAILOR_SDK_PATH_RE.test(text)) && isCliBinaryArgument(node, source, sourceStringVariables) ? renameBinary(text) - : isCliValueArgument(node, source) + : isCliValueArgument(node, source, sourceStringVariables) ? text : renameSourceCommandText(text); if (replacement !== text) { @@ -1452,15 +1468,29 @@ function templateSubstitutionPlaceholder( } } -function templateSubstitutionsNeedCliRenameMigration( +function restoreTemplateMigrationText( text: string, substitutions: ReadonlyArray<{ placeholder: string; migrationText: string }>, -): boolean { +): string { let restored = text; for (const substitution of substitutions) { restored = restored.replaceAll(substitution.placeholder, () => substitution.migrationText); } - return needsCliRenameMigration(restored); + return restored; +} + +function hasSourceTailorSdkReference(value: string): boolean { + TAILOR_SDK_RE.lastIndex = 0; + const matches = TAILOR_SDK_RE.test(value); + TAILOR_SDK_RE.lastIndex = 0; + return matches; +} + +function hasOnlyProtectedSourceCliValueLegacyToken(value: string): boolean { + const protectedValue = protectSourceCliValueReferences(value); + return ( + protectedValue.protectedValues.length > 0 && !hasSourceTailorSdkReference(protectedValue.source) + ); } function templateSubstitutionMigrationText( @@ -1522,9 +1552,12 @@ function pushTemplateStringEdit( text = `${text.slice(0, childStart)}${placeholder}${text.slice(childEnd)}`; } - let replacement = templateSubstitutionsNeedCliRenameMigration(text, substitutions) - ? text - : renameSourceCommandText(text); + const migrationText = restoreTemplateMigrationText(text, substitutions); + if (hasOnlyProtectedSourceCliValueLegacyToken(migrationText)) { + return; + } + + let replacement = needsCliRenameMigration(migrationText) ? text : renameSourceCommandText(text); for (const substitution of substitutions) { replacement = replacement.replaceAll(substitution.placeholder, () => substitution.text); } @@ -1574,7 +1607,7 @@ function transformSourceFile(source: string, filePath: string): string | null { pushSourceStringEdit(edits, source, node, sourceStringVariables); return; } - if (isCliValueArgument(node, source)) return; + if (isCliValueArgument(node, source, sourceStringVariables)) return; pushTemplateStringEdit(edits, source, node, sourceStringVariables); return; } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 329294e09..6b9b2a901 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -2,6 +2,8 @@ const script = "tailor deploy"; const proseApply = "Run tailor deploy to apply changes"; const spawned = spawn("tailor", ["deploy"]); const argSpawned = spawn("tailor", ["--arg", "tailor-sdk deploy", "deploy"]); +const tailorBin = "tailor"; +const aliasArgSpawned = spawn(tailorBin, ["--arg", "tailor-sdk deploy", "deploy"]); const inlineArgSpawned = spawn("tailor", ["--arg=tailor-sdk deploy", "deploy"]); const templateArgSpawned = spawn("tailor", ["--arg", `tailor-sdk ${cmd}`, "deploy"]); const inlineTemplateArgSpawned = spawn("tailor", [`--arg=tailor-sdk ${cmd}`, "deploy"]); @@ -11,6 +13,8 @@ const shellSpawned = spawn("sh", ["-c", "tailor deploy"]); const legacyApplyArg = "apply"; const legacyMachineUserArg = "--machineuser"; const legacyTemplateArg = "apply"; +const templateBin = "tailor"; +const templatedAliasArg = `${templateBin} --arg "tailor-sdk deploy" deploy`; const applySpawned = spawn("tailor-sdk", ["apply"]); const applyAliasSpawned = spawn("tailor-sdk", [legacyApplyArg]); function duplicateAliasOne() { diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 6ea8960a7..3b5a9aa51 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -2,6 +2,8 @@ const script = "tailor-sdk deploy"; const proseApply = "Run tailor-sdk deploy to apply changes"; const spawned = spawn("tailor-sdk", ["deploy"]); const argSpawned = spawn("tailor-sdk", ["--arg", "tailor-sdk deploy", "deploy"]); +const tailorBin = "tailor"; +const aliasArgSpawned = spawn(tailorBin, ["--arg", "tailor-sdk deploy", "deploy"]); const inlineArgSpawned = spawn("tailor-sdk", ["--arg=tailor-sdk deploy", "deploy"]); const templateArgSpawned = spawn("tailor-sdk", ["--arg", `tailor-sdk ${cmd}`, "deploy"]); const inlineTemplateArgSpawned = spawn("tailor-sdk", [`--arg=tailor-sdk ${cmd}`, "deploy"]); @@ -11,6 +13,8 @@ const shellSpawned = spawn("sh", ["-c", "tailor-sdk deploy"]); const legacyApplyArg = "apply"; const legacyMachineUserArg = "--machineuser"; const legacyTemplateArg = "apply"; +const templateBin = "tailor"; +const templatedAliasArg = `${templateBin} --arg "tailor-sdk deploy" deploy`; const applySpawned = spawn("tailor-sdk", ["apply"]); const applyAliasSpawned = spawn("tailor-sdk", [legacyApplyArg]); function duplicateAliasOne() { diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 8023d66c6..7737e340a 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -63,8 +63,8 @@ describe("getApplicableCodemods", () => { ); expect(renameBin?.filePatterns).toEqual(expect.arrayContaining([sourcePattern])); - expect(renameBin?.sourceStringLegacyPatterns).toHaveLength(2); - expect(renameBin?.sourceTextLegacyPatterns).toHaveLength(2); + expect(renameBin?.sourceStringLegacyPatterns).toHaveLength(3); + expect(renameBin?.sourceTextLegacyPatterns).toHaveLength(3); const sourceStringPatterns = renameBin?.sourceStringLegacyPatterns as RegExp[]; const matchesSourceStringPattern = (value: string) => sourceStringPatterns.some((pattern) => pattern.test(value)); @@ -72,6 +72,7 @@ describe("getApplicableCodemods", () => { expect(matchesSourceStringPattern("tailor-sdk apply")).toBe(true); expect(matchesSourceStringPattern('sh -c "tailor-sdk apply"')).toBe(true); expect(matchesSourceStringPattern('bash -lc "tailor-sdk crash-report list"')).toBe(true); + expect(matchesSourceStringPattern('Run "tailor-sdk crash-report list" manually')).toBe(true); expect(matchesSourceStringPattern("tailor-sdk.cmd crash-report list")).toBe(true); expect(matchesSourceStringPattern("tailor --profile tailor-sdk deploy")).toBe(false); expect(matchesSourceStringPattern("tailor --name tailor-sdk deploy")).toBe(false); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 7d4a64b2a..5f5b3d2b1 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -86,6 +86,13 @@ const RENAME_BIN_QUOTED_SOURCE_LEGACY_PATTERN = new RegExp( `(?=\\s*(?:$|${RENAME_BIN_SOURCE_COMMAND_OR_FLAG}\\b))`, ].join(""), ); +const RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN = new RegExp( + [ + "[\"']", + RENAME_BIN_SOURCE_COMMAND_TOKEN, + "(?=\\s*(?:apply\\b|crash-report\\b|[^\"'`]*\\s--machineuser\\b))", + ].join(""), +); /** All registered codemods, in registration order. */ export const allCodemods: CodemodPackage[] = [ @@ -621,10 +628,12 @@ export const allCodemods: CodemodPackage[] = [ sourceStringLegacyPatterns: [ RENAME_BIN_SOURCE_LEGACY_PATTERN, RENAME_BIN_QUOTED_SOURCE_LEGACY_PATTERN, + RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN, ], sourceTextLegacyPatterns: [ RENAME_BIN_SOURCE_LEGACY_PATTERN, RENAME_BIN_QUOTED_SOURCE_LEGACY_PATTERN, + RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN, ], examples: [ { diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index fa71f7239..98385f83e 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -776,6 +776,34 @@ describe("runCodemods", () => { expect(result.warnings[0]).toContain("rule: test/rename-bin"); }); + test("warns for quoted legacy CLI residuals", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "commands.ts"), + "const message = 'Run \"tailor-sdk crash-report list\" manually';", + "utf-8", + ); + const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: renameBin?.sourceStringLegacyPatterns, + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toHaveLength(1); + expect(result.warnings[0]).toContain("commands.ts: contains"); + expect(result.warnings[0]).toContain("rule: test/rename-bin"); + }); + test("keeps multiple-spaced Tailor option values out of rename-bin residual warnings", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); tmpDir = dir; From fe122e05e9d679f06b64293cf8989431bde1f20b Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 06:05:40 +0900 Subject: [PATCH 349/618] fix(sdk-codemod): avoid cross-scope source aliases --- .../v2/rename-bin/scripts/transform.ts | 152 ++++-------------- .../tests/source-js-string/expected.js | 6 + .../tests/source-js-string/input.js | 6 + 3 files changed, 40 insertions(+), 124 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 2486f0c17..d5be90fc3 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -829,29 +829,6 @@ function sourceStringRawContent(node: SgNode, source: string): string | null { return source.slice(range.start.index + 1, range.end.index - 1); } -function collectSourceStringVariables(root: SgNode, source: string): ReadonlyMap { - const values = new Map(); - const ambiguous = new Set(); - const visit = (node: SgNode): void => { - if (node.kind() === "variable_declarator" && isConstVariableDeclarator(node)) { - const children = node.children(); - const identifier = children.find((child) => child.kind() === "identifier"); - const initializer = children.findLast( - (child) => sourceConstInitializerContent(child, source) != null, - ); - const value = initializer == null ? null : sourceConstInitializerContent(initializer, source); - if (identifier != null && value != null) { - rememberSourceStringVariable(values, ambiguous, identifier.text(), value); - } - } - for (const child of node.children()) { - visit(child); - } - }; - visit(root); - return values; -} - function sourceConstInitializerContent(node: SgNode, source: string): string | null { const directValue = sourceStringContent(node, source); if (directValue != null) return directValue; @@ -878,35 +855,10 @@ function isConstVariableDeclarator(node: SgNode): boolean { ); } -function rememberSourceStringVariable( - values: Map, - ambiguous: Set, - name: string, - value: string, -): void { - if (ambiguous.has(name)) return; - const existingValue = values.get(name); - if (existingValue != null) { - if (existingValue === value) return; - values.delete(name); - ambiguous.add(name); - return; - } - values.set(name, value); -} - -function sourceStaticStringContent( - node: SgNode, - source: string, - sourceStringVariables: ReadonlyMap, -): string | null { +function sourceStaticStringContent(node: SgNode, source: string): string | null { return ( sourceStringContent(node, source) ?? - (node.kind() === "identifier" - ? (sourceScopedStringVariableContent(node, source) ?? - sourceStringVariables.get(node.text()) ?? - null) - : null) + (node.kind() === "identifier" ? sourceScopedStringVariableContent(node, source) : null) ); } @@ -1219,11 +1171,7 @@ function isPackageRunnerPackageArgument(node: SgNode, source: string): boolean { return firstTailorPackageIndex(elements, start, source, executable) === index; } -function isPackageRunnerCommandBinaryArgument( - node: SgNode, - source: string, - sourceStringVariables: ReadonlyMap, -): boolean { +function isPackageRunnerCommandBinaryArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() !== "array" || !isPackageRunnerArrayArgument(node, source)) return false; @@ -1245,7 +1193,7 @@ function isPackageRunnerCommandBinaryArgument( ) { return false; } - if (arrayHasCliRenameLegacyArgs(elements, index + 1, source, sourceStringVariables)) { + if (arrayHasCliRenameLegacyArgs(elements, index + 1, source)) { return false; } const commandIndex = packageRunnerCommandIndex(elements, start, source, executable); @@ -1281,11 +1229,7 @@ function packageRunnerCommandIndex( return null; } -function isPackageManagerExecBinaryArgument( - node: SgNode, - source: string, - sourceStringVariables: ReadonlyMap, -): boolean { +function isPackageManagerExecBinaryArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() !== "array") return false; @@ -1310,20 +1254,16 @@ function isPackageManagerExecBinaryArgument( } return ( firstNonOptionIndex(elements, execIndex + 1, source, executable) === index && - !arrayHasCliRenameLegacyArgs(elements, index + 1, source, sourceStringVariables) + !arrayHasCliRenameLegacyArgs(elements, index + 1, source) ); } -function isCliBinaryArgument( - node: SgNode, - source: string, - sourceStringVariables: ReadonlyMap, -): boolean { +function isCliBinaryArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() === "array") { - if (isPackageRunnerCommandBinaryArgument(node, source, sourceStringVariables)) return true; + if (isPackageRunnerCommandBinaryArgument(node, source)) return true; if (isPackageRunnerArrayArgument(node, source)) return false; - return isPackageManagerExecBinaryArgument(node, source, sourceStringVariables); + return isPackageManagerExecBinaryArgument(node, source); } if (parent?.kind() !== "arguments") return false; @@ -1332,21 +1272,15 @@ function isCliBinaryArgument( if (args[0] == null || nodeRangeKey(args[0]) !== nodeRangeKey(node)) return false; const argv = args[1]; return ( - argv?.kind() !== "array" || - !arrayHasCliRenameLegacyArgs(sourceArrayElements(argv), 0, source, sourceStringVariables) + argv?.kind() !== "array" || !arrayHasCliRenameLegacyArgs(sourceArrayElements(argv), 0, source) ); } -function arrayHasCliRenameLegacyArgs( - elements: SgNode[], - start: number, - source: string, - sourceStringVariables: ReadonlyMap, -): boolean { +function arrayHasCliRenameLegacyArgs(elements: SgNode[], start: number, source: string): boolean { let commandSeen = false; for (let index = start; index < elements.length; index += 1) { const value = - sourceStaticStringContent(elements[index]!, source, sourceStringVariables) ?? + sourceStaticStringContent(elements[index]!, source) ?? sourceStringRawContent(elements[index]!, source); if (value == null) continue; if (value === "--machineuser" || value.startsWith("--machineuser=")) return true; @@ -1367,40 +1301,28 @@ function arrayHasCliRenameLegacyArgs( return false; } -function isTailorCliArgumentArray( - arrayNode: SgNode, - index: number, - source: string, - sourceStringVariables: ReadonlyMap, -): boolean { +function isTailorCliArgumentArray(arrayNode: SgNode, index: number, source: string): boolean { const argumentsNode = arrayNode.parent(); if (argumentsNode?.kind() === "arguments") { const callArgs = sourceArrayElements(argumentsNode); - const executable = - callArgs[0] == null - ? null - : sourceStaticStringContent(callArgs[0]!, source, sourceStringVariables); + const executable = callArgs[0] == null ? null : sourceStaticStringContent(callArgs[0]!, source); if (executable != null && isTailorCliTokenValue(executable)) return true; } const elements = sourceArrayElements(arrayNode); return elements.slice(0, index).some((element) => { - const value = sourceStaticStringContent(element, source, sourceStringVariables); + const value = sourceStaticStringContent(element, source); return value != null && isTailorCliTokenValue(value); }); } -function isCliValueArgument( - node: SgNode, - source: string, - sourceStringVariables: ReadonlyMap, -): boolean { +function isCliValueArgument(node: SgNode, source: string): boolean { const parent = node.parent(); if (parent?.kind() !== "array") return false; const elements = sourceArrayElements(parent); const index = nodeIndex(elements, node); if (index < 0) return false; - if (!isTailorCliArgumentArray(parent, index, source, sourceStringVariables)) return false; + if (!isTailorCliArgumentArray(parent, index, source)) return false; const text = sourceStringContent(node, source) ?? sourceStringRawContent(node, source); if ( text != null && @@ -1411,11 +1333,7 @@ function isCliValueArgument( return true; } if (index === 0) return false; - const previousValue = sourceStaticStringContent( - elements[index - 1]!, - source, - sourceStringVariables, - ); + const previousValue = sourceStaticStringContent(elements[index - 1]!, source); return ( text != null && text.includes("tailor-sdk") && @@ -1429,7 +1347,6 @@ function pushSourceStringEdit( edits: Array<[number, number, string]>, source: string, node: SgNode, - sourceStringVariables: ReadonlyMap, ): void { const range = node.range(); const start = range.start.index + 1; @@ -1442,9 +1359,9 @@ function pushSourceStringEdit( : TAILOR_SDK_TOKEN_RE.test(text) && isPackageRunnerPackageArgument(node, source) ? renamePackageName(text) : (TAILOR_SDK_TOKEN_RE.test(text) || TAILOR_SDK_PATH_RE.test(text)) && - isCliBinaryArgument(node, source, sourceStringVariables) + isCliBinaryArgument(node, source) ? renameBinary(text) - : isCliValueArgument(node, source, sourceStringVariables) + : isCliValueArgument(node, source) ? text : renameSourceCommandText(text); if (replacement !== text) { @@ -1493,27 +1410,20 @@ function hasOnlyProtectedSourceCliValueLegacyToken(value: string): boolean { ); } -function templateSubstitutionMigrationText( - node: SgNode, - source: string, - value: string, - sourceStringVariables: ReadonlyMap, -): string { +function templateSubstitutionMigrationText(node: SgNode, source: string, value: string): string { const identifier = node.children().find((child) => child.kind() === "identifier"); if (identifier != null && node.text() === `\${${identifier.text()}}`) { - const identifierValue = sourceStaticStringContent(identifier, source, sourceStringVariables); + const identifierValue = sourceStaticStringContent(identifier, source); if (identifierValue != null) return identifierValue; } if (!value.startsWith("${") || !value.endsWith("}")) return value; - const expression = value.slice(2, -1).trim(); - return sourceStringVariables.get(expression) ?? expression; + return value.slice(2, -1).trim(); } function pushTemplateStringEdit( edits: Array<[number, number, string]>, source: string, node: SgNode, - sourceStringVariables: ReadonlyMap, ): void { const range = node.range(); const start = range.start.index + 1; @@ -1542,12 +1452,7 @@ function pushTemplateStringEdit( substitutions.push({ placeholder, text: substitutionText, - migrationText: templateSubstitutionMigrationText( - child, - source, - substitutionText, - sourceStringVariables, - ), + migrationText: templateSubstitutionMigrationText(child, source, substitutionText), }); text = `${text.slice(0, childStart)}${placeholder}${text.slice(childEnd)}`; } @@ -1583,7 +1488,6 @@ function transformSourceFile(source: string, filePath: string): string | null { return null; } - const sourceStringVariables = collectSourceStringVariables(root, source); const edits: Array<[number, number, string]> = []; const visit = (node: SgNode): void => { const kind = node.kind(); @@ -1595,7 +1499,7 @@ function transformSourceFile(source: string, filePath: string): string | null { } if (kind === "string") { - pushSourceStringEdit(edits, source, node, sourceStringVariables); + pushSourceStringEdit(edits, source, node); return; } @@ -1604,11 +1508,11 @@ function transformSourceFile(source: string, filePath: string): string | null { .children() .some((child: SgNode) => child.kind() === "template_substitution"); if (!hasSubstitution) { - pushSourceStringEdit(edits, source, node, sourceStringVariables); + pushSourceStringEdit(edits, source, node); return; } - if (isCliValueArgument(node, source, sourceStringVariables)) return; - pushTemplateStringEdit(edits, source, node, sourceStringVariables); + if (isCliValueArgument(node, source)) return; + pushTemplateStringEdit(edits, source, node); return; } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 6b9b2a901..98000fa2e 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -1,6 +1,8 @@ const script = "tailor deploy"; const proseApply = "Run tailor deploy to apply changes"; const spawned = spawn("tailor", ["deploy"]); +const runtimeArg = getArg(); +const scopedRuntimeArgSpawned = spawn("tailor", [runtimeArg]); const argSpawned = spawn("tailor", ["--arg", "tailor-sdk deploy", "deploy"]); const tailorBin = "tailor"; const aliasArgSpawned = spawn(tailorBin, ["--arg", "tailor-sdk deploy", "deploy"]); @@ -17,6 +19,10 @@ const templateBin = "tailor"; const templatedAliasArg = `${templateBin} --arg "tailor-sdk deploy" deploy`; const applySpawned = spawn("tailor-sdk", ["apply"]); const applyAliasSpawned = spawn("tailor-sdk", [legacyApplyArg]); +function unrelatedRuntimeArgScope() { + const runtimeArg = "apply"; + return runtimeArg; +} function duplicateAliasOne() { const duplicateLegacyArg = "apply"; return spawn("tailor-sdk", [duplicateLegacyArg]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 3b5a9aa51..98a432a0f 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -1,6 +1,8 @@ const script = "tailor-sdk deploy"; const proseApply = "Run tailor-sdk deploy to apply changes"; const spawned = spawn("tailor-sdk", ["deploy"]); +const runtimeArg = getArg(); +const scopedRuntimeArgSpawned = spawn("tailor-sdk", [runtimeArg]); const argSpawned = spawn("tailor-sdk", ["--arg", "tailor-sdk deploy", "deploy"]); const tailorBin = "tailor"; const aliasArgSpawned = spawn(tailorBin, ["--arg", "tailor-sdk deploy", "deploy"]); @@ -17,6 +19,10 @@ const templateBin = "tailor"; const templatedAliasArg = `${templateBin} --arg "tailor-sdk deploy" deploy`; const applySpawned = spawn("tailor-sdk", ["apply"]); const applyAliasSpawned = spawn("tailor-sdk", [legacyApplyArg]); +function unrelatedRuntimeArgScope() { + const runtimeArg = "apply"; + return runtimeArg; +} function duplicateAliasOne() { const duplicateLegacyArg = "apply"; return spawn("tailor-sdk", [duplicateLegacyArg]); From beaa43cfa899e987be6d793433455c6d5af93a71 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 06:22:37 +0900 Subject: [PATCH 350/618] fix(sdk-codemod): resolve scoped source residuals --- .../v2/rename-bin/scripts/transform.ts | 20 +- .../tests/source-template/expected.ts | 1 + .../rename-bin/tests/source-template/input.ts | 1 + packages/sdk-codemod/src/runner.test.ts | 35 ++++ packages/sdk-codemod/src/runner.ts | 178 ++++++++++-------- 5 files changed, 155 insertions(+), 80 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index d5be90fc3..0dc7e56c7 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -289,12 +289,30 @@ function sourceTokenSpans(value: string): Array<{ } if (value.slice(lastEnd).trim() !== "") return null; - return [...value.matchAll(new RegExp(SOURCE_CLI_ARG_VALUE, "g"))].map((match) => ({ + const spans = [...value.matchAll(new RegExp(SOURCE_CLI_ARG_VALUE, "g"))].map((match) => ({ start: match.index ?? 0, end: (match.index ?? 0) + match[0].length, text: match[0], value: sourceTokenValue(match[0]), })); + return mergeSourceEqualsQuotedTokenSpans(spans); +} + +function mergeSourceEqualsQuotedTokenSpans( + spans: Array<{ start: number; end: number; text: string; value: string }>, +): Array<{ start: number; end: number; text: string; value: string }> { + const merged: Array<{ start: number; end: number; text: string; value: string }> = []; + for (const span of spans) { + const previous = merged.at(-1); + if (previous != null && previous.end === span.start && previous.text.endsWith("=")) { + previous.end = span.end; + previous.text += span.text; + previous.value += span.value; + continue; + } + merged.push({ ...span }); + } + return merged; } function sourceTokenValue(token: string): string { diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 834daa4b9..fdc57b070 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -70,6 +70,7 @@ const npxPackageSplitHelp = "npx -p @tailor-platform/sdk --help"; const npxPackageFlagEquals = "npx --package=@tailor-platform/sdk tailor login"; const npxPackageSingleQuoted = "npx --package '@tailor-platform/sdk' tailor login"; const npxPackageDoubleQuoted = "npx --package \"@tailor-platform/sdk\" tailor login"; +const npxPackageEqualsSingleQuoted = "npx --package='@tailor-platform/sdk' tailor login"; const npxPackageEqualsDoubleQuoted = "npx --package=\"@tailor-platform/sdk\" tailor login"; const npxPackageDoubleQuotedBeforeAnd = "npx --package \"@tailor-platform/sdk\" tailor login && echo done"; const npxDynamicPackageFlag = `npx -p ${pkg} tailor-sdk login`; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index 54695ba05..a825e93e5 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -70,6 +70,7 @@ const npxPackageSplitHelp = "npx -p tailor-sdk --help"; const npxPackageFlagEquals = "npx --package=tailor-sdk tailor-sdk login"; const npxPackageSingleQuoted = "npx --package 'tailor-sdk' tailor-sdk login"; const npxPackageDoubleQuoted = "npx --package \"tailor-sdk\" tailor-sdk login"; +const npxPackageEqualsSingleQuoted = "npx --package='tailor-sdk' tailor-sdk login"; const npxPackageEqualsDoubleQuoted = "npx --package=\"tailor-sdk\" tailor-sdk login"; const npxPackageDoubleQuotedBeforeAnd = "npx --package \"tailor-sdk\" tailor-sdk login && echo done"; const npxDynamicPackageFlag = `npx -p ${pkg} tailor-sdk login`; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 98385f83e..eb2c973d5 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -804,6 +804,41 @@ describe("runCodemods", () => { expect(result.warnings[0]).toContain("rule: test/rename-bin"); }); + test("warns for source command residuals with shadowed aliases", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "commands.ts"), + [ + 'const bin = "tailor-sdk";', + 'spawn(bin, ["apply"]);', + "function shadow() {", + ' const bin = "tailor";', + " return bin;", + "}", + ].join("\n"), + "utf-8", + ); + const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: renameBin?.sourceStringLegacyPatterns, + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toHaveLength(1); + expect(result.warnings[0]).toContain("commands.ts: contains"); + expect(result.warnings[0]).toContain("rule: test/rename-bin"); + }); + test("keeps multiple-spaced Tailor option values out of rename-bin residual warnings", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 986f325fb..b0d853a96 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -183,25 +183,24 @@ function sourceStringContentForResidualMatching(relative: string, content: strin return null; } - const sourceStringVariables = collectSourceStringVariables(root, content); const fragments: string[] = []; const visit = (node: SgNode): void => { if (node.kind() === "arguments") { - const value = sourceArgumentsCommandContent(node, content, sourceStringVariables); + const value = sourceArgumentsCommandContent(node, content); if (value != null) fragments.push(value); } if (node.kind() === "array") { - const value = sourceArrayCommandContent(node, content, sourceStringVariables); + const value = sourceArrayCommandContent(node, content); if (value != null) fragments.push(value); } if (node.kind() === "string") { - if (isSourceTailorSdkValueArgument(node, content, sourceStringVariables)) return; + if (isSourceTailorSdkValueArgument(node, content)) return; const value = sourceStringNodeContent(node, content); if (value != null) fragments.push(value); return; } if (node.kind() === "string_fragment") { - if (isSourceTailorSdkValueArgument(node, content, sourceStringVariables)) return; + if (isSourceTailorSdkValueArgument(node, content)) return; fragments.push(node.text()); return; } @@ -213,28 +212,6 @@ function sourceStringContentForResidualMatching(relative: string, content: strin return fragments.join(SOURCE_STRING_FRAGMENT_SEPARATOR); } -function collectSourceStringVariables(root: SgNode, source: string): ReadonlyMap { - const values = new Map(); - const visit = (node: SgNode): void => { - if (node.kind() === "variable_declarator" && isConstVariableDeclarator(node)) { - const children = node.children(); - const identifier = children.find((child) => child.kind() === "identifier"); - const initializer = children.findLast( - (child) => sourceStringNodeContent(child, source) != null, - ); - if (identifier != null && initializer != null) { - const value = sourceStringNodeContent(initializer, source); - if (value != null) values.set(identifier.text(), value); - } - } - for (const child of node.children()) { - visit(child); - } - }; - visit(root); - return values; -} - function isConstVariableDeclarator(node: SgNode): boolean { return ( node @@ -269,58 +246,37 @@ function sourceTextContentForResidualMatching(relative: string, content: string) return fragments.join(SOURCE_STRING_FRAGMENT_SEPARATOR); } -function sourceArgumentsCommandContent( - node: SgNode, - source: string, - sourceStringVariables: ReadonlyMap, -): string | null { +function sourceArgumentsCommandContent(node: SgNode, source: string): string | null { const args = sourceArrayElements(node); - const executable = - args[0] == null ? null : sourceStringLikeNodeContent(args[0]!, source, sourceStringVariables); + const executable = args[0] == null ? null : sourceStringLikeNodeContent(args[0]!, source); const argv = args[1]; if (executable == null || argv?.kind() !== "array") return null; - const values = sourceArrayCommandValues(argv, source, sourceStringVariables); + const values = sourceArrayCommandValues(argv, source); return values.length === 0 ? null : [executable, ...values].join(" "); } -function sourceArrayCommandContent( - node: SgNode, - source: string, - sourceStringVariables: ReadonlyMap, -): string | null { - const values = sourceArrayCommandValues(node, source, sourceStringVariables); +function sourceArrayCommandContent(node: SgNode, source: string): string | null { + const values = sourceArrayCommandValues(node, source); return values.length < 2 ? null : values.join(" "); } -function sourceArrayCommandValues( - node: SgNode, - source: string, - sourceStringVariables: ReadonlyMap, -): string[] { +function sourceArrayCommandValues(node: SgNode, source: string): string[] { const values: string[] = []; for (const element of sourceArrayElements(node)) { - if (isSourceValueArgument(element, source, sourceStringVariables)) continue; - const value = sourceStringLikeNodeContent(element, source, sourceStringVariables); + if (isSourceValueArgument(element, source)) continue; + const value = sourceStringLikeNodeContent(element, source); if (value != null) values.push(value); } return values; } -function isSourceTailorSdkValueArgument( - fragment: SgNode, - source: string, - sourceStringVariables: ReadonlyMap, -): boolean { +function isSourceTailorSdkValueArgument(fragment: SgNode, source: string): boolean { const text = fragment.kind() === "string_fragment" ? fragment.text() - : sourceStringLikeNodeContent(fragment, source, sourceStringVariables); - return ( - text != null && - text.includes("tailor-sdk") && - isSourceValueArgument(fragment, source, sourceStringVariables) - ); + : sourceStringLikeNodeContent(fragment, source); + return text != null && text.includes("tailor-sdk") && isSourceValueArgument(fragment, source); } function isSyntaxOnlyNode(node: SgNode): boolean { @@ -344,14 +300,89 @@ function nodeRangeKey(node: SgNode): string { return `${range.start.index}:${range.end.index}`; } -function sourceStringLikeNodeContent( +function sourceStringLikeNodeContent(node: SgNode, source: string): string | null { + const directValue = sourceStringNodeContent(node, source); + if (directValue != null) return directValue; + return node.kind() === "identifier" ? sourceScopedStringVariableContent(node, source) : null; +} + +function sourceScopedStringVariableContent(identifier: SgNode, source: string): string | null { + const name = identifier.text(); + const before = identifier.range().start.index; + let current = identifier.parent(); + while (current != null) { + if (isSourceScopeNode(current)) { + const value = findSourceStringVariableInScope(current, name, before, source); + if (value != null) return value; + } + current = current.parent(); + } + return null; +} + +function findSourceStringVariableInScope( + scope: SgNode, + name: string, + before: number, + source: string, +): string | null { + let value: string | null = null; + const visit = (node: SgNode): void => { + if (node !== scope && isSourceScopeNode(node)) return; + if (node.kind() === "variable_declarator" && node.range().end.index < before) { + const declarationValue = sourceStringVariableDeclarationValue(node, name, source); + if (declarationValue != null) value = declarationValue; + return; + } + for (const child of node.children()) { + visit(child); + } + }; + visit(scope); + return value; +} + +function sourceStringVariableDeclarationValue( node: SgNode, + name: string, source: string, - sourceStringVariables: ReadonlyMap, ): string | null { + if (!isConstVariableDeclarator(node)) return null; + const children = node.children(); + const identifier = children.find((child) => child.kind() === "identifier"); + if (identifier?.text() !== name) return null; + const initializer = children.findLast( + (child) => sourceConstInitializerContent(child, source) != null, + ); + return initializer == null ? null : sourceConstInitializerContent(initializer, source); +} + +function sourceConstInitializerContent(node: SgNode, source: string): string | null { const directValue = sourceStringNodeContent(node, source); if (directValue != null) return directValue; - return node.kind() === "identifier" ? (sourceStringVariables.get(node.text()) ?? null) : null; + if ( + node.kind() !== "as_expression" && + node.kind() !== "satisfies_expression" && + node.kind() !== "parenthesized_expression" + ) { + return null; + } + for (const child of node.children()) { + const childValue = sourceConstInitializerContent(child, source); + if (childValue != null) return childValue; + } + return null; +} + +function isSourceScopeNode(node: SgNode): boolean { + const kind = node.kind(); + return ( + kind === "program" || + kind === "statement_block" || + kind === "function_declaration" || + kind === "arrow_function" || + kind === "method_definition" + ); } function sourceStringNodeContent(node: SgNode, source: string): string | null { @@ -367,11 +398,7 @@ function sourceStringNodeContent(node: SgNode, source: string): string | null { return source.slice(range.start.index + 1, range.end.index - 1); } -function isSourceValueArgument( - fragment: SgNode, - source: string, - sourceStringVariables: ReadonlyMap, -): boolean { +function isSourceValueArgument(fragment: SgNode, source: string): boolean { const stringNode = fragment.kind() === "string_fragment" ? fragment.parent() : fragment; if (stringNode == null) return false; const parent = stringNode.parent(); @@ -380,9 +407,9 @@ function isSourceValueArgument( const elements = sourceArrayElements(parent); const index = elements.findIndex((element) => nodeRangeKey(element) === nodeRangeKey(stringNode)); if (index <= 0) return false; - if (!isTailorCliArgumentArray(parent, index, source, sourceStringVariables)) return false; + if (!isTailorCliArgumentArray(parent, index, source)) return false; - const previous = sourceStringLikeNodeContent(elements[index - 1]!, source, sourceStringVariables); + const previous = sourceStringLikeNodeContent(elements[index - 1]!, source); return ( previous != null && SOURCE_VALUE_FLAGS.has(previous.split("=", 1)[0]!) && @@ -390,25 +417,18 @@ function isSourceValueArgument( ); } -function isTailorCliArgumentArray( - arrayNode: SgNode, - index: number, - source: string, - sourceStringVariables: ReadonlyMap, -): boolean { +function isTailorCliArgumentArray(arrayNode: SgNode, index: number, source: string): boolean { const argumentsNode = arrayNode.parent(); if (argumentsNode?.kind() === "arguments") { const callArgs = sourceArrayElements(argumentsNode); const executable = - callArgs[0] == null - ? null - : sourceStringLikeNodeContent(callArgs[0]!, source, sourceStringVariables); + callArgs[0] == null ? null : sourceStringLikeNodeContent(callArgs[0]!, source); if (executable != null && SOURCE_CLI_BINARY_RE.test(executable)) return true; } const elements = sourceArrayElements(arrayNode); return elements.slice(0, index).some((element) => { - const value = sourceStringLikeNodeContent(element, source, sourceStringVariables); + const value = sourceStringLikeNodeContent(element, source); return value != null && SOURCE_CLI_BINARY_RE.test(value); }); } From 8886157caff23b15967d28c815a878fb50c6f2b5 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 06:45:17 +0900 Subject: [PATCH 351/618] fix(sdk-codemod): warn on hidden source argv --- .../v2/rename-bin/scripts/transform.ts | 3 +- .../tests/source-js-string/expected.js | 2 ++ .../tests/source-js-string/input.js | 2 ++ packages/sdk-codemod/src/runner.test.ts | 30 +++++++++++++++++++ packages/sdk-codemod/src/runner.ts | 15 ++++++++-- 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 0dc7e56c7..d24ecc995 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -1289,8 +1289,9 @@ function isCliBinaryArgument(node: SgNode, source: string): boolean { const args = sourceArrayElements(parent); if (args[0] == null || nodeRangeKey(args[0]) !== nodeRangeKey(node)) return false; const argv = args[1]; + if (argv == null) return true; return ( - argv?.kind() !== "array" || !arrayHasCliRenameLegacyArgs(sourceArrayElements(argv), 0, source) + argv.kind() === "array" && !arrayHasCliRenameLegacyArgs(sourceArrayElements(argv), 0, source) ); } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 98000fa2e..3c6025803 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -3,6 +3,8 @@ const proseApply = "Run tailor deploy to apply changes"; const spawned = spawn("tailor", ["deploy"]); const runtimeArg = getArg(); const scopedRuntimeArgSpawned = spawn("tailor", [runtimeArg]); +const hiddenApplyArgs = ["apply"]; +const hiddenApplySpawned = spawn("tailor-sdk", hiddenApplyArgs); const argSpawned = spawn("tailor", ["--arg", "tailor-sdk deploy", "deploy"]); const tailorBin = "tailor"; const aliasArgSpawned = spawn(tailorBin, ["--arg", "tailor-sdk deploy", "deploy"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index 98a432a0f..b9459d42e 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -3,6 +3,8 @@ const proseApply = "Run tailor-sdk deploy to apply changes"; const spawned = spawn("tailor-sdk", ["deploy"]); const runtimeArg = getArg(); const scopedRuntimeArgSpawned = spawn("tailor-sdk", [runtimeArg]); +const hiddenApplyArgs = ["apply"]; +const hiddenApplySpawned = spawn("tailor-sdk", hiddenApplyArgs); const argSpawned = spawn("tailor-sdk", ["--arg", "tailor-sdk deploy", "deploy"]); const tailorBin = "tailor"; const aliasArgSpawned = spawn(tailorBin, ["--arg", "tailor-sdk deploy", "deploy"]); diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index eb2c973d5..b2d21abb4 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -839,6 +839,36 @@ describe("runCodemods", () => { expect(result.warnings[0]).toContain("rule: test/rename-bin"); }); + test("warns for source command residuals before later fragments", async () => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "commands.ts"), + ["const command = `${runner} tailor-sdk ${subcommand}`;", 'const later = "deploy";'].join( + "\n", + ), + "utf-8", + ); + const renameBin = allCodemods.find((codemod) => codemod.id === "v2/rename-bin"); + + const result = await runCodemods( + [ + { + codemod: makeCodemod("test/rename-bin", partialTransformPath, ["**/*.ts"], [], { + sourceStringLegacyPatterns: renameBin?.sourceStringLegacyPatterns, + }), + scriptPath: partialTransformPath, + }, + ], + dir, + true, + ); + + expect(result.warnings).toHaveLength(1); + expect(result.warnings[0]).toContain("commands.ts: contains"); + expect(result.warnings[0]).toContain("rule: test/rename-bin"); + }); + test("keeps multiple-spaced Tailor option values out of rename-bin residual warnings", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-warning-source-test-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index b0d853a96..b6cc75a13 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -522,6 +522,17 @@ function matchResidualPattern(content: string, pattern: CodemodPatternGroup): st : null; } +function matchResidualPatternFragment( + content: string, + pattern: CodemodPatternGroup, +): string | null { + for (const fragment of content.split(SOURCE_STRING_FRAGMENT_SEPARATOR)) { + const label = matchResidualPattern(fragment, pattern); + if (label != null) return label; + } + return null; +} + function legacyPatternWarnings( relative: string, content: string, @@ -537,13 +548,13 @@ function legacyPatternWarnings( ); if (sourceStringContent != null) { for (const pattern of lt.sourceStringLegacyPatterns) { - const label = matchResidualPattern(sourceStringContent, pattern); + const label = matchResidualPatternFragment(sourceStringContent, pattern); if (label != null) found.add(label); } } if (sourceTextContent != null) { for (const pattern of lt.sourceTextLegacyPatterns) { - const label = matchResidualPattern(sourceTextContent, pattern); + const label = matchResidualPatternFragment(sourceTextContent, pattern); if (label != null) found.add(label); } } From 3f1494507e3b519292d2c40bf0f781fedc03b617 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 06:58:28 +0900 Subject: [PATCH 352/618] fix(sdk-codemod): keep legacy package runners visible --- .../sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts | 6 +++++- .../v2/rename-bin/tests/source-js-string/expected.js | 4 ++++ .../codemods/v2/rename-bin/tests/source-js-string/input.js | 4 ++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index d24ecc995..d983ff7e0 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -1186,7 +1186,10 @@ function isPackageRunnerPackageArgument(node: SgNode, source: string): boolean { return isSplitPackageFlagValue(elements, index, source, start, executable); } - return firstTailorPackageIndex(elements, start, source, executable) === index; + return ( + firstTailorPackageIndex(elements, start, source, executable) === index && + !arrayHasCliRenameLegacyArgs(elements, index + 1, source) + ); } function isPackageRunnerCommandBinaryArgument(node: SgNode, source: string): boolean { @@ -1290,6 +1293,7 @@ function isCliBinaryArgument(node: SgNode, source: string): boolean { if (args[0] == null || nodeRangeKey(args[0]) !== nodeRangeKey(node)) return false; const argv = args[1]; if (argv == null) return true; + if (argv.kind() === "object") return true; return ( argv.kind() === "array" && !arrayHasCliRenameLegacyArgs(sourceArrayElements(argv), 0, source) ); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js index 3c6025803..19c241cf0 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/expected.js @@ -1,6 +1,7 @@ const script = "tailor deploy"; const proseApply = "Run tailor deploy to apply changes"; const spawned = spawn("tailor", ["deploy"]); +const optionOverloadSpawned = spawn("tailor", { stdio: "inherit" }); const runtimeArg = getArg(); const scopedRuntimeArgSpawned = spawn("tailor", [runtimeArg]); const hiddenApplyArgs = ["apply"]; @@ -49,6 +50,9 @@ const secretShortValueApplySpawned = spawn("tailor", ["secret", "create", "-v", const dynamicCliRenameCommandSpawned = spawn("tailor-sdk", [`${"apply"}`]); const dynamicCliRenameAliasCommand = `tailor-sdk ${legacyTemplateArg}`; const npxSpawned = spawn("npx", ["@tailor-platform/sdk", "login"]); +const npxLegacyApplySpawned = spawn("npx", ["tailor-sdk", "apply"]); +const npxLegacyCrashReportSpawned = spawn("npx", ["tailor-sdk", "crash-report", "list"]); +const npxLegacyMachineUserSpawned = spawn("npx", ["tailor-sdk", "login", "--machineuser"]); const npxOptionSpawned = spawn("npx", ["--yes", "@tailor-platform/sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "--profile", "dev", "login"]); const npxShortProfileSpawned = spawn("npx", ["@tailor-platform/sdk", "-p", "dev", "login"]); diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js index b9459d42e..4e7c65ff0 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-js-string/input.js @@ -1,6 +1,7 @@ const script = "tailor-sdk deploy"; const proseApply = "Run tailor-sdk deploy to apply changes"; const spawned = spawn("tailor-sdk", ["deploy"]); +const optionOverloadSpawned = spawn("tailor-sdk", { stdio: "inherit" }); const runtimeArg = getArg(); const scopedRuntimeArgSpawned = spawn("tailor-sdk", [runtimeArg]); const hiddenApplyArgs = ["apply"]; @@ -49,6 +50,9 @@ const secretShortValueApplySpawned = spawn("tailor-sdk", ["secret", "create", "- const dynamicCliRenameCommandSpawned = spawn("tailor-sdk", [`${"apply"}`]); const dynamicCliRenameAliasCommand = `tailor-sdk ${legacyTemplateArg}`; const npxSpawned = spawn("npx", ["tailor-sdk", "login"]); +const npxLegacyApplySpawned = spawn("npx", ["tailor-sdk", "apply"]); +const npxLegacyCrashReportSpawned = spawn("npx", ["tailor-sdk", "crash-report", "list"]); +const npxLegacyMachineUserSpawned = spawn("npx", ["tailor-sdk", "login", "--machineuser"]); const npxOptionSpawned = spawn("npx", ["--yes", "tailor-sdk@latest", "login"]); const npxProfileSpawned = spawn("npx", ["tailor-sdk", "--profile", "dev", "login"]); const npxShortProfileSpawned = spawn("npx", ["tailor-sdk", "-p", "dev", "login"]); From 39212359b658e0de1743545b7d4c23bab032ad09 Mon Sep 17 00:00:00 2001 From: dqn Date: Sun, 28 Jun 2026 07:18:39 +0900 Subject: [PATCH 353/618] fix(sdk-codemod): guard source option residuals --- .../v2/rename-bin/scripts/transform.ts | 38 +++++++++++++++---- .../tests/source-template/expected.ts | 3 ++ .../rename-bin/tests/source-template/input.ts | 3 ++ packages/sdk-codemod/src/registry.test.ts | 4 ++ packages/sdk-codemod/src/registry.ts | 1 + 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index d983ff7e0..689ede11c 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -103,7 +103,7 @@ const SOURCE_OPTION_VALUE_REFERENCE_RE = new RegExp( const SOURCE_TEMPLATE_EXPR_PLACEHOLDER = "__TAILOR_SDK_TEMPLATE_EXPR_\\d+_\\d+__"; const SOURCE_TEMPLATE_EXPR_PLACEHOLDER_RE = /^__TAILOR_SDK_TEMPLATE_EXPR_\d+_\d+__$/; const SOURCE_TEMPLATE_DYNAMIC_ARGS = `\\s+${SOURCE_TEMPLATE_EXPR_PLACEHOLDER}(?:\\s+${SOURCE_ARG_VALUE})*(?=\\s*(?:$|[;&|]))`; -const SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD = "\\s+(?:--help|-h|--version|-v)\\b"; +const SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD = `${SOURCE_COMMAND_GAP}\\s+(?:--help|-h|--version|-v)\\b`; const SOURCE_DIRECT_INVOCATION_LOOKAHEAD = `(?:${SOURCE_COMMAND_GAP}\\s+${TAILOR_CLI_COMMAND_PATTERN}\\b|${SOURCE_CLI_STANDALONE_FLAG_LOOKAHEAD})`; const SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD = `(?:${SOURCE_DIRECT_INVOCATION_LOOKAHEAD}|${SOURCE_TEMPLATE_DYNAMIC_ARGS}|\\s*$)`; const SOURCE_PKG_RUNNER_INVOCATION_LOOKAHEAD = `(?:${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD}|\\s+tailor-sdk(?![\\w-])(?:@[^\\s'"\`;|&)]+)?${SOURCE_PKG_RUNNER_COMMAND_LOOKAHEAD})`; @@ -224,11 +224,30 @@ function replaceSourceSpans( return updated; } +type ProtectedSourceValue = { placeholder: string; value: string }; + +function sourceValuePlaceholder( + index: number, + source: string, + usedPlaceholders: Set, +): string { + let attempt = 0; + while (true) { + const placeholder = `__TAILOR_SDK_SOURCE_VALUE_${index}_${attempt}__`; + if (!source.includes(placeholder) && !usedPlaceholders.has(placeholder)) { + usedPlaceholders.add(placeholder); + return placeholder; + } + attempt += 1; + } +} + function protectSourceCliValueReferences(value: string): { source: string; - protectedValues: string[]; + protectedValues: ProtectedSourceValue[]; } { - const protectedValues: string[] = []; + const protectedValues: ProtectedSourceValue[] = []; + const usedPlaceholders = new Set(); const source = value.replace( SOURCE_OPTION_VALUE_REFERENCE_RE, (match: string, prefix: string, arg: string, offset: number) => { @@ -244,18 +263,21 @@ function protectSourceCliValueReferences(value: string): { if (isPackageFlag(flag) && NPX_PACKAGE_FLAG_CONTEXT_RE.test(value.slice(0, offset))) { return match; } - const placeholder = `__TAILOR_SDK_SOURCE_VALUE_${protectedValues.length}__`; - protectedValues.push(arg); + const placeholder = sourceValuePlaceholder(protectedValues.length, value, usedPlaceholders); + protectedValues.push({ placeholder, value: arg }); return `${prefix}${placeholder}`; }, ); return { source, protectedValues }; } -function restoreSourceCliValueReferences(value: string, protectedValues: string[]): string { +function restoreSourceCliValueReferences( + value: string, + protectedValues: ProtectedSourceValue[], +): string { let restored = value; - for (const [index, protectedValue] of protectedValues.entries()) { - restored = restored.replaceAll(`__TAILOR_SDK_SOURCE_VALUE_${index}__`, () => protectedValue); + for (const protectedValue of protectedValues) { + restored = restored.replaceAll(protectedValue.placeholder, () => protectedValue.value); } return restored; } diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index fdc57b070..5fafb9e95 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -10,8 +10,11 @@ const directNameValue = "tailor tailordb migration generate --name \"tailor-sdk const namespaceValue = "tailor tailordb truncate --namespace tailor-sdk"; const dirValue = "tailor staticwebsite deploy --name site --dir tailor-sdk"; const pathQualifiedArgValue = "./node_modules/.bin/tailor --arg \"tailor-sdk deploy\" deploy"; +const placeholderCollision = "tailor deploy --arg \"tailor-sdk deploy\" __TAILOR_SDK_SOURCE_VALUE_0_0__"; const pathQualifiedCliRenameCommand = "./node_modules/.bin/tailor-sdk apply"; const help = "tailor --help"; +const optionHelp = "tailor --env-file .env --help"; +const optionVersion = "tailor --profile dev --version"; const windowsShimDeploy = "tailor.cmd deploy"; const pathQualifiedWindowsShimDeploy = "./node_modules/.bin/tailor.ps1 deploy"; const npxVersion = "npx @tailor-platform/sdk --version"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index a825e93e5..ebc39fd69 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -10,8 +10,11 @@ const directNameValue = "tailor-sdk tailordb migration generate --name \"tailor- const namespaceValue = "tailor tailordb truncate --namespace tailor-sdk"; const dirValue = "tailor staticwebsite deploy --name site --dir tailor-sdk"; const pathQualifiedArgValue = "./node_modules/.bin/tailor-sdk --arg \"tailor-sdk deploy\" deploy"; +const placeholderCollision = "tailor-sdk deploy --arg \"tailor-sdk deploy\" __TAILOR_SDK_SOURCE_VALUE_0_0__"; const pathQualifiedCliRenameCommand = "./node_modules/.bin/tailor-sdk apply"; const help = "tailor-sdk --help"; +const optionHelp = "tailor-sdk --env-file .env --help"; +const optionVersion = "tailor-sdk --profile dev --version"; const windowsShimDeploy = "tailor-sdk.cmd deploy"; const pathQualifiedWindowsShimDeploy = "./node_modules/.bin/tailor-sdk.ps1 deploy"; const npxVersion = "npx tailor-sdk --version"; diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 7737e340a..7ef0b9345 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -77,6 +77,10 @@ describe("getApplicableCodemods", () => { expect(matchesSourceStringPattern("tailor --profile tailor-sdk deploy")).toBe(false); expect(matchesSourceStringPattern("tailor --name tailor-sdk deploy")).toBe(false); expect(matchesSourceStringPattern('tailor --arg "tailor-sdk deploy" deploy')).toBe(false); + expect(matchesSourceStringPattern('tailor --arg "tailor-sdk apply" deploy')).toBe(false); + expect(matchesSourceStringPattern("tailor --name 'tailor-sdk crash-report list' deploy")).toBe( + false, + ); const matches = picomatch(renameBin?.filePatterns ?? [], { dot: true }); expect(matches("packages/app/frontend/e2e/global-setup.ts")).toBe(true); expect(matches("tailor.d.ts")).toBe(true); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 5f5b3d2b1..9e61aacd7 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -88,6 +88,7 @@ const RENAME_BIN_QUOTED_SOURCE_LEGACY_PATTERN = new RegExp( ); const RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN = new RegExp( [ + RENAME_BIN_SOURCE_VALUE_GUARDS, "[\"']", RENAME_BIN_SOURCE_COMMAND_TOKEN, "(?=\\s*(?:apply\\b|crash-report\\b|[^\"'`]*\\s--machineuser\\b))", From dbd052a272d9be2532afb5aa91caa16c826c791e Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 29 Jun 2026 10:47:33 +0900 Subject: [PATCH 354/618] fix: reduce duplicate codemod review work --- .../v2/principal-unify/scripts/transform.ts | 56 ++++++++++--------- packages/sdk-codemod/src/runner.ts | 21 +++++-- 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 52d1cbddf..81d0af2e9 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -2230,14 +2230,6 @@ function resolvesToReviewBinding( }); } -function resolvesToReviewPrincipalBinding( - node: SgNode, - bindings: ReviewPrincipalBinding[], - root: SgNode, -): boolean { - return resolvesToReviewBinding(node, bindings, root); -} - function isReviewContextCallerMemberExpression( node: SgNode, contextBindings: ReviewPrincipalBinding[], @@ -2260,7 +2252,7 @@ function isPrincipalOptionalMemberExpression( const object = node.field("object"); if (!object) return false; if (object.kind() === "identifier") { - return resolvesToReviewPrincipalBinding(object, principalBindings, root); + return resolvesToReviewBinding(object, principalBindings, root); } return isReviewContextCallerMemberExpression(object, contextBindings, root); } @@ -2271,7 +2263,7 @@ function isDirectPrincipalExpression( contextBindings: ReviewPrincipalBinding[], root: SgNode, ): boolean { - if (resolvesToReviewPrincipalBinding(node, principalBindings, root)) return true; + if (resolvesToReviewBinding(node, principalBindings, root)) return true; return isReviewContextCallerMemberExpression(node, contextBindings, root); } @@ -2581,20 +2573,23 @@ function collectResolverBodyArrows(root: SgNode): SgNode[] { return arrows; } -function collectResolverContextBodies(root: SgNode): ResolverContextBody[] { +function collectResolverContextBodies(resolverBodyArrows: SgNode[]): ResolverContextBody[] { const bodies: ResolverContextBody[] = []; - for (const arrow of collectResolverBodyArrows(root)) { + for (const arrow of resolverBodyArrows) { addResolverContextBody(arrow, bodies); } return bodies; } -function collectResolverContextBindings(root: SgNode): ReviewPrincipalBinding[] { +function collectResolverContextBindings( + root: SgNode, + resolverBodyArrows: SgNode[], +): ReviewPrincipalBinding[] { const bindings: ReviewPrincipalBinding[] = []; const rootRange = root.range(); const rootScope: [number, number] = [rootRange.start.index, rootRange.end.index]; - for (const arrow of collectResolverBodyArrows(root)) { + for (const arrow of resolverBodyArrows) { const param = getFirstFunctionParam(arrow); const pattern = param ? getFunctionParamPattern(param) : null; const body = arrow.field("body"); @@ -2675,9 +2670,10 @@ function collectCallerPatternBindings( function collectResolverPrincipalBindings( root: SgNode, contextBindings: ReviewPrincipalBinding[], + resolverBodyArrows: SgNode[], ): ReviewPrincipalBinding[] { const bindings: ReviewPrincipalBinding[] = []; - for (const arrow of collectResolverBodyArrows(root)) { + for (const arrow of resolverBodyArrows) { const param = getFirstFunctionParam(arrow); const pattern = param ? getFunctionParamPattern(param) : null; const body = arrow.field("body"); @@ -2708,7 +2704,7 @@ function collectResolverPrincipalBindings( if (bindings.some((binding) => binding.bindingStart === bindingStart)) continue; if ( !isReviewContextCallerMemberExpression(value, contextBindings, root) && - !resolvesToReviewPrincipalBinding(value, bindings, root) + !resolvesToReviewBinding(value, bindings, root) ) { continue; } @@ -2737,6 +2733,7 @@ function collectContextUserHelperReviewFindings( root: SgNode, source: string, file: string, + resolverBodyArrows: SgNode[], findings: LlmReviewFinding[], seen: Set, ): void { @@ -2744,7 +2741,7 @@ function collectContextUserHelperReviewFindings( if (helperBindings.length === 0) return; const reportedDefinitions = new Set(); - for (const { arrow, body, contextName } of collectResolverContextBodies(root)) { + for (const { arrow, body, contextName } of collectResolverContextBodies(resolverBodyArrows)) { const shadowRanges = collectCtxShadowRanges(body, contextName, arrow); const calls = body.findAll({ rule: { kind: "call_expression" } }); for (const call of calls) { @@ -2784,12 +2781,7 @@ export function reviewFindings( _filePath: string, relativePath: string, ): LlmReviewFinding[] { - if ( - !source.includes("?.") && - !source.includes(".user") && - !source.includes("user") && - !source.includes("caller") - ) { + if (!source.includes("?.") && !source.includes("user") && !source.includes("caller")) { return []; } @@ -2802,8 +2794,13 @@ export function reviewFindings( const findings: LlmReviewFinding[] = []; const seen = new Set(); - const contextBindings = collectResolverContextBindings(root); - const principalBindings = collectResolverPrincipalBindings(root, contextBindings); + const resolverBodyArrows = collectResolverBodyArrows(root); + const contextBindings = collectResolverContextBindings(root, resolverBodyArrows); + const principalBindings = collectResolverPrincipalBindings( + root, + contextBindings, + resolverBodyArrows, + ); const sdkFieldRootNames = collectSdkFieldRootNames(root); const sdkFieldLocalBindings = collectSdkFieldLocalBindings(root, sdkFieldRootNames); collectNullableCallerReviewFindings( @@ -2817,7 +2814,14 @@ export function reviewFindings( findings, seen, ); - collectContextUserHelperReviewFindings(root, source, relativePath, findings, seen); + collectContextUserHelperReviewFindings( + root, + source, + relativePath, + resolverBodyArrows, + findings, + seen, + ); return findings; } diff --git a/packages/sdk-codemod/src/runner.ts b/packages/sdk-codemod/src/runner.ts index 5572d2b18..ac6c916fc 100644 --- a/packages/sdk-codemod/src/runner.ts +++ b/packages/sdk-codemod/src/runner.ts @@ -395,21 +395,30 @@ export async function runCodemods( if (lt.reviewFindings) { const findings = await lt.reviewFindings(current, absolute, relative); if (findings.length > 0) { - const files = suspiciousByCodemod.get(lt.id) ?? new Set(); + let files = suspiciousByCodemod.get(lt.id); + if (!files) { + files = new Set(); + suspiciousByCodemod.set(lt.id, files); + } for (const finding of findings) { files.add(finding.file); } - suspiciousByCodemod.set(lt.id, files); - const existing = findingsByCodemod.get(lt.id) ?? []; + let existing = findingsByCodemod.get(lt.id); + if (!existing) { + existing = []; + findingsByCodemod.set(lt.id, existing); + } existing.push(...findings); - findingsByCodemod.set(lt.id, existing); } } if (lt.suspiciousPatterns.length === 0) continue; if (lt.suspiciousPatterns.some((p) => matchResidualPattern(residualContent, p) !== null)) { - const files = suspiciousByCodemod.get(lt.id) ?? new Set(); + let files = suspiciousByCodemod.get(lt.id); + if (!files) { + files = new Set(); + suspiciousByCodemod.set(lt.id, files); + } files.add(relative); - suspiciousByCodemod.set(lt.id, files); } } } From e3cb1adc4c027d51077e64d95d53edc60b6854ff Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 29 Jun 2026 10:56:51 +0900 Subject: [PATCH 355/618] fix(sdk-codemod): assert prerelease registry boundaries --- packages/sdk-codemod/src/registry.test.ts | 19 ++++++++ packages/sdk-codemod/src/registry.ts | 54 ++++++++++++++++++----- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 8843984ec..bc58e8481 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -51,6 +51,25 @@ describe("getApplicableCodemods", () => { expect(ids).not.toContain("v2/principal-unify"); }); + test("throws when a prerelease boundary is not a prerelease version", () => { + allCodemods.push({ + id: "v2/invalid-prerelease-boundary", + name: "Invalid prerelease boundary", + description: "Invalid prerelease boundary", + since: "1.0.0", + until: "2.0.0", + prereleaseUntil: "2.0.0", + }); + + try { + expect(() => getApplicableCodemods("1.0.0", "2.0.0-next.1")).toThrow( + "Codemod v2/invalid-prerelease-boundary prereleaseUntil must be a prerelease version: 2.0.0", + ); + } finally { + allCodemods.pop(); + } + }); + test("returns empty when the source prerelease already reached the codemod boundary", () => { expect(getApplicableCodemods("2.0.0-next.2", "2.0.0-next.2")).toEqual([]); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index b671ce7ed..f7579fa21 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -626,23 +626,14 @@ function reachesCodemodBoundary(toVersion: string, codemod: CodemodPackage): boo return false; } - const target = parse(toVersion); - const boundary = parse(codemod.until); - const prereleaseBoundary = parse(codemod.prereleaseUntil); + const target = parse(toVersion)!; + const boundary = parse(codemod.until)!; return ( - target !== null && - boundary !== null && - prereleaseBoundary !== null && target.prerelease.length > 0 && - boundary.prerelease.length === 0 && - prereleaseBoundary.prerelease.length > 0 && target.major === boundary.major && target.minor === boundary.minor && - target.patch === boundary.patch && - prereleaseBoundary.major === boundary.major && - prereleaseBoundary.minor === boundary.minor && - prereleaseBoundary.patch === boundary.patch + target.patch === boundary.patch ); } @@ -650,6 +641,44 @@ function effectiveCodemodBoundary(codemod: CodemodPackage): string { return codemod.prereleaseUntil ?? codemod.until; } +function assertCodemodBoundaries(codemods: CodemodPackage[]): void { + for (const codemod of codemods) { + const boundary = parse(codemod.until); + if (boundary === null) { + throw new Error( + `Codemod ${codemod.id} until must be a valid semver version: ${codemod.until}`, + ); + } + if (boundary.prerelease.length > 0) { + throw new Error(`Codemod ${codemod.id} until must be a stable version: ${codemod.until}`); + } + if (codemod.prereleaseUntil === undefined) { + continue; + } + + const prereleaseBoundary = parse(codemod.prereleaseUntil); + if (prereleaseBoundary === null) { + throw new Error( + `Codemod ${codemod.id} prereleaseUntil must be a valid semver version: ${codemod.prereleaseUntil}`, + ); + } + if (prereleaseBoundary.prerelease.length === 0) { + throw new Error( + `Codemod ${codemod.id} prereleaseUntil must be a prerelease version: ${codemod.prereleaseUntil}`, + ); + } + if ( + prereleaseBoundary.major !== boundary.major || + prereleaseBoundary.minor !== boundary.minor || + prereleaseBoundary.patch !== boundary.patch + ) { + throw new Error( + `Codemod ${codemod.id} prereleaseUntil must target the same version as until: ${codemod.prereleaseUntil}`, + ); + } + } +} + /** * Get codemod packages applicable for a version range. * A codemod applies when: since <= fromVersion < boundary <= toVersion. @@ -665,6 +694,7 @@ export function getApplicableCodemods(fromVersion: string, toVersion: string): C if (!valid(toVersion)) { throw new Error(`Invalid toVersion: ${toVersion}`); } + assertCodemodBoundaries(allCodemods); return allCodemods.filter( (codemod) => From de1b909b478c19fc8a08c4aa2dac59f256a9bbdb Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 29 Jun 2026 16:36:52 +0900 Subject: [PATCH 356/618] fix(sdk-codemod): allow aliased idp migrations with local idp --- .../scripts/transform.ts | 18 +++++++++++++++--- .../aliased-idp-import-local-idp/expected.ts | 8 ++++++++ .../aliased-idp-import-local-idp/input.ts | 8 ++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/aliased-idp-import-local-idp/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/aliased-idp-import-local-idp/input.ts diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts index 902a8a765..d77690d09 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts @@ -205,8 +205,18 @@ function runtimeIdpLocalName(imports: SgNode[]): string | null { return null; } -function hasCollision(imports: SgNode[], localNames: Set, idpLocal: string): boolean { - if (localNames.has("tailor") || localNames.has("idp") || localNames.has(idpLocal)) return true; +function hasCollision( + imports: SgNode[], + localNames: Set, + idpLocal: string, + injectingNewIdpName: boolean, +): boolean { + if ( + localNames.has("tailor") || + (injectingNewIdpName && localNames.has("idp")) || + localNames.has(idpLocal) + ) + return true; for (const importStmt of imports) { for (const binding of importBindings(importStmt)) { @@ -312,7 +322,9 @@ export default function transform(source: string, filePath: string): string | nu const imports = findImportStatements(root); const existingIdpLocal = runtimeIdpLocalName(imports); const idpLocal = existingIdpLocal ?? "idp"; - if (hasCollision(imports, localDeclarationNames(root), idpLocal)) return null; + if (hasCollision(imports, localDeclarationNames(root), idpLocal, existingIdpLocal === null)) { + return null; + } const edits: Edit[] = constructors.map((constructor) => constructor.replace(`${idpLocal}.Client`), diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/aliased-idp-import-local-idp/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/aliased-idp-import-local-idp/expected.ts new file mode 100644 index 000000000..8903f6607 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/aliased-idp-import-local-idp/expected.ts @@ -0,0 +1,8 @@ +import { idp as runtimeIdp } from "@tailor-platform/sdk/runtime"; + +const idp = createLocalIdp(); + +export async function run() { + const client = new runtimeIdp.Client({ namespace: "default" }); + return client.listUsers(idp); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/aliased-idp-import-local-idp/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/aliased-idp-import-local-idp/input.ts new file mode 100644 index 000000000..3b3fb0433 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/aliased-idp-import-local-idp/input.ts @@ -0,0 +1,8 @@ +import { idp as runtimeIdp } from "@tailor-platform/sdk/runtime"; + +const idp = createLocalIdp(); + +export async function run() { + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(idp); +} From 6ec3615329be3cf6dd6031b08554298ada9c0798 Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 29 Jun 2026 16:41:15 +0900 Subject: [PATCH 357/618] fix: tighten schema parsing review gaps --- .../src/cli/query/type-field-order.test.ts | 51 +++++++++++++++++++ .../sdk/src/cli/query/type-field-order.ts | 5 +- packages/sdk/src/parser/schema-strict.test.ts | 7 ++- .../sdk/src/parser/service/auth/schema.ts | 2 +- 4 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 packages/sdk/src/cli/query/type-field-order.test.ts diff --git a/packages/sdk/src/cli/query/type-field-order.test.ts b/packages/sdk/src/cli/query/type-field-order.test.ts new file mode 100644 index 000000000..1367c31b3 --- /dev/null +++ b/packages/sdk/src/cli/query/type-field-order.test.ts @@ -0,0 +1,51 @@ +import * as fs from "node:fs"; +import * as path from "pathe"; +import { afterEach, describe, expect, test } from "vitest"; +import { loadTypeFieldOrder } from "./type-field-order"; +import type { LoadedConfig } from "../shared/config-loader"; + +describe("loadTypeFieldOrder", () => { + let tmpDir: string | undefined; + + afterEach(() => { + if (tmpDir) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + tmpDir = undefined; + } + }); + + function writeTypeFile(name: string, source: string): string { + if (!tmpDir) { + tmpDir = fs.realpathSync(fs.mkdtempSync(path.join(import.meta.dirname, ".type-order-"))); + } + const file = path.join(tmpDir, name); + fs.writeFileSync(file, source); + return file; + } + + test("loads field order from db.type builder outputs", async () => { + const typeFile = writeTypeFile( + "user.ts", + ` +import { db } from "@tailor-platform/sdk"; +export const user = db.type("User", { + firstName: db.string(), + lastName: db.string(), +}); +`, + ); + const config = { + path: "tailor.config.ts", + name: "sample-app", + db: { + main: { + files: [typeFile], + }, + }, + } as LoadedConfig; + + await expect(loadTypeFieldOrder(config, "main")).resolves.toEqual( + new Map([["User", ["id", "firstName", "lastName"]]]), + ); + }); +}); diff --git a/packages/sdk/src/cli/query/type-field-order.ts b/packages/sdk/src/cli/query/type-field-order.ts index f7209bcf9..c3b16c49e 100644 --- a/packages/sdk/src/cli/query/type-field-order.ts +++ b/packages/sdk/src/cli/query/type-field-order.ts @@ -1,5 +1,6 @@ import { pathToFileURL } from "node:url"; import { loadFilesWithIgnores } from "#/cli/services/file-loader"; +import { stripTailorDBTypeBuilderHelpers } from "#/parser/service/tailordb/builder-helpers"; import { TailorDBTypeSchema } from "#/parser/service/tailordb/index"; import type { LoadedConfig } from "#/cli/shared/config-loader"; @@ -30,7 +31,9 @@ export async function loadTypeFieldOrder( const module = await import(pathToFileURL(typeFile).href); for (const exportedValue of Object.values(module)) { - const result = TailorDBTypeSchema.safeParse(exportedValue); + const result = TailorDBTypeSchema.safeParse( + stripTailorDBTypeBuilderHelpers(exportedValue), + ); if (!result.success) { continue; } diff --git a/packages/sdk/src/parser/schema-strict.test.ts b/packages/sdk/src/parser/schema-strict.test.ts index 001e9b7bc..59ea742c3 100644 --- a/packages/sdk/src/parser/schema-strict.test.ts +++ b/packages/sdk/src/parser/schema-strict.test.ts @@ -3,7 +3,7 @@ import { AppConfigSchema } from "./app-config/schema"; import { PluginConfigSchema } from "./plugin-config/schema"; import { AIGatewaySchema } from "./service/aigateway/schema"; import { AuthConnectionConfigSchema } from "./service/auth-connection/schema"; -import { AuthConfigSchema } from "./service/auth/schema"; +import { AuthConfigSchema, SCIMAttributeSchema } from "./service/auth/schema"; import { ExecutorSchema } from "./service/executor/schema"; import { TailorFieldSchema } from "./service/field/schema"; import { IdPSchema } from "./service/idp/schema"; @@ -58,6 +58,11 @@ const strictSchemaCases: StrictSchemaCase[] = [ schema: AuthConfigSchema, value: { name: "my-auth" }, }, + { + name: "SCIM attribute", + schema: SCIMAttributeSchema, + value: { type: "string", name: "userName" }, + }, { name: "executor", schema: ExecutorSchema, diff --git a/packages/sdk/src/parser/service/auth/schema.ts b/packages/sdk/src/parser/service/auth/schema.ts index ce6659c04..058990db2 100644 --- a/packages/sdk/src/parser/service/auth/schema.ts +++ b/packages/sdk/src/parser/service/auth/schema.ts @@ -153,7 +153,7 @@ export const SCIMAttributeTypeSchema = z ]) .describe("SCIM attribute data type"); -export const SCIMAttributeSchema = z.object({ +export const SCIMAttributeSchema = z.strictObject({ type: SCIMAttributeTypeSchema.describe("Attribute data type"), name: z.string().describe("Attribute name"), description: z.string().optional().describe("Attribute description"), From 144f3e30f2b0c5dac1a3288ff65c9dc5ca82c13b Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 29 Jun 2026 16:42:24 +0900 Subject: [PATCH 358/618] fix(sdk-codemod): fix principal-unify review findings --- .changeset/principal-unify-review-followup.md | 5 ++ .../v2/principal-unify/scripts/transform.ts | 60 +++++++++++++------ packages/sdk-codemod/src/index.ts | 7 ++- .../src/principal-unify-review.test.ts | 29 ++++++++- 4 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 .changeset/principal-unify-review-followup.md diff --git a/.changeset/principal-unify-review-followup.md b/.changeset/principal-unify-review-followup.md new file mode 100644 index 000000000..2dd4833ae --- /dev/null +++ b/.changeset/principal-unify-review-followup.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Fix `v2/principal-unify` review findings for nested SDK field parser invoker values and destructured context helper messages. diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 81d0af2e9..0d4059209 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -2272,9 +2272,23 @@ function nodeContainsArgumentPrincipalOptionalAccess( principalBindings: ReviewPrincipalBinding[], contextBindings: ReviewPrincipalBinding[], safePrincipalRanges: Array<[number, number]>, + sdkFieldRootNames: Set, + sdkFieldLocalBindings: SdkFieldLocalBinding[], root: SgNode, ): boolean { - if (isInsideAnyRange(node.range().start.index, safePrincipalRanges)) return false; + const nodeSafePrincipalRanges = + node.kind() === "call_expression" + ? [ + ...safePrincipalRanges, + ...collectSafeNullablePrincipalArgumentRanges( + node, + sdkFieldRootNames, + sdkFieldLocalBindings, + root, + ), + ] + : safePrincipalRanges; + if (isInsideAnyRange(node.range().start.index, nodeSafePrincipalRanges)) return false; if (isFunctionNode(node)) return false; if (isDirectPrincipalExpression(node, principalBindings, contextBindings, root)) return true; if (isPrincipalOptionalMemberExpression(node, principalBindings, contextBindings, root)) @@ -2286,7 +2300,9 @@ function nodeContainsArgumentPrincipalOptionalAccess( child, principalBindings, contextBindings, - safePrincipalRanges, + nodeSafePrincipalRanges, + sdkFieldRootNames, + sdkFieldLocalBindings, root, ), ); @@ -2366,6 +2382,8 @@ function collectNullableCallerReviewFindings( principalBindings, contextBindings, safePrincipalRanges, + sdkFieldRootNames, + sdkFieldLocalBindings, root, ), ); @@ -2401,24 +2419,32 @@ function functionIdentifierParamName(fn: SgNode): string | null { return pattern?.kind() === "identifier" ? pattern.text() : null; } -function objectPatternHasTopLevelProperty(pattern: SgNode, propertyName: string): boolean { - if (pattern.kind() !== "object_pattern") return false; +function objectPatternTopLevelPropertyBindingName( + pattern: SgNode, + propertyName: string, +): string | null { + if (pattern.kind() !== "object_pattern") return null; for (const child of pattern.children()) { const kind = child.kind(); if (kind === "shorthand_property_identifier_pattern" && child.text() === propertyName) { - return true; + return child.text(); } if (kind === "pair_pattern" && child.field("key")?.text() === propertyName) { - return true; + const value = child.field("value"); + return value?.kind() === "identifier" ? value.text() : propertyName; } if (kind === "object_assignment_pattern") { const inner = child .children() .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); - if (inner?.text() === propertyName) return true; + if (inner?.text() === propertyName) return inner.text(); } } - return false; + return null; +} + +function objectPatternHasTopLevelProperty(pattern: SgNode, propertyName: string): boolean { + return objectPatternTopLevelPropertyBindingName(pattern, propertyName) !== null; } function functionReadsContextUser(fn: SgNode, contextName: string): boolean { @@ -2456,23 +2482,23 @@ function functionReadsContextUser(fn: SgNode, contextName: string): boolean { return false; } -function functionContextUserSourceName(fn: SgNode): string | null { +function functionContextUserReadExpression(fn: SgNode): string | null { const param = getFirstFunctionParam(fn); const pattern = param ? getFunctionParamPattern(param) : null; if (!pattern) return null; if (pattern.kind() === "identifier") { - return functionReadsContextUser(fn, pattern.text()) ? pattern.text() : null; + return functionReadsContextUser(fn, pattern.text()) ? `${pattern.text()}.user` : null; } - return objectPatternHasTopLevelProperty(pattern, "user") ? "context" : null; + return objectPatternTopLevelPropertyBindingName(pattern, "user"); } -type ContextUserHelperBinding = LocalCallbackBinding & { contextName: string }; +type ContextUserHelperBinding = LocalCallbackBinding & { contextUserExpression: string }; function collectContextUserHelperBindings(root: SgNode): ContextUserHelperBinding[] { return collectLocalCallbackBindings(root).flatMap((binding) => { - const contextName = functionContextUserSourceName(binding.fn); - if (!contextName) return []; - return [{ ...binding, contextName }]; + const contextUserExpression = functionContextUserReadExpression(binding.fn); + if (!contextUserExpression) return []; + return [{ ...binding, contextUserExpression }]; }); } @@ -2761,7 +2787,7 @@ function collectContextUserHelperReviewFindings( source, file, helper.fn, - `Helper adapter ${helper.name} reads ${helper.contextName}.user and needs v2 caller/invoker semantics.`, + `Helper adapter ${helper.name} reads ${helper.contextUserExpression} and needs v2 caller/invoker semantics.`, ); } addReviewFinding( @@ -2770,7 +2796,7 @@ function collectContextUserHelperReviewFindings( source, file, call, - `${helper.name}(${contextName}) passes an SDK resolver context into a helper that reads ${helper.contextName}.user.`, + `${helper.name}(${contextName}) passes an SDK resolver context into a helper that reads ${helper.contextUserExpression}.`, ); } } diff --git a/packages/sdk-codemod/src/index.ts b/packages/sdk-codemod/src/index.ts index 7973b2683..2cb5e7e57 100644 --- a/packages/sdk-codemod/src/index.ts +++ b/packages/sdk-codemod/src/index.ts @@ -60,9 +60,12 @@ function printLlmReview(review: LlmReview): void { process.stderr.write(`\n🤖 LLM-assisted review suggested (${review.codemodId}) — ${scope}:\n`); const findingsByFile = new Map>(); for (const finding of review.findings ?? []) { - const findings = findingsByFile.get(finding.file) ?? []; + let findings = findingsByFile.get(finding.file); + if (!findings) { + findings = []; + findingsByFile.set(finding.file, findings); + } findings.push(finding); - findingsByFile.set(finding.file, findings); } for (const file of review.files) { process.stderr.write(` - ${file}\n`); diff --git a/packages/sdk-codemod/src/principal-unify-review.test.ts b/packages/sdk-codemod/src/principal-unify-review.test.ts index 4682fd10e..3459e16cf 100644 --- a/packages/sdk-codemod/src/principal-unify-review.test.ts +++ b/packages/sdk-codemod/src/principal-unify-review.test.ts @@ -191,12 +191,14 @@ describe("principal-unify review findings", () => { expect.objectContaining({ file: "resolvers/destructured-param-helper.ts", line: 3, - message: expect.stringContaining("createContext"), + message: + "Helper adapter createContext reads user and needs v2 caller/invoker semantics.", }), expect.objectContaining({ file: "resolvers/destructured-param-helper.ts", line: 8, - message: expect.stringContaining("createContext"), + message: + "createContext(context) passes an SDK resolver context into a helper that reads user.", }), ], }), @@ -388,6 +390,29 @@ describe("principal-unify review findings", () => { expect(result.llmReviews.flatMap((review) => review.findings ?? [])).toEqual([]); }); + test("does not report nested SDK field parse invoker arguments", async () => { + await writeProjectFile( + "resolvers/nested-parse-invoker.ts", + [ + 'import { createResolver, t } from "@tailor-platform/sdk";', + "", + "const nameField = t.string();", + "declare function wrap(value: unknown): unknown;", + "", + "export const resolver = createResolver({", + " body: async ({ input, user }) => {", + " return wrap(nameField.parse({ value: input.name, invoker: user.id }));", + " },", + "});", + "", + ].join("\n"), + ); + + const result = await runCodemods([principalUnifyEntry], tmpDir!, false); + + expect(result.llmReviews.flatMap((review) => review.findings ?? [])).toEqual([]); + }); + test("does not report matching alias names outside the resolver scope", async () => { await writeProjectFile( "resolvers/scoped-alias.ts", From f536ce0d3c8e64a001a3cffc381489941b8866ba Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 29 Jun 2026 16:54:49 +0900 Subject: [PATCH 359/618] test(sdk-codemod): cover aliased helper review findings --- .../src/principal-unify-review.test.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/sdk-codemod/src/principal-unify-review.test.ts b/packages/sdk-codemod/src/principal-unify-review.test.ts index 3459e16cf..761b1c442 100644 --- a/packages/sdk-codemod/src/principal-unify-review.test.ts +++ b/packages/sdk-codemod/src/principal-unify-review.test.ts @@ -205,6 +205,47 @@ describe("principal-unify review findings", () => { ]); }); + test("reports helper adapters with aliased destructured context parameters", async () => { + await writeProjectFile( + "resolvers/aliased-destructured-param-helper.ts", + [ + 'import { createResolver } from "@tailor-platform/sdk";', + "", + "function createContext({ user: currentUser }: any) {", + " return { userId: currentUser.id };", + "}", + "", + "export const resolver = createResolver({", + " body: async (context) => createContext(context),", + "});", + "", + ].join("\n"), + ); + + const result = await runCodemods([principalUnifyEntry], tmpDir!, false); + + expect(result.llmReviews).toEqual([ + expect.objectContaining({ + codemodId: "v2/principal-unify", + files: ["resolvers/aliased-destructured-param-helper.ts"], + findings: [ + expect.objectContaining({ + file: "resolvers/aliased-destructured-param-helper.ts", + line: 3, + message: + "Helper adapter createContext reads currentUser and needs v2 caller/invoker semantics.", + }), + expect.objectContaining({ + file: "resolvers/aliased-destructured-param-helper.ts", + line: 8, + message: + "createContext(context) passes an SDK resolver context into a helper that reads currentUser.", + }), + ], + }), + ]); + }); + test("keeps file-level suspicious-pattern fallback without precise findings", async () => { await writeProjectFile( "resolvers/context-type.ts", From 095674e80f85b58fbf4f433147e5133728cf1154 Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 29 Jun 2026 17:56:10 +0900 Subject: [PATCH 360/618] fix(sdk): strip branded parser helpers consistently --- .../src/cli/commands/function/detect.test.ts | 26 ++++++++++++++ .../sdk/src/cli/commands/function/detect.ts | 3 +- .../sdk/src/cli/services/resolver/service.ts | 3 +- .../src/cli/services/workflow/service.test.ts | 34 +++++++++++++++++++ .../sdk/src/cli/services/workflow/service.ts | 7 +++- .../service/resolver/builder-helpers.ts | 15 ++++++++ 6 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 packages/sdk/src/cli/services/workflow/service.test.ts create mode 100644 packages/sdk/src/parser/service/resolver/builder-helpers.ts diff --git a/packages/sdk/src/cli/commands/function/detect.test.ts b/packages/sdk/src/cli/commands/function/detect.test.ts index 0d71cd7a3..90b9164cd 100644 --- a/packages/sdk/src/cli/commands/function/detect.test.ts +++ b/packages/sdk/src/cli/commands/function/detect.test.ts @@ -40,6 +40,32 @@ export default { expect(result.type).toBe("resolver"); expect(result.name).toBe("my-resolver"); }); + + test("detects a branded resolver with runtime helper keys", async () => { + const filePath = path.join(testDir, "resolver-with-helper.mjs"); + fs.writeFileSync( + filePath, + ` +const resolver = { + operation: "query", + name: "my-resolver", + body: (ctx) => ctx.input, + output: { type: "string", metadata: {}, fields: {} }, + trigger: () => {}, +}; + +Object.defineProperty(resolver, Symbol.for("tailor-platform/sdk"), { + value: "resolver", +}); + +export default resolver; +`, + ); + + const result = await detectFunctionType({ filePath }); + expect(result.type).toBe("resolver"); + expect(result.name).toBe("my-resolver"); + }); }); describe("executor detection", () => { diff --git a/packages/sdk/src/cli/commands/function/detect.ts b/packages/sdk/src/cli/commands/function/detect.ts index feb0b1e04..e51ccdc9e 100644 --- a/packages/sdk/src/cli/commands/function/detect.ts +++ b/packages/sdk/src/cli/commands/function/detect.ts @@ -9,6 +9,7 @@ import { pathToFileURL } from "node:url"; import * as path from "pathe"; import { stripExecutorTriggerArgs } from "#/cli/services/executor/loader"; import { ExecutorSchema } from "#/parser/service/executor/index"; +import { stripResolverBuilderHelpers } from "#/parser/service/resolver/builder-helpers"; import { ResolverSchema } from "#/parser/service/resolver/index"; import { WorkflowJobSchema } from "#/parser/service/workflow/index"; import { assertDefined } from "#/utils/assert"; @@ -59,7 +60,7 @@ export async function detectFunctionType( // Priority: resolver → executor → workflow job → plain function // 1. Check resolver - const resolverResult = ResolverSchema.safeParse(module.default); + const resolverResult = ResolverSchema.safeParse(stripResolverBuilderHelpers(module.default)); if (resolverResult.success) { const rawInput = module.default.input; let inputSchema: DetectedFunction["inputSchema"]; diff --git a/packages/sdk/src/cli/services/resolver/service.ts b/packages/sdk/src/cli/services/resolver/service.ts index 8a636404e..2fd7bdfe6 100644 --- a/packages/sdk/src/cli/services/resolver/service.ts +++ b/packages/sdk/src/cli/services/resolver/service.ts @@ -2,6 +2,7 @@ import { pathToFileURL } from "node:url"; import * as path from "pathe"; import { loadFilesWithIgnores } from "#/cli/services/file-loader"; import { logger, styles } from "#/cli/shared/logger"; +import { stripResolverBuilderHelpers } from "#/parser/service/resolver/builder-helpers"; import { ResolverSchema } from "#/parser/service/resolver/index"; import { isSdkBranded } from "#/utils/brand"; import type { ResolverServiceConfig } from "#/configure/config/types"; @@ -29,7 +30,7 @@ export function createResolverService( const loadResolverForFile = async (resolverFile: string): Promise => { try { const resolverModule = await import(pathToFileURL(resolverFile).href); - const result = ResolverSchema.safeParse(resolverModule.default); + const result = ResolverSchema.safeParse(stripResolverBuilderHelpers(resolverModule.default)); if (result.success) { const relativePath = path.relative(process.cwd(), resolverFile); logger.log( diff --git a/packages/sdk/src/cli/services/workflow/service.test.ts b/packages/sdk/src/cli/services/workflow/service.test.ts new file mode 100644 index 000000000..2e07611f6 --- /dev/null +++ b/packages/sdk/src/cli/services/workflow/service.test.ts @@ -0,0 +1,34 @@ +import * as fs from "node:fs"; +import * as path from "pathe"; +import { describe, expect, test } from "vitest"; +import { tempCwd } from "#/cli/shared/test-helpers/temp-cwd"; +import { createWorkflowService } from "./service"; + +describe("createWorkflowService", () => { + test("does not strip runtime trigger from an unbranded default export", async () => { + using tmp = tempCwd("sdk-workflow-service-"); + const workflowFile = path.join(tmp.dir, "workflow.mjs"); + fs.writeFileSync( + workflowFile, + ` +export const mainJob = { + name: "main-job", + trigger: () => {}, + body: () => {}, +}; + +export default { + name: "looks-like-workflow", + mainJob, + trigger: () => {}, +}; +`, + ); + + const service = createWorkflowService({ config: { files: ["workflow.mjs"] } }); + + await service.loadWorkflows(); + + expect(service.workflows).toEqual({}); + }); +}); diff --git a/packages/sdk/src/cli/services/workflow/service.ts b/packages/sdk/src/cli/services/workflow/service.ts index 475d42a8a..be7bf11c0 100644 --- a/packages/sdk/src/cli/services/workflow/service.ts +++ b/packages/sdk/src/cli/services/workflow/service.ts @@ -14,7 +14,12 @@ export interface CollectedJob { } function stripRuntimeTrigger(workflow: unknown): unknown { - if (workflow === null || typeof workflow !== "object" || !("trigger" in workflow)) { + if ( + !isSdkBranded(workflow, "workflow") || + workflow === null || + typeof workflow !== "object" || + !("trigger" in workflow) + ) { return workflow; } const { trigger: _trigger, ...rest } = workflow as Record; diff --git a/packages/sdk/src/parser/service/resolver/builder-helpers.ts b/packages/sdk/src/parser/service/resolver/builder-helpers.ts new file mode 100644 index 000000000..655f57f00 --- /dev/null +++ b/packages/sdk/src/parser/service/resolver/builder-helpers.ts @@ -0,0 +1,15 @@ +import { isSdkBranded } from "#/utils/brand"; + +const RESOLVER_BUILDER_HELPER_KEYS = ["trigger"] as const; + +export function stripResolverBuilderHelpers(resolver: unknown): unknown { + if (!isSdkBranded(resolver, "resolver") || resolver === null || typeof resolver !== "object") { + return resolver; + } + + const config = { ...(resolver as Record) }; + for (const key of RESOLVER_BUILDER_HELPER_KEYS) { + delete config[key]; + } + return config; +} From 8976f078faee02a878f5307497fe06e6f9406406 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 1 Jul 2026 01:20:27 +0900 Subject: [PATCH 361/618] fix(sdk): reject resolver helper keys --- .../sdk/src/cli/commands/function/detect.test.ts | 8 ++++---- packages/sdk/src/cli/commands/function/detect.ts | 3 +-- packages/sdk/src/cli/services/resolver/service.ts | 3 +-- .../parser/service/resolver/builder-helpers.ts | 15 --------------- 4 files changed, 6 insertions(+), 23 deletions(-) delete mode 100644 packages/sdk/src/parser/service/resolver/builder-helpers.ts diff --git a/packages/sdk/src/cli/commands/function/detect.test.ts b/packages/sdk/src/cli/commands/function/detect.test.ts index 90b9164cd..b4146e32f 100644 --- a/packages/sdk/src/cli/commands/function/detect.test.ts +++ b/packages/sdk/src/cli/commands/function/detect.test.ts @@ -41,7 +41,7 @@ export default { expect(result.name).toBe("my-resolver"); }); - test("detects a branded resolver with runtime helper keys", async () => { + test("rejects a branded resolver with unknown helper keys", async () => { const filePath = path.join(testDir, "resolver-with-helper.mjs"); fs.writeFileSync( filePath, @@ -62,9 +62,9 @@ export default resolver; `, ); - const result = await detectFunctionType({ filePath }); - expect(result.type).toBe("resolver"); - expect(result.name).toBe("my-resolver"); + await expect(detectFunctionType({ filePath })).rejects.toThrow( + "Could not detect function type", + ); }); }); diff --git a/packages/sdk/src/cli/commands/function/detect.ts b/packages/sdk/src/cli/commands/function/detect.ts index e51ccdc9e..feb0b1e04 100644 --- a/packages/sdk/src/cli/commands/function/detect.ts +++ b/packages/sdk/src/cli/commands/function/detect.ts @@ -9,7 +9,6 @@ import { pathToFileURL } from "node:url"; import * as path from "pathe"; import { stripExecutorTriggerArgs } from "#/cli/services/executor/loader"; import { ExecutorSchema } from "#/parser/service/executor/index"; -import { stripResolverBuilderHelpers } from "#/parser/service/resolver/builder-helpers"; import { ResolverSchema } from "#/parser/service/resolver/index"; import { WorkflowJobSchema } from "#/parser/service/workflow/index"; import { assertDefined } from "#/utils/assert"; @@ -60,7 +59,7 @@ export async function detectFunctionType( // Priority: resolver → executor → workflow job → plain function // 1. Check resolver - const resolverResult = ResolverSchema.safeParse(stripResolverBuilderHelpers(module.default)); + const resolverResult = ResolverSchema.safeParse(module.default); if (resolverResult.success) { const rawInput = module.default.input; let inputSchema: DetectedFunction["inputSchema"]; diff --git a/packages/sdk/src/cli/services/resolver/service.ts b/packages/sdk/src/cli/services/resolver/service.ts index 2fd7bdfe6..8a636404e 100644 --- a/packages/sdk/src/cli/services/resolver/service.ts +++ b/packages/sdk/src/cli/services/resolver/service.ts @@ -2,7 +2,6 @@ import { pathToFileURL } from "node:url"; import * as path from "pathe"; import { loadFilesWithIgnores } from "#/cli/services/file-loader"; import { logger, styles } from "#/cli/shared/logger"; -import { stripResolverBuilderHelpers } from "#/parser/service/resolver/builder-helpers"; import { ResolverSchema } from "#/parser/service/resolver/index"; import { isSdkBranded } from "#/utils/brand"; import type { ResolverServiceConfig } from "#/configure/config/types"; @@ -30,7 +29,7 @@ export function createResolverService( const loadResolverForFile = async (resolverFile: string): Promise => { try { const resolverModule = await import(pathToFileURL(resolverFile).href); - const result = ResolverSchema.safeParse(stripResolverBuilderHelpers(resolverModule.default)); + const result = ResolverSchema.safeParse(resolverModule.default); if (result.success) { const relativePath = path.relative(process.cwd(), resolverFile); logger.log( diff --git a/packages/sdk/src/parser/service/resolver/builder-helpers.ts b/packages/sdk/src/parser/service/resolver/builder-helpers.ts deleted file mode 100644 index 655f57f00..000000000 --- a/packages/sdk/src/parser/service/resolver/builder-helpers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { isSdkBranded } from "#/utils/brand"; - -const RESOLVER_BUILDER_HELPER_KEYS = ["trigger"] as const; - -export function stripResolverBuilderHelpers(resolver: unknown): unknown { - if (!isSdkBranded(resolver, "resolver") || resolver === null || typeof resolver !== "object") { - return resolver; - } - - const config = { ...(resolver as Record) }; - for (const key of RESOLVER_BUILDER_HELPER_KEYS) { - delete config[key]; - } - return config; -} From 768ba692970dbd3993574dbb19f027ad4ee371b4 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 1 Jul 2026 01:24:52 +0900 Subject: [PATCH 362/618] fix(sdk-codemod): handle escaped quote backslashes --- .../codemods/v2/rename-bin/scripts/transform.ts | 12 +++++++++--- .../v2/rename-bin/tests/source-template/expected.ts | 1 + .../v2/rename-bin/tests/source-template/input.ts | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts index 689ede11c..bd8ceef52 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/scripts/transform.ts @@ -359,9 +359,15 @@ function activeQuoteStart(source: string, start: number, offset: number): number const char = source[index]; if (quote != null) { if (quote.escaped) { - if (char === "\\" && source[index + 1] === quote.delimiter) { - quote = null; - index += 1; + if (char === "\\") { + let runEnd = index + 1; + while (source[runEnd] === "\\") runEnd += 1; + if (runEnd === index + 1 && source[runEnd] === quote.delimiter) { + quote = null; + index = runEnd; + } else { + index = runEnd - 1; + } } } else if (char === "\\") { index += 1; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts index 5fafb9e95..95a2434a2 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/expected.ts @@ -90,6 +90,7 @@ const yarnDlxPackageFlag = "yarn dlx --package @tailor-platform/sdk tailor login const pnpmDlxOtherPackageFlag = "pnpm dlx foo -p tailor-sdk tailor-sdk login"; const npxOtherPackageQuoted = "npx foo \"tailor-sdk login\""; const npxOtherPackageSingleQuoted = "npx foo 'tailor-sdk login'"; +const npxOtherPackageEscapedQuoteValue = "npx foo \"hello\\\"tailor-sdk login\""; const shellWrapped = "sh -c \"tailor deploy\""; const escapedArg = "tailor --arg \"tailor-sdk deploy\" deploy"; const dollarArg = "tailor --arg \"$& tailor-sdk deploy\" deploy"; diff --git a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts index ebc39fd69..3d2266f05 100644 --- a/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts +++ b/packages/sdk-codemod/codemods/v2/rename-bin/tests/source-template/input.ts @@ -90,6 +90,7 @@ const yarnDlxPackageFlag = "yarn dlx --package tailor-sdk tailor-sdk login"; const pnpmDlxOtherPackageFlag = "pnpm dlx foo -p tailor-sdk tailor-sdk login"; const npxOtherPackageQuoted = "npx foo \"tailor-sdk login\""; const npxOtherPackageSingleQuoted = "npx foo 'tailor-sdk login'"; +const npxOtherPackageEscapedQuoteValue = "npx foo \"hello\\\"tailor-sdk login\""; const shellWrapped = "sh -c \"tailor-sdk deploy\""; const escapedArg = "tailor-sdk --arg \"tailor-sdk deploy\" deploy"; const dollarArg = "tailor-sdk --arg \"$& tailor-sdk deploy\" deploy"; From e6903b4f245199b0469048ba2ae2d2d76a2e8f9c Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 1 Jul 2026 12:00:30 +0900 Subject: [PATCH 363/618] refactor(sdk): simplify branded parser strip helpers --- packages/sdk/src/cli/services/application.ts | 18 ++++++++---------- .../sdk/src/cli/services/executor/loader.ts | 2 +- .../parser/service/tailordb/builder-helpers.ts | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/sdk/src/cli/services/application.ts b/packages/sdk/src/cli/services/application.ts index 5021e2c46..7d9fa0c86 100644 --- a/packages/sdk/src/cli/services/application.ts +++ b/packages/sdk/src/cli/services/application.ts @@ -33,8 +33,8 @@ import { type ResolverServiceInput, type WorkflowServiceConfig, } from "#/configure/config/types"; -import { type AuthConfig } from "#/configure/services/auth/types"; -import { type IdPConfig } from "#/configure/services/idp/types"; +import { type AuthConfig, type AuthOwnConfig } from "#/configure/services/auth/types"; +import { type IdPConfig, type IdPOwnConfig } from "#/configure/services/idp/types"; import { AIGatewaySchema } from "#/parser/service/aigateway/index"; import { AuthConfigSchema } from "#/parser/service/auth/index"; import { IdPSchema } from "#/parser/service/idp/index"; @@ -155,12 +155,11 @@ type DefineIdpResult = { subgraphs: Array<{ Type: string; Name: string }>; }; -function stripIdpProviderHelper(idpConfig: IdPConfig): IdPConfig { - if ("external" in idpConfig) return idpConfig; - const configWithProvider = idpConfig as IdPConfig & { provider?: unknown }; +function stripIdpProviderHelper(idpConfig: IdPOwnConfig): IdPOwnConfig { + const configWithProvider = idpConfig as IdPOwnConfig & { provider?: unknown }; if (typeof configWithProvider.provider !== "function") return idpConfig; const { provider: _provider, ...config } = configWithProvider; - return config as IdPConfig; + return config as IdPOwnConfig; } function defineIdp(config: readonly IdPConfig[] | undefined): DefineIdpResult { @@ -193,12 +192,11 @@ type DefineAuthResult = { subgraphs: Array<{ Type: string; Name: string }>; }; -function stripAuthConnectionTokenHelper(config: AuthConfig): AuthConfig { - if ("external" in config) return config; - const configWithConnectionToken = config as AuthConfig & { getConnectionToken?: unknown }; +function stripAuthConnectionTokenHelper(config: AuthOwnConfig): AuthOwnConfig { + const configWithConnectionToken = config as AuthOwnConfig & { getConnectionToken?: unknown }; if (typeof configWithConnectionToken.getConnectionToken !== "function") return config; const { getConnectionToken: _getConnectionToken, ...authConfig } = configWithConnectionToken; - return authConfig as AuthConfig; + return authConfig as AuthOwnConfig; } function defineAuth( diff --git a/packages/sdk/src/cli/services/executor/loader.ts b/packages/sdk/src/cli/services/executor/loader.ts index f6e658d70..a47351cfe 100644 --- a/packages/sdk/src/cli/services/executor/loader.ts +++ b/packages/sdk/src/cli/services/executor/loader.ts @@ -4,7 +4,7 @@ import { isSdkBranded } from "#/utils/brand"; import type { Executor } from "#/types/executor.generated"; export function stripExecutorTriggerArgs(executor: unknown): unknown { - if (!isSdkBranded(executor, "executor") || executor === null || typeof executor !== "object") { + if (!isSdkBranded(executor, "executor")) { return executor; } diff --git a/packages/sdk/src/parser/service/tailordb/builder-helpers.ts b/packages/sdk/src/parser/service/tailordb/builder-helpers.ts index 54fd3afd9..905e03862 100644 --- a/packages/sdk/src/parser/service/tailordb/builder-helpers.ts +++ b/packages/sdk/src/parser/service/tailordb/builder-helpers.ts @@ -18,7 +18,7 @@ const TAILORDB_TYPE_BUILDER_HELPER_KEYS = [ ] as const; export function stripTailorDBTypeBuilderHelpers(type: unknown): unknown { - if (!isSdkBranded(type, "tailordb-type") || type === null || typeof type !== "object") { + if (!isSdkBranded(type, "tailordb-type")) { return type; } From 1d71a528ac57dd6fc0de2ab9b898b538608ac13e Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 03:52:07 +0900 Subject: [PATCH 364/618] feat!: stop importing tailorctl config --- .../remove-tailorctl-config-migration.md | 5 + .../src/cli/commands/deploy/tailordb/index.ts | 2 +- packages/sdk/src/cli/shared/context.test.ts | 56 +++++++++++ packages/sdk/src/cli/shared/context.ts | 94 +++---------------- 4 files changed, 73 insertions(+), 84 deletions(-) create mode 100644 .changeset/remove-tailorctl-config-migration.md diff --git a/.changeset/remove-tailorctl-config-migration.md b/.changeset/remove-tailorctl-config-migration.md new file mode 100644 index 000000000..891110581 --- /dev/null +++ b/.changeset/remove-tailorctl-config-migration.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Stop importing credentials and profiles from legacy `~/.tailorctl/config` when the platform config is missing. New CLI configs now start empty in the current platform config format. diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/index.ts b/packages/sdk/src/cli/commands/deploy/tailordb/index.ts index cf6afabb3..bdd748cc8 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/index.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/index.ts @@ -1427,7 +1427,7 @@ async function planServices( request: { workspaceId, namespaceName: tailordb.namespace, - // Set UTC to match tailorctl/terraform + // Keep generated TailorDB services aligned with Terraform defaults. defaultTimezone: "UTC", }, metaRequest, diff --git a/packages/sdk/src/cli/shared/context.test.ts b/packages/sdk/src/cli/shared/context.test.ts index 1807ccf0a..b1c793129 100644 --- a/packages/sdk/src/cli/shared/context.test.ts +++ b/packages/sdk/src/cli/shared/context.test.ts @@ -882,6 +882,62 @@ describe("profile readonly field", () => { }); }); +describe("initial platform config", () => { + const configPath = path.join(xdgTempDir, "tailor-platform", "config.yaml"); + const legacyHomeDir = path.join(xdgTempDir, "legacy-home"); + const legacyConfigPath = path.join(legacyHomeDir, ".tailorctl", "config"); + + beforeEach(() => { + vi.resetModules(); + resetKeyringState(); + vi.stubEnv("HOME", legacyHomeDir); + fs.rmSync(configPath, { force: true }); + fs.rmSync(legacyHomeDir, { recursive: true, force: true }); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + test("creates an empty latest-version config when the platform config is missing", async () => { + const config = await readPlatformConfig(); + + expect(config).toEqual({ + version: 3, + min_sdk_version: "2.0.0", + users: {}, + profiles: {}, + current_user: null, + }); + expect(parseYAML(fs.readFileSync(configPath, "utf-8"))).toEqual(config); + }); + + test("ignores legacy tailorctl config when the platform config is missing", async () => { + fs.mkdirSync(path.dirname(legacyConfigPath), { recursive: true }); + fs.writeFileSync( + legacyConfigPath, + [ + "[global]", + 'context = "default"', + "", + "[default]", + 'username = "user@example.com"', + 'workspaceid = "12345678-1234-4abc-8def-123456789012"', + 'controlplaneaccesstoken = "legacy-access-token"', + 'controlplanerefreshtoken = "legacy-refresh-token"', + 'controlplanetokenexpiresat = "2099-01-01T00:00:00.000Z"', + "", + ].join("\n"), + ); + + const config = await readPlatformConfig(); + + expect(config.users).toEqual({}); + expect(config.profiles).toEqual({}); + expect(config.current_user).toBeNull(); + }); +}); + describe("saveUserTokens", () => { const futureDate = new Date(Date.now() + 3600 * 1000).toISOString(); const originalEnv = process.env; diff --git a/packages/sdk/src/cli/shared/context.ts b/packages/sdk/src/cli/shared/context.ts index 0f84d3300..fc02edd03 100644 --- a/packages/sdk/src/cli/shared/context.ts +++ b/packages/sdk/src/cli/shared/context.ts @@ -1,6 +1,5 @@ import * as fs from "node:fs"; -import * as os from "node:os"; -import { parseYAML, stringifyYAML, parseTOML } from "confbox"; +import { parseYAML, stringifyYAML } from "confbox"; import { findUpSync } from "find-up-simple"; import * as path from "pathe"; import { lt as semverLt } from "semver"; @@ -226,21 +225,22 @@ async function warnIfNewerConfigAvailable(config: { } /** - * Read Tailor Platform CLI configuration, migrating from tailorctl or v1 if necessary. + * Read Tailor Platform CLI configuration, migrating from v1 if necessary. * @returns Parsed platform configuration */ export async function readPlatformConfig(): Promise { const configPath = platformConfigPath(); - // If platform config doesn't exist, try to read tailorctl config and migrate if (!fs.existsSync(configPath)) { - logger.warn(`Config not found at ${configPath}, migrating from tailorctl config...`); - const tcConfig = readTailorctlConfig(); - const v1Config = tcConfig - ? fromTailorctlConfig(tcConfig) - : ({ version: 1, users: {}, profiles: {}, current_user: null } as const); - writePlatformConfig(v1Config); - return migrateV1ToV3(v1Config); + const config: PfConfig = { + version: LATEST_CONFIG_VERSION, + min_sdk_version: V3_MIN_SDK_VERSION, + users: {}, + profiles: {}, + current_user: null, + }; + writePlatformConfig(config); + return config; } const rawConfig = parseYAML(fs.readFileSync(configPath, "utf-8")); @@ -340,78 +340,6 @@ export function writePlatformConfig(config: PfConfig | PfConfigV2 | PfConfigV1) writeSecretFile(configPath, stringifyYAML(diskConfig)); } -// strip unknown keys -const tcContextConfigSchema = z.object({ - username: z.string().optional(), - controlplaneaccesstoken: z.string().optional(), - controlplanerefreshtoken: z.string().optional(), - controlplanetokenexpiresat: z.string().optional(), - workspaceid: z.string().optional(), -}); - -// strip unknown keys -const tcConfigSchema = z - .object({ - // strip unknown keys - global: z - .object({ - context: z.string().optional(), - }) - .optional(), - }) - .catchall(tcContextConfigSchema.optional()); - -type TcConfig = z.output; -type TcContextConfig = z.output; - -function readTailorctlConfig(): TcConfig | undefined { - const configPath = path.join(os.homedir(), ".tailorctl", "config"); - if (!fs.existsSync(configPath)) { - return; - } - const rawConfig = parseTOML(fs.readFileSync(configPath, "utf-8")); - return tcConfigSchema.parse(rawConfig); -} - -function fromTailorctlConfig(config: TcConfig): PfConfigV1 { - const users: PfConfigV1["users"] = {}; - const profiles: PfConfigV1["profiles"] = {}; - let currentUser: PfConfigV1["current_user"] = null; - - const currentContext = config.global?.context || "default"; - for (const [key, val] of Object.entries(config)) { - if (key === "global") { - continue; - } - const context = val as TcContextConfig; - if ( - !context.username || - !context.controlplaneaccesstoken || - !context.controlplanerefreshtoken || - !context.controlplanetokenexpiresat || - !context.workspaceid - ) { - continue; - } - if (key === currentContext) { - currentUser = context.username; - } - profiles[key] = { - user: context.username, - workspace_id: context.workspaceid, - }; - const user = users[context.username]; - if (!user || new Date(user.token_expires_at) < new Date(context.controlplanetokenexpiresat)) { - users[context.username] = { - access_token: context.controlplaneaccesstoken, - refresh_token: context.controlplanerefreshtoken, - token_expires_at: context.controlplanetokenexpiresat, - }; - } - } - return { version: 1, users, profiles, current_user: currentUser }; -} - function validateUUID(value: string, source: string): string { const result = z.uuid().safeParse(value); if (!result.success) { From 0fe8bad9afbb7702bc067ac9635b77c0438497a6 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 03:54:14 +0900 Subject: [PATCH 365/618] feat: remove deprecated auth connection token helper --- .changeset/remove-auth-connection-token.md | 5 +++++ packages/sdk/docs/services/auth.md | 2 +- packages/sdk/src/cli/services/application.ts | 11 ++--------- .../sdk/src/configure/services/auth/index.test.ts | 2 ++ packages/sdk/src/configure/services/auth/index.ts | 9 ++------- packages/sdk/src/configure/services/auth/types.ts | 7 ------- 6 files changed, 12 insertions(+), 24 deletions(-) create mode 100644 .changeset/remove-auth-connection-token.md diff --git a/.changeset/remove-auth-connection-token.md b/.changeset/remove-auth-connection-token.md new file mode 100644 index 000000000..8f2cb4c6a --- /dev/null +++ b/.changeset/remove-auth-connection-token.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Remove the deprecated `auth.getConnectionToken()` helper from values returned by `defineAuth()`. Use `authconnection.getConnectionToken(...)` from `@tailor-platform/sdk/runtime` in resolvers, executors, and workflows instead. diff --git a/packages/sdk/docs/services/auth.md b/packages/sdk/docs/services/auth.md index 3dc1e8714..9aa7a5af5 100644 --- a/packages/sdk/docs/services/auth.md +++ b/packages/sdk/docs/services/auth.md @@ -445,7 +445,7 @@ const response = await fetch("https://www.googleapis.com/...", { Type narrowing is provided by the generated `tailor.d.ts` (the `ConnectionNameRegistry` interface). Run `tailor generate` (or `deploy`) after defining new connections to refresh it. Before the first generate, or when `connections` is not defined in `defineAuth()`, `getConnectionToken()` accepts any string — this also supports connections managed entirely via the CLI. -> **Deprecated:** `auth.getConnectionToken("")` still works, but is deprecated. Importing `auth` from `tailor.config.ts` into runtime files pulls config-layer (Node-only) dependencies into the bundle. +This keeps runtime files independent from `tailor.config.ts`. See [Built-in Interfaces](https://docs.tailor.tech/guides/function/builtin-interfaces.html#auth-connection) for the full runtime API. diff --git a/packages/sdk/src/cli/services/application.ts b/packages/sdk/src/cli/services/application.ts index 7d9fa0c86..72fc5d90b 100644 --- a/packages/sdk/src/cli/services/application.ts +++ b/packages/sdk/src/cli/services/application.ts @@ -33,7 +33,7 @@ import { type ResolverServiceInput, type WorkflowServiceConfig, } from "#/configure/config/types"; -import { type AuthConfig, type AuthOwnConfig } from "#/configure/services/auth/types"; +import { type AuthConfig } from "#/configure/services/auth/types"; import { type IdPConfig, type IdPOwnConfig } from "#/configure/services/idp/types"; import { AIGatewaySchema } from "#/parser/service/aigateway/index"; import { AuthConfigSchema } from "#/parser/service/auth/index"; @@ -192,13 +192,6 @@ type DefineAuthResult = { subgraphs: Array<{ Type: string; Name: string }>; }; -function stripAuthConnectionTokenHelper(config: AuthOwnConfig): AuthOwnConfig { - const configWithConnectionToken = config as AuthOwnConfig & { getConnectionToken?: unknown }; - if (typeof configWithConnectionToken.getConnectionToken !== "function") return config; - const { getConnectionToken: _getConnectionToken, ...authConfig } = configWithConnectionToken; - return authConfig as AuthOwnConfig; -} - function defineAuth( config: AuthConfig | undefined, tailorDBServices: ReadonlyArray, @@ -213,7 +206,7 @@ function defineAuth( let authService: AuthService | undefined; if (!("external" in config)) { authService = createAuthService( - AuthConfigSchema.parse(stripAuthConnectionTokenHelper(config)), + AuthConfigSchema.parse(config), tailorDBServices, externalTailorDBNamespaces, ); diff --git a/packages/sdk/src/configure/services/auth/index.test.ts b/packages/sdk/src/configure/services/auth/index.test.ts index a83010b39..18583f2dd 100644 --- a/packages/sdk/src/configure/services/auth/index.test.ts +++ b/packages/sdk/src/configure/services/auth/index.test.ts @@ -77,6 +77,8 @@ describe("defineAuth", () => { expect(authConfig.name).toBe("minimal"); expect(authConfig.userProfile.type).toBe(userType); expect(authConfig.machineUsers).toBeUndefined(); + expect(authConfig).not.toHaveProperty("getConnectionToken"); + expectTypeOf(authConfig).not.toHaveProperty("getConnectionToken"); }); test("creates auth configuration with machineUsers only", () => { diff --git a/packages/sdk/src/configure/services/auth/index.ts b/packages/sdk/src/configure/services/auth/index.ts index 24b5b3822..87bca9d67 100644 --- a/packages/sdk/src/configure/services/auth/index.ts +++ b/packages/sdk/src/configure/services/auth/index.ts @@ -1,6 +1,5 @@ import { type TailorDBInstance } from "../tailordb/schema"; import type { - AuthConnectionTokenResult, AuthDefinitionBrand, AuthServiceInput, DefinedAuth, @@ -154,15 +153,11 @@ export function defineAuth< const result = { ...config, name, - getConnectionToken(connectionName: C): Promise { - return tailor.authconnection.getConnectionToken(connectionName); - }, } as const satisfies ( - | UserProfileAuthInput - | MachineUserOnlyAuthInput + | UserProfileAuthInput + | MachineUserOnlyAuthInput ) & { name: string; - getConnectionToken(connectionName: C): Promise; }; return result as typeof result & AuthDefinitionBrand; diff --git a/packages/sdk/src/configure/services/auth/types.ts b/packages/sdk/src/configure/services/auth/types.ts index f3752d8f9..ad47bd1f9 100644 --- a/packages/sdk/src/configure/services/auth/types.ts +++ b/packages/sdk/src/configure/services/auth/types.ts @@ -329,15 +329,8 @@ export type AuthServiceInput< declare const authDefinitionBrand: unique symbol; export type AuthDefinitionBrand = { readonly [authDefinitionBrand]: true }; -type ConnectionNames = Config extends { connections?: Record } - ? K & string - : string; - export type DefinedAuth = Config & { name: Name; - getConnectionToken>( - connectionName: C, - ): Promise; } & AuthDefinitionBrand; export type AuthExternalConfig = { name: string; external: true }; From 76166389efc15fe5f5ab0c9526a3e50bbb048ee6 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 04:04:15 +0900 Subject: [PATCH 366/618] docs: add auth token helper migration guidance --- packages/sdk-codemod/src/registry.test.ts | 17 +++++++++ packages/sdk-codemod/src/registry.ts | 33 ++++++++++++++++++ packages/sdk/docs/migration/v2.md | 42 +++++++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 7bd69e2c0..272236ba4 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -286,4 +286,21 @@ describe("getApplicableCodemods", () => { expect.arrayContaining(["openDownloadStream", "openFileDownloadStream"]), ); }); + + test("auth connection token helper review is scoped to deprecated helper calls", () => { + const codemod = getApplicableCodemods("1.67.1", "2.0.0-next.2").find( + (entry) => entry.id === "v2/auth-connection-token-helper", + ); + const pattern = codemod?.suspiciousPatterns?.[0]; + + expect(codemod?.scriptPath).toBeUndefined(); + expect(codemod?.filePatterns).toContain("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"); + expect(pattern).toBeInstanceOf(RegExp); + expect((pattern as RegExp).test('await auth.getConnectionToken("google")')).toBe(true); + expect((pattern as RegExp).test('await auth . getConnectionToken("google")')).toBe(true); + expect((pattern as RegExp).test('await authconnection.getConnectionToken("google")')).toBe( + false, + ); + expect(codemod?.prompt).toContain("@tailor-platform/sdk/runtime"); + }); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 81b5cc46c..9ddb0b5b8 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -463,6 +463,39 @@ export const allCodemods: CodemodPackage[] = [ }, ], }, + { + id: "v2/auth-connection-token-helper", + name: "auth.getConnectionToken() → runtime authconnection", + description: + "The deprecated `auth.getConnectionToken()` helper returned by `defineAuth()` is removed in v2. Use `authconnection.getConnectionToken(...)` from `@tailor-platform/sdk/runtime` in resolvers, executors, and workflows instead.", + since: "1.0.0", + until: "2.0.0", + prereleaseUntil: V2_NEXT_2, + filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], + suspiciousPatterns: [/\bauth\s*\.\s*getConnectionToken\s*\(/], + examples: [ + { + before: + 'import { auth } from "../tailor.config";\n\nconst token = await auth.getConnectionToken("google");', + after: + 'import { authconnection } from "@tailor-platform/sdk/runtime";\n\nconst token = await authconnection.getConnectionToken("google");', + }, + ], + prompt: [ + "In Tailor SDK v2 the auth.getConnectionToken() helper returned by defineAuth()", + "is removed. Runtime code should call authconnection.getConnectionToken(...) from", + "@tailor-platform/sdk/runtime instead of importing auth from tailor.config.ts.", + "", + "For each auth.getConnectionToken() call:", + "1. Replace it with authconnection.getConnectionToken().", + '2. Add or reuse `import { authconnection } from "@tailor-platform/sdk/runtime"`.', + "3. Remove the auth import from tailor.config.ts only when no other auth reference", + " remains in the file.", + "", + "Leave existing tailor.authconnection.getConnectionToken(...) or runtime", + "authconnection.getConnectionToken(...) calls unchanged.", + ].join("\n"), + }, { id: "v2/tailordb-namespace", name: "Tailordb → tailordb (lowercase ambient namespace)", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 5cf9858d9..bbb936d79 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -389,6 +389,48 @@ Do not change behavior beyond the SDK option rename and auth.invoker() removal.
+## auth.getConnectionToken() → runtime authconnection + +**Migration:** Manual + +The deprecated `auth.getConnectionToken()` helper returned by `defineAuth()` is removed in v2. Use `authconnection.getConnectionToken(...)` from `@tailor-platform/sdk/runtime` in resolvers, executors, and workflows instead. + +Before: + +```ts +import { auth } from "../tailor.config"; + +const token = await auth.getConnectionToken("google"); +``` + +After: + +```ts +import { authconnection } from "@tailor-platform/sdk/runtime"; + +const token = await authconnection.getConnectionToken("google"); +``` + +
+Prompt for an AI agent (to perform this migration) + +```text +In Tailor SDK v2 the auth.getConnectionToken() helper returned by defineAuth() +is removed. Runtime code should call authconnection.getConnectionToken(...) from +@tailor-platform/sdk/runtime instead of importing auth from tailor.config.ts. + +For each auth.getConnectionToken() call: +1. Replace it with authconnection.getConnectionToken(). +2. Add or reuse `import { authconnection } from "@tailor-platform/sdk/runtime"`. +3. Remove the auth import from tailor.config.ts only when no other auth reference + remains in the file. + +Leave existing tailor.authconnection.getConnectionToken(...) or runtime +authconnection.getConnectionToken(...) calls unchanged. +``` + +
+ ## Tailordb → tailordb (lowercase ambient namespace) **Migration:** Partially automatic From 17f9490e04811020ca341bf8c2a5853157cf5065 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 04:12:20 +0900 Subject: [PATCH 367/618] fix: broaden auth token helper migration coverage --- .changeset/remove-auth-connection-token.md | 1 + packages/sdk-codemod/src/registry.test.ts | 6 ++---- packages/sdk-codemod/src/registry.ts | 9 +++++---- packages/sdk/docs/migration/v2.md | 7 ++++--- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.changeset/remove-auth-connection-token.md b/.changeset/remove-auth-connection-token.md index 8f2cb4c6a..7476ec1fe 100644 --- a/.changeset/remove-auth-connection-token.md +++ b/.changeset/remove-auth-connection-token.md @@ -1,5 +1,6 @@ --- "@tailor-platform/sdk": major +"@tailor-platform/sdk-codemod": patch --- Remove the deprecated `auth.getConnectionToken()` helper from values returned by `defineAuth()`. Use `authconnection.getConnectionToken(...)` from `@tailor-platform/sdk/runtime` in resolvers, executors, and workflows instead. diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 272236ba4..cf187cb68 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -297,10 +297,8 @@ describe("getApplicableCodemods", () => { expect(codemod?.filePatterns).toContain("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"); expect(pattern).toBeInstanceOf(RegExp); expect((pattern as RegExp).test('await auth.getConnectionToken("google")')).toBe(true); - expect((pattern as RegExp).test('await auth . getConnectionToken("google")')).toBe(true); - expect((pattern as RegExp).test('await authconnection.getConnectionToken("google")')).toBe( - false, - ); + expect((pattern as RegExp).test('await mainAuth . getConnectionToken("google")')).toBe(true); + expect((pattern as RegExp).test('await getConnectionToken("google")')).toBe(false); expect(codemod?.prompt).toContain("@tailor-platform/sdk/runtime"); }); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 9ddb0b5b8..431b58d65 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -472,7 +472,7 @@ export const allCodemods: CodemodPackage[] = [ until: "2.0.0", prereleaseUntil: V2_NEXT_2, filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], - suspiciousPatterns: [/\bauth\s*\.\s*getConnectionToken\s*\(/], + suspiciousPatterns: [/\.\s*getConnectionToken\s*\(/], examples: [ { before: @@ -486,14 +486,15 @@ export const allCodemods: CodemodPackage[] = [ "is removed. Runtime code should call authconnection.getConnectionToken(...) from", "@tailor-platform/sdk/runtime instead of importing auth from tailor.config.ts.", "", - "For each auth.getConnectionToken() call:", + "For each .getConnectionToken() call where is a", + "defineAuth() result imported from tailor.config.ts:", "1. Replace it with authconnection.getConnectionToken().", '2. Add or reuse `import { authconnection } from "@tailor-platform/sdk/runtime"`.', "3. Remove the auth import from tailor.config.ts only when no other auth reference", " remains in the file.", "", - "Leave existing tailor.authconnection.getConnectionToken(...) or runtime", - "authconnection.getConnectionToken(...) calls unchanged.", + "Leave calls unchanged when the receiver is already the runtime authconnection", + "wrapper or global tailor.authconnection.", ].join("\n"), }, { diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index bbb936d79..ec5fb489c 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -419,14 +419,15 @@ In Tailor SDK v2 the auth.getConnectionToken() helper returned by defineAuth() is removed. Runtime code should call authconnection.getConnectionToken(...) from @tailor-platform/sdk/runtime instead of importing auth from tailor.config.ts. -For each auth.getConnectionToken() call: +For each .getConnectionToken() call where is a +defineAuth() result imported from tailor.config.ts: 1. Replace it with authconnection.getConnectionToken(). 2. Add or reuse `import { authconnection } from "@tailor-platform/sdk/runtime"`. 3. Remove the auth import from tailor.config.ts only when no other auth reference remains in the file. -Leave existing tailor.authconnection.getConnectionToken(...) or runtime -authconnection.getConnectionToken(...) calls unchanged. +Leave calls unchanged when the receiver is already the runtime authconnection +wrapper or global tailor.authconnection. ``` From 1e9862e01d8e2dfea378e823c53b60d9d775cb15 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 04:37:44 +0900 Subject: [PATCH 368/618] feat: add auth connection token codemod --- .changeset/remove-auth-connection-token.md | 2 +- .../auth-connection-token-helper/codemod.yaml | 7 + .../scripts/transform.ts | 528 ++++++++++++++++++ .../expected.ts | 6 + .../add-to-existing-runtime-import/input.ts | 7 + .../aliased-auth-and-runtime/expected.ts | 7 + .../tests/aliased-auth-and-runtime/input.ts | 7 + .../tests/basic/expected.ts | 6 + .../tests/basic/input.ts | 6 + .../tests/keep-auth-import/expected.ts | 7 + .../tests/keep-auth-import/input.ts | 6 + .../local-authconnection-collision/input.ts | 7 + .../tests/runtime-authconnection/input.ts | 5 + .../tests/unrelated-receiver/input.ts | 5 + packages/sdk-codemod/src/registry.test.ts | 19 +- packages/sdk-codemod/src/registry.ts | 2 +- packages/sdk-codemod/src/runner.test.ts | 56 ++ packages/sdk-codemod/src/transform.test.ts | 4 + packages/sdk-codemod/tsdown.config.ts | 2 + packages/sdk/docs/migration/v2.md | 4 +- 20 files changed, 684 insertions(+), 9 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/add-to-existing-runtime-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/add-to-existing-runtime-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/basic/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/basic/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/keep-auth-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/keep-auth-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/local-authconnection-collision/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/runtime-authconnection/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/unrelated-receiver/input.ts diff --git a/.changeset/remove-auth-connection-token.md b/.changeset/remove-auth-connection-token.md index 7476ec1fe..e78da7822 100644 --- a/.changeset/remove-auth-connection-token.md +++ b/.changeset/remove-auth-connection-token.md @@ -3,4 +3,4 @@ "@tailor-platform/sdk-codemod": patch --- -Remove the deprecated `auth.getConnectionToken()` helper from values returned by `defineAuth()`. Use `authconnection.getConnectionToken(...)` from `@tailor-platform/sdk/runtime` in resolvers, executors, and workflows instead. +Remove the deprecated `auth.getConnectionToken()` helper from values returned by `defineAuth()`. Use `authconnection.getConnectionToken(...)` from `@tailor-platform/sdk/runtime` in resolvers, executors, and workflows instead. The v2 codemod rewrites direct `auth.getConnectionToken(...)` calls when the `auth` binding is imported from `tailor.config`. diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/codemod.yaml b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/codemod.yaml new file mode 100644 index 000000000..b6cf26447 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/auth-connection-token-helper" +version: "1.0.0" +description: "Replace defineAuth auth.getConnectionToken() calls with the authconnection runtime wrapper where the auth binding is imported from tailor.config" +engine: jssg +language: typescript +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts new file mode 100644 index 000000000..211ff18de --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -0,0 +1,528 @@ +import { parse, Lang } from "@ast-grep/napi"; +import type { LlmReviewFinding } from "../../../../src/types"; +import type { Edit, SgNode } from "@ast-grep/napi"; + +const RUNTIME_MODULE = "@tailor-platform/sdk/runtime"; +const AUTHCONNECTION = "authconnection"; +const GET_CONNECTION_TOKEN = "getConnectionToken"; +const SOURCE_FILE_EXTENSIONS = new Set([".tsx", ".jsx"]); +const REFERENCE_KINDS = new Set([ + "identifier", + "shorthand_property_identifier", + "shorthand_property_identifier_pattern", +]); + +interface ImportBinding { + importStmt: SgNode; + localName: string; + importedName?: string; + source: string; + spec?: SgNode; + namespace: boolean; + typeOnly: boolean; +} + +interface AuthBinding { + importStmt: SgNode; + localName: string; + spec: SgNode; +} + +interface TokenCall { + objectNode: SgNode; + localName: string; + range: [number, number]; +} + +function quickFilter(source: string): boolean { + return source.includes(GET_CONNECTION_TOKEN) && source.includes("tailor.config"); +} + +function sourceLang(filePath: string, source: string): Lang { + const lower = filePath.toLowerCase(); + return SOURCE_FILE_EXTENSIONS.has(lower.slice(lower.lastIndexOf("."))) || source.includes(" child.kind() === "type"); +} + +function importSource(stmt: SgNode): string | null { + return stringValue(stmt.find({ rule: { kind: "string" } }) ?? null); +} + +function namedImportsNode(importStmt: SgNode): SgNode | null { + return importStmt.find({ rule: { kind: "named_imports" } }) ?? null; +} + +function isTailorConfigSource(source: string): boolean { + return /(^|\/)tailor\.config(?:\.(?:ts|tsx|js|jsx|mts|cts|mjs|cjs))?$/.test(source); +} + +function importSpecNames( + spec: SgNode, +): { importedName: string; localName: string; typeOnly: boolean } | null { + const ids = spec.children().filter((child) => child.kind() === "identifier"); + if (ids.length === 0) return null; + return { + importedName: ids[0]!.text(), + localName: ids[1]?.text() ?? ids[0]!.text(), + typeOnly: spec.children().some((child) => child.kind() === "type"), + }; +} + +function importBindings(importStmt: SgNode): ImportBinding[] { + const source = importSource(importStmt); + if (!source) return []; + + const stmtTypeOnly = isTypeOnlyImport(importStmt); + const clause = importStmt.children().find((child) => child.kind() === "import_clause"); + if (!clause) return []; + + const bindings: ImportBinding[] = []; + for (const child of clause.children()) { + if (child.kind() === "identifier") { + bindings.push({ + importStmt, + localName: child.text(), + source, + namespace: false, + typeOnly: stmtTypeOnly, + }); + continue; + } + + if (child.kind() === "namespace_import") { + const local = child.children().find((c) => c.kind() === "identifier"); + if (local) { + bindings.push({ + importStmt, + localName: local.text(), + source, + namespace: true, + typeOnly: stmtTypeOnly, + }); + } + continue; + } + + if (child.kind() !== "named_imports") continue; + for (const spec of child.findAll({ rule: { kind: "import_specifier" } })) { + const names = importSpecNames(spec); + if (!names) continue; + bindings.push({ + importStmt, + spec, + source, + importedName: names.importedName, + localName: names.localName, + namespace: false, + typeOnly: stmtTypeOnly || names.typeOnly, + }); + } + } + + return bindings; +} + +function findImportStatements(root: SgNode): SgNode[] { + return root + .findAll({ rule: { kind: "import_statement" } }) + .filter((stmt) => stmt.parent()?.kind() === "program") + .toSorted((a, b) => a.range().start.index - b.range().start.index); +} + +function findTailorConfigAuthBindings(imports: SgNode[]): AuthBinding[] { + return imports.flatMap((importStmt) => + importBindings(importStmt) + .filter( + (binding): binding is ImportBinding & { spec: SgNode; importedName: string } => + binding.spec != null && + binding.importedName === "auth" && + !binding.typeOnly && + isTailorConfigSource(binding.source), + ) + .map((binding) => ({ + importStmt: binding.importStmt, + localName: binding.localName, + spec: binding.spec, + })), + ); +} + +function runtimeAuthconnectionReference(imports: SgNode[]): string | null { + for (const importStmt of imports) { + for (const binding of importBindings(importStmt)) { + if (binding.source !== RUNTIME_MODULE || binding.typeOnly) continue; + if (binding.importedName === AUTHCONNECTION) return binding.localName; + if (binding.namespace) return `${binding.localName}.${AUTHCONNECTION}`; + } + } + return null; +} + +function runtimeNamedValueImport(imports: SgNode[]): SgNode | null { + return ( + imports.find( + (stmt) => + importSource(stmt) === RUNTIME_MODULE && !isTypeOnlyImport(stmt) && namedImportsNode(stmt), + ) ?? null + ); +} + +function collectBindingNames(node: SgNode, out: Set): void { + const kind = node.kind(); + if ( + kind === "identifier" || + kind === "type_identifier" || + kind === "shorthand_property_identifier_pattern" + ) { + out.add(node.text()); + return; + } + + for (const child of node.children()) { + if (child.kind() === "property_identifier") continue; + collectBindingNames(child, out); + } +} + +function firstDeclaratorChild(node: SgNode): SgNode | null { + return node.children().find((child) => child.kind() !== "=") ?? null; +} + +function localDeclarationNames(root: SgNode): Set { + const names = new Set(); + + for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { + const binding = firstDeclaratorChild(decl); + if (binding) collectBindingNames(binding, names); + } + + for (const param of root.findAll({ + rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] }, + })) { + const binding = param + .children() + .find((child) => + ["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind()), + ); + if (binding) collectBindingNames(binding, names); + } + + for (const decl of root.findAll({ + rule: { + any: [ + { kind: "function_declaration" }, + { kind: "function_expression" }, + { kind: "class_declaration" }, + { kind: "class" }, + { kind: "enum_declaration" }, + { kind: "internal_module" }, + { kind: "import_alias" }, + ], + }, + })) { + const name = decl.children().find((child) => child.kind() === "identifier"); + if (name) names.add(name.text()); + } + + return names; +} + +function hasRuntimeImportCollision(root: SgNode, imports: SgNode[]): boolean { + const localNames = localDeclarationNames(root); + if (localNames.has(AUTHCONNECTION)) return true; + + return imports.some((importStmt) => + importBindings(importStmt).some( + (binding) => + !binding.typeOnly && + binding.localName === AUTHCONNECTION && + !( + binding.source === RUNTIME_MODULE && + binding.importedName === AUTHCONNECTION && + !binding.namespace + ), + ), + ); +} + +function findAuthConnectionTokenCalls(root: SgNode, authLocalNames: Set): TokenCall[] { + const calls: TokenCall[] = []; + for (const call of root.findAll({ rule: { kind: "call_expression" } })) { + const callee = call.field("function"); + if (callee?.kind() !== "member_expression") continue; + + const property = callee.field("property"); + const object = callee.field("object"); + if ( + property?.text() !== GET_CONNECTION_TOKEN || + object?.kind() !== "identifier" || + !authLocalNames.has(object.text()) + ) { + continue; + } + + const range = object.range(); + calls.push({ + objectNode: object, + localName: object.text(), + range: [range.start.index, range.end.index], + }); + } + return calls; +} + +function isInsideImportStatement(node: SgNode): boolean { + let current: SgNode | null = node.parent(); + while (current) { + if (current.kind() === "import_statement") return true; + current = current.parent(); + } + return false; +} + +function isInsideScheduledRange(node: SgNode, ranges: Array<[number, number]>): boolean { + const start = node.range().start.index; + return ranges.some(([rangeStart, rangeEnd]) => start >= rangeStart && start < rangeEnd); +} + +function countRemainingRefs( + root: SgNode, + localName: string, + scheduledRanges: Array<[number, number]>, +): number { + let count = 0; + const visit = (node: SgNode): void => { + if ( + REFERENCE_KINDS.has(node.kind()) && + node.text() === localName && + !isInsideImportStatement(node) && + !isInsideScheduledRange(node, scheduledRanges) + ) { + count++; + return; + } + for (const child of node.children()) visit(child); + }; + visit(root); + return count; +} + +function importClause(importStmt: SgNode): SgNode | null { + return importStmt.children().find((child) => child.kind() === "import_clause") ?? null; +} + +function hasDefaultOrNamespaceImport(importStmt: SgNode): boolean { + return ( + importClause(importStmt) + ?.children() + .some((child) => child.kind() === "identifier" || child.kind() === "namespace_import") ?? + false + ); +} + +function buildOnlyNamedImportRemovalEdit(source: string, importStmt: SgNode): Edit | null { + const named = namedImportsNode(importStmt); + if (!named) return null; + + let start = named.range().start.index; + const end = named.range().end.index; + while (start > 0 && (source[start - 1] === " " || source[start - 1] === "\t")) start--; + if (source[start - 1] === ",") { + start--; + while (start > 0 && (source[start - 1] === " " || source[start - 1] === "\t")) start--; + } + return { startPos: start, endPos: end, insertedText: "" }; +} + +function buildImportSpecRemovalEdit(source: string, binding: AuthBinding): Edit | null { + const allSpecs = binding.importStmt.findAll({ rule: { kind: "import_specifier" } }); + if (allSpecs.length === 1) { + if (hasDefaultOrNamespaceImport(binding.importStmt)) { + return buildOnlyNamedImportRemovalEdit(source, binding.importStmt); + } + + const r = binding.importStmt.range(); + return { startPos: r.start.index, endPos: r.end.index, insertedText: "" }; + } + + const r = binding.spec.range(); + let start = r.start.index; + let end = r.end.index; + while (end < source.length && (source[end] === " " || source[end] === "\t")) end++; + if (source[end] === ",") { + end++; + while (end < source.length && (source[end] === " " || source[end] === "\t")) end++; + return { startPos: start, endPos: end, insertedText: "" }; + } + + while (start > 0 && (source[start - 1] === " " || source[start - 1] === "\t")) start--; + if (source[start - 1] === ",") { + start--; + while (start > 0 && (source[start - 1] === " " || source[start - 1] === "\t")) start--; + return { startPos: start, endPos: end, insertedText: "" }; + } + + return { startPos: r.start.index, endPos: r.end.index, insertedText: "" }; +} + +function isDirectiveStatement(node: SgNode): boolean { + return node.kind() === "expression_statement" && node.children()[0]?.kind() === "string"; +} + +function importInsertionIndex(root: SgNode, imports: SgNode[], source: string): number { + const lastImport = imports.at(-1); + if (lastImport) return lastImport.range().end.index; + + let pos = 0; + if (source.startsWith("#!")) { + const newlineIndex = source.indexOf("\n"); + pos = newlineIndex === -1 ? source.length : newlineIndex + 1; + } + + for (const child of root.children()) { + if (child.range().start.index < pos) continue; + if (child.kind() === "comment") { + pos = child.range().end.index; + continue; + } + if (!isDirectiveStatement(child)) break; + pos = child.range().end.index; + } + + return pos; +} + +function buildAddRuntimeImportEdit(root: SgNode, source: string, imports: SgNode[]): Edit { + const existingRuntimeImport = runtimeNamedValueImport(imports); + const namedImports = existingRuntimeImport ? namedImportsNode(existingRuntimeImport) : null; + if (namedImports) { + const specTexts = namedImports + .findAll({ rule: { kind: "import_specifier" } }) + .map((spec) => spec.text()); + return namedImports.replace(`{ ${[...specTexts, AUTHCONNECTION].join(", ")} }`); + } + + const pos = importInsertionIndex(root, imports, source); + const insertedText = + pos === 0 || source[pos - 1] === "\n" + ? `import { ${AUTHCONNECTION} } from "${RUNTIME_MODULE}";\n\n` + : `\nimport { ${AUTHCONNECTION} } from "${RUNTIME_MODULE}";`; + return { startPos: pos, endPos: pos, insertedText }; +} + +function applyEdits(source: string, edits: Edit[]): string { + return edits + .toSorted((a, b) => b.startPos - a.startPos || b.endPos - a.endPos) + .reduce( + (current, edit) => + `${current.slice(0, edit.startPos)}${edit.insertedText}${current.slice(edit.endPos)}`, + source, + ); +} + +function normalizeSource(source: string): string { + return source.replace(/^[\t ]*\n+/, "").replace(/\n{3,}/g, "\n\n"); +} + +function transformParsed(source: string, root: SgNode): string | null { + const imports = findImportStatements(root); + const authBindings = findTailorConfigAuthBindings(imports); + if (authBindings.length === 0) return null; + + const authLocalNames = new Set(authBindings.map((binding) => binding.localName)); + const calls = findAuthConnectionTokenCalls(root, authLocalNames); + if (calls.length === 0) return null; + + const existingRuntimeRef = runtimeAuthconnectionReference(imports); + if (!existingRuntimeRef && hasRuntimeImportCollision(root, imports)) return null; + + const runtimeRef = existingRuntimeRef ?? AUTHCONNECTION; + const edits: Edit[] = calls.map((call) => call.objectNode.replace(runtimeRef)); + + if (!existingRuntimeRef) { + edits.push(buildAddRuntimeImportEdit(root, source, imports)); + } + + const scheduledRangesByLocalName = new Map>(); + for (const call of calls) { + const ranges = scheduledRangesByLocalName.get(call.localName) ?? []; + ranges.push(call.range); + scheduledRangesByLocalName.set(call.localName, ranges); + } + + for (const binding of authBindings) { + if (calls.every((call) => call.localName !== binding.localName)) continue; + const remainingRefs = countRemainingRefs( + root, + binding.localName, + scheduledRangesByLocalName.get(binding.localName) ?? [], + ); + if (remainingRefs > 0) continue; + const edit = buildImportSpecRemovalEdit(source, binding); + if (edit) edits.push(edit); + } + + const result = normalizeSource(applyEdits(source, edits)); + return result === source ? null : result; +} + +export default function transform(source: string, filePath: string): string | null { + if (!quickFilter(source)) return null; + + let root: SgNode; + try { + root = parse(sourceLang(filePath, source), source).root(); + } catch { + return null; + } + + return transformParsed(source, root); +} + +function lineForIndex(source: string, index: number): number { + return source.slice(0, index).split(/\r\n|\r|\n/).length; +} + +function excerptForIndex(source: string, index: number): string { + const lineStart = source.lastIndexOf("\n", index - 1) + 1; + const lineEnd = source.indexOf("\n", index); + return source.slice(lineStart, lineEnd === -1 ? source.length : lineEnd).trim(); +} + +export function reviewFindings( + source: string, + filePath: string, + relativePath: string, +): LlmReviewFinding[] { + if (!quickFilter(source)) return []; + + let root: SgNode; + try { + root = parse(sourceLang(filePath, source), source).root(); + } catch { + return []; + } + + const imports = findImportStatements(root); + const authBindings = findTailorConfigAuthBindings(imports); + const calls = findAuthConnectionTokenCalls( + root, + new Set(authBindings.map((binding) => binding.localName)), + ); + + return calls.map((call) => ({ + file: relativePath, + line: lineForIndex(source, call.range[0]), + message: "Replace defineAuth auth.getConnectionToken() with runtime authconnection.", + excerpt: excerptForIndex(source, call.range[0]), + })); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/add-to-existing-runtime-import/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/add-to-existing-runtime-import/expected.ts new file mode 100644 index 000000000..67f3c4f27 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/add-to-existing-runtime-import/expected.ts @@ -0,0 +1,6 @@ +import { workflow, authconnection } from "@tailor-platform/sdk/runtime"; + +export async function run() { + await workflow.wait("ready"); + return authconnection.getConnectionToken("google"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/add-to-existing-runtime-import/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/add-to-existing-runtime-import/input.ts new file mode 100644 index 000000000..2c9a8d975 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/add-to-existing-runtime-import/input.ts @@ -0,0 +1,7 @@ +import { workflow } from "@tailor-platform/sdk/runtime"; +import { auth } from "../tailor.config"; + +export async function run() { + await workflow.wait("ready"); + return auth.getConnectionToken("google"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/expected.ts new file mode 100644 index 000000000..22f06f047 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/expected.ts @@ -0,0 +1,7 @@ +import { db } from "../tailor.config"; +import { authconnection as runtimeAuthconnection } from "@tailor-platform/sdk/runtime"; + +export async function run() { + const token = await runtimeAuthconnection.getConnectionToken("google"); + return { token, db }; +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/input.ts new file mode 100644 index 000000000..0ef3e7da3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/input.ts @@ -0,0 +1,7 @@ +import { auth as mainAuth, db } from "../tailor.config"; +import { authconnection as runtimeAuthconnection } from "@tailor-platform/sdk/runtime"; + +export async function run() { + const token = await mainAuth.getConnectionToken("google"); + return { token, db }; +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/basic/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/basic/expected.ts new file mode 100644 index 000000000..e12e2d964 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/basic/expected.ts @@ -0,0 +1,6 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export async function run() { + const token = await authconnection.getConnectionToken("google"); + return token; +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/basic/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/basic/input.ts new file mode 100644 index 000000000..b5c86587c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/basic/input.ts @@ -0,0 +1,6 @@ +import { auth } from "../tailor.config"; + +export async function run() { + const token = await auth.getConnectionToken("google"); + return token; +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/keep-auth-import/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/keep-auth-import/expected.ts new file mode 100644 index 000000000..2fd9d3f15 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/keep-auth-import/expected.ts @@ -0,0 +1,7 @@ +import { auth } from "../tailor.config"; +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export async function run() { + const token = await authconnection.getConnectionToken("google"); + return { token, invoker: auth.invoker("manager") }; +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/keep-auth-import/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/keep-auth-import/input.ts new file mode 100644 index 000000000..d702215b0 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/keep-auth-import/input.ts @@ -0,0 +1,6 @@ +import { auth } from "../tailor.config"; + +export async function run() { + const token = await auth.getConnectionToken("google"); + return { token, invoker: auth.invoker("manager") }; +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/local-authconnection-collision/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/local-authconnection-collision/input.ts new file mode 100644 index 000000000..e29367f3d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/local-authconnection-collision/input.ts @@ -0,0 +1,7 @@ +import { auth } from "../tailor.config"; + +const authconnection = createClient(); + +export async function run() { + return auth.getConnectionToken("google"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/runtime-authconnection/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/runtime-authconnection/input.ts new file mode 100644 index 000000000..ad4c2010e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/runtime-authconnection/input.ts @@ -0,0 +1,5 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export async function run() { + return authconnection.getConnectionToken("google"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/unrelated-receiver/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/unrelated-receiver/input.ts new file mode 100644 index 000000000..a637bf45e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/unrelated-receiver/input.ts @@ -0,0 +1,5 @@ +import { client } from "./client"; + +export async function run() { + return client.getConnectionToken("google"); +} diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index cf187cb68..f94766c4a 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -1,3 +1,5 @@ +import * as fs from "node:fs"; +import * as path from "pathe"; import picomatch from "picomatch"; import { describe, expect, test } from "vitest"; import { allCodemods, getApplicableCodemods } from "./registry"; @@ -15,6 +17,16 @@ describe("getApplicableCodemods", () => { ); }); + test("bundles every registered transform script", () => { + const tsdownConfig = fs.readFileSync(path.resolve(__dirname, "../tsdown.config.ts"), "utf-8"); + const missing = allCodemods + .flatMap((codemod) => (codemod.scriptPath ? [codemod.scriptPath] : [])) + .map((scriptPath) => `codemods/${scriptPath.replace(/\.js$/, ".ts")}`) + .filter((scriptPath) => !tsdownConfig.includes(scriptPath)); + + expect(missing).toEqual([]); + }); + test("returns codemods when upgrading to a prerelease at their version boundary", () => { const prereleaseCodemods = getApplicableCodemods("1.67.1", "2.0.0-next.2"); const prereleaseIds = prereleaseCodemods.map((codemod) => codemod.id); @@ -293,12 +305,9 @@ describe("getApplicableCodemods", () => { ); const pattern = codemod?.suspiciousPatterns?.[0]; - expect(codemod?.scriptPath).toBeUndefined(); + expect(codemod?.scriptPath).toBe("v2/auth-connection-token-helper/scripts/transform.js"); expect(codemod?.filePatterns).toContain("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"); - expect(pattern).toBeInstanceOf(RegExp); - expect((pattern as RegExp).test('await auth.getConnectionToken("google")')).toBe(true); - expect((pattern as RegExp).test('await mainAuth . getConnectionToken("google")')).toBe(true); - expect((pattern as RegExp).test('await getConnectionToken("google")')).toBe(false); + expect(pattern).toBeUndefined(); expect(codemod?.prompt).toContain("@tailor-platform/sdk/runtime"); }); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 431b58d65..fe022a68f 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -471,8 +471,8 @@ export const allCodemods: CodemodPackage[] = [ since: "1.0.0", until: "2.0.0", prereleaseUntil: V2_NEXT_2, + scriptPath: "v2/auth-connection-token-helper/scripts/transform.js", filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], - suspiciousPatterns: [/\.\s*getConnectionToken\s*\(/], examples: [ { before: diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 528e08d4b..9ed614b32 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1041,6 +1041,62 @@ describe("runCodemods", () => { ]); }); + test("flags only unresolved auth connection token helper calls for LLM review", async () => { + const codemod = allCodemods.find((entry) => entry.id === "v2/auth-connection-token-helper"); + if (!codemod?.scriptPath) throw new Error("auth connection token codemod missing script"); + const scriptPath = path.resolve( + __dirname, + "../codemods", + codemod.scriptPath.replace(/\.js$/, ".ts"), + ); + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-auth-token-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "migrated.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + "export async function run() {", + ' return auth.getConnectionToken("google");', + "}", + "", + ].join("\n"), + ); + await fs.promises.writeFile( + path.join(dir, "collision.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + "const authconnection = createClient();", + "", + "export async function run() {", + ' return auth.getConnectionToken("google");', + "}", + "", + ].join("\n"), + ); + + using _stderrSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true); + + const result = await runCodemods([{ codemod, scriptPath }], dir, true); + + expect(result.changed).toBe(true); + expect(result.llmReviews).toEqual([ + { + codemodId: "v2/auth-connection-token-helper", + prompt: codemod.prompt, + files: ["collision.ts"], + findings: [ + expect.objectContaining({ + file: "collision.ts", + line: 6, + excerpt: 'return auth.getConnectionToken("google");', + }), + ], + }, + ]); + }); + test("suppresses LLM review when a superseding codemod is selected", async () => { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-llm-superseded-")); tmpDir = dir; diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index 9f5f2b91a..7d9c859e5 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -103,6 +103,10 @@ describe("codemod transforms", () => { await expect(runFixtureCases("v2/auth-invoker-unwrap")).resolves.toBeUndefined(); }); + test("v2/auth-connection-token-helper transforms correctly", async () => { + await expect(runFixtureCases("v2/auth-connection-token-helper")).resolves.toBeUndefined(); + }); + test("v2/tailordb-namespace transforms correctly", async () => { await expect(runFixtureCases("v2/tailordb-namespace")).resolves.toBeUndefined(); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index c0223c59e..6548b5bf7 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -32,6 +32,8 @@ export default defineConfig([ "codemods/v2/auth-invoker-call-unwrap/scripts/transform.ts", "v2/auth-invoker-unwrap/scripts/transform": "codemods/v2/auth-invoker-unwrap/scripts/transform.ts", + "v2/auth-connection-token-helper/scripts/transform": + "codemods/v2/auth-connection-token-helper/scripts/transform.ts", "v2/tailordb-namespace/scripts/transform": "codemods/v2/tailordb-namespace/scripts/transform.ts", "v2/runtime-globals-opt-in/scripts/transform": diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index ec5fb489c..dabb75f80 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -391,7 +391,7 @@ Do not change behavior beyond the SDK option rename and auth.invoker() removal. ## auth.getConnectionToken() → runtime authconnection -**Migration:** Manual +**Migration:** Partially automatic The deprecated `auth.getConnectionToken()` helper returned by `defineAuth()` is removed in v2. Use `authconnection.getConnectionToken(...)` from `@tailor-platform/sdk/runtime` in resolvers, executors, and workflows instead. @@ -412,7 +412,7 @@ const token = await authconnection.getConnectionToken("google"); ```
-Prompt for an AI agent (to perform this migration) +Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text In Tailor SDK v2 the auth.getConnectionToken() helper returned by defineAuth() From d2ab4d90ddd92ff04cf8d3de8d83f431fa7a519b Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 04:44:09 +0900 Subject: [PATCH 369/618] fix: avoid shadowed authconnection codemod rewrites --- .../scripts/transform.ts | 75 ++++++++++++------- .../tests/existing-runtime-shadowed/input.ts | 6 ++ 2 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/existing-runtime-shadowed/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 211ff18de..6f8cf62d4 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -233,6 +233,34 @@ function localDeclarationNames(root: SgNode): Set { if (name) names.add(name.text()); } + for (const catchClause of root.findAll({ rule: { kind: "catch_clause" } })) { + for (const child of catchClause.children()) { + if (["identifier", "object_pattern", "array_pattern"].includes(child.kind())) { + collectBindingNames(child, names); + } + } + } + + for (const arrow of root.findAll({ rule: { kind: "arrow_function" } })) { + const children = arrow.children(); + const arrowIndex = children.findIndex((child) => child.kind() === "=>"); + if (arrowIndex === -1) continue; + for (const child of children.slice(0, arrowIndex)) { + collectBindingNames(child, names); + } + } + + for (const loop of root.findAll({ rule: { kind: "for_in_statement" } })) { + const children = loop.children(); + const keywordIndex = children.findIndex( + (child) => child.kind() === "in" || child.kind() === "of", + ); + if (keywordIndex === -1) continue; + for (const child of children.slice(0, keywordIndex)) { + collectBindingNames(child, names); + } + } + return names; } @@ -254,6 +282,10 @@ function hasRuntimeImportCollision(root: SgNode, imports: SgNode[]): boolean { ); } +function hasRuntimeReferenceShadow(root: SgNode, runtimeRef: string): boolean { + return localDeclarationNames(root).has(runtimeRef.split(".")[0]!); +} + function findAuthConnectionTokenCalls(root: SgNode, authLocalNames: Set): TokenCall[] { const calls: TokenCall[] = []; for (const call of root.findAll({ rule: { kind: "call_expression" } })) { @@ -299,21 +331,12 @@ function countRemainingRefs( localName: string, scheduledRanges: Array<[number, number]>, ): number { - let count = 0; - const visit = (node: SgNode): void => { - if ( - REFERENCE_KINDS.has(node.kind()) && - node.text() === localName && - !isInsideImportStatement(node) && - !isInsideScheduledRange(node, scheduledRanges) - ) { - count++; - return; - } - for (const child of node.children()) visit(child); - }; - visit(root); - return count; + return root + .findAll({ rule: { any: [...REFERENCE_KINDS].map((kind) => ({ kind })) } }) + .filter((node) => node.text() === localName) + .filter( + (node) => !isInsideImportStatement(node) && !isInsideScheduledRange(node, scheduledRanges), + ).length; } function importClause(importStmt: SgNode): SgNode | null { @@ -444,6 +467,7 @@ function transformParsed(source: string, root: SgNode): string | null { const existingRuntimeRef = runtimeAuthconnectionReference(imports); if (!existingRuntimeRef && hasRuntimeImportCollision(root, imports)) return null; + if (existingRuntimeRef && hasRuntimeReferenceShadow(root, existingRuntimeRef)) return null; const runtimeRef = existingRuntimeRef ?? AUTHCONNECTION; const edits: Edit[] = calls.map((call) => call.objectNode.replace(runtimeRef)); @@ -475,17 +499,18 @@ function transformParsed(source: string, root: SgNode): string | null { return result === source ? null : result; } -export default function transform(source: string, filePath: string): string | null { +function parseRoot(source: string, filePath: string): SgNode | null { if (!quickFilter(source)) return null; - - let root: SgNode; try { - root = parse(sourceLang(filePath, source), source).root(); + return parse(sourceLang(filePath, source), source).root(); } catch { return null; } +} - return transformParsed(source, root); +export default function transform(source: string, filePath: string): string | null { + const root = parseRoot(source, filePath); + return root ? transformParsed(source, root) : null; } function lineForIndex(source: string, index: number): number { @@ -503,14 +528,8 @@ export function reviewFindings( filePath: string, relativePath: string, ): LlmReviewFinding[] { - if (!quickFilter(source)) return []; - - let root: SgNode; - try { - root = parse(sourceLang(filePath, source), source).root(); - } catch { - return []; - } + const root = parseRoot(source, filePath); + if (!root) return []; const imports = findImportStatements(root); const authBindings = findTailorConfigAuthBindings(imports); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/existing-runtime-shadowed/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/existing-runtime-shadowed/input.ts new file mode 100644 index 000000000..4e779b438 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/existing-runtime-shadowed/input.ts @@ -0,0 +1,6 @@ +import { auth } from "../tailor.config"; +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export async function run(authconnection: { getConnectionToken(name: string): Promise }) { + return auth.getConnectionToken("google"); +} From 6b0cef26f8d88f81ee16b1f0eb556d013d51da90 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 04:55:09 +0900 Subject: [PATCH 370/618] fix: skip shadowed auth token codemod calls --- .../scripts/transform.ts | 16 +++++++++------- .../tests/auth-shadowed/input.ts | 5 +++++ 2 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/auth-shadowed/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 6f8cf62d4..c12e243e8 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -264,8 +264,7 @@ function localDeclarationNames(root: SgNode): Set { return names; } -function hasRuntimeImportCollision(root: SgNode, imports: SgNode[]): boolean { - const localNames = localDeclarationNames(root); +function hasRuntimeImportCollision(localNames: Set, imports: SgNode[]): boolean { if (localNames.has(AUTHCONNECTION)) return true; return imports.some((importStmt) => @@ -282,8 +281,8 @@ function hasRuntimeImportCollision(root: SgNode, imports: SgNode[]): boolean { ); } -function hasRuntimeReferenceShadow(root: SgNode, runtimeRef: string): boolean { - return localDeclarationNames(root).has(runtimeRef.split(".")[0]!); +function hasRuntimeReferenceShadow(localNames: Set, runtimeRef: string): boolean { + return localNames.has(runtimeRef.split(".")[0]!); } function findAuthConnectionTokenCalls(root: SgNode, authLocalNames: Set): TokenCall[] { @@ -458,7 +457,10 @@ function normalizeSource(source: string): string { function transformParsed(source: string, root: SgNode): string | null { const imports = findImportStatements(root); - const authBindings = findTailorConfigAuthBindings(imports); + const localNames = localDeclarationNames(root); + const authBindings = findTailorConfigAuthBindings(imports).filter( + (binding) => !localNames.has(binding.localName), + ); if (authBindings.length === 0) return null; const authLocalNames = new Set(authBindings.map((binding) => binding.localName)); @@ -466,8 +468,8 @@ function transformParsed(source: string, root: SgNode): string | null { if (calls.length === 0) return null; const existingRuntimeRef = runtimeAuthconnectionReference(imports); - if (!existingRuntimeRef && hasRuntimeImportCollision(root, imports)) return null; - if (existingRuntimeRef && hasRuntimeReferenceShadow(root, existingRuntimeRef)) return null; + if (!existingRuntimeRef && hasRuntimeImportCollision(localNames, imports)) return null; + if (existingRuntimeRef && hasRuntimeReferenceShadow(localNames, existingRuntimeRef)) return null; const runtimeRef = existingRuntimeRef ?? AUTHCONNECTION; const edits: Edit[] = calls.map((call) => call.objectNode.replace(runtimeRef)); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/auth-shadowed/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/auth-shadowed/input.ts new file mode 100644 index 000000000..b177f29cd --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/auth-shadowed/input.ts @@ -0,0 +1,5 @@ +import { auth } from "../tailor.config"; + +export async function run(auth: { getConnectionToken(name: string): Promise }) { + return auth.getConnectionToken("google"); +} From a6b1569a0017a847ff10370a6711ede2f8d09e93 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 05:06:55 +0900 Subject: [PATCH 371/618] fix: preserve codemod string newlines --- .../scripts/transform.ts | 26 +++++++++++++++++-- .../preserve-template-newlines/expected.ts | 10 +++++++ .../tests/preserve-template-newlines/input.ts | 10 +++++++ packages/sdk-codemod/src/runner.test.ts | 11 ++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index c12e243e8..60679892b 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -452,7 +452,26 @@ function applyEdits(source: string, edits: Edit[]): string { } function normalizeSource(source: string): string { - return source.replace(/^[\t ]*\n+/, "").replace(/\n{3,}/g, "\n\n"); + const lines = source.replace(/^[\t ]*\n+/, "").split("\n"); + const out: string[] = []; + let sawImport = false; + + for (let index = 0; index < lines.length; index++) { + const line = lines[index]!; + if (line.startsWith("import ")) { + out.push(line); + sawImport = true; + continue; + } + if (sawImport && line.trim() === "") { + if (out.at(-1)?.trim() !== "") out.push(line); + continue; + } + out.push(...lines.slice(index)); + return out.join("\n"); + } + + return out.join("\n"); } function transformParsed(source: string, root: SgNode): string | null { @@ -534,7 +553,10 @@ export function reviewFindings( if (!root) return []; const imports = findImportStatements(root); - const authBindings = findTailorConfigAuthBindings(imports); + const localNames = localDeclarationNames(root); + const authBindings = findTailorConfigAuthBindings(imports).filter( + (binding) => !localNames.has(binding.localName), + ); const calls = findAuthConnectionTokenCalls( root, new Set(authBindings.map((binding) => binding.localName)), diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/expected.ts new file mode 100644 index 000000000..ed8e70ffe --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/expected.ts @@ -0,0 +1,10 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +const body = `line 1 + + +line 4`; + +export async function run() { + return authconnection.getConnectionToken("google") + body; +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/input.ts new file mode 100644 index 000000000..f5043d240 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/input.ts @@ -0,0 +1,10 @@ +import { auth } from "../tailor.config"; + +const body = `line 1 + + +line 4`; + +export async function run() { + return auth.getConnectionToken("google") + body; +} diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 9ed614b32..e2841d823 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1075,6 +1075,17 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "shadowed.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + "export async function run(auth: { getConnectionToken(name: string): Promise }) {", + ' return auth.getConnectionToken("google");', + "}", + "", + ].join("\n"), + ); using _stderrSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true); From 4b9c6b870cf8765037c480fb3ec9c686c83101ab Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 05:22:16 +0900 Subject: [PATCH 372/618] fix: scope auth token codemod shadow checks --- .../scripts/transform.ts | 111 ++++++++++++++++-- .../tests/method-auth-shadow/expected.ts | 9 ++ .../tests/method-auth-shadow/input.ts | 9 ++ .../tests/mixed-auth-shadow/expected.ts | 7 ++ .../tests/mixed-auth-shadow/input.ts | 7 ++ 5 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 60679892b..19e40c3da 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -285,6 +285,101 @@ function hasRuntimeReferenceShadow(localNames: Set, runtimeRef: string): return localNames.has(runtimeRef.split(".")[0]!); } +function collectParameterNames(scope: SgNode, names: Set): void { + const params = scope.children().find((child) => child.kind() === "formal_parameters"); + if (!params) return; + + for (const param of params.children()) { + const binding = param + .children() + .find((child) => + ["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind()), + ); + if (binding) collectBindingNames(binding, names); + } +} + +function collectArrowParameterNames(scope: SgNode, names: Set): void { + const children = scope.children(); + const arrowIndex = children.findIndex((child) => child.kind() === "=>"); + if (arrowIndex === -1) return; + + for (const child of children.slice(0, arrowIndex)) { + if ( + [ + "identifier", + "object_pattern", + "array_pattern", + "rest_pattern", + "formal_parameters", + ].includes(child.kind()) + ) { + collectBindingNames(child, names); + } + } +} + +function collectDirectBlockNames(scope: SgNode, names: Set): void { + for (const child of scope.children()) { + if (child.kind() === "lexical_declaration" || child.kind() === "variable_declaration") { + for (const decl of child + .children() + .filter((grandchild) => grandchild.kind() === "variable_declarator")) { + const binding = firstDeclaratorChild(decl); + if (binding) collectBindingNames(binding, names); + } + continue; + } + + if (["function_declaration", "class_declaration", "enum_declaration"].includes(child.kind())) { + const name = child.children().find((grandchild) => grandchild.kind() === "identifier"); + if (name) names.add(name.text()); + } + } +} + +function directlyDeclaredNames(scope: SgNode): Set { + const names = new Set(); + const kind = scope.kind(); + + if (scope.children().some((child) => child.kind() === "formal_parameters")) { + collectParameterNames(scope, names); + } + + if (kind === "arrow_function") { + collectArrowParameterNames(scope, names); + } else if (kind === "catch_clause") { + for (const child of scope.children()) { + if (["identifier", "object_pattern", "array_pattern"].includes(child.kind())) { + collectBindingNames(child, names); + } + } + } else if (kind === "statement_block" || kind === "program") { + collectDirectBlockNames(scope, names); + } else if (kind === "for_in_statement") { + const children = scope.children(); + const keywordIndex = children.findIndex( + (child) => child.kind() === "in" || child.kind() === "of", + ); + if (keywordIndex !== -1) { + for (const child of children.slice(0, keywordIndex)) { + collectBindingNames(child, names); + } + } + } + + return names; +} + +function isReferenceShadowed(node: SgNode, localName: string): boolean { + let current = node.parent(); + while (current) { + if (directlyDeclaredNames(current).has(localName)) return true; + current = current.parent(); + } + return false; +} + function findAuthConnectionTokenCalls(root: SgNode, authLocalNames: Set): TokenCall[] { const calls: TokenCall[] = []; for (const call of root.findAll({ rule: { kind: "call_expression" } })) { @@ -301,6 +396,8 @@ function findAuthConnectionTokenCalls(root: SgNode, authLocalNames: Set) continue; } + if (isReferenceShadowed(object, object.text())) continue; + const range = object.range(); calls.push({ objectNode: object, @@ -334,7 +431,10 @@ function countRemainingRefs( .findAll({ rule: { any: [...REFERENCE_KINDS].map((kind) => ({ kind })) } }) .filter((node) => node.text() === localName) .filter( - (node) => !isInsideImportStatement(node) && !isInsideScheduledRange(node, scheduledRanges), + (node) => + !isInsideImportStatement(node) && + !isInsideScheduledRange(node, scheduledRanges) && + !isReferenceShadowed(node, localName), ).length; } @@ -477,9 +577,7 @@ function normalizeSource(source: string): string { function transformParsed(source: string, root: SgNode): string | null { const imports = findImportStatements(root); const localNames = localDeclarationNames(root); - const authBindings = findTailorConfigAuthBindings(imports).filter( - (binding) => !localNames.has(binding.localName), - ); + const authBindings = findTailorConfigAuthBindings(imports); if (authBindings.length === 0) return null; const authLocalNames = new Set(authBindings.map((binding) => binding.localName)); @@ -553,10 +651,7 @@ export function reviewFindings( if (!root) return []; const imports = findImportStatements(root); - const localNames = localDeclarationNames(root); - const authBindings = findTailorConfigAuthBindings(imports).filter( - (binding) => !localNames.has(binding.localName), - ); + const authBindings = findTailorConfigAuthBindings(imports); const calls = findAuthConnectionTokenCalls( root, new Set(authBindings.map((binding) => binding.localName)), diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/expected.ts new file mode 100644 index 000000000..5f835bc3e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/expected.ts @@ -0,0 +1,9 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); + +class Client { + async run(auth: { getConnectionToken(name: string): Promise }) { + return auth.getConnectionToken("github"); + } +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/input.ts new file mode 100644 index 000000000..3ac28a730 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/input.ts @@ -0,0 +1,9 @@ +import { auth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); + +class Client { + async run(auth: { getConnectionToken(name: string): Promise }) { + return auth.getConnectionToken("github"); + } +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/expected.ts new file mode 100644 index 000000000..d01d444ca --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/expected.ts @@ -0,0 +1,7 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); + +export async function run(auth: { getConnectionToken(name: string): Promise }) { + return auth.getConnectionToken("github"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/input.ts new file mode 100644 index 000000000..d03b4b7d6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/input.ts @@ -0,0 +1,7 @@ +import { auth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); + +export async function run(auth: { getConnectionToken(name: string): Promise }) { + return auth.getConnectionToken("github"); +} From 3f7b7292930f98af682ea446b62b2d8f60a96446 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 05:37:25 +0900 Subject: [PATCH 373/618] fix: handle loop and var auth token shadows --- .../scripts/transform.ts | 63 +++++++++++++++++-- .../tests/for-auth-shadow/expected.ts | 9 +++ .../tests/for-auth-shadow/input.ts | 9 +++ .../tests/var-auth-shadow/expected.ts | 8 +++ .../tests/var-auth-shadow/input.ts | 8 +++ packages/sdk-codemod/src/runner.test.ts | 25 ++++++++ 6 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 19e40c3da..4afab4955 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -285,6 +285,13 @@ function hasRuntimeReferenceShadow(localNames: Set, runtimeRef: string): return localNames.has(runtimeRef.split(".")[0]!); } +function collectVariableDeclaratorNames(scope: SgNode, names: Set): void { + for (const decl of scope.children().filter((child) => child.kind() === "variable_declarator")) { + const binding = firstDeclaratorChild(decl); + if (binding) collectBindingNames(binding, names); + } +} + function collectParameterNames(scope: SgNode, names: Set): void { const params = scope.children().find((child) => child.kind() === "formal_parameters"); if (!params) return; @@ -322,12 +329,7 @@ function collectArrowParameterNames(scope: SgNode, names: Set): void { function collectDirectBlockNames(scope: SgNode, names: Set): void { for (const child of scope.children()) { if (child.kind() === "lexical_declaration" || child.kind() === "variable_declaration") { - for (const decl of child - .children() - .filter((grandchild) => grandchild.kind() === "variable_declarator")) { - const binding = firstDeclaratorChild(decl); - if (binding) collectBindingNames(binding, names); - } + collectVariableDeclaratorNames(child, names); continue; } @@ -338,12 +340,59 @@ function collectDirectBlockNames(scope: SgNode, names: Set): void { } } +function collectForInitializerNames(scope: SgNode, names: Set): void { + const children = scope.children(); + const start = children.findIndex((child) => child.kind() === "("); + const end = children.findIndex((child, index) => index > start && child.kind() === ";"); + if (start === -1 || end === -1) return; + + for (const child of children.slice(start + 1, end)) { + if (child.kind() === "lexical_declaration" || child.kind() === "variable_declaration") { + collectVariableDeclaratorNames(child, names); + } + } +} + +function isNestedFunctionOrClassScope(scope: SgNode): boolean { + return ( + [ + "function_declaration", + "function_expression", + "arrow_function", + "method_definition", + "class_declaration", + "class", + ].includes(scope.kind()) || + scope.children().some((child) => child.kind() === "formal_parameters") + ); +} + +function isVarDeclaration(scope: SgNode): boolean { + return ( + scope.kind() === "variable_declaration" && + scope.children().some((child) => child.kind() === "var") + ); +} + +function collectFunctionScopedVarNames(scope: SgNode, names: Set): void { + for (const child of scope.children()) { + if (isNestedFunctionOrClassScope(child)) continue; + + if (isVarDeclaration(child)) { + collectVariableDeclaratorNames(child, names); + } + + collectFunctionScopedVarNames(child, names); + } +} + function directlyDeclaredNames(scope: SgNode): Set { const names = new Set(); const kind = scope.kind(); if (scope.children().some((child) => child.kind() === "formal_parameters")) { collectParameterNames(scope, names); + collectFunctionScopedVarNames(scope, names); } if (kind === "arrow_function") { @@ -356,6 +405,8 @@ function directlyDeclaredNames(scope: SgNode): Set { } } else if (kind === "statement_block" || kind === "program") { collectDirectBlockNames(scope, names); + } else if (kind === "for_statement") { + collectForInitializerNames(scope, names); } else if (kind === "for_in_statement") { const children = scope.children(); const keywordIndex = children.findIndex( diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/expected.ts new file mode 100644 index 000000000..c6eb77045 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/expected.ts @@ -0,0 +1,9 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); + +export function run() { + for (let auth = createClient(); ready; tick()) { + auth.getConnectionToken("github"); + } +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/input.ts new file mode 100644 index 000000000..4a5f1be80 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/input.ts @@ -0,0 +1,9 @@ +import { auth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); + +export function run() { + for (let auth = createClient(); ready; tick()) { + auth.getConnectionToken("github"); + } +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/expected.ts new file mode 100644 index 000000000..95697dff2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/expected.ts @@ -0,0 +1,8 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); + +export function run() { + if (ready) var auth = createClient(); + return auth.getConnectionToken("github"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/input.ts new file mode 100644 index 000000000..b50ad3244 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/input.ts @@ -0,0 +1,8 @@ +import { auth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); + +export function run() { + if (ready) var auth = createClient(); + return auth.getConnectionToken("github"); +} diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index e2841d823..12316b18e 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1086,6 +1086,31 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "for-shadowed.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + "export function run() {", + " for (let auth = createClient(); ready; tick()) {", + ' auth.getConnectionToken("google");', + " }", + "}", + "", + ].join("\n"), + ); + await fs.promises.writeFile( + path.join(dir, "var-shadowed.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + "export function run() {", + " if (ready) var auth = createClient();", + ' return auth.getConnectionToken("google");', + "}", + "", + ].join("\n"), + ); using _stderrSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true); From 5826f626bc1cd0b3a2420d30f6f600be041dc23a Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 05:51:41 +0900 Subject: [PATCH 374/618] fix: preserve auth refs in codemod defaults --- .../scripts/transform.ts | 16 ++++++++++-- .../default-initializer-reference/expected.ts | 8 ++++++ .../default-initializer-reference/input.ts | 7 +++++ .../default-parameter-auth-shadow/expected.ts | 7 +++++ .../default-parameter-auth-shadow/input.ts | 7 +++++ .../tests/switch-case-auth-shadow/expected.ts | 9 +++++++ .../tests/switch-case-auth-shadow/input.ts | 9 +++++++ packages/sdk-codemod/src/runner.test.ts | 26 +++++++++++++++++++ 8 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 4afab4955..efc51289b 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -187,7 +187,9 @@ function collectBindingNames(node: SgNode, out: Set): void { return; } - for (const child of node.children()) { + const children = node.children(); + const defaultIndex = children.findIndex((child) => child.kind() === "="); + for (const child of defaultIndex === -1 ? children : children.slice(0, defaultIndex)) { if (child.kind() === "property_identifier") continue; collectBindingNames(child, out); } @@ -340,6 +342,14 @@ function collectDirectBlockNames(scope: SgNode, names: Set): void { } } +function collectSwitchBodyNames(scope: SgNode, names: Set): void { + for (const child of scope.children()) { + if (child.kind() === "switch_case" || child.kind() === "switch_default") { + collectDirectBlockNames(child, names); + } + } +} + function collectForInitializerNames(scope: SgNode, names: Set): void { const children = scope.children(); const start = children.findIndex((child) => child.kind() === "("); @@ -403,8 +413,10 @@ function directlyDeclaredNames(scope: SgNode): Set { collectBindingNames(child, names); } } - } else if (kind === "statement_block" || kind === "program") { + } else if (["statement_block", "program", "switch_case", "switch_default"].includes(kind)) { collectDirectBlockNames(scope, names); + } else if (kind === "switch_body") { + collectSwitchBodyNames(scope, names); } else if (kind === "for_statement") { collectForInitializerNames(scope, names); } else if (kind === "for_in_statement") { diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/expected.ts new file mode 100644 index 000000000..0a891dc21 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/expected.ts @@ -0,0 +1,8 @@ +import { auth } from "../tailor.config"; +import { authconnection } from "@tailor-platform/sdk/runtime"; + +function read({ x = auth }) { + return x; +} + +export const token = await authconnection.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/input.ts new file mode 100644 index 000000000..3577aea9e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/input.ts @@ -0,0 +1,7 @@ +import { auth } from "../tailor.config"; + +function read({ x = auth }) { + return x; +} + +export const token = await auth.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/expected.ts new file mode 100644 index 000000000..190a59ff8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/expected.ts @@ -0,0 +1,7 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); + +export function run(auth = createClient()) { + return auth.getConnectionToken("github"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/input.ts new file mode 100644 index 000000000..bec5e0075 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/input.ts @@ -0,0 +1,7 @@ +import { auth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); + +export function run(auth = createClient()) { + return auth.getConnectionToken("github"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/expected.ts new file mode 100644 index 000000000..905ae3dc3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/expected.ts @@ -0,0 +1,9 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); + +switch (kind) { + case "github": + const auth = createClient(); + auth.getConnectionToken("github"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/input.ts new file mode 100644 index 000000000..f29397a3c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/input.ts @@ -0,0 +1,9 @@ +import { auth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); + +switch (kind) { + case "github": + const auth = createClient(); + auth.getConnectionToken("github"); +} diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 12316b18e..d57b19373 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1111,6 +1111,32 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "switch-shadowed.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + "switch (kind) {", + ' case "github":', + " const auth = createClient();", + ' auth.getConnectionToken("google");', + "}", + "", + ].join("\n"), + ); + await fs.promises.writeFile( + path.join(dir, "default-ref.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + "function read({ x = auth }) {", + " return x;", + "}", + "", + 'export const token = await auth.getConnectionToken("google");', + "", + ].join("\n"), + ); using _stderrSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true); From a4b2c51fe96f958dfbe3129f0d53ccafee881d04 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 06:07:41 +0900 Subject: [PATCH 375/618] fix: group auth token codemod import removals --- .../scripts/transform.ts | 54 ++++++++++++++++++- .../expected.ts | 6 +++ .../input.ts | 5 ++ .../tests/duplicate-auth-aliases/expected.ts | 4 ++ .../tests/duplicate-auth-aliases/input.ts | 4 ++ 5 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index efc51289b..a51aa3be8 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -559,6 +559,43 @@ function buildImportSpecRemovalEdit(source: string, binding: AuthBinding): Edit return { startPos: r.start.index, endPos: r.end.index, insertedText: "" }; } +function importRangeKey(importStmt: SgNode): string { + const range = importStmt.range(); + return `${range.start.index}:${range.end.index}`; +} + +function buildGroupedImportSpecRemovalEdits( + source: string, + importStmt: SgNode, + bindings: AuthBinding[], +): Edit[] { + const allSpecs = importStmt.findAll({ rule: { kind: "import_specifier" } }); + const removableSpecStarts = new Set(bindings.map((binding) => binding.spec.range().start.index)); + + if (removableSpecStarts.size === allSpecs.length) { + if (hasDefaultOrNamespaceImport(importStmt)) { + const edit = buildOnlyNamedImportRemovalEdit(source, importStmt); + return edit ? [edit] : []; + } + + const range = importStmt.range(); + return [{ startPos: range.start.index, endPos: range.end.index, insertedText: "" }]; + } + + if (bindings.length === 1) { + const edit = buildImportSpecRemovalEdit(source, bindings[0]!); + return edit ? [edit] : []; + } + + const named = namedImportsNode(importStmt); + if (!named) return []; + + const keptSpecTexts = allSpecs + .filter((spec) => !removableSpecStarts.has(spec.range().start.index)) + .map((spec) => spec.text()); + return [named.replace(`{ ${keptSpecTexts.join(", ")} }`)]; +} + function isDirectiveStatement(node: SgNode): boolean { return node.kind() === "expression_statement" && node.children()[0]?.kind() === "string"; } @@ -665,6 +702,10 @@ function transformParsed(source: string, root: SgNode): string | null { scheduledRangesByLocalName.set(call.localName, ranges); } + const removableBindingsByImport = new Map< + string, + { importStmt: SgNode; bindings: AuthBinding[] } + >(); for (const binding of authBindings) { if (calls.every((call) => call.localName !== binding.localName)) continue; const remainingRefs = countRemainingRefs( @@ -673,8 +714,17 @@ function transformParsed(source: string, root: SgNode): string | null { scheduledRangesByLocalName.get(binding.localName) ?? [], ); if (remainingRefs > 0) continue; - const edit = buildImportSpecRemovalEdit(source, binding); - if (edit) edits.push(edit); + const key = importRangeKey(binding.importStmt); + const group = removableBindingsByImport.get(key) ?? { + importStmt: binding.importStmt, + bindings: [], + }; + group.bindings.push(binding); + removableBindingsByImport.set(key, group); + } + + for (const group of removableBindingsByImport.values()) { + edits.push(...buildGroupedImportSpecRemovalEdits(source, group.importStmt, group.bindings)); } const result = normalizeSource(applyEdits(source, edits)); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/expected.ts new file mode 100644 index 000000000..f6a9b27be --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/expected.ts @@ -0,0 +1,6 @@ +import { defineAuth } from "../tailor.config"; +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const googleToken = await authconnection.getConnectionToken("google"); +export const githubToken = await authconnection.getConnectionToken("github"); +export const authConfig = defineAuth(); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/input.ts new file mode 100644 index 000000000..cfa6f109f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/input.ts @@ -0,0 +1,5 @@ +import { auth as googleAuth, defineAuth, auth as githubAuth } from "../tailor.config"; + +export const googleToken = await googleAuth.getConnectionToken("google"); +export const githubToken = await githubAuth.getConnectionToken("github"); +export const authConfig = defineAuth(); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/expected.ts new file mode 100644 index 000000000..b52f810b6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/expected.ts @@ -0,0 +1,4 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const googleToken = await authconnection.getConnectionToken("google"); +export const githubToken = await authconnection.getConnectionToken("github"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/input.ts new file mode 100644 index 000000000..0db886a44 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/input.ts @@ -0,0 +1,4 @@ +import { auth as googleAuth, auth as githubAuth } from "../tailor.config"; + +export const googleToken = await googleAuth.getConnectionToken("google"); +export const githubToken = await githubAuth.getConnectionToken("github"); From 4a47d21650f1f675012c3d4754fc14e2d4aacd30 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 06:19:47 +0900 Subject: [PATCH 376/618] fix: treat type-only runtime imports as codemod collisions --- .../scripts/transform.ts | 4 ++-- .../type-only-runtime-collision/input.ts | 7 +++++++ packages/sdk-codemod/src/runner.test.ts | 20 ++++++++++++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-collision/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index a51aa3be8..6740e4ca2 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -272,12 +272,12 @@ function hasRuntimeImportCollision(localNames: Set, imports: SgNode[]): return imports.some((importStmt) => importBindings(importStmt).some( (binding) => - !binding.typeOnly && binding.localName === AUTHCONNECTION && !( binding.source === RUNTIME_MODULE && binding.importedName === AUTHCONNECTION && - !binding.namespace + !binding.namespace && + !binding.typeOnly ), ), ); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-collision/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-collision/input.ts new file mode 100644 index 000000000..9d959adef --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-collision/input.ts @@ -0,0 +1,7 @@ +import { auth } from "../tailor.config"; +import { type authconnection, workflow } from "@tailor-platform/sdk/runtime"; + +export async function run() { + await workflow.wait("ready"); + return auth.getConnectionToken("google"); +} diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index d57b19373..e915681d4 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1137,6 +1137,19 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "type-collision.ts"), + [ + 'import { auth } from "../tailor.config";', + 'import { type authconnection, workflow } from "@tailor-platform/sdk/runtime";', + "", + "export async function run() {", + ' await workflow.wait("ready");', + ' return auth.getConnectionToken("google");', + "}", + "", + ].join("\n"), + ); using _stderrSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true); @@ -1147,13 +1160,18 @@ describe("runCodemods", () => { { codemodId: "v2/auth-connection-token-helper", prompt: codemod.prompt, - files: ["collision.ts"], + files: ["collision.ts", "type-collision.ts"], findings: [ expect.objectContaining({ file: "collision.ts", line: 6, excerpt: 'return auth.getConnectionToken("google");', }), + expect.objectContaining({ + file: "type-collision.ts", + line: 6, + excerpt: 'return auth.getConnectionToken("google");', + }), ], }, ]); From ed8b02fa297af84d9ad2155edf86f7d1496b0280 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 06:37:47 +0900 Subject: [PATCH 377/618] fix: flag auth token codemod member references --- .../scripts/transform.ts | 62 ++++++++++++------- .../tests/arrow-var-auth-shadow/expected.ts | 8 +++ .../tests/arrow-var-auth-shadow/input.ts | 8 +++ .../tests/non-call-reference/expected.ts | 5 ++ .../tests/non-call-reference/input.ts | 4 ++ packages/sdk-codemod/src/runner.test.ts | 16 ++++- 6 files changed, 80 insertions(+), 23 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 6740e4ca2..de9ad8a6e 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -406,6 +406,7 @@ function directlyDeclaredNames(scope: SgNode): Set { } if (kind === "arrow_function") { + collectFunctionScopedVarNames(scope, names); collectArrowParameterNames(scope, names); } else if (kind === "catch_clause") { for (const child of scope.children()) { @@ -443,30 +444,47 @@ function isReferenceShadowed(node: SgNode, localName: string): boolean { return false; } +function authConnectionTokenReferenceFromMember( + member: SgNode, + authLocalNames: Set, +): TokenCall | null { + const property = member.field("property"); + const object = member.field("object"); + if ( + property?.text() !== GET_CONNECTION_TOKEN || + object?.kind() !== "identifier" || + !authLocalNames.has(object.text()) + ) { + return null; + } + + if (isReferenceShadowed(object, object.text())) return null; + + const range = object.range(); + return { + objectNode: object, + localName: object.text(), + range: [range.start.index, range.end.index], + }; +} + +function findAuthConnectionTokenReferences(root: SgNode, authLocalNames: Set): TokenCall[] { + const references: TokenCall[] = []; + for (const member of root.findAll({ rule: { kind: "member_expression" } })) { + const reference = authConnectionTokenReferenceFromMember(member, authLocalNames); + if (reference) references.push(reference); + } + return references; +} + function findAuthConnectionTokenCalls(root: SgNode, authLocalNames: Set): TokenCall[] { const calls: TokenCall[] = []; for (const call of root.findAll({ rule: { kind: "call_expression" } })) { const callee = call.field("function"); if (callee?.kind() !== "member_expression") continue; - const property = callee.field("property"); - const object = callee.field("object"); - if ( - property?.text() !== GET_CONNECTION_TOKEN || - object?.kind() !== "identifier" || - !authLocalNames.has(object.text()) - ) { - continue; - } - - if (isReferenceShadowed(object, object.text())) continue; - - const range = object.range(); - calls.push({ - objectNode: object, - localName: object.text(), - range: [range.start.index, range.end.index], - }); + const reference = authConnectionTokenReferenceFromMember(callee, authLocalNames); + if (reference) calls.push(reference); } return calls; } @@ -765,15 +783,15 @@ export function reviewFindings( const imports = findImportStatements(root); const authBindings = findTailorConfigAuthBindings(imports); - const calls = findAuthConnectionTokenCalls( + const references = findAuthConnectionTokenReferences( root, new Set(authBindings.map((binding) => binding.localName)), ); - return calls.map((call) => ({ + return references.map((reference) => ({ file: relativePath, - line: lineForIndex(source, call.range[0]), + line: lineForIndex(source, reference.range[0]), message: "Replace defineAuth auth.getConnectionToken() with runtime authconnection.", - excerpt: excerptForIndex(source, call.range[0]), + excerpt: excerptForIndex(source, reference.range[0]), })); } diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/expected.ts new file mode 100644 index 000000000..6c302a9dd --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/expected.ts @@ -0,0 +1,8 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); + +export const run = input => { + if (ready) var auth = createClient(input); + return auth.getConnectionToken("github"); +}; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/input.ts new file mode 100644 index 000000000..620b174c8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/input.ts @@ -0,0 +1,8 @@ +import { auth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); + +export const run = input => { + if (ready) var auth = createClient(input); + return auth.getConnectionToken("github"); +}; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/expected.ts new file mode 100644 index 000000000..4fdd0dcc0 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/expected.ts @@ -0,0 +1,5 @@ +import { auth } from "../tailor.config"; +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); +export const tokenGetter = auth.getConnectionToken; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/input.ts new file mode 100644 index 000000000..ca4cc87eb --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/input.ts @@ -0,0 +1,4 @@ +import { auth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); +export const tokenGetter = auth.getConnectionToken; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index e915681d4..816a1c2ef 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1150,6 +1150,16 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "non-call.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + 'export const token = await auth.getConnectionToken("google");', + "export const tokenGetter = auth.getConnectionToken;", + "", + ].join("\n"), + ); using _stderrSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true); @@ -1160,13 +1170,17 @@ describe("runCodemods", () => { { codemodId: "v2/auth-connection-token-helper", prompt: codemod.prompt, - files: ["collision.ts", "type-collision.ts"], + files: ["collision.ts", "non-call.ts", "type-collision.ts"], findings: [ expect.objectContaining({ file: "collision.ts", line: 6, excerpt: 'return auth.getConnectionToken("google");', }), + expect.objectContaining({ + file: "non-call.ts", + excerpt: "export const tokenGetter = auth.getConnectionToken;", + }), expect.objectContaining({ file: "type-collision.ts", line: 6, From 9ee0d561a32c88625d499c8bfabc4ec8691e7b2f Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 06:49:11 +0900 Subject: [PATCH 378/618] fix: respect named auth token codemod expressions --- .../scripts/transform.ts | 17 +++++++++++++++++ .../named-expression-auth-shadow/expected.ts | 13 +++++++++++++ .../tests/named-expression-auth-shadow/input.ts | 13 +++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index de9ad8a6e..058b067eb 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -396,10 +396,27 @@ function collectFunctionScopedVarNames(scope: SgNode, names: Set): void } } +function collectOwnExpressionName(scope: SgNode, names: Set): void { + if (scope.kind() === "function_expression" || scope.kind() === "function_declaration") { + const name = scope.children().find((child) => child.kind() === "identifier"); + if (name) names.add(name.text()); + return; + } + + if (scope.kind() === "class" || scope.kind() === "class_declaration") { + const name = scope + .children() + .find((child) => child.kind() === "identifier" || child.kind() === "type_identifier"); + if (name) names.add(name.text()); + } +} + function directlyDeclaredNames(scope: SgNode): Set { const names = new Set(); const kind = scope.kind(); + collectOwnExpressionName(scope, names); + if (scope.children().some((child) => child.kind() === "formal_parameters")) { collectParameterNames(scope, names); collectFunctionScopedVarNames(scope, names); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/expected.ts new file mode 100644 index 000000000..cd0dbcc34 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/expected.ts @@ -0,0 +1,13 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); + +const fn = function auth() { + return auth.getConnectionToken("github"); +}; + +const Client = class auth { + run() { + return auth.getConnectionToken("github"); + } +}; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/input.ts new file mode 100644 index 000000000..dd61d2879 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/input.ts @@ -0,0 +1,13 @@ +import { auth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); + +const fn = function auth() { + return auth.getConnectionToken("github"); +}; + +const Client = class auth { + run() { + return auth.getConnectionToken("github"); + } +}; From 589a7bb6bfa1e884f7dd733e86029aade3685539 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 07:03:13 +0900 Subject: [PATCH 379/618] fix: respect exported auth token codemod shadows --- .../auth-connection-token-helper/scripts/transform.ts | 5 +++++ .../tests/namespace-export-auth-shadow/expected.ts | 11 +++++++++++ .../tests/namespace-export-auth-shadow/input.ts | 11 +++++++++++ 3 files changed, 27 insertions(+) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 058b067eb..742f83c7a 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -330,6 +330,11 @@ function collectArrowParameterNames(scope: SgNode, names: Set): void { function collectDirectBlockNames(scope: SgNode, names: Set): void { for (const child of scope.children()) { + if (child.kind() === "export_statement") { + collectDirectBlockNames(child, names); + continue; + } + if (child.kind() === "lexical_declaration" || child.kind() === "variable_declaration") { collectVariableDeclaratorNames(child, names); continue; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/expected.ts new file mode 100644 index 000000000..144a63924 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/expected.ts @@ -0,0 +1,11 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); + +namespace Clients { + export const auth = createClient(); + + export function run() { + return auth.getConnectionToken("github"); + } +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/input.ts new file mode 100644 index 000000000..0420e7012 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/input.ts @@ -0,0 +1,11 @@ +import { auth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); + +namespace Clients { + export const auth = createClient(); + + export function run() { + return auth.getConnectionToken("github"); + } +} From 9b7715ba2414ae1cecdcaee7ce03045c14151086 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 07:16:48 +0900 Subject: [PATCH 380/618] fix: surface auth token codemod leftovers --- .../scripts/transform.ts | 108 +++++++++++++++++- .../commented-auth-import-spec/expected.ts | 5 + .../tests/commented-auth-import-spec/input.ts | 4 + .../expected.ts | 5 + .../input.ts | 4 + .../tests/using-auth-shadow/expected.ts | 8 ++ .../tests/using-auth-shadow/input.ts | 8 ++ packages/sdk-codemod/src/runner.test.ts | 16 ++- 8 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 742f83c7a..9557b4612 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -335,6 +335,11 @@ function collectDirectBlockNames(scope: SgNode, names: Set): void { continue; } + if (child.kind() === "expression_statement") { + collectUsingDeclarationNames(child, names); + continue; + } + if (child.kind() === "lexical_declaration" || child.kind() === "variable_declaration") { collectVariableDeclaratorNames(child, names); continue; @@ -347,6 +352,22 @@ function collectDirectBlockNames(scope: SgNode, names: Set): void { } } +function collectUsingDeclarationNames(scope: SgNode, names: Set): void { + const assignment = scope.children().find((child) => child.kind() === "assignment_expression"); + if (!assignment) return; + + const children = assignment.children(); + const usingIndex = children.findIndex((child) => child.kind() === "using"); + const end = children.findIndex((child, index) => index > usingIndex && child.kind() === "="); + if (usingIndex === -1 || end === -1) return; + + for (const child of children.slice(usingIndex + 1, end)) { + if (["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind())) { + collectBindingNames(child, names); + } + } +} + function collectSwitchBodyNames(scope: SgNode, names: Set): void { for (const child of scope.children()) { if (child.kind() === "switch_case" || child.kind() === "switch_default") { @@ -499,6 +520,59 @@ function findAuthConnectionTokenReferences(root: SgNode, authLocalNames: Set { + if ( + child.kind() === "shorthand_property_identifier_pattern" && + child.text() === GET_CONNECTION_TOKEN + ) { + return true; + } + + if (child.kind() !== "pair_pattern") return false; + return child + .children() + .some( + (grandchild) => + grandchild.kind() === "property_identifier" && grandchild.text() === GET_CONNECTION_TOKEN, + ); + }); +} + +function findAuthConnectionTokenDestructures( + root: SgNode, + authLocalNames: Set, +): TokenCall[] { + const references: TokenCall[] = []; + for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { + const children = decl.children(); + const equalIndex = children.findIndex((child) => child.kind() === "="); + const binding = firstDeclaratorChild(decl); + const initializer = children + .slice(equalIndex + 1) + .find((child) => child.kind() === "identifier"); + if ( + equalIndex === -1 || + binding?.kind() !== "object_pattern" || + !objectPatternHasGetConnectionToken(binding) || + initializer?.kind() !== "identifier" || + !authLocalNames.has(initializer.text()) + ) { + continue; + } + + if (isReferenceShadowed(initializer, initializer.text())) continue; + + const range = decl.range(); + references.push({ + objectNode: initializer, + localName: initializer.text(), + range: [range.start.index, range.end.index], + }); + } + return references; +} + function findAuthConnectionTokenCalls(root: SgNode, authLocalNames: Set): TokenCall[] { const calls: TokenCall[] = []; for (const call of root.findAll({ rule: { kind: "call_expression" } })) { @@ -568,6 +642,29 @@ function buildOnlyNamedImportRemovalEdit(source: string, importStmt: SgNode): Ed return { startPos: start, endPos: end, insertedText: "" }; } +function skipInlineTrivia(source: string, index: number): number { + let pos = index; + while (pos < source.length) { + while (pos < source.length && (source[pos] === " " || source[pos] === "\t")) pos++; + + if (source.startsWith("/*", pos)) { + const end = source.indexOf("*/", pos + 2); + if (end === -1) return pos; + pos = end + 2; + continue; + } + + if (source.startsWith("//", pos)) { + const end = source.indexOf("\n", pos + 2); + pos = end === -1 ? source.length : end + 1; + continue; + } + + return pos; + } + return pos; +} + function buildImportSpecRemovalEdit(source: string, binding: AuthBinding): Edit | null { const allSpecs = binding.importStmt.findAll({ rule: { kind: "import_specifier" } }); if (allSpecs.length === 1) { @@ -582,7 +679,7 @@ function buildImportSpecRemovalEdit(source: string, binding: AuthBinding): Edit const r = binding.spec.range(); let start = r.start.index; let end = r.end.index; - while (end < source.length && (source[end] === " " || source[end] === "\t")) end++; + end = skipInlineTrivia(source, end); if (source[end] === ",") { end++; while (end < source.length && (source[end] === " " || source[end] === "\t")) end++; @@ -805,10 +902,11 @@ export function reviewFindings( const imports = findImportStatements(root); const authBindings = findTailorConfigAuthBindings(imports); - const references = findAuthConnectionTokenReferences( - root, - new Set(authBindings.map((binding) => binding.localName)), - ); + const authLocalNames = new Set(authBindings.map((binding) => binding.localName)); + const references = [ + ...findAuthConnectionTokenReferences(root, authLocalNames), + ...findAuthConnectionTokenDestructures(root, authLocalNames), + ].toSorted((a, b) => a.range[0] - b.range[0]); return references.map((reference) => ({ file: relativePath, diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/expected.ts new file mode 100644 index 000000000..a11c8810b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/expected.ts @@ -0,0 +1,5 @@ +import { defineAuth } from "../tailor.config"; +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); +export const authConfig = defineAuth(); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/input.ts new file mode 100644 index 000000000..dd6416471 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/input.ts @@ -0,0 +1,4 @@ +import { auth /* cfg */, defineAuth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); +export const authConfig = defineAuth(); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/expected.ts new file mode 100644 index 000000000..0375525c1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/expected.ts @@ -0,0 +1,5 @@ +import { auth } from "../tailor.config"; +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); +export const { getConnectionToken } = auth; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/input.ts new file mode 100644 index 000000000..b8a33fe79 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/input.ts @@ -0,0 +1,4 @@ +import { auth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); +export const { getConnectionToken } = auth; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/expected.ts new file mode 100644 index 000000000..82957c99e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/expected.ts @@ -0,0 +1,8 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); + +{ + using auth = createClient(); + auth.getConnectionToken("github"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/input.ts new file mode 100644 index 000000000..65f557b72 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/input.ts @@ -0,0 +1,8 @@ +import { auth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); + +{ + using auth = createClient(); + auth.getConnectionToken("github"); +} diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 816a1c2ef..66ed1e0f5 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1160,6 +1160,16 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "destructure.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + 'export const token = await auth.getConnectionToken("google");', + "export const { getConnectionToken } = auth;", + "", + ].join("\n"), + ); using _stderrSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true); @@ -1170,13 +1180,17 @@ describe("runCodemods", () => { { codemodId: "v2/auth-connection-token-helper", prompt: codemod.prompt, - files: ["collision.ts", "non-call.ts", "type-collision.ts"], + files: ["collision.ts", "destructure.ts", "non-call.ts", "type-collision.ts"], findings: [ expect.objectContaining({ file: "collision.ts", line: 6, excerpt: 'return auth.getConnectionToken("google");', }), + expect.objectContaining({ + file: "destructure.ts", + excerpt: "export const { getConnectionToken } = auth;", + }), expect.objectContaining({ file: "non-call.ts", excerpt: "export const tokenGetter = auth.getConnectionToken;", From 51324525ccebbab510ccbcb5b217161df4fb9dc3 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 07:33:55 +0900 Subject: [PATCH 381/618] fix: handle auth token codemod import collisions --- .../scripts/transform.ts | 56 ++++++++++++++----- .../expected.ts | 5 ++ .../input.ts | 4 ++ .../import-equals-runtime-collision/input.ts | 6 ++ packages/sdk-codemod/src/runner.test.ts | 25 ++++++++- 5 files changed, 81 insertions(+), 15 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/import-equals-runtime-collision/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 9557b4612..0b05e4119 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -269,17 +269,29 @@ function localDeclarationNames(root: SgNode): Set { function hasRuntimeImportCollision(localNames: Set, imports: SgNode[]): boolean { if (localNames.has(AUTHCONNECTION)) return true; - return imports.some((importStmt) => - importBindings(importStmt).some( - (binding) => - binding.localName === AUTHCONNECTION && - !( - binding.source === RUNTIME_MODULE && - binding.importedName === AUTHCONNECTION && - !binding.namespace && - !binding.typeOnly - ), - ), + return imports.some( + (importStmt) => + importEqualsLocalName(importStmt) === AUTHCONNECTION || + importBindings(importStmt).some( + (binding) => + binding.localName === AUTHCONNECTION && + !( + binding.source === RUNTIME_MODULE && + binding.importedName === AUTHCONNECTION && + !binding.namespace && + !binding.typeOnly + ), + ), + ); +} + +function importEqualsLocalName(importStmt: SgNode): string | null { + const clause = importStmt.children().find((child) => child.kind() === "import_require_clause"); + return ( + clause + ?.children() + .find((child) => child.kind() === "identifier") + ?.text() ?? null ); } @@ -632,16 +644,32 @@ function buildOnlyNamedImportRemovalEdit(source: string, importStmt: SgNode): Ed const named = namedImportsNode(importStmt); if (!named) return null; - let start = named.range().start.index; + let start = skipBackwardImportTrivia(source, named.range().start.index); const end = named.range().end.index; - while (start > 0 && (source[start - 1] === " " || source[start - 1] === "\t")) start--; if (source[start - 1] === ",") { start--; - while (start > 0 && (source[start - 1] === " " || source[start - 1] === "\t")) start--; + start = skipBackwardImportTrivia(source, start); } return { startPos: start, endPos: end, insertedText: "" }; } +function skipBackwardImportTrivia(source: string, index: number): number { + let pos = index; + while (pos > 0) { + while (pos > 0 && /\s/.test(source[pos - 1]!)) pos--; + + if (source.slice(pos - 2, pos) === "*/") { + const start = source.lastIndexOf("/*", pos - 2); + if (start === -1) return pos; + pos = start; + continue; + } + + return pos; + } + return pos; +} + function skipInlineTrivia(source: string, index: number): number { let pos = index; while (pos < source.length) { diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/expected.ts new file mode 100644 index 000000000..98e63933d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/expected.ts @@ -0,0 +1,5 @@ +import cfg from "../tailor.config"; +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); +export const config = cfg; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/input.ts new file mode 100644 index 000000000..7c54c8996 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/input.ts @@ -0,0 +1,4 @@ +import cfg, /* note */ { auth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); +export const config = cfg; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/import-equals-runtime-collision/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/import-equals-runtime-collision/input.ts new file mode 100644 index 000000000..b129699ec --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/import-equals-runtime-collision/input.ts @@ -0,0 +1,6 @@ +import { auth } from "../tailor.config"; +import authconnection = require("./client"); + +export async function run() { + return auth.getConnectionToken("google"); +} diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 66ed1e0f5..f126b22b3 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1150,6 +1150,18 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "import-equals-collision.ts"), + [ + 'import { auth } from "../tailor.config";', + 'import authconnection = require("./client");', + "", + "export async function run() {", + ' return auth.getConnectionToken("google");', + "}", + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "non-call.ts"), [ @@ -1180,7 +1192,13 @@ describe("runCodemods", () => { { codemodId: "v2/auth-connection-token-helper", prompt: codemod.prompt, - files: ["collision.ts", "destructure.ts", "non-call.ts", "type-collision.ts"], + files: [ + "collision.ts", + "destructure.ts", + "import-equals-collision.ts", + "non-call.ts", + "type-collision.ts", + ], findings: [ expect.objectContaining({ file: "collision.ts", @@ -1191,6 +1209,11 @@ describe("runCodemods", () => { file: "destructure.ts", excerpt: "export const { getConnectionToken } = auth;", }), + expect.objectContaining({ + file: "import-equals-collision.ts", + line: 5, + excerpt: 'return auth.getConnectionToken("google");', + }), expect.objectContaining({ file: "non-call.ts", excerpt: "export const tokenGetter = auth.getConnectionToken;", From 54cad2fab1eda0604ecdd4bd9ef6e317d63872bc Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 07:48:24 +0900 Subject: [PATCH 382/618] fix: respect wrapped auth token receivers --- .../scripts/transform.ts | 59 ++++++++++++++++--- .../tests/namespace-auth-shadow/input.ts | 11 ++++ .../tests/wrapped-auth-receiver/expected.ts | 7 +++ .../tests/wrapped-auth-receiver/input.ts | 7 +++ packages/sdk-codemod/src/runner.test.ts | 18 ++++++ 5 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-auth-shadow/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 0b05e4119..6abe5f2f6 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -348,6 +348,10 @@ function collectDirectBlockNames(scope: SgNode, names: Set): void { } if (child.kind() === "expression_statement") { + const moduleDecl = child + .children() + .find((grandchild) => grandchild.kind() === "internal_module"); + if (moduleDecl) collectDeclarationName(moduleDecl, names); collectUsingDeclarationNames(child, names); continue; } @@ -357,13 +361,25 @@ function collectDirectBlockNames(scope: SgNode, names: Set): void { continue; } - if (["function_declaration", "class_declaration", "enum_declaration"].includes(child.kind())) { - const name = child.children().find((grandchild) => grandchild.kind() === "identifier"); - if (name) names.add(name.text()); + if ( + [ + "function_declaration", + "class_declaration", + "enum_declaration", + "internal_module", + "import_alias", + ].includes(child.kind()) + ) { + collectDeclarationName(child, names); } } } +function collectDeclarationName(node: SgNode, names: Set): void { + const name = node.children().find((child) => child.kind() === "identifier"); + if (name) names.add(name.text()); +} + function collectUsingDeclarationNames(scope: SgNode, names: Set): void { const assignment = scope.children().find((child) => child.kind() === "assignment_expression"); if (!assignment) return; @@ -446,6 +462,11 @@ function collectOwnExpressionName(scope: SgNode, names: Set): void { .children() .find((child) => child.kind() === "identifier" || child.kind() === "type_identifier"); if (name) names.add(name.text()); + return; + } + + if (scope.kind() === "internal_module") { + collectDeclarationName(scope, names); } } @@ -505,24 +526,48 @@ function authConnectionTokenReferenceFromMember( ): TokenCall | null { const property = member.field("property"); const object = member.field("object"); + const receiver = authReceiverIdentifier(object); if ( property?.text() !== GET_CONNECTION_TOKEN || - object?.kind() !== "identifier" || - !authLocalNames.has(object.text()) + !object || + !receiver || + !authLocalNames.has(receiver.text()) ) { return null; } - if (isReferenceShadowed(object, object.text())) return null; + if (isReferenceShadowed(receiver, receiver.text())) return null; const range = object.range(); return { objectNode: object, - localName: object.text(), + localName: receiver.text(), range: [range.start.index, range.end.index], }; } +function authReceiverIdentifier(node: SgNode | null): SgNode | null { + if (!node) return null; + if (node.kind() === "identifier") return node; + if ( + ![ + "parenthesized_expression", + "as_expression", + "satisfies_expression", + "non_null_expression", + "type_assertion", + ].includes(node.kind()) + ) { + return null; + } + + for (const child of node.children()) { + const receiver = authReceiverIdentifier(child); + if (receiver) return receiver; + } + return null; +} + function findAuthConnectionTokenReferences(root: SgNode, authLocalNames: Set): TokenCall[] { const references: TokenCall[] = []; for (const member of root.findAll({ rule: { kind: "member_expression" } })) { diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-auth-shadow/input.ts new file mode 100644 index 000000000..fb73f5f83 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-auth-shadow/input.ts @@ -0,0 +1,11 @@ +import { auth } from "../tailor.config"; + +namespace auth { + export const token = auth.getConnectionToken("google"); +} + +namespace box { + import auth = other.auth; + + export const token = auth.getConnectionToken("github"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/expected.ts new file mode 100644 index 000000000..1cd9cddc0 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/expected.ts @@ -0,0 +1,7 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const parenthesized = await authconnection.getConnectionToken("google"); +export const asserted = await authconnection.getConnectionToken("github"); +export const satisfied = await authconnection.getConnectionToken("okta"); +export const nonNull = await authconnection.getConnectionToken("microsoft"); +export const typeAsserted = await authconnection.getConnectionToken("azure"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/input.ts new file mode 100644 index 000000000..bed683ba8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/input.ts @@ -0,0 +1,7 @@ +import { auth } from "../tailor.config"; + +export const parenthesized = await (auth).getConnectionToken("google"); +export const asserted = await (auth as any).getConnectionToken("github"); +export const satisfied = await (auth satisfies unknown).getConnectionToken("okta"); +export const nonNull = await auth!.getConnectionToken("microsoft"); +export const typeAsserted = await (auth).getConnectionToken("azure"); diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index f126b22b3..1a906b302 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1162,6 +1162,18 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "wrapped-collision.ts"), + [ + 'import { auth } from "../tailor.config";', + 'import authconnection = require("./client");', + "", + "export async function run() {", + ' return (auth as any).getConnectionToken("google");', + "}", + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "non-call.ts"), [ @@ -1198,6 +1210,7 @@ describe("runCodemods", () => { "import-equals-collision.ts", "non-call.ts", "type-collision.ts", + "wrapped-collision.ts", ], findings: [ expect.objectContaining({ @@ -1223,6 +1236,11 @@ describe("runCodemods", () => { line: 6, excerpt: 'return auth.getConnectionToken("google");', }), + expect.objectContaining({ + file: "wrapped-collision.ts", + line: 5, + excerpt: 'return (auth as any).getConnectionToken("google");', + }), ], }, ]); From 300bcf8afa1d03c919e82f8277695f2d36ba1bbd Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 07:59:40 +0900 Subject: [PATCH 383/618] fix: flag namespace auth token references --- .../scripts/transform.ts | 90 ++++++++++++++++++- packages/sdk-codemod/src/runner.test.ts | 17 ++++ 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 6abe5f2f6..6ea19a68d 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -156,6 +156,19 @@ function findTailorConfigAuthBindings(imports: SgNode[]): AuthBinding[] { ); } +function findTailorConfigNamespaceAuthLocalNames(imports: SgNode[]): Set { + return new Set( + imports.flatMap((importStmt) => + importBindings(importStmt) + .filter( + (binding) => + binding.namespace && !binding.typeOnly && isTailorConfigSource(binding.source), + ) + .map((binding) => binding.localName), + ), + ); +} + function runtimeAuthconnectionReference(imports: SgNode[]): string | null { for (const importStmt of imports) { for (const binding of importBindings(importStmt)) { @@ -547,8 +560,12 @@ function authConnectionTokenReferenceFromMember( } function authReceiverIdentifier(node: SgNode | null): SgNode | null { + const receiver = unwrapReceiverExpression(node); + return receiver?.kind() === "identifier" ? receiver : null; +} + +function unwrapReceiverExpression(node: SgNode | null): SgNode | null { if (!node) return null; - if (node.kind() === "identifier") return node; if ( ![ "parenthesized_expression", @@ -558,11 +575,24 @@ function authReceiverIdentifier(node: SgNode | null): SgNode | null { "type_assertion", ].includes(node.kind()) ) { - return null; + return node; } for (const child of node.children()) { - const receiver = authReceiverIdentifier(child); + if ( + ![ + "identifier", + "member_expression", + "parenthesized_expression", + "as_expression", + "satisfies_expression", + "non_null_expression", + "type_assertion", + ].includes(child.kind()) + ) { + continue; + } + const receiver = unwrapReceiverExpression(child); if (receiver) return receiver; } return null; @@ -577,6 +607,58 @@ function findAuthConnectionTokenReferences(root: SgNode, authLocalNames: Set, +): TokenCall | null { + const property = member.field("property"); + const object = member.field("object"); + const receiver = namespaceAuthReceiverIdentifier(object, namespaceAuthLocalNames); + if (property?.text() !== GET_CONNECTION_TOKEN || !object || !receiver) return null; + if (isReferenceShadowed(receiver, receiver.text())) return null; + + const range = object.range(); + return { + objectNode: object, + localName: receiver.text(), + range: [range.start.index, range.end.index], + }; +} + +function namespaceAuthReceiverIdentifier( + node: SgNode | null, + namespaceAuthLocalNames: Set, +): SgNode | null { + const receiver = unwrapReceiverExpression(node); + if (receiver?.kind() !== "member_expression") return null; + + const property = receiver.field("property"); + const object = receiver.field("object"); + if ( + property?.text() !== "auth" || + object?.kind() !== "identifier" || + !namespaceAuthLocalNames.has(object.text()) + ) { + return null; + } + return object; +} + +function findAuthConnectionTokenNamespaceReferences( + root: SgNode, + namespaceAuthLocalNames: Set, +): TokenCall[] { + const references: TokenCall[] = []; + for (const member of root.findAll({ rule: { kind: "member_expression" } })) { + const reference = authConnectionTokenNamespaceReferenceFromMember( + member, + namespaceAuthLocalNames, + ); + if (reference) references.push(reference); + } + return references; +} + function objectPatternHasGetConnectionToken(pattern: SgNode): boolean { return pattern.children().some((child) => { if ( @@ -976,8 +1058,10 @@ export function reviewFindings( const imports = findImportStatements(root); const authBindings = findTailorConfigAuthBindings(imports); const authLocalNames = new Set(authBindings.map((binding) => binding.localName)); + const namespaceAuthLocalNames = findTailorConfigNamespaceAuthLocalNames(imports); const references = [ ...findAuthConnectionTokenReferences(root, authLocalNames), + ...findAuthConnectionTokenNamespaceReferences(root, namespaceAuthLocalNames), ...findAuthConnectionTokenDestructures(root, authLocalNames), ].toSorted((a, b) => a.range[0] - b.range[0]); diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 1a906b302..16e6961fe 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1174,6 +1174,17 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "namespace-import.ts"), + [ + 'import * as cfg from "../tailor.config";', + "", + "export async function run() {", + ' return cfg.auth.getConnectionToken("google");', + "}", + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "non-call.ts"), [ @@ -1208,6 +1219,7 @@ describe("runCodemods", () => { "collision.ts", "destructure.ts", "import-equals-collision.ts", + "namespace-import.ts", "non-call.ts", "type-collision.ts", "wrapped-collision.ts", @@ -1227,6 +1239,11 @@ describe("runCodemods", () => { line: 5, excerpt: 'return auth.getConnectionToken("google");', }), + expect.objectContaining({ + file: "namespace-import.ts", + line: 4, + excerpt: 'return cfg.auth.getConnectionToken("google");', + }), expect.objectContaining({ file: "non-call.ts", excerpt: "export const tokenGetter = auth.getConnectionToken;", From 935bed2affc74684ccdb9256dd8b2e9a3697b27c Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 08:16:29 +0900 Subject: [PATCH 384/618] fix: flag computed auth token references --- .../scripts/transform.ts | 68 +++++++++++++++++++ packages/sdk-codemod/src/runner.test.ts | 38 +++++++++++ 2 files changed, 106 insertions(+) diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 6ea19a68d..76959ac48 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -659,6 +659,72 @@ function findAuthConnectionTokenNamespaceReferences( return references; } +function isGetConnectionTokenSubscript(subscript: SgNode): boolean { + return stringValue(subscript.field("index")) === GET_CONNECTION_TOKEN; +} + +function authConnectionTokenSubscriptReferenceFromSubscript( + subscript: SgNode, + authLocalNames: Set, +): TokenCall | null { + const object = subscript.field("object"); + const receiver = authReceiverIdentifier(object); + if (!isGetConnectionTokenSubscript(subscript) || !object || !receiver) return null; + if (!authLocalNames.has(receiver.text())) return null; + if (isReferenceShadowed(receiver, receiver.text())) return null; + + const range = subscript.range(); + return { + objectNode: object, + localName: receiver.text(), + range: [range.start.index, range.end.index], + }; +} + +function findAuthConnectionTokenSubscriptReferences( + root: SgNode, + authLocalNames: Set, +): TokenCall[] { + const references: TokenCall[] = []; + for (const subscript of root.findAll({ rule: { kind: "subscript_expression" } })) { + const reference = authConnectionTokenSubscriptReferenceFromSubscript(subscript, authLocalNames); + if (reference) references.push(reference); + } + return references; +} + +function authConnectionTokenNamespaceSubscriptReferenceFromSubscript( + subscript: SgNode, + namespaceAuthLocalNames: Set, +): TokenCall | null { + const object = subscript.field("object"); + const receiver = namespaceAuthReceiverIdentifier(object, namespaceAuthLocalNames); + if (!isGetConnectionTokenSubscript(subscript) || !object || !receiver) return null; + if (isReferenceShadowed(receiver, receiver.text())) return null; + + const range = subscript.range(); + return { + objectNode: object, + localName: receiver.text(), + range: [range.start.index, range.end.index], + }; +} + +function findAuthConnectionTokenNamespaceSubscriptReferences( + root: SgNode, + namespaceAuthLocalNames: Set, +): TokenCall[] { + const references: TokenCall[] = []; + for (const subscript of root.findAll({ rule: { kind: "subscript_expression" } })) { + const reference = authConnectionTokenNamespaceSubscriptReferenceFromSubscript( + subscript, + namespaceAuthLocalNames, + ); + if (reference) references.push(reference); + } + return references; +} + function objectPatternHasGetConnectionToken(pattern: SgNode): boolean { return pattern.children().some((child) => { if ( @@ -1062,6 +1128,8 @@ export function reviewFindings( const references = [ ...findAuthConnectionTokenReferences(root, authLocalNames), ...findAuthConnectionTokenNamespaceReferences(root, namespaceAuthLocalNames), + ...findAuthConnectionTokenSubscriptReferences(root, authLocalNames), + ...findAuthConnectionTokenNamespaceSubscriptReferences(root, namespaceAuthLocalNames), ...findAuthConnectionTokenDestructures(root, authLocalNames), ].toSorted((a, b) => a.range[0] - b.range[0]); diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 16e6961fe..02150303a 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1185,6 +1185,27 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "computed.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + 'export const token = await auth["getConnectionToken"]("google");', + 'export const tokenGetter = auth["getConnectionToken"];', + "", + ].join("\n"), + ); + await fs.promises.writeFile( + path.join(dir, "namespace-computed.ts"), + [ + 'import * as cfg from "../tailor.config";', + "", + "export async function run() {", + ' return cfg.auth["getConnectionToken"]("google");', + "}", + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "non-call.ts"), [ @@ -1217,8 +1238,10 @@ describe("runCodemods", () => { prompt: codemod.prompt, files: [ "collision.ts", + "computed.ts", "destructure.ts", "import-equals-collision.ts", + "namespace-computed.ts", "namespace-import.ts", "non-call.ts", "type-collision.ts", @@ -1230,6 +1253,16 @@ describe("runCodemods", () => { line: 6, excerpt: 'return auth.getConnectionToken("google");', }), + expect.objectContaining({ + file: "computed.ts", + line: 3, + excerpt: 'export const token = await auth["getConnectionToken"]("google");', + }), + expect.objectContaining({ + file: "computed.ts", + line: 4, + excerpt: 'export const tokenGetter = auth["getConnectionToken"];', + }), expect.objectContaining({ file: "destructure.ts", excerpt: "export const { getConnectionToken } = auth;", @@ -1239,6 +1272,11 @@ describe("runCodemods", () => { line: 5, excerpt: 'return auth.getConnectionToken("google");', }), + expect.objectContaining({ + file: "namespace-computed.ts", + line: 4, + excerpt: 'return cfg.auth["getConnectionToken"]("google");', + }), expect.objectContaining({ file: "namespace-import.ts", line: 4, From 5a95832a6d13b587b6d70da4f7afea2daa63708f Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 08:27:13 +0900 Subject: [PATCH 385/618] fix: respect class auth token shadows --- .../v2/auth-connection-token-helper/scripts/transform.ts | 7 ++++--- .../tests/class-auth-shadow/input.ts | 7 +++++++ .../tests/class-authconnection-collision/input.ts | 5 +++++ .../tests/named-class-authconnection-collision/input.ts | 5 +++++ 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-auth-shadow/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-authconnection-collision/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-class-authconnection-collision/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 76959ac48..ab2e09fa1 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -244,8 +244,7 @@ function localDeclarationNames(root: SgNode): Set { ], }, })) { - const name = decl.children().find((child) => child.kind() === "identifier"); - if (name) names.add(name.text()); + collectDeclarationName(decl, names); } for (const catchClause of root.findAll({ rule: { kind: "catch_clause" } })) { @@ -389,7 +388,9 @@ function collectDirectBlockNames(scope: SgNode, names: Set): void { } function collectDeclarationName(node: SgNode, names: Set): void { - const name = node.children().find((child) => child.kind() === "identifier"); + const name = node + .children() + .find((child) => child.kind() === "identifier" || child.kind() === "type_identifier"); if (name) names.add(name.text()); } diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-auth-shadow/input.ts new file mode 100644 index 000000000..6d9b04f10 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-auth-shadow/input.ts @@ -0,0 +1,7 @@ +import { auth } from "../tailor.config"; + +{ + class auth {} + + const token = auth.getConnectionToken("google"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-authconnection-collision/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-authconnection-collision/input.ts new file mode 100644 index 000000000..e95e726fe --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-authconnection-collision/input.ts @@ -0,0 +1,5 @@ +import { auth } from "../tailor.config"; + +class authconnection {} + +export const token = await auth.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-class-authconnection-collision/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-class-authconnection-collision/input.ts new file mode 100644 index 000000000..26f62fce4 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-class-authconnection-collision/input.ts @@ -0,0 +1,5 @@ +import { auth } from "../tailor.config"; + +const Local = class authconnection {}; + +export const token = await auth.getConnectionToken("google"); From 864934aba527ec783be4cbed660e49eceafd3d07 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 08:41:45 +0900 Subject: [PATCH 386/618] fix: respect auth token parameter scope --- .../scripts/transform.ts | 18 ++++++++++++++---- .../default-parameter-var-auth/expected.ts | 6 ++++++ .../tests/default-parameter-var-auth/input.ts | 6 ++++++ 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index ab2e09fa1..47ce89474 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -484,19 +484,29 @@ function collectOwnExpressionName(scope: SgNode, names: Set): void { } } -function directlyDeclaredNames(scope: SgNode): Set { +function isInsideFormalParameters(node: SgNode, scope: SgNode): boolean { + const params = scope.children().find((child) => child.kind() === "formal_parameters"); + if (!params) return false; + + const nodeStart = node.range().start.index; + const paramsRange = params.range(); + return nodeStart >= paramsRange.start.index && nodeStart < paramsRange.end.index; +} + +function directlyDeclaredNames(scope: SgNode, reference: SgNode): Set { const names = new Set(); const kind = scope.kind(); + const referenceInParameters = isInsideFormalParameters(reference, scope); collectOwnExpressionName(scope, names); if (scope.children().some((child) => child.kind() === "formal_parameters")) { collectParameterNames(scope, names); - collectFunctionScopedVarNames(scope, names); + if (!referenceInParameters) collectFunctionScopedVarNames(scope, names); } if (kind === "arrow_function") { - collectFunctionScopedVarNames(scope, names); + if (!referenceInParameters) collectFunctionScopedVarNames(scope, names); collectArrowParameterNames(scope, names); } else if (kind === "catch_clause") { for (const child of scope.children()) { @@ -528,7 +538,7 @@ function directlyDeclaredNames(scope: SgNode): Set { function isReferenceShadowed(node: SgNode, localName: string): boolean { let current = node.parent(); while (current) { - if (directlyDeclaredNames(current).has(localName)) return true; + if (directlyDeclaredNames(current, node).has(localName)) return true; current = current.parent(); } return false; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/expected.ts new file mode 100644 index 000000000..a89049bde --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/expected.ts @@ -0,0 +1,6 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export function run(token = authconnection.getConnectionToken("google")) { + var auth = createClient(); + return token; +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/input.ts new file mode 100644 index 000000000..e053615ed --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/input.ts @@ -0,0 +1,6 @@ +import { auth } from "../tailor.config"; + +export function run(token = auth.getConnectionToken("google")) { + var auth = createClient(); + return token; +} From 0f7a5192ab4a388639aebc2a701cef4483c020cb Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 08:55:31 +0900 Subject: [PATCH 387/618] fix: ignore auth token parameter types --- .../scripts/transform.ts | 19 ++++++++++--------- .../arrow-parameter-type-auth/expected.ts | 4 ++++ .../tests/arrow-parameter-type-auth/input.ts | 3 +++ 3 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 47ce89474..f8b93d990 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -322,6 +322,10 @@ function collectParameterNames(scope: SgNode, names: Set): void { const params = scope.children().find((child) => child.kind() === "formal_parameters"); if (!params) return; + collectFormalParameterNames(params, names); +} + +function collectFormalParameterNames(params: SgNode, names: Set): void { for (const param of params.children()) { const binding = param .children() @@ -338,15 +342,12 @@ function collectArrowParameterNames(scope: SgNode, names: Set): void { if (arrowIndex === -1) return; for (const child of children.slice(0, arrowIndex)) { - if ( - [ - "identifier", - "object_pattern", - "array_pattern", - "rest_pattern", - "formal_parameters", - ].includes(child.kind()) - ) { + if (child.kind() === "formal_parameters") { + collectFormalParameterNames(child, names); + continue; + } + + if (["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind())) { collectBindingNames(child, names); } } diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/expected.ts new file mode 100644 index 000000000..46c4af62e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/expected.ts @@ -0,0 +1,4 @@ +import { auth } from "../tailor.config"; +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const run = (input: typeof auth) => authconnection.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/input.ts new file mode 100644 index 000000000..0a997401f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/input.ts @@ -0,0 +1,3 @@ +import { auth } from "../tailor.config"; + +export const run = (input: typeof auth) => auth.getConnectionToken("google"); From 9c3e841d051a6639857bb9b16988efe0601bdc74 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 09:11:42 +0900 Subject: [PATCH 388/618] fix: flag auth token require references --- .../scripts/transform.ts | 233 +++++++++++++++--- packages/sdk-codemod/src/runner.test.ts | 52 ++++ 2 files changed, 254 insertions(+), 31 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index f8b93d990..e32abf2f6 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -34,6 +34,8 @@ interface TokenCall { range: [number, number]; } +type BindingScopeMap = Map>; + function quickFilter(source: string): boolean { return source.includes(GET_CONNECTION_TOKEN) && source.includes("tailor.config"); } @@ -169,6 +171,122 @@ function findTailorConfigNamespaceAuthLocalNames(imports: SgNode[]): Set ); } +function initializerChild(decl: SgNode): SgNode | null { + const children = decl.children(); + const equalIndex = children.findIndex((child) => child.kind() === "="); + if (equalIndex === -1) return null; + return ( + children.slice(equalIndex + 1).find((child) => child.kind() !== "," && child.kind() !== ";") ?? + null + ); +} + +function requireCallSource(call: SgNode | null): string | null { + if (call?.kind() !== "call_expression") return null; + + const callee = call.field("function"); + if (callee?.kind() !== "identifier" || callee.text() !== "require") return null; + + const args = call.children().find((child) => child.kind() === "arguments"); + const source = args?.children().find((child) => child.kind() === "string") ?? null; + return stringValue(source); +} + +function objectPatternLocalNames(pattern: SgNode, importedName: string): string[] { + const names: string[] = []; + for (const child of pattern.children()) { + if (child.kind() === "shorthand_property_identifier_pattern" && child.text() === importedName) { + names.push(child.text()); + continue; + } + + if (child.kind() !== "pair_pattern") continue; + const property = child + .children() + .find((grandchild) => grandchild.kind() === "property_identifier"); + if (property?.text() !== importedName) continue; + + const binding = child.children().find((grandchild) => grandchild.kind() === "identifier"); + if (binding) names.push(binding.text()); + } + return names; +} + +function rangeKey(node: SgNode): string { + const range = node.range(); + return `${range.start.index}:${range.end.index}`; +} + +function addBindingScope(scopes: BindingScopeMap, localName: string, binding: SgNode): void { + const scope = declarationScope(binding); + if (!scope) return; + const existing = scopes.get(localName) ?? new Set(); + existing.add(rangeKey(scope)); + scopes.set(localName, existing); +} + +function declarationScope(binding: SgNode): SgNode | null { + let current = binding.parent(); + let variableDeclaration: SgNode | null = null; + while (current) { + if (current.kind() === "variable_declaration") { + variableDeclaration = current; + break; + } + current = current.parent(); + } + + const isVar = variableDeclaration?.children().some((child) => child.kind() === "var") ?? false; + current = binding.parent(); + while (current) { + if ( + isVar && + (current.kind() === "program" || + current.children().some((child) => child.kind() === "formal_parameters")) + ) { + return current; + } + + if ( + !isVar && + ["program", "statement_block", "switch_case", "switch_default"].includes(current.kind()) + ) { + return current; + } + current = current.parent(); + } + return null; +} + +function bindingScopeLocalNames(scopes: BindingScopeMap): Set { + return new Set(scopes.keys()); +} + +function findTailorConfigRequireAuthBindingScopes(root: SgNode): BindingScopeMap { + const scopes: BindingScopeMap = new Map(); + for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { + const binding = firstDeclaratorChild(decl); + if (binding?.kind() !== "object_pattern") continue; + const source = requireCallSource(initializerChild(decl)); + if (!source || !isTailorConfigSource(source)) continue; + for (const localName of objectPatternLocalNames(binding, "auth")) { + addBindingScope(scopes, localName, binding); + } + } + return scopes; +} + +function findTailorConfigRequireNamespaceAuthBindingScopes(root: SgNode): BindingScopeMap { + const scopes: BindingScopeMap = new Map(); + for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { + const binding = firstDeclaratorChild(decl); + if (binding?.kind() !== "identifier") continue; + const source = requireCallSource(initializerChild(decl)); + if (source && isTailorConfigSource(source)) addBindingScope(scopes, binding.text(), binding); + } + return scopes; +} + function runtimeAuthconnectionReference(imports: SgNode[]): string | null { for (const importStmt of imports) { for (const binding of importBindings(importStmt)) { @@ -536,10 +654,20 @@ function directlyDeclaredNames(scope: SgNode, reference: SgNode): Set { return names; } -function isReferenceShadowed(node: SgNode, localName: string): boolean { +function isReferenceShadowed( + node: SgNode, + localName: string, + allowedBindingScopes: BindingScopeMap = new Map(), +): boolean { let current = node.parent(); while (current) { - if (directlyDeclaredNames(current, node).has(localName)) return true; + if (directlyDeclaredNames(current, node).has(localName)) { + if (allowedBindingScopes.get(localName)?.has(rangeKey(current))) { + current = current.parent(); + continue; + } + return true; + } current = current.parent(); } return false; @@ -548,6 +676,7 @@ function isReferenceShadowed(node: SgNode, localName: string): boolean { function authConnectionTokenReferenceFromMember( member: SgNode, authLocalNames: Set, + allowedBindingScopes: BindingScopeMap = new Map(), ): TokenCall | null { const property = member.field("property"); const object = member.field("object"); @@ -561,7 +690,7 @@ function authConnectionTokenReferenceFromMember( return null; } - if (isReferenceShadowed(receiver, receiver.text())) return null; + if (isReferenceShadowed(receiver, receiver.text(), allowedBindingScopes)) return null; const range = object.range(); return { @@ -610,10 +739,18 @@ function unwrapReceiverExpression(node: SgNode | null): SgNode | null { return null; } -function findAuthConnectionTokenReferences(root: SgNode, authLocalNames: Set): TokenCall[] { +function findAuthConnectionTokenReferences( + root: SgNode, + authLocalNames: Set, + allowedBindingScopes: BindingScopeMap = new Map(), +): TokenCall[] { const references: TokenCall[] = []; for (const member of root.findAll({ rule: { kind: "member_expression" } })) { - const reference = authConnectionTokenReferenceFromMember(member, authLocalNames); + const reference = authConnectionTokenReferenceFromMember( + member, + authLocalNames, + allowedBindingScopes, + ); if (reference) references.push(reference); } return references; @@ -622,12 +759,13 @@ function findAuthConnectionTokenReferences(root: SgNode, authLocalNames: Set, + allowedBindingScopes: BindingScopeMap = new Map(), ): TokenCall | null { const property = member.field("property"); const object = member.field("object"); const receiver = namespaceAuthReceiverIdentifier(object, namespaceAuthLocalNames); if (property?.text() !== GET_CONNECTION_TOKEN || !object || !receiver) return null; - if (isReferenceShadowed(receiver, receiver.text())) return null; + if (isReferenceShadowed(receiver, receiver.text(), allowedBindingScopes)) return null; const range = object.range(); return { @@ -659,12 +797,14 @@ function namespaceAuthReceiverIdentifier( function findAuthConnectionTokenNamespaceReferences( root: SgNode, namespaceAuthLocalNames: Set, + allowedBindingScopes: BindingScopeMap = new Map(), ): TokenCall[] { const references: TokenCall[] = []; for (const member of root.findAll({ rule: { kind: "member_expression" } })) { const reference = authConnectionTokenNamespaceReferenceFromMember( member, namespaceAuthLocalNames, + allowedBindingScopes, ); if (reference) references.push(reference); } @@ -678,12 +818,13 @@ function isGetConnectionTokenSubscript(subscript: SgNode): boolean { function authConnectionTokenSubscriptReferenceFromSubscript( subscript: SgNode, authLocalNames: Set, + allowedBindingScopes: BindingScopeMap = new Map(), ): TokenCall | null { const object = subscript.field("object"); const receiver = authReceiverIdentifier(object); if (!isGetConnectionTokenSubscript(subscript) || !object || !receiver) return null; if (!authLocalNames.has(receiver.text())) return null; - if (isReferenceShadowed(receiver, receiver.text())) return null; + if (isReferenceShadowed(receiver, receiver.text(), allowedBindingScopes)) return null; const range = subscript.range(); return { @@ -696,10 +837,15 @@ function authConnectionTokenSubscriptReferenceFromSubscript( function findAuthConnectionTokenSubscriptReferences( root: SgNode, authLocalNames: Set, + allowedBindingScopes: BindingScopeMap = new Map(), ): TokenCall[] { const references: TokenCall[] = []; for (const subscript of root.findAll({ rule: { kind: "subscript_expression" } })) { - const reference = authConnectionTokenSubscriptReferenceFromSubscript(subscript, authLocalNames); + const reference = authConnectionTokenSubscriptReferenceFromSubscript( + subscript, + authLocalNames, + allowedBindingScopes, + ); if (reference) references.push(reference); } return references; @@ -708,11 +854,12 @@ function findAuthConnectionTokenSubscriptReferences( function authConnectionTokenNamespaceSubscriptReferenceFromSubscript( subscript: SgNode, namespaceAuthLocalNames: Set, + allowedBindingScopes: BindingScopeMap = new Map(), ): TokenCall | null { const object = subscript.field("object"); const receiver = namespaceAuthReceiverIdentifier(object, namespaceAuthLocalNames); if (!isGetConnectionTokenSubscript(subscript) || !object || !receiver) return null; - if (isReferenceShadowed(receiver, receiver.text())) return null; + if (isReferenceShadowed(receiver, receiver.text(), allowedBindingScopes)) return null; const range = subscript.range(); return { @@ -725,12 +872,14 @@ function authConnectionTokenNamespaceSubscriptReferenceFromSubscript( function findAuthConnectionTokenNamespaceSubscriptReferences( root: SgNode, namespaceAuthLocalNames: Set, + allowedBindingScopes: BindingScopeMap = new Map(), ): TokenCall[] { const references: TokenCall[] = []; for (const subscript of root.findAll({ rule: { kind: "subscript_expression" } })) { const reference = authConnectionTokenNamespaceSubscriptReferenceFromSubscript( subscript, namespaceAuthLocalNames, + allowedBindingScopes, ); if (reference) references.push(reference); } @@ -759,31 +908,31 @@ function objectPatternHasGetConnectionToken(pattern: SgNode): boolean { function findAuthConnectionTokenDestructures( root: SgNode, authLocalNames: Set, + namespaceAuthLocalNames: Set, + allowedBindingScopes: BindingScopeMap = new Map(), + namespaceAllowedBindingScopes: BindingScopeMap = new Map(), ): TokenCall[] { const references: TokenCall[] = []; for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { - const children = decl.children(); - const equalIndex = children.findIndex((child) => child.kind() === "="); const binding = firstDeclaratorChild(decl); - const initializer = children - .slice(equalIndex + 1) - .find((child) => child.kind() === "identifier"); - if ( - equalIndex === -1 || - binding?.kind() !== "object_pattern" || - !objectPatternHasGetConnectionToken(binding) || - initializer?.kind() !== "identifier" || - !authLocalNames.has(initializer.text()) - ) { + if (binding?.kind() !== "object_pattern" || !objectPatternHasGetConnectionToken(binding)) { continue; } - if (isReferenceShadowed(initializer, initializer.text())) continue; + const initializer = initializerChild(decl); + const authReceiver = authReceiverIdentifier(initializer); + const namespaceReceiver = namespaceAuthReceiverIdentifier(initializer, namespaceAuthLocalNames); + const receiver = + authReceiver && authLocalNames.has(authReceiver.text()) ? authReceiver : namespaceReceiver; + const receiverAllowedScopes = + receiver === authReceiver ? allowedBindingScopes : namespaceAllowedBindingScopes; + if (!receiver) continue; + if (isReferenceShadowed(receiver, receiver.text(), receiverAllowedScopes)) continue; const range = decl.range(); references.push({ - objectNode: initializer, - localName: initializer.text(), + objectNode: initializer ?? receiver, + localName: receiver.text(), range: [range.start.index, range.end.index], }); } @@ -1135,14 +1284,36 @@ export function reviewFindings( const imports = findImportStatements(root); const authBindings = findTailorConfigAuthBindings(imports); - const authLocalNames = new Set(authBindings.map((binding) => binding.localName)); - const namespaceAuthLocalNames = findTailorConfigNamespaceAuthLocalNames(imports); + const requireAuthBindingScopes = findTailorConfigRequireAuthBindingScopes(root); + const requireNamespaceAuthBindingScopes = findTailorConfigRequireNamespaceAuthBindingScopes(root); + const authLocalNames = new Set([ + ...authBindings.map((binding) => binding.localName), + ...bindingScopeLocalNames(requireAuthBindingScopes), + ]); + const namespaceAuthLocalNames = new Set([ + ...findTailorConfigNamespaceAuthLocalNames(imports), + ...bindingScopeLocalNames(requireNamespaceAuthBindingScopes), + ]); const references = [ - ...findAuthConnectionTokenReferences(root, authLocalNames), - ...findAuthConnectionTokenNamespaceReferences(root, namespaceAuthLocalNames), - ...findAuthConnectionTokenSubscriptReferences(root, authLocalNames), - ...findAuthConnectionTokenNamespaceSubscriptReferences(root, namespaceAuthLocalNames), - ...findAuthConnectionTokenDestructures(root, authLocalNames), + ...findAuthConnectionTokenReferences(root, authLocalNames, requireAuthBindingScopes), + ...findAuthConnectionTokenNamespaceReferences( + root, + namespaceAuthLocalNames, + requireNamespaceAuthBindingScopes, + ), + ...findAuthConnectionTokenSubscriptReferences(root, authLocalNames, requireAuthBindingScopes), + ...findAuthConnectionTokenNamespaceSubscriptReferences( + root, + namespaceAuthLocalNames, + requireNamespaceAuthBindingScopes, + ), + ...findAuthConnectionTokenDestructures( + root, + authLocalNames, + namespaceAuthLocalNames, + requireAuthBindingScopes, + requireNamespaceAuthBindingScopes, + ), ].toSorted((a, b) => a.range[0] - b.range[0]); return references.map((reference) => ({ diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 02150303a..d762a8322 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1195,6 +1195,17 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "cjs-require.js"), + [ + 'const { auth } = require("../tailor.config");', + 'const cfg = require("../tailor.config");', + "", + 'exports.token = auth.getConnectionToken("google");', + 'exports.other = cfg.auth.getConnectionToken("github");', + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "namespace-computed.ts"), [ @@ -1206,6 +1217,24 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "wrapped-destructure.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + "export const { getConnectionToken } = (auth);", + "", + ].join("\n"), + ); + await fs.promises.writeFile( + path.join(dir, "namespace-destructure.ts"), + [ + 'import * as cfg from "../tailor.config";', + "", + "export const { getConnectionToken } = cfg.auth;", + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "non-call.ts"), [ @@ -1237,17 +1266,30 @@ describe("runCodemods", () => { codemodId: "v2/auth-connection-token-helper", prompt: codemod.prompt, files: [ + "cjs-require.js", "collision.ts", "computed.ts", "destructure.ts", "import-equals-collision.ts", "namespace-computed.ts", + "namespace-destructure.ts", "namespace-import.ts", "non-call.ts", "type-collision.ts", "wrapped-collision.ts", + "wrapped-destructure.ts", ], findings: [ + expect.objectContaining({ + file: "cjs-require.js", + line: 4, + excerpt: 'exports.token = auth.getConnectionToken("google");', + }), + expect.objectContaining({ + file: "cjs-require.js", + line: 5, + excerpt: 'exports.other = cfg.auth.getConnectionToken("github");', + }), expect.objectContaining({ file: "collision.ts", line: 6, @@ -1277,6 +1319,11 @@ describe("runCodemods", () => { line: 4, excerpt: 'return cfg.auth["getConnectionToken"]("google");', }), + expect.objectContaining({ + file: "namespace-destructure.ts", + line: 3, + excerpt: "export const { getConnectionToken } = cfg.auth;", + }), expect.objectContaining({ file: "namespace-import.ts", line: 4, @@ -1296,6 +1343,11 @@ describe("runCodemods", () => { line: 5, excerpt: 'return (auth as any).getConnectionToken("google");', }), + expect.objectContaining({ + file: "wrapped-destructure.ts", + line: 3, + excerpt: "export const { getConnectionToken } = (auth);", + }), ], }, ]); From 7a1b86aca658f01b6c760205dcdb5a589e5f8508 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 09:25:53 +0900 Subject: [PATCH 389/618] fix: handle auth token import edge cases --- .../scripts/transform.ts | 6 +++++- .../interface-authconnection-collision/input.ts | 7 +++++++ .../type-authconnection-collision/input.ts | 7 +++++++ packages/sdk-codemod/src/runner.test.ts | 17 +++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/interface-authconnection-collision/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-authconnection-collision/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index e32abf2f6..36a0ee3ce 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -164,7 +164,9 @@ function findTailorConfigNamespaceAuthLocalNames(imports: SgNode[]): Set importBindings(importStmt) .filter( (binding) => - binding.namespace && !binding.typeOnly && isTailorConfigSource(binding.source), + (binding.namespace || binding.importedName == null) && + !binding.typeOnly && + isTailorConfigSource(binding.source), ) .map((binding) => binding.localName), ), @@ -357,6 +359,8 @@ function localDeclarationNames(root: SgNode): Set { { kind: "class_declaration" }, { kind: "class" }, { kind: "enum_declaration" }, + { kind: "interface_declaration" }, + { kind: "type_alias_declaration" }, { kind: "internal_module" }, { kind: "import_alias" }, ], diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/interface-authconnection-collision/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/interface-authconnection-collision/input.ts new file mode 100644 index 000000000..69d837439 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/interface-authconnection-collision/input.ts @@ -0,0 +1,7 @@ +import { auth } from "../tailor.config"; + +interface authconnection { + token: string; +} + +export const token = await auth.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-authconnection-collision/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-authconnection-collision/input.ts new file mode 100644 index 000000000..9dd0f1b7e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-authconnection-collision/input.ts @@ -0,0 +1,7 @@ +import { auth } from "../tailor.config"; + +type authconnection = { + token: string; +}; + +export const token = await auth.getConnectionToken("google"); diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index d762a8322..ec3b1693b 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1235,6 +1235,17 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "default-import.ts"), + [ + 'import config from "../tailor.config";', + "", + "export async function run() {", + ' return config.auth.getConnectionToken("google");', + "}", + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "non-call.ts"), [ @@ -1269,6 +1280,7 @@ describe("runCodemods", () => { "cjs-require.js", "collision.ts", "computed.ts", + "default-import.ts", "destructure.ts", "import-equals-collision.ts", "namespace-computed.ts", @@ -1305,6 +1317,11 @@ describe("runCodemods", () => { line: 4, excerpt: 'export const tokenGetter = auth["getConnectionToken"];', }), + expect.objectContaining({ + file: "default-import.ts", + line: 4, + excerpt: 'return config.auth.getConnectionToken("google");', + }), expect.objectContaining({ file: "destructure.ts", excerpt: "export const { getConnectionToken } = auth;", From 293cb06769651fe52854c9165799b9446e9ecee3 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 09:40:49 +0900 Subject: [PATCH 390/618] fix: handle auth token require and using scopes --- .../scripts/transform.ts | 59 ++++++++++++++----- .../tests/await-using-auth-shadow/expected.ts | 8 +++ .../tests/await-using-auth-shadow/input.ts | 8 +++ packages/sdk-codemod/src/runner.test.ts | 15 +++++ 4 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 36a0ee3ce..ec7719cea 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -194,6 +194,15 @@ function requireCallSource(call: SgNode | null): string | null { return stringValue(source); } +function requireAuthMemberSource(member: SgNode | null): string | null { + if (member?.kind() !== "member_expression") return null; + + const property = member.field("property"); + if (property?.text() !== "auth") return null; + + return requireCallSource(unwrapReceiverExpression(member.field("object"))); +} + function objectPatternLocalNames(pattern: SgNode, importedName: string): string[] { const names: string[] = []; for (const child of pattern.children()) { @@ -268,11 +277,19 @@ function findTailorConfigRequireAuthBindingScopes(root: SgNode): BindingScopeMap const scopes: BindingScopeMap = new Map(); for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { const binding = firstDeclaratorChild(decl); - if (binding?.kind() !== "object_pattern") continue; - const source = requireCallSource(initializerChild(decl)); - if (!source || !isTailorConfigSource(source)) continue; - for (const localName of objectPatternLocalNames(binding, "auth")) { - addBindingScope(scopes, localName, binding); + const initializer = initializerChild(decl); + if (binding?.kind() === "object_pattern") { + const source = requireCallSource(initializer); + if (!source || !isTailorConfigSource(source)) continue; + for (const localName of objectPatternLocalNames(binding, "auth")) { + addBindingScope(scopes, localName, binding); + } + continue; + } + + if (binding?.kind() === "identifier") { + const source = requireAuthMemberSource(initializer); + if (source && isTailorConfigSource(source)) addBindingScope(scopes, binding.text(), binding); } } return scopes; @@ -340,6 +357,10 @@ function localDeclarationNames(root: SgNode): Set { if (binding) collectBindingNames(binding, names); } + for (const stmt of root.findAll({ rule: { kind: "expression_statement" } })) { + collectUsingDeclarationNames(stmt, names); + } + for (const param of root.findAll({ rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] }, })) { @@ -518,17 +539,27 @@ function collectDeclarationName(node: SgNode, names: Set): void { } function collectUsingDeclarationNames(scope: SgNode, names: Set): void { - const assignment = scope.children().find((child) => child.kind() === "assignment_expression"); - if (!assignment) return; + const assignments = scope.children().flatMap((child) => { + if (child.kind() === "assignment_expression") return [child]; + if (child.kind() !== "await_expression") return []; + const assignment = child + .children() + .find((grandchild) => grandchild.kind() === "assignment_expression"); + return assignment ? [assignment] : []; + }); - const children = assignment.children(); - const usingIndex = children.findIndex((child) => child.kind() === "using"); - const end = children.findIndex((child, index) => index > usingIndex && child.kind() === "="); - if (usingIndex === -1 || end === -1) return; + for (const assignment of assignments) { + const children = assignment.children(); + const usingIndex = children.findIndex((child) => child.kind() === "using"); + const end = children.findIndex((child, index) => index > usingIndex && child.kind() === "="); + if (usingIndex === -1 || end === -1) continue; - for (const child of children.slice(usingIndex + 1, end)) { - if (["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind())) { - collectBindingNames(child, names); + for (const child of children.slice(usingIndex + 1, end)) { + if ( + ["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind()) + ) { + collectBindingNames(child, names); + } } } } diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/expected.ts new file mode 100644 index 000000000..d933329ed --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/expected.ts @@ -0,0 +1,8 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const token = await authconnection.getConnectionToken("google"); + +{ + await using auth = createClient(); + auth.getConnectionToken("github"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/input.ts new file mode 100644 index 000000000..8f400a9c5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/input.ts @@ -0,0 +1,8 @@ +import { auth } from "../tailor.config"; + +export const token = await auth.getConnectionToken("google"); + +{ + await using auth = createClient(); + auth.getConnectionToken("github"); +} diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index ec3b1693b..b550a3312 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1206,6 +1206,15 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "cjs-member-require.js"), + [ + 'const auth = require("../tailor.config").auth;', + "", + 'exports.token = auth.getConnectionToken("google");', + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "namespace-computed.ts"), [ @@ -1277,6 +1286,7 @@ describe("runCodemods", () => { codemodId: "v2/auth-connection-token-helper", prompt: codemod.prompt, files: [ + "cjs-member-require.js", "cjs-require.js", "collision.ts", "computed.ts", @@ -1292,6 +1302,11 @@ describe("runCodemods", () => { "wrapped-destructure.ts", ], findings: [ + expect.objectContaining({ + file: "cjs-member-require.js", + line: 3, + excerpt: 'exports.token = auth.getConnectionToken("google");', + }), expect.objectContaining({ file: "cjs-require.js", line: 4, From 77b37d61b71188b9ec47306bbd25a9aef44cbfe0 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 09:54:02 +0900 Subject: [PATCH 391/618] docs: cover auth token non-call migration --- packages/sdk-codemod/src/registry.test.ts | 2 ++ packages/sdk-codemod/src/registry.ts | 16 ++++++++++------ packages/sdk/docs/migration/v2.md | 16 ++++++++++------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index f94766c4a..fb80aee1f 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -309,5 +309,7 @@ describe("getApplicableCodemods", () => { expect(codemod?.filePatterns).toContain("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"); expect(pattern).toBeUndefined(); expect(codemod?.prompt).toContain("@tailor-platform/sdk/runtime"); + expect(codemod?.prompt).toContain("non-call"); + expect(codemod?.prompt).toContain("destructuring"); }); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index fe022a68f..81862ce1e 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -486,14 +486,18 @@ export const allCodemods: CodemodPackage[] = [ "is removed. Runtime code should call authconnection.getConnectionToken(...) from", "@tailor-platform/sdk/runtime instead of importing auth from tailor.config.ts.", "", - "For each .getConnectionToken() call where is a", - "defineAuth() result imported from tailor.config.ts:", - "1. Replace it with authconnection.getConnectionToken().", - '2. Add or reuse `import { authconnection } from "@tailor-platform/sdk/runtime"`.', - "3. Remove the auth import from tailor.config.ts only when no other auth reference", + "For each getConnectionToken usage where is a defineAuth() result", + "imported from tailor.config.ts:", + "1. Replace .getConnectionToken() calls with", + " authconnection.getConnectionToken().", + "2. Update non-call references, including .getConnectionToken,", + ' ["getConnectionToken"], and destructuring from , to', + " reference authconnection instead.", + '3. Add or reuse `import { authconnection } from "@tailor-platform/sdk/runtime"`.', + "4. Remove the auth import from tailor.config.ts only when no other auth reference", " remains in the file.", "", - "Leave calls unchanged when the receiver is already the runtime authconnection", + "Leave usages unchanged when the receiver is already the runtime authconnection", "wrapper or global tailor.authconnection.", ].join("\n"), }, diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index dabb75f80..c04f240fa 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -419,14 +419,18 @@ In Tailor SDK v2 the auth.getConnectionToken() helper returned by defineAuth() is removed. Runtime code should call authconnection.getConnectionToken(...) from @tailor-platform/sdk/runtime instead of importing auth from tailor.config.ts. -For each .getConnectionToken() call where is a -defineAuth() result imported from tailor.config.ts: -1. Replace it with authconnection.getConnectionToken(). -2. Add or reuse `import { authconnection } from "@tailor-platform/sdk/runtime"`. -3. Remove the auth import from tailor.config.ts only when no other auth reference +For each getConnectionToken usage where is a defineAuth() result +imported from tailor.config.ts: +1. Replace .getConnectionToken() calls with + authconnection.getConnectionToken(). +2. Update non-call references, including .getConnectionToken, + ["getConnectionToken"], and destructuring from , to + reference authconnection instead. +3. Add or reuse `import { authconnection } from "@tailor-platform/sdk/runtime"`. +4. Remove the auth import from tailor.config.ts only when no other auth reference remains in the file. -Leave calls unchanged when the receiver is already the runtime authconnection +Leave usages unchanged when the receiver is already the runtime authconnection wrapper or global tailor.authconnection. ``` From f928b360df62d07bb72960d95c5508e8f15aaf7b Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 10:12:31 +0900 Subject: [PATCH 392/618] fix: catch auth token destructuring gaps --- .../scripts/transform.ts | 99 +++++++++++++------ .../expected.ts | 5 + .../input.ts | 5 + packages/sdk-codemod/src/runner.test.ts | 31 ++++++ 4 files changed, 111 insertions(+), 29 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index ec7719cea..840630049 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -42,9 +42,10 @@ function quickFilter(source: string): boolean { function sourceLang(filePath: string, source: string): Lang { const lower = filePath.toLowerCase(); - return SOURCE_FILE_EXTENSIONS.has(lower.slice(lower.lastIndexOf("."))) || source.includes(" - grandchild.kind() === "property_identifier" && grandchild.text() === GET_CONNECTION_TOKEN, - ); + return pairPatternKeyName(child) === GET_CONNECTION_TOKEN; }); } +function pairPatternKeyName(pair: SgNode): string | null { + for (const child of pair.children()) { + if (child.kind() === ":") return null; + if (child.kind() === "property_identifier") return child.text(); + if (child.kind() === "computed_property_name") { + return stringValue( + child.children().find((grandchild) => grandchild.kind() === "string") ?? null, + ); + } + } + return null; +} + +function authConnectionTokenDestructureReference( + node: SgNode, + binding: SgNode | null, + initializer: SgNode | null, + authLocalNames: Set, + namespaceAuthLocalNames: Set, + allowedBindingScopes: BindingScopeMap, + namespaceAllowedBindingScopes: BindingScopeMap, +): TokenCall | null { + if (binding?.kind() !== "object_pattern" || !objectPatternHasGetConnectionToken(binding)) { + return null; + } + + const authReceiver = authReceiverIdentifier(initializer); + const namespaceReceiver = namespaceAuthReceiverIdentifier(initializer, namespaceAuthLocalNames); + const receiver = + authReceiver && authLocalNames.has(authReceiver.text()) ? authReceiver : namespaceReceiver; + const receiverAllowedScopes = + receiver === authReceiver ? allowedBindingScopes : namespaceAllowedBindingScopes; + if (!receiver) return null; + if (isReferenceShadowed(receiver, receiver.text(), receiverAllowedScopes)) return null; + + const range = node.range(); + return { + objectNode: initializer ?? receiver, + localName: receiver.text(), + range: [range.start.index, range.end.index], + }; +} + function findAuthConnectionTokenDestructures( root: SgNode, authLocalNames: Set, @@ -949,27 +988,29 @@ function findAuthConnectionTokenDestructures( ): TokenCall[] { const references: TokenCall[] = []; for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { - const binding = firstDeclaratorChild(decl); - if (binding?.kind() !== "object_pattern" || !objectPatternHasGetConnectionToken(binding)) { - continue; - } + const reference = authConnectionTokenDestructureReference( + decl, + firstDeclaratorChild(decl), + initializerChild(decl), + authLocalNames, + namespaceAuthLocalNames, + allowedBindingScopes, + namespaceAllowedBindingScopes, + ); + if (reference) references.push(reference); + } - const initializer = initializerChild(decl); - const authReceiver = authReceiverIdentifier(initializer); - const namespaceReceiver = namespaceAuthReceiverIdentifier(initializer, namespaceAuthLocalNames); - const receiver = - authReceiver && authLocalNames.has(authReceiver.text()) ? authReceiver : namespaceReceiver; - const receiverAllowedScopes = - receiver === authReceiver ? allowedBindingScopes : namespaceAllowedBindingScopes; - if (!receiver) continue; - if (isReferenceShadowed(receiver, receiver.text(), receiverAllowedScopes)) continue; - - const range = decl.range(); - references.push({ - objectNode: initializer ?? receiver, - localName: receiver.text(), - range: [range.start.index, range.end.index], - }); + for (const assignment of root.findAll({ rule: { kind: "assignment_expression" } })) { + const reference = authConnectionTokenDestructureReference( + assignment, + firstDeclaratorChild(assignment), + initializerChild(assignment), + authLocalNames, + namespaceAuthLocalNames, + allowedBindingScopes, + namespaceAllowedBindingScopes, + ); + if (reference) references.push(reference); } return references; } diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/expected.ts new file mode 100644 index 000000000..50d4a4e57 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/expected.ts @@ -0,0 +1,5 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +const html = ""; + +export const token = await authconnection.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/input.ts new file mode 100644 index 000000000..8d103bb9e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/input.ts @@ -0,0 +1,5 @@ +import { auth } from "../tailor.config"; + +const html = ""; + +export const token = await (auth).getConnectionToken("google"); diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index b550a3312..440cb7900 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1051,6 +1051,16 @@ describe("runCodemods", () => { ); const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-auth-token-test-")); tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "assignment-destructure.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + "let getConnectionToken;", + "({ getConnectionToken } = auth);", + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "migrated.ts"), [ @@ -1195,6 +1205,15 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "computed-destructure.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + 'export const { ["getConnectionToken"]: getToken } = auth;', + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "cjs-require.js"), [ @@ -1286,9 +1305,11 @@ describe("runCodemods", () => { codemodId: "v2/auth-connection-token-helper", prompt: codemod.prompt, files: [ + "assignment-destructure.ts", "cjs-member-require.js", "cjs-require.js", "collision.ts", + "computed-destructure.ts", "computed.ts", "default-import.ts", "destructure.ts", @@ -1302,6 +1323,11 @@ describe("runCodemods", () => { "wrapped-destructure.ts", ], findings: [ + expect.objectContaining({ + file: "assignment-destructure.ts", + line: 4, + excerpt: "({ getConnectionToken } = auth);", + }), expect.objectContaining({ file: "cjs-member-require.js", line: 3, @@ -1322,6 +1348,11 @@ describe("runCodemods", () => { line: 6, excerpt: 'return auth.getConnectionToken("google");', }), + expect.objectContaining({ + file: "computed-destructure.ts", + line: 3, + excerpt: 'export const { ["getConnectionToken"]: getToken } = auth;', + }), expect.objectContaining({ file: "computed.ts", line: 3, From c37ffc6e41ee47004b7ba25c0a6834d0fe40442a Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 10:25:04 +0900 Subject: [PATCH 393/618] fix: flag defaulted auth token destructures --- .../scripts/transform.ts | 12 ++++++++++++ packages/sdk-codemod/src/runner.test.ts | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 840630049..d5cf2b33f 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -931,11 +931,23 @@ function objectPatternHasGetConnectionToken(pattern: SgNode): boolean { return true; } + if (child.kind() === "object_assignment_pattern") { + return objectAssignmentPatternKeyName(child) === GET_CONNECTION_TOKEN; + } + if (child.kind() !== "pair_pattern") return false; return pairPatternKeyName(child) === GET_CONNECTION_TOKEN; }); } +function objectAssignmentPatternKeyName(pattern: SgNode): string | null { + for (const child of pattern.children()) { + if (child.kind() === "=") return null; + if (child.kind() === "shorthand_property_identifier_pattern") return child.text(); + } + return null; +} + function pairPatternKeyName(pair: SgNode): string | null { for (const child of pair.children()) { if (child.kind() === ":") return null; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 440cb7900..f07776df7 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1274,6 +1274,15 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "defaulted-destructure.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + "export const { getConnectionToken = fallback } = auth;", + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "non-call.ts"), [ @@ -1312,6 +1321,7 @@ describe("runCodemods", () => { "computed-destructure.ts", "computed.ts", "default-import.ts", + "defaulted-destructure.ts", "destructure.ts", "import-equals-collision.ts", "namespace-computed.ts", @@ -1368,6 +1378,11 @@ describe("runCodemods", () => { line: 4, excerpt: 'return config.auth.getConnectionToken("google");', }), + expect.objectContaining({ + file: "defaulted-destructure.ts", + line: 3, + excerpt: "export const { getConnectionToken = fallback } = auth;", + }), expect.objectContaining({ file: "destructure.ts", excerpt: "export const { getConnectionToken } = auth;", From 779792344cf12154520d36909949c13fb181ba27 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 10:58:05 +0900 Subject: [PATCH 394/618] fix: review nonstandard auth token exports --- .../scripts/transform.ts | 17 +++++++++++++++++ packages/sdk-codemod/src/runner.test.ts | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index d5cf2b33f..339f77fdd 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -159,6 +159,22 @@ function findTailorConfigAuthBindings(imports: SgNode[]): AuthBinding[] { ); } +function findTailorConfigNamedValueLocalNames(imports: SgNode[]): Set { + return new Set( + imports.flatMap((importStmt) => + importBindings(importStmt) + .filter( + (binding) => + binding.spec != null && + binding.importedName != null && + !binding.typeOnly && + isTailorConfigSource(binding.source), + ) + .map((binding) => binding.localName), + ), + ); +} + function findTailorConfigNamespaceAuthLocalNames(imports: SgNode[]): Set { return new Set( imports.flatMap((importStmt) => @@ -1375,6 +1391,7 @@ export function reviewFindings( const requireAuthBindingScopes = findTailorConfigRequireAuthBindingScopes(root); const requireNamespaceAuthBindingScopes = findTailorConfigRequireNamespaceAuthBindingScopes(root); const authLocalNames = new Set([ + ...findTailorConfigNamedValueLocalNames(imports), ...authBindings.map((binding) => binding.localName), ...bindingScopeLocalNames(requireAuthBindingScopes), ]); diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index f07776df7..0d1a16a81 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1214,6 +1214,17 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "custom-auth.ts"), + [ + 'import { myAuth } from "../tailor.config";', + "", + "export async function run() {", + ' return myAuth.getConnectionToken("google");', + "}", + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "cjs-require.js"), [ @@ -1320,6 +1331,7 @@ describe("runCodemods", () => { "collision.ts", "computed-destructure.ts", "computed.ts", + "custom-auth.ts", "default-import.ts", "defaulted-destructure.ts", "destructure.ts", @@ -1373,6 +1385,11 @@ describe("runCodemods", () => { line: 4, excerpt: 'export const tokenGetter = auth["getConnectionToken"];', }), + expect.objectContaining({ + file: "custom-auth.ts", + line: 4, + excerpt: 'return myAuth.getConnectionToken("google");', + }), expect.objectContaining({ file: "default-import.ts", line: 4, From 7bdbce687d0cac8f1e9cad19b1ba209a11c21738 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 11:21:08 +0900 Subject: [PATCH 395/618] fix: track derived auth token config aliases --- .../scripts/transform.ts | 114 +++++++++++++++--- .../expected.ts | 5 + .../input.ts | 5 + packages/sdk-codemod/src/runner.test.ts | 47 ++++++++ 4 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 339f77fdd..074b72af0 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -176,18 +176,24 @@ function findTailorConfigNamedValueLocalNames(imports: SgNode[]): Set { } function findTailorConfigNamespaceAuthLocalNames(imports: SgNode[]): Set { - return new Set( - imports.flatMap((importStmt) => - importBindings(importStmt) - .filter( - (binding) => - (binding.namespace || binding.importedName == null) && - !binding.typeOnly && - isTailorConfigSource(binding.source), - ) - .map((binding) => binding.localName), - ), + const localNames = imports.flatMap((importStmt) => + importBindings(importStmt) + .filter( + (binding) => + (binding.namespace || binding.importedName == null) && + !binding.typeOnly && + isTailorConfigSource(binding.source), + ) + .map((binding) => binding.localName), ); + + for (const importStmt of imports) { + const localName = importEqualsLocalName(importStmt); + const source = importSource(importStmt); + if (localName && source && isTailorConfigSource(source)) localNames.push(localName); + } + + return new Set(localNames); } function initializerChild(decl: SgNode): SgNode | null { @@ -290,6 +296,20 @@ function bindingScopeLocalNames(scopes: BindingScopeMap): Set { return new Set(scopes.keys()); } +function mergeBindingScopeMaps(...maps: BindingScopeMap[]): BindingScopeMap { + const merged: BindingScopeMap = new Map(); + for (const map of maps) { + for (const [localName, scopeKeys] of map.entries()) { + const existing = merged.get(localName) ?? new Set(); + for (const scopeKey of scopeKeys) { + existing.add(scopeKey); + } + merged.set(localName, existing); + } + } + return merged; +} + function findTailorConfigRequireAuthBindingScopes(root: SgNode): BindingScopeMap { const scopes: BindingScopeMap = new Map(); for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { @@ -323,6 +343,45 @@ function findTailorConfigRequireNamespaceAuthBindingScopes(root: SgNode): Bindin return scopes; } +function findTailorConfigNamespaceDerivedAuthBindingScopes( + root: SgNode, + namespaceAuthLocalNames: Set, + namespaceAllowedBindingScopes: BindingScopeMap, +): BindingScopeMap { + const scopes: BindingScopeMap = new Map(); + for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { + const binding = firstDeclaratorChild(decl); + const initializer = initializerChild(decl); + + const directReceiver = authReceiverIdentifier(initializer); + if ( + binding?.kind() === "object_pattern" && + directReceiver && + namespaceAuthLocalNames.has(directReceiver.text()) && + !isReferenceShadowed(directReceiver, directReceiver.text(), namespaceAllowedBindingScopes) + ) { + for (const localName of objectPatternLocalNames(binding, "auth")) { + addBindingScope(scopes, localName, binding); + } + continue; + } + + const namespaceReceiver = namespaceAuthReceiverIdentifier(initializer, namespaceAuthLocalNames); + if ( + binding?.kind() === "identifier" && + namespaceReceiver && + !isReferenceShadowed( + namespaceReceiver, + namespaceReceiver.text(), + namespaceAllowedBindingScopes, + ) + ) { + addBindingScope(scopes, binding.text(), binding); + } + } + return scopes; +} + function runtimeAuthconnectionReference(imports: SgNode[]): string | null { for (const importStmt of imports) { for (const binding of importBindings(importStmt)) { @@ -1116,6 +1175,14 @@ function skipBackwardImportTrivia(source: string, index: number): number { while (pos > 0) { while (pos > 0 && /\s/.test(source[pos - 1]!)) pos--; + const lineStart = source.lastIndexOf("\n", pos - 1) + 1; + const line = source.slice(lineStart, pos); + const lineCommentIndex = line.lastIndexOf("//"); + if (lineCommentIndex !== -1) { + pos = lineStart + lineCommentIndex; + continue; + } + if (source.slice(pos - 2, pos) === "*/") { const start = source.lastIndexOf("/*", pos - 2); if (start === -1) return pos; @@ -1390,23 +1457,32 @@ export function reviewFindings( const authBindings = findTailorConfigAuthBindings(imports); const requireAuthBindingScopes = findTailorConfigRequireAuthBindingScopes(root); const requireNamespaceAuthBindingScopes = findTailorConfigRequireNamespaceAuthBindingScopes(root); - const authLocalNames = new Set([ - ...findTailorConfigNamedValueLocalNames(imports), - ...authBindings.map((binding) => binding.localName), - ...bindingScopeLocalNames(requireAuthBindingScopes), - ]); const namespaceAuthLocalNames = new Set([ ...findTailorConfigNamespaceAuthLocalNames(imports), ...bindingScopeLocalNames(requireNamespaceAuthBindingScopes), ]); + const derivedAuthBindingScopes = findTailorConfigNamespaceDerivedAuthBindingScopes( + root, + namespaceAuthLocalNames, + requireNamespaceAuthBindingScopes, + ); + const authAllowedBindingScopes = mergeBindingScopeMaps( + requireAuthBindingScopes, + derivedAuthBindingScopes, + ); + const authLocalNames = new Set([ + ...findTailorConfigNamedValueLocalNames(imports), + ...authBindings.map((binding) => binding.localName), + ...bindingScopeLocalNames(authAllowedBindingScopes), + ]); const references = [ - ...findAuthConnectionTokenReferences(root, authLocalNames, requireAuthBindingScopes), + ...findAuthConnectionTokenReferences(root, authLocalNames, authAllowedBindingScopes), ...findAuthConnectionTokenNamespaceReferences( root, namespaceAuthLocalNames, requireNamespaceAuthBindingScopes, ), - ...findAuthConnectionTokenSubscriptReferences(root, authLocalNames, requireAuthBindingScopes), + ...findAuthConnectionTokenSubscriptReferences(root, authLocalNames, authAllowedBindingScopes), ...findAuthConnectionTokenNamespaceSubscriptReferences( root, namespaceAuthLocalNames, @@ -1416,7 +1492,7 @@ export function reviewFindings( root, authLocalNames, namespaceAuthLocalNames, - requireAuthBindingScopes, + authAllowedBindingScopes, requireNamespaceAuthBindingScopes, ), ].toSorted((a, b) => a.range[0] - b.range[0]); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/expected.ts new file mode 100644 index 000000000..5ae33b064 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/expected.ts @@ -0,0 +1,5 @@ +import config from "../tailor.config"; +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const configName = config.name; +export const token = await authconnection.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/input.ts new file mode 100644 index 000000000..e0a512e01 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/input.ts @@ -0,0 +1,5 @@ +import config, // keep config import +{ auth } from "../tailor.config"; + +export const configName = config.name; +export const token = await auth.getConnectionToken("google"); diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 0d1a16a81..ac21005fb 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1172,6 +1172,15 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "import-equals-config.ts"), + [ + 'import cfg = require("../tailor.config");', + "", + 'export const token = cfg.auth.getConnectionToken("google");', + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "wrapped-collision.ts"), [ @@ -1184,6 +1193,16 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "namespace-alias.ts"), + [ + 'import * as cfg from "../tailor.config";', + "", + "const { auth } = cfg;", + 'export const token = auth.getConnectionToken("google");', + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "namespace-import.ts"), [ @@ -1195,6 +1214,16 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "namespace-member-alias.ts"), + [ + 'import config from "../tailor.config";', + "", + "const myAuth = config.auth;", + 'export const token = myAuth.getConnectionToken("google");', + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "computed.ts"), [ @@ -1336,9 +1365,12 @@ describe("runCodemods", () => { "defaulted-destructure.ts", "destructure.ts", "import-equals-collision.ts", + "import-equals-config.ts", + "namespace-alias.ts", "namespace-computed.ts", "namespace-destructure.ts", "namespace-import.ts", + "namespace-member-alias.ts", "non-call.ts", "type-collision.ts", "wrapped-collision.ts", @@ -1409,6 +1441,16 @@ describe("runCodemods", () => { line: 5, excerpt: 'return auth.getConnectionToken("google");', }), + expect.objectContaining({ + file: "import-equals-config.ts", + line: 3, + excerpt: 'export const token = cfg.auth.getConnectionToken("google");', + }), + expect.objectContaining({ + file: "namespace-alias.ts", + line: 4, + excerpt: 'export const token = auth.getConnectionToken("google");', + }), expect.objectContaining({ file: "namespace-computed.ts", line: 4, @@ -1424,6 +1466,11 @@ describe("runCodemods", () => { line: 4, excerpt: 'return cfg.auth.getConnectionToken("google");', }), + expect.objectContaining({ + file: "namespace-member-alias.ts", + line: 4, + excerpt: 'export const token = myAuth.getConnectionToken("google");', + }), expect.objectContaining({ file: "non-call.ts", excerpt: "export const tokenGetter = auth.getConnectionToken;", From afbcb6b8f8737db6576e03dca274837f3ff3474c Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 11:40:04 +0900 Subject: [PATCH 396/618] fix: parse js self-closing jsx in auth token codemod --- .../v2/auth-connection-token-helper/scripts/transform.ts | 7 ++++++- .../tests/js-self-closing-jsx/expected.js | 3 +++ .../tests/js-self-closing-jsx/input.js | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/expected.js create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/input.js diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 074b72af0..92b19757a 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -6,6 +6,7 @@ const RUNTIME_MODULE = "@tailor-platform/sdk/runtime"; const AUTHCONNECTION = "authconnection"; const GET_CONNECTION_TOKEN = "getConnectionToken"; const SOURCE_FILE_EXTENSIONS = new Set([".tsx", ".jsx"]); +const JS_FILE_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]); const REFERENCE_KINDS = new Set([ "identifier", "shorthand_property_identifier", @@ -44,10 +45,14 @@ function sourceLang(filePath: string, source: string): Lang { const lower = filePath.toLowerCase(); const extension = lower.slice(lower.lastIndexOf(".")); if (SOURCE_FILE_EXTENSIONS.has(extension)) return Lang.Tsx; - if ([".js", ".mjs", ".cjs"].includes(extension) && source.includes("|<[A-Za-z][\w.$:-]*(?:\s[^<>]*)?\/>/.test(source); +} + function stringValue(node: SgNode | null): string | null { return node?.text().replace(/^['"]|['"]$/g, "") ?? null; } diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/expected.js b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/expected.js new file mode 100644 index 000000000..65feff683 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/expected.js @@ -0,0 +1,3 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const view = ; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/input.js b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/input.js new file mode 100644 index 000000000..020e85e46 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/input.js @@ -0,0 +1,3 @@ +import { auth } from "../tailor.config"; + +export const view = ; From 8f80043596227c1d622613a4f50699c7bbc929a5 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 11:52:22 +0900 Subject: [PATCH 397/618] fix: catch auth token jsx fragments and parameter destructures --- .../scripts/transform.ts | 17 ++++++++++++++++- .../tests/js-fragment-jsx/expected.js | 3 +++ .../tests/js-fragment-jsx/input.js | 3 +++ packages/sdk-codemod/src/runner.test.ts | 17 +++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/expected.js create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/input.js diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 92b19757a..9c3fcde4a 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -50,7 +50,7 @@ function sourceLang(filePath: string, source: string): Lang { } function hasJsxSyntax(source: string): boolean { - return /<\/[A-Za-z][\w.$:-]*\s*>|<[A-Za-z][\w.$:-]*(?:\s[^<>]*)?\/>/.test(source); + return /<>|<\/>|<\/[A-Za-z][\w.$:-]*\s*>|<[A-Za-z][\w.$:-]*(?:\s[^<>]*)?\/>/.test(source); } function stringValue(node: SgNode | null): string | null { @@ -1104,6 +1104,21 @@ function findAuthConnectionTokenDestructures( ); if (reference) references.push(reference); } + + for (const parameter of root.findAll({ + rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] }, + })) { + const reference = authConnectionTokenDestructureReference( + parameter, + firstDeclaratorChild(parameter), + initializerChild(parameter), + authLocalNames, + namespaceAuthLocalNames, + allowedBindingScopes, + namespaceAllowedBindingScopes, + ); + if (reference) references.push(reference); + } return references; } diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/expected.js b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/expected.js new file mode 100644 index 000000000..33ef404dd --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/expected.js @@ -0,0 +1,3 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export const view = <>{await authconnection.getConnectionToken("google")}; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/input.js b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/input.js new file mode 100644 index 000000000..a7ebf19a6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/input.js @@ -0,0 +1,3 @@ +import { auth } from "../tailor.config"; + +export const view = <>{await auth.getConnectionToken("google")}; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index ac21005fb..aac94b112 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1343,6 +1343,17 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "parameter-destructure.ts"), + [ + 'import { auth } from "../tailor.config";', + "", + "export function run({ getConnectionToken } = auth) {", + ' return getConnectionToken("google");', + "}", + "", + ].join("\n"), + ); using _stderrSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true); @@ -1372,6 +1383,7 @@ describe("runCodemods", () => { "namespace-import.ts", "namespace-member-alias.ts", "non-call.ts", + "parameter-destructure.ts", "type-collision.ts", "wrapped-collision.ts", "wrapped-destructure.ts", @@ -1475,6 +1487,11 @@ describe("runCodemods", () => { file: "non-call.ts", excerpt: "export const tokenGetter = auth.getConnectionToken;", }), + expect.objectContaining({ + file: "parameter-destructure.ts", + line: 3, + excerpt: "export function run({ getConnectionToken } = auth) {", + }), expect.objectContaining({ file: "type-collision.ts", line: 6, From 7bd70faddc2693fd6b0dc3bc5744042c363256ff Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 12:14:43 +0900 Subject: [PATCH 398/618] refactor: simplify auth token codemod scope --- .../scripts/transform.ts | 1442 ++--------------- .../aliased-auth-and-runtime/expected.ts | 7 - .../tests/aliased-auth-and-runtime/input.ts | 7 - .../arrow-parameter-type-auth/expected.ts | 4 - .../tests/arrow-parameter-type-auth/input.ts | 3 - .../tests/arrow-var-auth-shadow/expected.ts | 8 - .../tests/arrow-var-auth-shadow/input.ts | 8 - .../tests/await-using-auth-shadow/expected.ts | 8 - .../tests/await-using-auth-shadow/input.ts | 8 - .../tests/class-auth-shadow/input.ts | 7 - .../class-authconnection-collision/input.ts | 5 - .../commented-auth-import-spec/expected.ts | 5 - .../tests/commented-auth-import-spec/input.ts | 4 - .../expected.ts | 5 - .../input.ts | 4 - .../expected.ts | 5 - .../input.ts | 5 - .../default-initializer-reference/expected.ts | 8 - .../default-initializer-reference/input.ts | 7 - .../default-parameter-auth-shadow/expected.ts | 7 - .../default-parameter-auth-shadow/input.ts | 7 - .../default-parameter-var-auth/expected.ts | 6 - .../tests/default-parameter-var-auth/input.ts | 6 - .../expected.ts | 5 - .../input.ts | 4 - .../expected.ts | 6 - .../input.ts | 5 - .../tests/duplicate-auth-aliases/expected.ts | 4 - .../tests/duplicate-auth-aliases/input.ts | 4 - .../tests/existing-runtime-shadowed/input.ts | 6 - .../tests/for-auth-shadow/expected.ts | 9 - .../tests/for-auth-shadow/input.ts | 9 - .../import-equals-runtime-collision/input.ts | 6 - .../input.ts | 7 - .../tests/js-fragment-jsx/expected.js | 3 - .../tests/js-fragment-jsx/input.js | 3 - .../tests/js-self-closing-jsx/expected.js | 3 - .../tests/js-self-closing-jsx/input.js | 3 - .../tests/method-auth-shadow/expected.ts | 9 - .../tests/method-auth-shadow/input.ts | 9 - .../tests/mixed-auth-shadow/expected.ts | 7 - .../tests/mixed-auth-shadow/input.ts | 7 - .../input.ts | 5 - .../named-expression-auth-shadow/expected.ts | 13 - .../named-expression-auth-shadow/input.ts | 13 - .../tests/namespace-auth-shadow/input.ts | 11 - .../namespace-export-auth-shadow/expected.ts | 11 - .../namespace-export-auth-shadow/input.ts | 11 - .../tests/non-call-reference/expected.ts | 5 - .../tests/non-call-reference/input.ts | 4 - .../expected.ts | 5 - .../input.ts | 5 - .../preserve-template-newlines/expected.ts | 10 - .../tests/preserve-template-newlines/input.ts | 10 - .../tests/runtime-authconnection/input.ts | 5 - .../tests/switch-case-auth-shadow/expected.ts | 9 - .../tests/switch-case-auth-shadow/input.ts | 9 - .../type-authconnection-collision/input.ts | 7 - .../type-only-runtime-collision/input.ts | 7 - .../tests/unrelated-receiver/input.ts | 5 - .../tests/using-auth-shadow/expected.ts | 8 - .../tests/using-auth-shadow/input.ts | 8 - .../tests/var-auth-shadow/expected.ts | 8 - .../tests/var-auth-shadow/input.ts | 8 - .../tests/wrapped-auth-receiver/expected.ts | 7 - .../tests/wrapped-auth-receiver/input.ts | 7 - packages/sdk-codemod/src/runner.test.ts | 366 +---- 67 files changed, 192 insertions(+), 2050 deletions(-) delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-auth-shadow/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-authconnection-collision/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/existing-runtime-shadowed/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/import-equals-runtime-collision/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/interface-authconnection-collision/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/expected.js delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/input.js delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/expected.js delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/input.js delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-class-authconnection-collision/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-auth-shadow/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/runtime-authconnection/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-authconnection-collision/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-collision/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/unrelated-receiver/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/input.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 9c3fcde4a..9040e4094 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -5,25 +5,10 @@ import type { Edit, SgNode } from "@ast-grep/napi"; const RUNTIME_MODULE = "@tailor-platform/sdk/runtime"; const AUTHCONNECTION = "authconnection"; const GET_CONNECTION_TOKEN = "getConnectionToken"; -const SOURCE_FILE_EXTENSIONS = new Set([".tsx", ".jsx"]); +const JSX_FILE_EXTENSIONS = new Set([".tsx", ".jsx"]); const JS_FILE_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]); -const REFERENCE_KINDS = new Set([ - "identifier", - "shorthand_property_identifier", - "shorthand_property_identifier_pattern", -]); -interface ImportBinding { - importStmt: SgNode; - localName: string; - importedName?: string; - source: string; - spec?: SgNode; - namespace: boolean; - typeOnly: boolean; -} - -interface AuthBinding { +interface AuthImport { importStmt: SgNode; localName: string; spec: SgNode; @@ -35,8 +20,6 @@ interface TokenCall { range: [number, number]; } -type BindingScopeMap = Map>; - function quickFilter(source: string): boolean { return source.includes(GET_CONNECTION_TOKEN) && source.includes("tailor.config"); } @@ -44,101 +27,52 @@ function quickFilter(source: string): boolean { function sourceLang(filePath: string, source: string): Lang { const lower = filePath.toLowerCase(); const extension = lower.slice(lower.lastIndexOf(".")); - if (SOURCE_FILE_EXTENSIONS.has(extension)) return Lang.Tsx; - if (JS_FILE_EXTENSIONS.has(extension) && hasJsxSyntax(source)) return Lang.Tsx; + if (JSX_FILE_EXTENSIONS.has(extension)) return Lang.Tsx; + if (JS_FILE_EXTENSIONS.has(extension) && /<>|<\/>|<[A-Za-z][\w.$:-]/.test(source)) { + return Lang.Tsx; + } return Lang.TypeScript; } -function hasJsxSyntax(source: string): boolean { - return /<>|<\/>|<\/[A-Za-z][\w.$:-]*\s*>|<[A-Za-z][\w.$:-]*(?:\s[^<>]*)?\/>/.test(source); +function parseRoot(source: string, filePath: string): SgNode | null { + if (!quickFilter(source)) return null; + try { + return parse(sourceLang(filePath, source), source).root(); + } catch { + return null; + } } function stringValue(node: SgNode | null): string | null { return node?.text().replace(/^['"]|['"]$/g, "") ?? null; } -function isTypeOnlyImport(stmt: SgNode): boolean { - return stmt.children().some((child) => child.kind() === "type"); +function importSource(importStmt: SgNode): string | null { + return stringValue(importStmt.find({ rule: { kind: "string" } }) ?? null); } -function importSource(stmt: SgNode): string | null { - return stringValue(stmt.find({ rule: { kind: "string" } }) ?? null); +function isTailorConfigSource(source: string): boolean { + return /(^|\/)tailor\.config(?:\.(?:ts|tsx|js|jsx|mts|cts|mjs|cjs))?$/.test(source); } -function namedImportsNode(importStmt: SgNode): SgNode | null { - return importStmt.find({ rule: { kind: "named_imports" } }) ?? null; +function isTypeOnlyImport(importStmt: SgNode): boolean { + return importStmt.children().some((child) => child.kind() === "type"); } -function isTailorConfigSource(source: string): boolean { - return /(^|\/)tailor\.config(?:\.(?:ts|tsx|js|jsx|mts|cts|mjs|cjs))?$/.test(source); +function namedImportsNode(importStmt: SgNode): SgNode | null { + return importStmt.find({ rule: { kind: "named_imports" } }) ?? null; } -function importSpecNames( - spec: SgNode, -): { importedName: string; localName: string; typeOnly: boolean } | null { +function importSpecNames(spec: SgNode): { importedName: string; localName: string } | null { + if (spec.children().some((child) => child.kind() === "type")) return null; const ids = spec.children().filter((child) => child.kind() === "identifier"); if (ids.length === 0) return null; return { importedName: ids[0]!.text(), localName: ids[1]?.text() ?? ids[0]!.text(), - typeOnly: spec.children().some((child) => child.kind() === "type"), }; } -function importBindings(importStmt: SgNode): ImportBinding[] { - const source = importSource(importStmt); - if (!source) return []; - - const stmtTypeOnly = isTypeOnlyImport(importStmt); - const clause = importStmt.children().find((child) => child.kind() === "import_clause"); - if (!clause) return []; - - const bindings: ImportBinding[] = []; - for (const child of clause.children()) { - if (child.kind() === "identifier") { - bindings.push({ - importStmt, - localName: child.text(), - source, - namespace: false, - typeOnly: stmtTypeOnly, - }); - continue; - } - - if (child.kind() === "namespace_import") { - const local = child.children().find((c) => c.kind() === "identifier"); - if (local) { - bindings.push({ - importStmt, - localName: local.text(), - source, - namespace: true, - typeOnly: stmtTypeOnly, - }); - } - continue; - } - - if (child.kind() !== "named_imports") continue; - for (const spec of child.findAll({ rule: { kind: "import_specifier" } })) { - const names = importSpecNames(spec); - if (!names) continue; - bindings.push({ - importStmt, - spec, - source, - importedName: names.importedName, - localName: names.localName, - namespace: false, - typeOnly: stmtTypeOnly || names.typeOnly, - }); - } - } - - return bindings; -} - function findImportStatements(root: SgNode): SgNode[] { return root .findAll({ rule: { kind: "import_statement" } }) @@ -146,254 +80,57 @@ function findImportStatements(root: SgNode): SgNode[] { .toSorted((a, b) => a.range().start.index - b.range().start.index); } -function findTailorConfigAuthBindings(imports: SgNode[]): AuthBinding[] { - return imports.flatMap((importStmt) => - importBindings(importStmt) - .filter( - (binding): binding is ImportBinding & { spec: SgNode; importedName: string } => - binding.spec != null && - binding.importedName === "auth" && - !binding.typeOnly && - isTailorConfigSource(binding.source), - ) - .map((binding) => ({ - importStmt: binding.importStmt, - localName: binding.localName, - spec: binding.spec, - })), - ); -} - -function findTailorConfigNamedValueLocalNames(imports: SgNode[]): Set { - return new Set( - imports.flatMap((importStmt) => - importBindings(importStmt) - .filter( - (binding) => - binding.spec != null && - binding.importedName != null && - !binding.typeOnly && - isTailorConfigSource(binding.source), - ) - .map((binding) => binding.localName), - ), - ); -} - -function findTailorConfigNamespaceAuthLocalNames(imports: SgNode[]): Set { - const localNames = imports.flatMap((importStmt) => - importBindings(importStmt) - .filter( - (binding) => - (binding.namespace || binding.importedName == null) && - !binding.typeOnly && - isTailorConfigSource(binding.source), - ) - .map((binding) => binding.localName), - ); - +function findAuthImports(imports: SgNode[]): AuthImport[] { + const authImports: AuthImport[] = []; for (const importStmt of imports) { - const localName = importEqualsLocalName(importStmt); const source = importSource(importStmt); - if (localName && source && isTailorConfigSource(source)) localNames.push(localName); - } - - return new Set(localNames); -} - -function initializerChild(decl: SgNode): SgNode | null { - const children = decl.children(); - const equalIndex = children.findIndex((child) => child.kind() === "="); - if (equalIndex === -1) return null; - return ( - children.slice(equalIndex + 1).find((child) => child.kind() !== "," && child.kind() !== ";") ?? - null - ); -} - -function requireCallSource(call: SgNode | null): string | null { - if (call?.kind() !== "call_expression") return null; - - const callee = call.field("function"); - if (callee?.kind() !== "identifier" || callee.text() !== "require") return null; - - const args = call.children().find((child) => child.kind() === "arguments"); - const source = args?.children().find((child) => child.kind() === "string") ?? null; - return stringValue(source); -} - -function requireAuthMemberSource(member: SgNode | null): string | null { - if (member?.kind() !== "member_expression") return null; + if (!source || !isTailorConfigSource(source) || isTypeOnlyImport(importStmt)) continue; - const property = member.field("property"); - if (property?.text() !== "auth") return null; - - return requireCallSource(unwrapReceiverExpression(member.field("object"))); -} - -function objectPatternLocalNames(pattern: SgNode, importedName: string): string[] { - const names: string[] = []; - for (const child of pattern.children()) { - if (child.kind() === "shorthand_property_identifier_pattern" && child.text() === importedName) { - names.push(child.text()); - continue; - } - - if (child.kind() !== "pair_pattern") continue; - const property = child - .children() - .find((grandchild) => grandchild.kind() === "property_identifier"); - if (property?.text() !== importedName) continue; - - const binding = child.children().find((grandchild) => grandchild.kind() === "identifier"); - if (binding) names.push(binding.text()); - } - return names; -} - -function rangeKey(node: SgNode): string { - const range = node.range(); - return `${range.start.index}:${range.end.index}`; -} - -function addBindingScope(scopes: BindingScopeMap, localName: string, binding: SgNode): void { - const scope = declarationScope(binding); - if (!scope) return; - const existing = scopes.get(localName) ?? new Set(); - existing.add(rangeKey(scope)); - scopes.set(localName, existing); -} - -function declarationScope(binding: SgNode): SgNode | null { - let current = binding.parent(); - let variableDeclaration: SgNode | null = null; - while (current) { - if (current.kind() === "variable_declaration") { - variableDeclaration = current; - break; - } - current = current.parent(); - } - - const isVar = variableDeclaration?.children().some((child) => child.kind() === "var") ?? false; - current = binding.parent(); - while (current) { - if ( - isVar && - (current.kind() === "program" || - current.children().some((child) => child.kind() === "formal_parameters")) - ) { - return current; - } - - if ( - !isVar && - ["program", "statement_block", "switch_case", "switch_default"].includes(current.kind()) - ) { - return current; + for (const spec of importStmt.findAll({ rule: { kind: "import_specifier" } })) { + const names = importSpecNames(spec); + if (names?.importedName !== "auth") continue; + authImports.push({ importStmt, localName: names.localName, spec }); } - current = current.parent(); } - return null; + return authImports; } -function bindingScopeLocalNames(scopes: BindingScopeMap): Set { - return new Set(scopes.keys()); -} - -function mergeBindingScopeMaps(...maps: BindingScopeMap[]): BindingScopeMap { - const merged: BindingScopeMap = new Map(); - for (const map of maps) { - for (const [localName, scopeKeys] of map.entries()) { - const existing = merged.get(localName) ?? new Set(); - for (const scopeKey of scopeKeys) { - existing.add(scopeKey); - } - merged.set(localName, existing); - } - } - return merged; -} +function importLocalNames(importStmt: SgNode): Set { + const names = new Set(); + const clause = importStmt.children().find((child) => child.kind() === "import_clause"); + if (!clause) return names; -function findTailorConfigRequireAuthBindingScopes(root: SgNode): BindingScopeMap { - const scopes: BindingScopeMap = new Map(); - for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { - const binding = firstDeclaratorChild(decl); - const initializer = initializerChild(decl); - if (binding?.kind() === "object_pattern") { - const source = requireCallSource(initializer); - if (!source || !isTailorConfigSource(source)) continue; - for (const localName of objectPatternLocalNames(binding, "auth")) { - addBindingScope(scopes, localName, binding); - } + for (const child of clause.children()) { + if (child.kind() === "identifier") { + names.add(child.text()); continue; } - - if (binding?.kind() === "identifier") { - const source = requireAuthMemberSource(initializer); - if (source && isTailorConfigSource(source)) addBindingScope(scopes, binding.text(), binding); - } - } - return scopes; -} - -function findTailorConfigRequireNamespaceAuthBindingScopes(root: SgNode): BindingScopeMap { - const scopes: BindingScopeMap = new Map(); - for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { - const binding = firstDeclaratorChild(decl); - if (binding?.kind() !== "identifier") continue; - const source = requireCallSource(initializerChild(decl)); - if (source && isTailorConfigSource(source)) addBindingScope(scopes, binding.text(), binding); - } - return scopes; -} - -function findTailorConfigNamespaceDerivedAuthBindingScopes( - root: SgNode, - namespaceAuthLocalNames: Set, - namespaceAllowedBindingScopes: BindingScopeMap, -): BindingScopeMap { - const scopes: BindingScopeMap = new Map(); - for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { - const binding = firstDeclaratorChild(decl); - const initializer = initializerChild(decl); - - const directReceiver = authReceiverIdentifier(initializer); - if ( - binding?.kind() === "object_pattern" && - directReceiver && - namespaceAuthLocalNames.has(directReceiver.text()) && - !isReferenceShadowed(directReceiver, directReceiver.text(), namespaceAllowedBindingScopes) - ) { - for (const localName of objectPatternLocalNames(binding, "auth")) { - addBindingScope(scopes, localName, binding); - } + if (child.kind() === "namespace_import") { + const local = child.children().find((grandchild) => grandchild.kind() === "identifier"); + if (local) names.add(local.text()); continue; } - - const namespaceReceiver = namespaceAuthReceiverIdentifier(initializer, namespaceAuthLocalNames); - if ( - binding?.kind() === "identifier" && - namespaceReceiver && - !isReferenceShadowed( - namespaceReceiver, - namespaceReceiver.text(), - namespaceAllowedBindingScopes, - ) - ) { - addBindingScope(scopes, binding.text(), binding); + if (child.kind() !== "named_imports") continue; + for (const spec of child.findAll({ rule: { kind: "import_specifier" } })) { + const specNames = importSpecNames(spec); + if (specNames) names.add(specNames.localName); } } - return scopes; + return names; } function runtimeAuthconnectionReference(imports: SgNode[]): string | null { for (const importStmt of imports) { - for (const binding of importBindings(importStmt)) { - if (binding.source !== RUNTIME_MODULE || binding.typeOnly) continue; - if (binding.importedName === AUTHCONNECTION) return binding.localName; - if (binding.namespace) return `${binding.localName}.${AUTHCONNECTION}`; + if (importSource(importStmt) !== RUNTIME_MODULE || isTypeOnlyImport(importStmt)) continue; + for (const spec of importStmt.findAll({ rule: { kind: "import_specifier" } })) { + const names = importSpecNames(spec); + if (names?.importedName === AUTHCONNECTION) return names.localName; } + + const clause = importStmt.children().find((child) => child.kind() === "import_clause"); + const namespace = clause?.children().find((child) => child.kind() === "namespace_import"); + const local = namespace?.children().find((child) => child.kind() === "identifier"); + if (local) return `${local.text()}.${AUTHCONNECTION}`; } return null; } @@ -401,28 +138,29 @@ function runtimeAuthconnectionReference(imports: SgNode[]): string | null { function runtimeNamedValueImport(imports: SgNode[]): SgNode | null { return ( imports.find( - (stmt) => - importSource(stmt) === RUNTIME_MODULE && !isTypeOnlyImport(stmt) && namedImportsNode(stmt), + (importStmt) => + importSource(importStmt) === RUNTIME_MODULE && + !isTypeOnlyImport(importStmt) && + namedImportsNode(importStmt), ) ?? null ); } -function collectBindingNames(node: SgNode, out: Set): void { +function collectBindingNames(node: SgNode, names: Set): void { const kind = node.kind(); if ( kind === "identifier" || kind === "type_identifier" || kind === "shorthand_property_identifier_pattern" ) { - out.add(node.text()); + names.add(node.text()); return; } - const children = node.children(); - const defaultIndex = children.findIndex((child) => child.kind() === "="); - for (const child of defaultIndex === -1 ? children : children.slice(0, defaultIndex)) { + for (const child of node.children()) { if (child.kind() === "property_identifier") continue; - collectBindingNames(child, out); + if (child.kind() === "=") break; + collectBindingNames(child, names); } } @@ -432,16 +170,10 @@ function firstDeclaratorChild(node: SgNode): SgNode | null { function localDeclarationNames(root: SgNode): Set { const names = new Set(); - for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { const binding = firstDeclaratorChild(decl); if (binding) collectBindingNames(binding, names); } - - for (const stmt of root.findAll({ rule: { kind: "expression_statement" } })) { - collectUsingDeclarationNames(stmt, names); - } - for (const param of root.findAll({ rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] }, })) { @@ -452,690 +184,69 @@ function localDeclarationNames(root: SgNode): Set { ); if (binding) collectBindingNames(binding, names); } - for (const decl of root.findAll({ rule: { any: [ { kind: "function_declaration" }, - { kind: "function_expression" }, { kind: "class_declaration" }, { kind: "class" }, { kind: "enum_declaration" }, { kind: "interface_declaration" }, { kind: "type_alias_declaration" }, - { kind: "internal_module" }, { kind: "import_alias" }, ], }, })) { - collectDeclarationName(decl, names); - } - - for (const catchClause of root.findAll({ rule: { kind: "catch_clause" } })) { - for (const child of catchClause.children()) { - if (["identifier", "object_pattern", "array_pattern"].includes(child.kind())) { - collectBindingNames(child, names); - } - } - } - - for (const arrow of root.findAll({ rule: { kind: "arrow_function" } })) { - const children = arrow.children(); - const arrowIndex = children.findIndex((child) => child.kind() === "=>"); - if (arrowIndex === -1) continue; - for (const child of children.slice(0, arrowIndex)) { - collectBindingNames(child, names); - } - } - - for (const loop of root.findAll({ rule: { kind: "for_in_statement" } })) { - const children = loop.children(); - const keywordIndex = children.findIndex( - (child) => child.kind() === "in" || child.kind() === "of", - ); - if (keywordIndex === -1) continue; - for (const child of children.slice(0, keywordIndex)) { - collectBindingNames(child, names); - } - } - - return names; -} - -function hasRuntimeImportCollision(localNames: Set, imports: SgNode[]): boolean { - if (localNames.has(AUTHCONNECTION)) return true; - - return imports.some( - (importStmt) => - importEqualsLocalName(importStmt) === AUTHCONNECTION || - importBindings(importStmt).some( - (binding) => - binding.localName === AUTHCONNECTION && - !( - binding.source === RUNTIME_MODULE && - binding.importedName === AUTHCONNECTION && - !binding.namespace && - !binding.typeOnly - ), - ), - ); -} - -function importEqualsLocalName(importStmt: SgNode): string | null { - const clause = importStmt.children().find((child) => child.kind() === "import_require_clause"); - return ( - clause - ?.children() - .find((child) => child.kind() === "identifier") - ?.text() ?? null - ); -} - -function hasRuntimeReferenceShadow(localNames: Set, runtimeRef: string): boolean { - return localNames.has(runtimeRef.split(".")[0]!); -} - -function collectVariableDeclaratorNames(scope: SgNode, names: Set): void { - for (const decl of scope.children().filter((child) => child.kind() === "variable_declarator")) { - const binding = firstDeclaratorChild(decl); - if (binding) collectBindingNames(binding, names); - } -} - -function collectParameterNames(scope: SgNode, names: Set): void { - const params = scope.children().find((child) => child.kind() === "formal_parameters"); - if (!params) return; - - collectFormalParameterNames(params, names); -} - -function collectFormalParameterNames(params: SgNode, names: Set): void { - for (const param of params.children()) { - const binding = param - .children() - .find((child) => - ["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind()), - ); - if (binding) collectBindingNames(binding, names); - } -} - -function collectArrowParameterNames(scope: SgNode, names: Set): void { - const children = scope.children(); - const arrowIndex = children.findIndex((child) => child.kind() === "=>"); - if (arrowIndex === -1) return; - - for (const child of children.slice(0, arrowIndex)) { - if (child.kind() === "formal_parameters") { - collectFormalParameterNames(child, names); - continue; - } - - if (["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind())) { - collectBindingNames(child, names); - } - } -} - -function collectDirectBlockNames(scope: SgNode, names: Set): void { - for (const child of scope.children()) { - if (child.kind() === "export_statement") { - collectDirectBlockNames(child, names); - continue; - } - - if (child.kind() === "expression_statement") { - const moduleDecl = child - .children() - .find((grandchild) => grandchild.kind() === "internal_module"); - if (moduleDecl) collectDeclarationName(moduleDecl, names); - collectUsingDeclarationNames(child, names); - continue; - } - - if (child.kind() === "lexical_declaration" || child.kind() === "variable_declaration") { - collectVariableDeclaratorNames(child, names); - continue; - } - - if ( - [ - "function_declaration", - "class_declaration", - "enum_declaration", - "internal_module", - "import_alias", - ].includes(child.kind()) - ) { - collectDeclarationName(child, names); - } - } -} - -function collectDeclarationName(node: SgNode, names: Set): void { - const name = node - .children() - .find((child) => child.kind() === "identifier" || child.kind() === "type_identifier"); - if (name) names.add(name.text()); -} - -function collectUsingDeclarationNames(scope: SgNode, names: Set): void { - const assignments = scope.children().flatMap((child) => { - if (child.kind() === "assignment_expression") return [child]; - if (child.kind() !== "await_expression") return []; - const assignment = child - .children() - .find((grandchild) => grandchild.kind() === "assignment_expression"); - return assignment ? [assignment] : []; - }); - - for (const assignment of assignments) { - const children = assignment.children(); - const usingIndex = children.findIndex((child) => child.kind() === "using"); - const end = children.findIndex((child, index) => index > usingIndex && child.kind() === "="); - if (usingIndex === -1 || end === -1) continue; - - for (const child of children.slice(usingIndex + 1, end)) { - if ( - ["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind()) - ) { - collectBindingNames(child, names); - } - } - } -} - -function collectSwitchBodyNames(scope: SgNode, names: Set): void { - for (const child of scope.children()) { - if (child.kind() === "switch_case" || child.kind() === "switch_default") { - collectDirectBlockNames(child, names); - } - } -} - -function collectForInitializerNames(scope: SgNode, names: Set): void { - const children = scope.children(); - const start = children.findIndex((child) => child.kind() === "("); - const end = children.findIndex((child, index) => index > start && child.kind() === ";"); - if (start === -1 || end === -1) return; - - for (const child of children.slice(start + 1, end)) { - if (child.kind() === "lexical_declaration" || child.kind() === "variable_declaration") { - collectVariableDeclaratorNames(child, names); - } - } -} - -function isNestedFunctionOrClassScope(scope: SgNode): boolean { - return ( - [ - "function_declaration", - "function_expression", - "arrow_function", - "method_definition", - "class_declaration", - "class", - ].includes(scope.kind()) || - scope.children().some((child) => child.kind() === "formal_parameters") - ); -} - -function isVarDeclaration(scope: SgNode): boolean { - return ( - scope.kind() === "variable_declaration" && - scope.children().some((child) => child.kind() === "var") - ); -} - -function collectFunctionScopedVarNames(scope: SgNode, names: Set): void { - for (const child of scope.children()) { - if (isNestedFunctionOrClassScope(child)) continue; - - if (isVarDeclaration(child)) { - collectVariableDeclaratorNames(child, names); - } - - collectFunctionScopedVarNames(child, names); - } -} - -function collectOwnExpressionName(scope: SgNode, names: Set): void { - if (scope.kind() === "function_expression" || scope.kind() === "function_declaration") { - const name = scope.children().find((child) => child.kind() === "identifier"); - if (name) names.add(name.text()); - return; - } - - if (scope.kind() === "class" || scope.kind() === "class_declaration") { - const name = scope + const name = decl .children() .find((child) => child.kind() === "identifier" || child.kind() === "type_identifier"); if (name) names.add(name.text()); - return; - } - - if (scope.kind() === "internal_module") { - collectDeclarationName(scope, names); } -} - -function isInsideFormalParameters(node: SgNode, scope: SgNode): boolean { - const params = scope.children().find((child) => child.kind() === "formal_parameters"); - if (!params) return false; - - const nodeStart = node.range().start.index; - const paramsRange = params.range(); - return nodeStart >= paramsRange.start.index && nodeStart < paramsRange.end.index; -} - -function directlyDeclaredNames(scope: SgNode, reference: SgNode): Set { - const names = new Set(); - const kind = scope.kind(); - const referenceInParameters = isInsideFormalParameters(reference, scope); - - collectOwnExpressionName(scope, names); - - if (scope.children().some((child) => child.kind() === "formal_parameters")) { - collectParameterNames(scope, names); - if (!referenceInParameters) collectFunctionScopedVarNames(scope, names); - } - - if (kind === "arrow_function") { - if (!referenceInParameters) collectFunctionScopedVarNames(scope, names); - collectArrowParameterNames(scope, names); - } else if (kind === "catch_clause") { - for (const child of scope.children()) { - if (["identifier", "object_pattern", "array_pattern"].includes(child.kind())) { - collectBindingNames(child, names); - } - } - } else if (["statement_block", "program", "switch_case", "switch_default"].includes(kind)) { - collectDirectBlockNames(scope, names); - } else if (kind === "switch_body") { - collectSwitchBodyNames(scope, names); - } else if (kind === "for_statement") { - collectForInitializerNames(scope, names); - } else if (kind === "for_in_statement") { - const children = scope.children(); - const keywordIndex = children.findIndex( - (child) => child.kind() === "in" || child.kind() === "of", - ); - if (keywordIndex !== -1) { - for (const child of children.slice(0, keywordIndex)) { - collectBindingNames(child, names); - } - } - } - return names; } -function isReferenceShadowed( - node: SgNode, - localName: string, - allowedBindingScopes: BindingScopeMap = new Map(), -): boolean { - let current = node.parent(); - while (current) { - if (directlyDeclaredNames(current, node).has(localName)) { - if (allowedBindingScopes.get(localName)?.has(rangeKey(current))) { - current = current.parent(); - continue; - } - return true; - } - current = current.parent(); - } - return false; -} - -function authConnectionTokenReferenceFromMember( - member: SgNode, - authLocalNames: Set, - allowedBindingScopes: BindingScopeMap = new Map(), -): TokenCall | null { - const property = member.field("property"); - const object = member.field("object"); - const receiver = authReceiverIdentifier(object); - if ( - property?.text() !== GET_CONNECTION_TOKEN || - !object || - !receiver || - !authLocalNames.has(receiver.text()) - ) { - return null; - } - - if (isReferenceShadowed(receiver, receiver.text(), allowedBindingScopes)) return null; - - const range = object.range(); - return { - objectNode: object, - localName: receiver.text(), - range: [range.start.index, range.end.index], - }; +function hasRuntimeImportCollision(root: SgNode, imports: SgNode[]): boolean { + if (localDeclarationNames(root).has(AUTHCONNECTION)) return true; + return imports.some( + (importStmt) => + importLocalNames(importStmt).has(AUTHCONNECTION) && + importSource(importStmt) !== RUNTIME_MODULE, + ); } -function authReceiverIdentifier(node: SgNode | null): SgNode | null { - const receiver = unwrapReceiverExpression(node); - return receiver?.kind() === "identifier" ? receiver : null; +function hasAuthLocalCollision(root: SgNode, authLocalNames: Set): boolean { + const localNames = localDeclarationNames(root); + return Array.from(authLocalNames).some((name) => localNames.has(name)); } -function unwrapReceiverExpression(node: SgNode | null): SgNode | null { - if (!node) return null; - if ( - ![ - "parenthesized_expression", - "as_expression", - "satisfies_expression", - "non_null_expression", - "type_assertion", - ].includes(node.kind()) - ) { - return node; - } +function findDirectAuthCalls(root: SgNode, authLocalNames: Set): TokenCall[] { + const calls: TokenCall[] = []; + for (const call of root.findAll({ rule: { kind: "call_expression" } })) { + const callee = call.field("function"); + if (callee?.kind() !== "member_expression") continue; - for (const child of node.children()) { + const object = callee.field("object"); + const property = callee.field("property"); if ( - ![ - "identifier", - "member_expression", - "parenthesized_expression", - "as_expression", - "satisfies_expression", - "non_null_expression", - "type_assertion", - ].includes(child.kind()) + object?.kind() !== "identifier" || + property?.text() !== GET_CONNECTION_TOKEN || + !authLocalNames.has(object.text()) ) { continue; } - const receiver = unwrapReceiverExpression(child); - if (receiver) return receiver; - } - return null; -} - -function findAuthConnectionTokenReferences( - root: SgNode, - authLocalNames: Set, - allowedBindingScopes: BindingScopeMap = new Map(), -): TokenCall[] { - const references: TokenCall[] = []; - for (const member of root.findAll({ rule: { kind: "member_expression" } })) { - const reference = authConnectionTokenReferenceFromMember( - member, - authLocalNames, - allowedBindingScopes, - ); - if (reference) references.push(reference); - } - return references; -} - -function authConnectionTokenNamespaceReferenceFromMember( - member: SgNode, - namespaceAuthLocalNames: Set, - allowedBindingScopes: BindingScopeMap = new Map(), -): TokenCall | null { - const property = member.field("property"); - const object = member.field("object"); - const receiver = namespaceAuthReceiverIdentifier(object, namespaceAuthLocalNames); - if (property?.text() !== GET_CONNECTION_TOKEN || !object || !receiver) return null; - if (isReferenceShadowed(receiver, receiver.text(), allowedBindingScopes)) return null; - - const range = object.range(); - return { - objectNode: object, - localName: receiver.text(), - range: [range.start.index, range.end.index], - }; -} - -function namespaceAuthReceiverIdentifier( - node: SgNode | null, - namespaceAuthLocalNames: Set, -): SgNode | null { - const receiver = unwrapReceiverExpression(node); - if (receiver?.kind() !== "member_expression") return null; - - const property = receiver.field("property"); - const object = receiver.field("object"); - if ( - property?.text() !== "auth" || - object?.kind() !== "identifier" || - !namespaceAuthLocalNames.has(object.text()) - ) { - return null; - } - return object; -} - -function findAuthConnectionTokenNamespaceReferences( - root: SgNode, - namespaceAuthLocalNames: Set, - allowedBindingScopes: BindingScopeMap = new Map(), -): TokenCall[] { - const references: TokenCall[] = []; - for (const member of root.findAll({ rule: { kind: "member_expression" } })) { - const reference = authConnectionTokenNamespaceReferenceFromMember( - member, - namespaceAuthLocalNames, - allowedBindingScopes, - ); - if (reference) references.push(reference); - } - return references; -} - -function isGetConnectionTokenSubscript(subscript: SgNode): boolean { - return stringValue(subscript.field("index")) === GET_CONNECTION_TOKEN; -} - -function authConnectionTokenSubscriptReferenceFromSubscript( - subscript: SgNode, - authLocalNames: Set, - allowedBindingScopes: BindingScopeMap = new Map(), -): TokenCall | null { - const object = subscript.field("object"); - const receiver = authReceiverIdentifier(object); - if (!isGetConnectionTokenSubscript(subscript) || !object || !receiver) return null; - if (!authLocalNames.has(receiver.text())) return null; - if (isReferenceShadowed(receiver, receiver.text(), allowedBindingScopes)) return null; - - const range = subscript.range(); - return { - objectNode: object, - localName: receiver.text(), - range: [range.start.index, range.end.index], - }; -} - -function findAuthConnectionTokenSubscriptReferences( - root: SgNode, - authLocalNames: Set, - allowedBindingScopes: BindingScopeMap = new Map(), -): TokenCall[] { - const references: TokenCall[] = []; - for (const subscript of root.findAll({ rule: { kind: "subscript_expression" } })) { - const reference = authConnectionTokenSubscriptReferenceFromSubscript( - subscript, - authLocalNames, - allowedBindingScopes, - ); - if (reference) references.push(reference); - } - return references; -} - -function authConnectionTokenNamespaceSubscriptReferenceFromSubscript( - subscript: SgNode, - namespaceAuthLocalNames: Set, - allowedBindingScopes: BindingScopeMap = new Map(), -): TokenCall | null { - const object = subscript.field("object"); - const receiver = namespaceAuthReceiverIdentifier(object, namespaceAuthLocalNames); - if (!isGetConnectionTokenSubscript(subscript) || !object || !receiver) return null; - if (isReferenceShadowed(receiver, receiver.text(), allowedBindingScopes)) return null; - - const range = subscript.range(); - return { - objectNode: object, - localName: receiver.text(), - range: [range.start.index, range.end.index], - }; -} - -function findAuthConnectionTokenNamespaceSubscriptReferences( - root: SgNode, - namespaceAuthLocalNames: Set, - allowedBindingScopes: BindingScopeMap = new Map(), -): TokenCall[] { - const references: TokenCall[] = []; - for (const subscript of root.findAll({ rule: { kind: "subscript_expression" } })) { - const reference = authConnectionTokenNamespaceSubscriptReferenceFromSubscript( - subscript, - namespaceAuthLocalNames, - allowedBindingScopes, - ); - if (reference) references.push(reference); - } - return references; -} - -function objectPatternHasGetConnectionToken(pattern: SgNode): boolean { - return pattern.children().some((child) => { - if ( - child.kind() === "shorthand_property_identifier_pattern" && - child.text() === GET_CONNECTION_TOKEN - ) { - return true; - } - - if (child.kind() === "object_assignment_pattern") { - return objectAssignmentPatternKeyName(child) === GET_CONNECTION_TOKEN; - } - - if (child.kind() !== "pair_pattern") return false; - return pairPatternKeyName(child) === GET_CONNECTION_TOKEN; - }); -} - -function objectAssignmentPatternKeyName(pattern: SgNode): string | null { - for (const child of pattern.children()) { - if (child.kind() === "=") return null; - if (child.kind() === "shorthand_property_identifier_pattern") return child.text(); - } - return null; -} - -function pairPatternKeyName(pair: SgNode): string | null { - for (const child of pair.children()) { - if (child.kind() === ":") return null; - if (child.kind() === "property_identifier") return child.text(); - if (child.kind() === "computed_property_name") { - return stringValue( - child.children().find((grandchild) => grandchild.kind() === "string") ?? null, - ); - } - } - return null; -} - -function authConnectionTokenDestructureReference( - node: SgNode, - binding: SgNode | null, - initializer: SgNode | null, - authLocalNames: Set, - namespaceAuthLocalNames: Set, - allowedBindingScopes: BindingScopeMap, - namespaceAllowedBindingScopes: BindingScopeMap, -): TokenCall | null { - if (binding?.kind() !== "object_pattern" || !objectPatternHasGetConnectionToken(binding)) { - return null; - } - - const authReceiver = authReceiverIdentifier(initializer); - const namespaceReceiver = namespaceAuthReceiverIdentifier(initializer, namespaceAuthLocalNames); - const receiver = - authReceiver && authLocalNames.has(authReceiver.text()) ? authReceiver : namespaceReceiver; - const receiverAllowedScopes = - receiver === authReceiver ? allowedBindingScopes : namespaceAllowedBindingScopes; - if (!receiver) return null; - if (isReferenceShadowed(receiver, receiver.text(), receiverAllowedScopes)) return null; - - const range = node.range(); - return { - objectNode: initializer ?? receiver, - localName: receiver.text(), - range: [range.start.index, range.end.index], - }; -} - -function findAuthConnectionTokenDestructures( - root: SgNode, - authLocalNames: Set, - namespaceAuthLocalNames: Set, - allowedBindingScopes: BindingScopeMap = new Map(), - namespaceAllowedBindingScopes: BindingScopeMap = new Map(), -): TokenCall[] { - const references: TokenCall[] = []; - for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { - const reference = authConnectionTokenDestructureReference( - decl, - firstDeclaratorChild(decl), - initializerChild(decl), - authLocalNames, - namespaceAuthLocalNames, - allowedBindingScopes, - namespaceAllowedBindingScopes, - ); - if (reference) references.push(reference); - } - - for (const assignment of root.findAll({ rule: { kind: "assignment_expression" } })) { - const reference = authConnectionTokenDestructureReference( - assignment, - firstDeclaratorChild(assignment), - initializerChild(assignment), - authLocalNames, - namespaceAuthLocalNames, - allowedBindingScopes, - namespaceAllowedBindingScopes, - ); - if (reference) references.push(reference); - } - - for (const parameter of root.findAll({ - rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] }, - })) { - const reference = authConnectionTokenDestructureReference( - parameter, - firstDeclaratorChild(parameter), - initializerChild(parameter), - authLocalNames, - namespaceAuthLocalNames, - allowedBindingScopes, - namespaceAllowedBindingScopes, - ); - if (reference) references.push(reference); - } - return references; -} - -function findAuthConnectionTokenCalls(root: SgNode, authLocalNames: Set): TokenCall[] { - const calls: TokenCall[] = []; - for (const call of root.findAll({ rule: { kind: "call_expression" } })) { - const callee = call.field("function"); - if (callee?.kind() !== "member_expression") continue; - const reference = authConnectionTokenReferenceFromMember(callee, authLocalNames); - if (reference) calls.push(reference); + const range = object.range(); + calls.push({ + objectNode: object, + localName: object.text(), + range: [range.start.index, range.end.index], + }); } return calls; } function isInsideImportStatement(node: SgNode): boolean { - let current: SgNode | null = node.parent(); + let current = node.parent(); while (current) { if (current.kind() === "import_statement") return true; current = current.parent(); @@ -1154,183 +265,27 @@ function countRemainingRefs( scheduledRanges: Array<[number, number]>, ): number { return root - .findAll({ rule: { any: [...REFERENCE_KINDS].map((kind) => ({ kind })) } }) + .findAll({ rule: { any: [{ kind: "identifier" }, { kind: "shorthand_property_identifier" }] } }) .filter((node) => node.text() === localName) .filter( - (node) => - !isInsideImportStatement(node) && - !isInsideScheduledRange(node, scheduledRanges) && - !isReferenceShadowed(node, localName), + (node) => !isInsideImportStatement(node) && !isInsideScheduledRange(node, scheduledRanges), ).length; } -function importClause(importStmt: SgNode): SgNode | null { - return importStmt.children().find((child) => child.kind() === "import_clause") ?? null; -} - -function hasDefaultOrNamespaceImport(importStmt: SgNode): boolean { - return ( - importClause(importStmt) - ?.children() - .some((child) => child.kind() === "identifier" || child.kind() === "namespace_import") ?? - false - ); -} - -function buildOnlyNamedImportRemovalEdit(source: string, importStmt: SgNode): Edit | null { - const named = namedImportsNode(importStmt); - if (!named) return null; - - let start = skipBackwardImportTrivia(source, named.range().start.index); - const end = named.range().end.index; - if (source[start - 1] === ",") { - start--; - start = skipBackwardImportTrivia(source, start); - } - return { startPos: start, endPos: end, insertedText: "" }; -} - -function skipBackwardImportTrivia(source: string, index: number): number { - let pos = index; - while (pos > 0) { - while (pos > 0 && /\s/.test(source[pos - 1]!)) pos--; - - const lineStart = source.lastIndexOf("\n", pos - 1) + 1; - const line = source.slice(lineStart, pos); - const lineCommentIndex = line.lastIndexOf("//"); - if (lineCommentIndex !== -1) { - pos = lineStart + lineCommentIndex; - continue; - } - - if (source.slice(pos - 2, pos) === "*/") { - const start = source.lastIndexOf("/*", pos - 2); - if (start === -1) return pos; - pos = start; - continue; - } - - return pos; - } - return pos; -} - -function skipInlineTrivia(source: string, index: number): number { - let pos = index; - while (pos < source.length) { - while (pos < source.length && (source[pos] === " " || source[pos] === "\t")) pos++; - - if (source.startsWith("/*", pos)) { - const end = source.indexOf("*/", pos + 2); - if (end === -1) return pos; - pos = end + 2; - continue; - } - - if (source.startsWith("//", pos)) { - const end = source.indexOf("\n", pos + 2); - pos = end === -1 ? source.length : end + 1; - continue; - } - - return pos; - } - return pos; -} - -function buildImportSpecRemovalEdit(source: string, binding: AuthBinding): Edit | null { - const allSpecs = binding.importStmt.findAll({ rule: { kind: "import_specifier" } }); - if (allSpecs.length === 1) { - if (hasDefaultOrNamespaceImport(binding.importStmt)) { - return buildOnlyNamedImportRemovalEdit(source, binding.importStmt); - } - - const r = binding.importStmt.range(); - return { startPos: r.start.index, endPos: r.end.index, insertedText: "" }; - } - - const r = binding.spec.range(); - let start = r.start.index; - let end = r.end.index; - end = skipInlineTrivia(source, end); - if (source[end] === ",") { - end++; - while (end < source.length && (source[end] === " " || source[end] === "\t")) end++; - return { startPos: start, endPos: end, insertedText: "" }; - } - - while (start > 0 && (source[start - 1] === " " || source[start - 1] === "\t")) start--; - if (source[start - 1] === ",") { - start--; - while (start > 0 && (source[start - 1] === " " || source[start - 1] === "\t")) start--; - return { startPos: start, endPos: end, insertedText: "" }; - } - - return { startPos: r.start.index, endPos: r.end.index, insertedText: "" }; -} - -function importRangeKey(importStmt: SgNode): string { - const range = importStmt.range(); - return `${range.start.index}:${range.end.index}`; -} - -function buildGroupedImportSpecRemovalEdits( - source: string, - importStmt: SgNode, - bindings: AuthBinding[], -): Edit[] { - const allSpecs = importStmt.findAll({ rule: { kind: "import_specifier" } }); - const removableSpecStarts = new Set(bindings.map((binding) => binding.spec.range().start.index)); - - if (removableSpecStarts.size === allSpecs.length) { - if (hasDefaultOrNamespaceImport(importStmt)) { - const edit = buildOnlyNamedImportRemovalEdit(source, importStmt); - return edit ? [edit] : []; - } - - const range = importStmt.range(); - return [{ startPos: range.start.index, endPos: range.end.index, insertedText: "" }]; - } - - if (bindings.length === 1) { - const edit = buildImportSpecRemovalEdit(source, bindings[0]!); - return edit ? [edit] : []; - } - - const named = namedImportsNode(importStmt); - if (!named) return []; - - const keptSpecTexts = allSpecs - .filter((spec) => !removableSpecStarts.has(spec.range().start.index)) - .map((spec) => spec.text()); - return [named.replace(`{ ${keptSpecTexts.join(", ")} }`)]; -} - -function isDirectiveStatement(node: SgNode): boolean { - return node.kind() === "expression_statement" && node.children()[0]?.kind() === "string"; -} - function importInsertionIndex(root: SgNode, imports: SgNode[], source: string): number { const lastImport = imports.at(-1); if (lastImport) return lastImport.range().end.index; - let pos = 0; if (source.startsWith("#!")) { const newlineIndex = source.indexOf("\n"); - pos = newlineIndex === -1 ? source.length : newlineIndex + 1; + return newlineIndex === -1 ? source.length : newlineIndex + 1; } - - for (const child of root.children()) { - if (child.range().start.index < pos) continue; - if (child.kind() === "comment") { - pos = child.range().end.index; - continue; - } - if (!isDirectiveStatement(child)) break; - pos = child.range().end.index; - } - - return pos; + return ( + root + .children() + .find((child) => child.kind() !== "comment") + ?.range().start.index ?? 0 + ); } function buildAddRuntimeImportEdit(root: SgNode, source: string, imports: SgNode[]): Edit { @@ -1351,6 +306,30 @@ function buildAddRuntimeImportEdit(root: SgNode, source: string, imports: SgNode return { startPos: pos, endPos: pos, insertedText }; } +function buildImportRemovalEdit(source: string, binding: AuthImport): Edit | null { + const allSpecs = binding.importStmt.findAll({ rule: { kind: "import_specifier" } }); + if (allSpecs.length === 1) { + const range = binding.importStmt.range(); + let end = range.end.index; + if (source[end] === "\n") end++; + return { startPos: range.start.index, endPos: end, insertedText: "" }; + } + + const range = binding.spec.range(); + let start = range.start.index; + let end = range.end.index; + while (end < source.length && /[ \t]/.test(source[end]!)) end++; + if (source[end] === ",") { + end++; + while (end < source.length && /[ \t]/.test(source[end]!)) end++; + return { startPos: start, endPos: end, insertedText: "" }; + } + + while (start > 0 && /[ \t]/.test(source[start - 1]!)) start--; + if (source[start - 1] === ",") start--; + return { startPos: start, endPos, insertedText: "" }; +} + function applyEdits(source: string, edits: Edit[]): string { return edits .toSorted((a, b) => b.startPos - a.startPos || b.endPos - a.endPos) @@ -1358,52 +337,27 @@ function applyEdits(source: string, edits: Edit[]): string { (current, edit) => `${current.slice(0, edit.startPos)}${edit.insertedText}${current.slice(edit.endPos)}`, source, - ); -} - -function normalizeSource(source: string): string { - const lines = source.replace(/^[\t ]*\n+/, "").split("\n"); - const out: string[] = []; - let sawImport = false; - - for (let index = 0; index < lines.length; index++) { - const line = lines[index]!; - if (line.startsWith("import ")) { - out.push(line); - sawImport = true; - continue; - } - if (sawImport && line.trim() === "") { - if (out.at(-1)?.trim() !== "") out.push(line); - continue; - } - out.push(...lines.slice(index)); - return out.join("\n"); - } - - return out.join("\n"); + ) + .replace(/^\n+/, ""); } function transformParsed(source: string, root: SgNode): string | null { const imports = findImportStatements(root); - const localNames = localDeclarationNames(root); - const authBindings = findTailorConfigAuthBindings(imports); - if (authBindings.length === 0) return null; + const authImports = findAuthImports(imports); + if (authImports.length === 0) return null; + + const authLocalNames = new Set(authImports.map((binding) => binding.localName)); + if (hasAuthLocalCollision(root, authLocalNames)) return null; - const authLocalNames = new Set(authBindings.map((binding) => binding.localName)); - const calls = findAuthConnectionTokenCalls(root, authLocalNames); + const calls = findDirectAuthCalls(root, authLocalNames); if (calls.length === 0) return null; const existingRuntimeRef = runtimeAuthconnectionReference(imports); - if (!existingRuntimeRef && hasRuntimeImportCollision(localNames, imports)) return null; - if (existingRuntimeRef && hasRuntimeReferenceShadow(localNames, existingRuntimeRef)) return null; + if (!existingRuntimeRef && hasRuntimeImportCollision(root, imports)) return null; const runtimeRef = existingRuntimeRef ?? AUTHCONNECTION; const edits: Edit[] = calls.map((call) => call.objectNode.replace(runtimeRef)); - - if (!existingRuntimeRef) { - edits.push(buildAddRuntimeImportEdit(root, source, imports)); - } + if (!existingRuntimeRef) edits.push(buildAddRuntimeImportEdit(root, source, imports)); const scheduledRangesByLocalName = new Map>(); for (const call of calls) { @@ -1412,44 +366,22 @@ function transformParsed(source: string, root: SgNode): string | null { scheduledRangesByLocalName.set(call.localName, ranges); } - const removableBindingsByImport = new Map< - string, - { importStmt: SgNode; bindings: AuthBinding[] } - >(); - for (const binding of authBindings) { - if (calls.every((call) => call.localName !== binding.localName)) continue; + for (const binding of authImports) { + if (!scheduledRangesByLocalName.has(binding.localName)) continue; const remainingRefs = countRemainingRefs( root, binding.localName, scheduledRangesByLocalName.get(binding.localName) ?? [], ); if (remainingRefs > 0) continue; - const key = importRangeKey(binding.importStmt); - const group = removableBindingsByImport.get(key) ?? { - importStmt: binding.importStmt, - bindings: [], - }; - group.bindings.push(binding); - removableBindingsByImport.set(key, group); - } - - for (const group of removableBindingsByImport.values()) { - edits.push(...buildGroupedImportSpecRemovalEdits(source, group.importStmt, group.bindings)); + const edit = buildImportRemovalEdit(source, binding); + if (edit) edits.push(edit); } - const result = normalizeSource(applyEdits(source, edits)); + const result = applyEdits(source, edits); return result === source ? null : result; } -function parseRoot(source: string, filePath: string): SgNode | null { - if (!quickFilter(source)) return null; - try { - return parse(sourceLang(filePath, source), source).root(); - } catch { - return null; - } -} - export default function transform(source: string, filePath: string): string | null { const root = parseRoot(source, filePath); return root ? transformParsed(source, root) : null; @@ -1459,68 +391,46 @@ function lineForIndex(source: string, index: number): number { return source.slice(0, index).split(/\r\n|\r|\n/).length; } -function excerptForIndex(source: string, index: number): string { - const lineStart = source.lastIndexOf("\n", index - 1) + 1; - const lineEnd = source.indexOf("\n", index); - return source.slice(lineStart, lineEnd === -1 ? source.length : lineEnd).trim(); +function excerptForLine(line: string): string { + return line.trim(); +} + +function isReviewLine(excerpt: string): boolean { + if (!excerpt.includes(GET_CONNECTION_TOKEN)) return false; + if ( + excerpt.includes(`${AUTHCONNECTION}.${GET_CONNECTION_TOKEN}`) || + excerpt.includes(`tailor.${AUTHCONNECTION}.${GET_CONNECTION_TOKEN}`) + ) { + return false; + } + return ( + excerpt.includes(`.${GET_CONNECTION_TOKEN}`) || + excerpt.includes(`["${GET_CONNECTION_TOKEN}"]`) || + excerpt.includes(`['${GET_CONNECTION_TOKEN}']`) || + new RegExp(`[,{]\\s*${GET_CONNECTION_TOKEN}\\s*[:}=,]`).test(excerpt) + ); } export function reviewFindings( source: string, - filePath: string, + _filePath: string, relativePath: string, ): LlmReviewFinding[] { - const root = parseRoot(source, filePath); - if (!root) return []; - - const imports = findImportStatements(root); - const authBindings = findTailorConfigAuthBindings(imports); - const requireAuthBindingScopes = findTailorConfigRequireAuthBindingScopes(root); - const requireNamespaceAuthBindingScopes = findTailorConfigRequireNamespaceAuthBindingScopes(root); - const namespaceAuthLocalNames = new Set([ - ...findTailorConfigNamespaceAuthLocalNames(imports), - ...bindingScopeLocalNames(requireNamespaceAuthBindingScopes), - ]); - const derivedAuthBindingScopes = findTailorConfigNamespaceDerivedAuthBindingScopes( - root, - namespaceAuthLocalNames, - requireNamespaceAuthBindingScopes, - ); - const authAllowedBindingScopes = mergeBindingScopeMaps( - requireAuthBindingScopes, - derivedAuthBindingScopes, - ); - const authLocalNames = new Set([ - ...findTailorConfigNamedValueLocalNames(imports), - ...authBindings.map((binding) => binding.localName), - ...bindingScopeLocalNames(authAllowedBindingScopes), - ]); - const references = [ - ...findAuthConnectionTokenReferences(root, authLocalNames, authAllowedBindingScopes), - ...findAuthConnectionTokenNamespaceReferences( - root, - namespaceAuthLocalNames, - requireNamespaceAuthBindingScopes, - ), - ...findAuthConnectionTokenSubscriptReferences(root, authLocalNames, authAllowedBindingScopes), - ...findAuthConnectionTokenNamespaceSubscriptReferences( - root, - namespaceAuthLocalNames, - requireNamespaceAuthBindingScopes, - ), - ...findAuthConnectionTokenDestructures( - root, - authLocalNames, - namespaceAuthLocalNames, - authAllowedBindingScopes, - requireNamespaceAuthBindingScopes, - ), - ].toSorted((a, b) => a.range[0] - b.range[0]); - - return references.map((reference) => ({ - file: relativePath, - line: lineForIndex(source, reference.range[0]), - message: "Replace defineAuth auth.getConnectionToken() with runtime authconnection.", - excerpt: excerptForIndex(source, reference.range[0]), - })); + if (!quickFilter(source)) return []; + + const findings: LlmReviewFinding[] = []; + let offset = 0; + for (const line of source.split(/\n/)) { + const excerpt = excerptForLine(line); + if (isReviewLine(excerpt)) { + findings.push({ + file: relativePath, + line: lineForIndex(source, offset), + message: "Replace defineAuth auth.getConnectionToken() with runtime authconnection.", + excerpt, + }); + } + offset += line.length + 1; + } + return findings; } diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/expected.ts deleted file mode 100644 index 22f06f047..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { db } from "../tailor.config"; -import { authconnection as runtimeAuthconnection } from "@tailor-platform/sdk/runtime"; - -export async function run() { - const token = await runtimeAuthconnection.getConnectionToken("google"); - return { token, db }; -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/input.ts deleted file mode 100644 index 0ef3e7da3..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/aliased-auth-and-runtime/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { auth as mainAuth, db } from "../tailor.config"; -import { authconnection as runtimeAuthconnection } from "@tailor-platform/sdk/runtime"; - -export async function run() { - const token = await mainAuth.getConnectionToken("google"); - return { token, db }; -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/expected.ts deleted file mode 100644 index 46c4af62e..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/expected.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { auth } from "../tailor.config"; -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const run = (input: typeof auth) => authconnection.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/input.ts deleted file mode 100644 index 0a997401f..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-parameter-type-auth/input.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { auth } from "../tailor.config"; - -export const run = (input: typeof auth) => auth.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/expected.ts deleted file mode 100644 index 6c302a9dd..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); - -export const run = input => { - if (ready) var auth = createClient(input); - return auth.getConnectionToken("github"); -}; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/input.ts deleted file mode 100644 index 620b174c8..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/arrow-var-auth-shadow/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { auth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); - -export const run = input => { - if (ready) var auth = createClient(input); - return auth.getConnectionToken("github"); -}; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/expected.ts deleted file mode 100644 index d933329ed..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); - -{ - await using auth = createClient(); - auth.getConnectionToken("github"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/input.ts deleted file mode 100644 index 8f400a9c5..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/await-using-auth-shadow/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { auth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); - -{ - await using auth = createClient(); - auth.getConnectionToken("github"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-auth-shadow/input.ts deleted file mode 100644 index 6d9b04f10..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-auth-shadow/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { auth } from "../tailor.config"; - -{ - class auth {} - - const token = auth.getConnectionToken("google"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-authconnection-collision/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-authconnection-collision/input.ts deleted file mode 100644 index e95e726fe..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/class-authconnection-collision/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { auth } from "../tailor.config"; - -class authconnection {} - -export const token = await auth.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/expected.ts deleted file mode 100644 index a11c8810b..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defineAuth } from "../tailor.config"; -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); -export const authConfig = defineAuth(); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/input.ts deleted file mode 100644 index dd6416471..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/commented-auth-import-spec/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { auth /* cfg */, defineAuth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); -export const authConfig = defineAuth(); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/expected.ts deleted file mode 100644 index 98e63933d..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import cfg from "../tailor.config"; -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); -export const config = cfg; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/input.ts deleted file mode 100644 index 7c54c8996..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-commented-named-auth/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -import cfg, /* note */ { auth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); -export const config = cfg; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/expected.ts deleted file mode 100644 index 5ae33b064..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import config from "../tailor.config"; -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const configName = config.name; -export const token = await authconnection.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/input.ts deleted file mode 100644 index e0a512e01..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-import-line-comment-named-auth/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -import config, // keep config import -{ auth } from "../tailor.config"; - -export const configName = config.name; -export const token = await auth.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/expected.ts deleted file mode 100644 index 0a891dc21..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { auth } from "../tailor.config"; -import { authconnection } from "@tailor-platform/sdk/runtime"; - -function read({ x = auth }) { - return x; -} - -export const token = await authconnection.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/input.ts deleted file mode 100644 index 3577aea9e..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-initializer-reference/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { auth } from "../tailor.config"; - -function read({ x = auth }) { - return x; -} - -export const token = await auth.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/expected.ts deleted file mode 100644 index 190a59ff8..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); - -export function run(auth = createClient()) { - return auth.getConnectionToken("github"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/input.ts deleted file mode 100644 index bec5e0075..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-auth-shadow/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { auth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); - -export function run(auth = createClient()) { - return auth.getConnectionToken("github"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/expected.ts deleted file mode 100644 index a89049bde..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/expected.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export function run(token = authconnection.getConnectionToken("google")) { - var auth = createClient(); - return token; -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/input.ts deleted file mode 100644 index e053615ed..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-parameter-var-auth/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { auth } from "../tailor.config"; - -export function run(token = auth.getConnectionToken("google")) { - var auth = createClient(); - return token; -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/expected.ts deleted file mode 100644 index 0375525c1..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { auth } from "../tailor.config"; -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); -export const { getConnectionToken } = auth; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/input.ts deleted file mode 100644 index b8a33fe79..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/destructured-token-helper-reference/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { auth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); -export const { getConnectionToken } = auth; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/expected.ts deleted file mode 100644 index f6a9b27be..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/expected.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineAuth } from "../tailor.config"; -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const googleToken = await authconnection.getConnectionToken("google"); -export const githubToken = await authconnection.getConnectionToken("github"); -export const authConfig = defineAuth(); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/input.ts deleted file mode 100644 index cfa6f109f..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases-with-other/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { auth as googleAuth, defineAuth, auth as githubAuth } from "../tailor.config"; - -export const googleToken = await googleAuth.getConnectionToken("google"); -export const githubToken = await githubAuth.getConnectionToken("github"); -export const authConfig = defineAuth(); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/expected.ts deleted file mode 100644 index b52f810b6..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/expected.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const googleToken = await authconnection.getConnectionToken("google"); -export const githubToken = await authconnection.getConnectionToken("github"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/input.ts deleted file mode 100644 index 0db886a44..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/duplicate-auth-aliases/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { auth as googleAuth, auth as githubAuth } from "../tailor.config"; - -export const googleToken = await googleAuth.getConnectionToken("google"); -export const githubToken = await githubAuth.getConnectionToken("github"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/existing-runtime-shadowed/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/existing-runtime-shadowed/input.ts deleted file mode 100644 index 4e779b438..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/existing-runtime-shadowed/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { auth } from "../tailor.config"; -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export async function run(authconnection: { getConnectionToken(name: string): Promise }) { - return auth.getConnectionToken("google"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/expected.ts deleted file mode 100644 index c6eb77045..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/expected.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); - -export function run() { - for (let auth = createClient(); ready; tick()) { - auth.getConnectionToken("github"); - } -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/input.ts deleted file mode 100644 index 4a5f1be80..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/for-auth-shadow/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { auth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); - -export function run() { - for (let auth = createClient(); ready; tick()) { - auth.getConnectionToken("github"); - } -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/import-equals-runtime-collision/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/import-equals-runtime-collision/input.ts deleted file mode 100644 index b129699ec..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/import-equals-runtime-collision/input.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { auth } from "../tailor.config"; -import authconnection = require("./client"); - -export async function run() { - return auth.getConnectionToken("google"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/interface-authconnection-collision/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/interface-authconnection-collision/input.ts deleted file mode 100644 index 69d837439..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/interface-authconnection-collision/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { auth } from "../tailor.config"; - -interface authconnection { - token: string; -} - -export const token = await auth.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/expected.js b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/expected.js deleted file mode 100644 index 33ef404dd..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/expected.js +++ /dev/null @@ -1,3 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const view = <>{await authconnection.getConnectionToken("google")}; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/input.js b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/input.js deleted file mode 100644 index a7ebf19a6..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-fragment-jsx/input.js +++ /dev/null @@ -1,3 +0,0 @@ -import { auth } from "../tailor.config"; - -export const view = <>{await auth.getConnectionToken("google")}; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/expected.js b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/expected.js deleted file mode 100644 index 65feff683..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/expected.js +++ /dev/null @@ -1,3 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const view = ; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/input.js b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/input.js deleted file mode 100644 index 020e85e46..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/js-self-closing-jsx/input.js +++ /dev/null @@ -1,3 +0,0 @@ -import { auth } from "../tailor.config"; - -export const view = ; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/expected.ts deleted file mode 100644 index 5f835bc3e..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/expected.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); - -class Client { - async run(auth: { getConnectionToken(name: string): Promise }) { - return auth.getConnectionToken("github"); - } -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/input.ts deleted file mode 100644 index 3ac28a730..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/method-auth-shadow/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { auth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); - -class Client { - async run(auth: { getConnectionToken(name: string): Promise }) { - return auth.getConnectionToken("github"); - } -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/expected.ts deleted file mode 100644 index d01d444ca..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); - -export async function run(auth: { getConnectionToken(name: string): Promise }) { - return auth.getConnectionToken("github"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/input.ts deleted file mode 100644 index d03b4b7d6..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/mixed-auth-shadow/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { auth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); - -export async function run(auth: { getConnectionToken(name: string): Promise }) { - return auth.getConnectionToken("github"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-class-authconnection-collision/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-class-authconnection-collision/input.ts deleted file mode 100644 index 26f62fce4..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-class-authconnection-collision/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { auth } from "../tailor.config"; - -const Local = class authconnection {}; - -export const token = await auth.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/expected.ts deleted file mode 100644 index cd0dbcc34..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/expected.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); - -const fn = function auth() { - return auth.getConnectionToken("github"); -}; - -const Client = class auth { - run() { - return auth.getConnectionToken("github"); - } -}; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/input.ts deleted file mode 100644 index dd61d2879..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/named-expression-auth-shadow/input.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { auth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); - -const fn = function auth() { - return auth.getConnectionToken("github"); -}; - -const Client = class auth { - run() { - return auth.getConnectionToken("github"); - } -}; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-auth-shadow/input.ts deleted file mode 100644 index fb73f5f83..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-auth-shadow/input.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { auth } from "../tailor.config"; - -namespace auth { - export const token = auth.getConnectionToken("google"); -} - -namespace box { - import auth = other.auth; - - export const token = auth.getConnectionToken("github"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/expected.ts deleted file mode 100644 index 144a63924..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/expected.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); - -namespace Clients { - export const auth = createClient(); - - export function run() { - return auth.getConnectionToken("github"); - } -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/input.ts deleted file mode 100644 index 0420e7012..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/namespace-export-auth-shadow/input.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { auth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); - -namespace Clients { - export const auth = createClient(); - - export function run() { - return auth.getConnectionToken("github"); - } -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/expected.ts deleted file mode 100644 index 4fdd0dcc0..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { auth } from "../tailor.config"; -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); -export const tokenGetter = auth.getConnectionToken; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/input.ts deleted file mode 100644 index ca4cc87eb..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/non-call-reference/input.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { auth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); -export const tokenGetter = auth.getConnectionToken; diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/expected.ts deleted file mode 100644 index 50d4a4e57..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/expected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -const html = ""; - -export const token = await authconnection.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/input.ts deleted file mode 100644 index 8d103bb9e..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/plain-ts-angle-assertion-with-html-string/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { auth } from "../tailor.config"; - -const html = ""; - -export const token = await (auth).getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/expected.ts deleted file mode 100644 index ed8e70ffe..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/expected.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -const body = `line 1 - - -line 4`; - -export async function run() { - return authconnection.getConnectionToken("google") + body; -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/input.ts deleted file mode 100644 index f5043d240..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/preserve-template-newlines/input.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { auth } from "../tailor.config"; - -const body = `line 1 - - -line 4`; - -export async function run() { - return auth.getConnectionToken("google") + body; -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/runtime-authconnection/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/runtime-authconnection/input.ts deleted file mode 100644 index ad4c2010e..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/runtime-authconnection/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export async function run() { - return authconnection.getConnectionToken("google"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/expected.ts deleted file mode 100644 index 905ae3dc3..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/expected.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); - -switch (kind) { - case "github": - const auth = createClient(); - auth.getConnectionToken("github"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/input.ts deleted file mode 100644 index f29397a3c..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/switch-case-auth-shadow/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { auth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); - -switch (kind) { - case "github": - const auth = createClient(); - auth.getConnectionToken("github"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-authconnection-collision/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-authconnection-collision/input.ts deleted file mode 100644 index 9dd0f1b7e..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-authconnection-collision/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { auth } from "../tailor.config"; - -type authconnection = { - token: string; -}; - -export const token = await auth.getConnectionToken("google"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-collision/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-collision/input.ts deleted file mode 100644 index 9d959adef..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-collision/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { auth } from "../tailor.config"; -import { type authconnection, workflow } from "@tailor-platform/sdk/runtime"; - -export async function run() { - await workflow.wait("ready"); - return auth.getConnectionToken("google"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/unrelated-receiver/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/unrelated-receiver/input.ts deleted file mode 100644 index a637bf45e..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/unrelated-receiver/input.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { client } from "./client"; - -export async function run() { - return client.getConnectionToken("google"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/expected.ts deleted file mode 100644 index 82957c99e..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); - -{ - using auth = createClient(); - auth.getConnectionToken("github"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/input.ts deleted file mode 100644 index 65f557b72..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/using-auth-shadow/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { auth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); - -{ - using auth = createClient(); - auth.getConnectionToken("github"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/expected.ts deleted file mode 100644 index 95697dff2..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/expected.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const token = await authconnection.getConnectionToken("google"); - -export function run() { - if (ready) var auth = createClient(); - return auth.getConnectionToken("github"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/input.ts deleted file mode 100644 index b50ad3244..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/var-auth-shadow/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { auth } from "../tailor.config"; - -export const token = await auth.getConnectionToken("google"); - -export function run() { - if (ready) var auth = createClient(); - return auth.getConnectionToken("github"); -} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/expected.ts deleted file mode 100644 index 1cd9cddc0..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/expected.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { authconnection } from "@tailor-platform/sdk/runtime"; - -export const parenthesized = await authconnection.getConnectionToken("google"); -export const asserted = await authconnection.getConnectionToken("github"); -export const satisfied = await authconnection.getConnectionToken("okta"); -export const nonNull = await authconnection.getConnectionToken("microsoft"); -export const typeAsserted = await authconnection.getConnectionToken("azure"); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/input.ts deleted file mode 100644 index bed683ba8..000000000 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/wrapped-auth-receiver/input.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { auth } from "../tailor.config"; - -export const parenthesized = await (auth).getConnectionToken("google"); -export const asserted = await (auth as any).getConnectionToken("github"); -export const satisfied = await (auth satisfies unknown).getConnectionToken("okta"); -export const nonNull = await auth!.getConnectionToken("microsoft"); -export const typeAsserted = await (auth).getConnectionToken("azure"); diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index aac94b112..799c9c6b5 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1041,7 +1041,7 @@ describe("runCodemods", () => { ]); }); - test("flags only unresolved auth connection token helper calls for LLM review", async () => { + test("flags unresolved auth connection token helper usages for LLM review", async () => { const codemod = allCodemods.find((entry) => entry.id === "v2/auth-connection-token-helper"); if (!codemod?.scriptPath) throw new Error("auth connection token codemod missing script"); const scriptPath = path.resolve( @@ -1051,16 +1051,6 @@ describe("runCodemods", () => { ); const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-auth-token-test-")); tmpDir = dir; - await fs.promises.writeFile( - path.join(dir, "assignment-destructure.ts"), - [ - 'import { auth } from "../tailor.config";', - "", - "let getConnectionToken;", - "({ getConnectionToken } = auth);", - "", - ].join("\n"), - ); await fs.promises.writeFile( path.join(dir, "migrated.ts"), [ @@ -1073,184 +1063,31 @@ describe("runCodemods", () => { ].join("\n"), ); await fs.promises.writeFile( - path.join(dir, "collision.ts"), - [ - 'import { auth } from "../tailor.config";', - "", - "const authconnection = createClient();", - "", - "export async function run() {", - ' return auth.getConnectionToken("google");', - "}", - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "shadowed.ts"), - [ - 'import { auth } from "../tailor.config";', - "", - "export async function run(auth: { getConnectionToken(name: string): Promise }) {", - ' return auth.getConnectionToken("google");', - "}", - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "for-shadowed.ts"), - [ - 'import { auth } from "../tailor.config";', - "", - "export function run() {", - " for (let auth = createClient(); ready; tick()) {", - ' auth.getConnectionToken("google");', - " }", - "}", - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "var-shadowed.ts"), - [ - 'import { auth } from "../tailor.config";', - "", - "export function run() {", - " if (ready) var auth = createClient();", - ' return auth.getConnectionToken("google");', - "}", - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "switch-shadowed.ts"), - [ - 'import { auth } from "../tailor.config";', - "", - "switch (kind) {", - ' case "github":', - " const auth = createClient();", - ' auth.getConnectionToken("google");', - "}", - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "default-ref.ts"), - [ - 'import { auth } from "../tailor.config";', - "", - "function read({ x = auth }) {", - " return x;", - "}", - "", - 'export const token = await auth.getConnectionToken("google");', - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "type-collision.ts"), - [ - 'import { auth } from "../tailor.config";', - 'import { type authconnection, workflow } from "@tailor-platform/sdk/runtime";', - "", - "export async function run() {", - ' await workflow.wait("ready");', - ' return auth.getConnectionToken("google");', - "}", - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "import-equals-collision.ts"), - [ - 'import { auth } from "../tailor.config";', - 'import authconnection = require("./client");', - "", - "export async function run() {", - ' return auth.getConnectionToken("google");', - "}", - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "import-equals-config.ts"), - [ - 'import cfg = require("../tailor.config");', - "", - 'export const token = cfg.auth.getConnectionToken("google");', - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "wrapped-collision.ts"), - [ - 'import { auth } from "../tailor.config";', - 'import authconnection = require("./client");', - "", - "export async function run() {", - ' return (auth as any).getConnectionToken("google");', - "}", - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "namespace-alias.ts"), - [ - 'import * as cfg from "../tailor.config";', - "", - "const { auth } = cfg;", - 'export const token = auth.getConnectionToken("google");', - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "namespace-import.ts"), + path.join(dir, "default-import.ts"), [ - 'import * as cfg from "../tailor.config";', + 'import config from "../tailor.config";', "", "export async function run() {", - ' return cfg.auth.getConnectionToken("google");', + ' return config.auth.getConnectionToken("google");', "}", "", ].join("\n"), ); - await fs.promises.writeFile( - path.join(dir, "namespace-member-alias.ts"), - [ - 'import config from "../tailor.config";', - "", - "const myAuth = config.auth;", - 'export const token = myAuth.getConnectionToken("google");', - "", - ].join("\n"), - ); await fs.promises.writeFile( path.join(dir, "computed.ts"), [ 'import { auth } from "../tailor.config";', "", 'export const token = await auth["getConnectionToken"]("google");', - 'export const tokenGetter = auth["getConnectionToken"];', "", ].join("\n"), ); await fs.promises.writeFile( - path.join(dir, "computed-destructure.ts"), + path.join(dir, "destructure.ts"), [ 'import { auth } from "../tailor.config";', "", - 'export const { ["getConnectionToken"]: getToken } = auth;', - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "custom-auth.ts"), - [ - 'import { myAuth } from "../tailor.config";', - "", - "export async function run() {", - ' return myAuth.getConnectionToken("google");', - "}", + "export const { getConnectionToken } = auth;", "", ].join("\n"), ); @@ -1258,98 +1095,31 @@ describe("runCodemods", () => { path.join(dir, "cjs-require.js"), [ 'const { auth } = require("../tailor.config");', - 'const cfg = require("../tailor.config");', "", 'exports.token = auth.getConnectionToken("google");', - 'exports.other = cfg.auth.getConnectionToken("github");', - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "cjs-member-require.js"), - [ - 'const auth = require("../tailor.config").auth;', - "", - 'exports.token = auth.getConnectionToken("google");', - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "namespace-computed.ts"), - [ - 'import * as cfg from "../tailor.config";', - "", - "export async function run() {", - ' return cfg.auth["getConnectionToken"]("google");', - "}", "", ].join("\n"), ); await fs.promises.writeFile( - path.join(dir, "wrapped-destructure.ts"), + path.join(dir, "collision.ts"), [ 'import { auth } from "../tailor.config";', "", - "export const { getConnectionToken } = (auth);", - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "namespace-destructure.ts"), - [ - 'import * as cfg from "../tailor.config";', - "", - "export const { getConnectionToken } = cfg.auth;", - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "default-import.ts"), - [ - 'import config from "../tailor.config";', + "const authconnection = createClient();", "", "export async function run() {", - ' return config.auth.getConnectionToken("google");', + ' return auth.getConnectionToken("google");', "}", "", ].join("\n"), ); await fs.promises.writeFile( - path.join(dir, "defaulted-destructure.ts"), - [ - 'import { auth } from "../tailor.config";', - "", - "export const { getConnectionToken = fallback } = auth;", - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "non-call.ts"), - [ - 'import { auth } from "../tailor.config";', - "", - 'export const token = await auth.getConnectionToken("google");', - "export const tokenGetter = auth.getConnectionToken;", - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "destructure.ts"), - [ - 'import { auth } from "../tailor.config";', - "", - 'export const token = await auth.getConnectionToken("google");', - "export const { getConnectionToken } = auth;", - "", - ].join("\n"), - ); - await fs.promises.writeFile( - path.join(dir, "parameter-destructure.ts"), + path.join(dir, "shadowed.ts"), [ 'import { auth } from "../tailor.config";', "", - "export function run({ getConnectionToken } = auth) {", - ' return getConnectionToken("google");', + "export async function run(auth: { getConnectionToken(name: string): Promise }) {", + ' return auth.getConnectionToken("google");', "}", "", ].join("\n"), @@ -1365,148 +1135,44 @@ describe("runCodemods", () => { codemodId: "v2/auth-connection-token-helper", prompt: codemod.prompt, files: [ - "assignment-destructure.ts", - "cjs-member-require.js", "cjs-require.js", "collision.ts", - "computed-destructure.ts", "computed.ts", - "custom-auth.ts", "default-import.ts", - "defaulted-destructure.ts", "destructure.ts", - "import-equals-collision.ts", - "import-equals-config.ts", - "namespace-alias.ts", - "namespace-computed.ts", - "namespace-destructure.ts", - "namespace-import.ts", - "namespace-member-alias.ts", - "non-call.ts", - "parameter-destructure.ts", - "type-collision.ts", - "wrapped-collision.ts", - "wrapped-destructure.ts", + "shadowed.ts", ], findings: [ - expect.objectContaining({ - file: "assignment-destructure.ts", - line: 4, - excerpt: "({ getConnectionToken } = auth);", - }), - expect.objectContaining({ - file: "cjs-member-require.js", - line: 3, - excerpt: 'exports.token = auth.getConnectionToken("google");', - }), expect.objectContaining({ file: "cjs-require.js", - line: 4, + line: 3, excerpt: 'exports.token = auth.getConnectionToken("google");', }), - expect.objectContaining({ - file: "cjs-require.js", - line: 5, - excerpt: 'exports.other = cfg.auth.getConnectionToken("github");', - }), expect.objectContaining({ file: "collision.ts", line: 6, excerpt: 'return auth.getConnectionToken("google");', }), - expect.objectContaining({ - file: "computed-destructure.ts", - line: 3, - excerpt: 'export const { ["getConnectionToken"]: getToken } = auth;', - }), expect.objectContaining({ file: "computed.ts", line: 3, excerpt: 'export const token = await auth["getConnectionToken"]("google");', }), - expect.objectContaining({ - file: "computed.ts", - line: 4, - excerpt: 'export const tokenGetter = auth["getConnectionToken"];', - }), - expect.objectContaining({ - file: "custom-auth.ts", - line: 4, - excerpt: 'return myAuth.getConnectionToken("google");', - }), expect.objectContaining({ file: "default-import.ts", line: 4, excerpt: 'return config.auth.getConnectionToken("google");', }), - expect.objectContaining({ - file: "defaulted-destructure.ts", - line: 3, - excerpt: "export const { getConnectionToken = fallback } = auth;", - }), expect.objectContaining({ file: "destructure.ts", - excerpt: "export const { getConnectionToken } = auth;", - }), - expect.objectContaining({ - file: "import-equals-collision.ts", - line: 5, - excerpt: 'return auth.getConnectionToken("google");', - }), - expect.objectContaining({ - file: "import-equals-config.ts", line: 3, - excerpt: 'export const token = cfg.auth.getConnectionToken("google");', - }), - expect.objectContaining({ - file: "namespace-alias.ts", - line: 4, - excerpt: 'export const token = auth.getConnectionToken("google");', - }), - expect.objectContaining({ - file: "namespace-computed.ts", - line: 4, - excerpt: 'return cfg.auth["getConnectionToken"]("google");', - }), - expect.objectContaining({ - file: "namespace-destructure.ts", - line: 3, - excerpt: "export const { getConnectionToken } = cfg.auth;", - }), - expect.objectContaining({ - file: "namespace-import.ts", - line: 4, - excerpt: 'return cfg.auth.getConnectionToken("google");', + excerpt: "export const { getConnectionToken } = auth;", }), expect.objectContaining({ - file: "namespace-member-alias.ts", + file: "shadowed.ts", line: 4, - excerpt: 'export const token = myAuth.getConnectionToken("google");', - }), - expect.objectContaining({ - file: "non-call.ts", - excerpt: "export const tokenGetter = auth.getConnectionToken;", - }), - expect.objectContaining({ - file: "parameter-destructure.ts", - line: 3, - excerpt: "export function run({ getConnectionToken } = auth) {", - }), - expect.objectContaining({ - file: "type-collision.ts", - line: 6, excerpt: 'return auth.getConnectionToken("google");', }), - expect.objectContaining({ - file: "wrapped-collision.ts", - line: 5, - excerpt: 'return (auth as any).getConnectionToken("google");', - }), - expect.objectContaining({ - file: "wrapped-destructure.ts", - line: 3, - excerpt: "export const { getConnectionToken } = (auth);", - }), ], }, ]); From fbc83e1af848b283d35512fec5673b86a6222b62 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 12:35:45 +0900 Subject: [PATCH 399/618] test: update ERD workflow CLI expectation --- packages/sdk/src/cli/commands/setup/workflow-lint.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts b/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts index 3e54dc802..9c51a9a11 100644 --- a/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts +++ b/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts @@ -109,7 +109,7 @@ describe("repository ERD preview workflow", () => { expect(content).toContain("Base ERD namespace '$NAMESPACE' not found"); expect(content).toContain('diff_args=(--namespace "$NAMESPACE"'); expect(content).toContain('diff_args+=(--base-html "$base_html")'); - expect(content).toContain("pnpm exec tailor-sdk tailordb erd diff"); + expect(content).toContain("pnpm exec tailor tailordb erd diff"); }); test("uploads artifacts with names matched by the sticky comment", () => { From a0bc8e7fe66147b5492bba358d7d2ec9b47c09be Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 17:10:37 +0900 Subject: [PATCH 400/618] fix(sdk): validate user profile type schema --- .changeset/user-profile-type-schema.md | 5 + .../sdk/src/parser/service/auth/index.test.ts | 42 + .../sdk/src/parser/service/auth/schema.ts | 18 +- packages/sdk/src/types/auth.generated.ts | 972 +++++++++++++++++- 4 files changed, 999 insertions(+), 38 deletions(-) create mode 100644 .changeset/user-profile-type-schema.md diff --git a/.changeset/user-profile-type-schema.md b/.changeset/user-profile-type-schema.md new file mode 100644 index 000000000..d5a8dc8da --- /dev/null +++ b/.changeset/user-profile-type-schema.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": patch +--- + +Validate auth user profile TailorDB types with the strict TailorDB parser schema. diff --git a/packages/sdk/src/parser/service/auth/index.test.ts b/packages/sdk/src/parser/service/auth/index.test.ts index dbf8f49d8..347f58500 100644 --- a/packages/sdk/src/parser/service/auth/index.test.ts +++ b/packages/sdk/src/parser/service/auth/index.test.ts @@ -1,6 +1,7 @@ import { describe, expectTypeOf, expect, test } from "vitest"; import { db } from "#/configure/services/tailordb/schema"; import { t } from "#/configure/types/type"; +import { brandValue } from "#/utils/brand"; import { AuthConfigSchema, OAuth2ClientSchema } from "./schema"; import type { AuthServiceInput } from "#/configure/services/auth/types"; import type { OptionalKeysOf } from "type-fest"; @@ -377,4 +378,45 @@ describe("AuthConfigSchema userProfile/machineUserAttributes validation", () => const result = AuthConfigSchema.parse(config); expect(result.userProfile?.namespace).toBe("external-ns"); }); + + test("strips TailorDB type builder helpers from userProfile.type", () => { + const result = AuthConfigSchema.parse({ + name: "my-auth", + userProfile: { + type: userType, + usernameField: "email", + }, + }); + + expect(result.userProfile?.type).toMatchObject({ + name: "User", + fields: expect.any(Object), + }); + expect(result.userProfile?.type).not.toHaveProperty("hooks"); + expect(result.userProfile?.type).not.toHaveProperty("_output"); + }); + + test("rejects unknown userProfile.type keys after stripping TailorDB builder helpers", () => { + const typeWithUnknownKey = brandValue( + { + ...userType, + unknownOption: true, + }, + "tailordb-type", + ); + + const result = AuthConfigSchema.safeParse({ + name: "my-auth", + userProfile: { + type: typeWithUnknownKey, + usernameField: "email", + }, + }); + + expect(result.success).toBe(false); + if (result.success) { + throw new Error("Expected AuthConfigSchema parsing to fail"); + } + expect(JSON.stringify(result.error.issues)).toContain("unknownOption"); + }); }); diff --git a/packages/sdk/src/parser/service/auth/schema.ts b/packages/sdk/src/parser/service/auth/schema.ts index 4b947fe77..90a570659 100644 --- a/packages/sdk/src/parser/service/auth/schema.ts +++ b/packages/sdk/src/parser/service/auth/schema.ts @@ -1,6 +1,8 @@ import { z } from "zod"; import { AuthConnectionConfigSchema } from "#/parser/service/auth-connection/index"; import { TailorFieldSchema } from "#/parser/service/field/schema"; +import { stripTailorDBTypeBuilderHelpers } from "#/parser/service/tailordb/builder-helpers"; +import { TailorDBTypeSchema } from "#/parser/service/tailordb/index"; import type { ValueOperand } from "#/configure/services/auth/types"; export const AuthInvokerObjectSchema = z.strictObject({ @@ -197,21 +199,7 @@ export const TenantProviderSchema = z.strictObject({ const UserProfileSchema = z.strictObject({ namespace: z.string().optional().describe("TailorDB namespace where the user type is defined"), - // FIXME: improve TailorDBInstance schema validation - // strip unknown keys - type: z.object({ - name: z.string(), - fields: z.any(), - metadata: z.any(), - hooks: z.any(), - validate: z.any(), - features: z.any(), - indexes: z.any(), - files: z.any(), - permission: z.any(), - gqlPermission: z.any(), - _output: z.any(), - }), + type: z.preprocess(stripTailorDBTypeBuilderHelpers, TailorDBTypeSchema), usernameField: z.string(), attributes: z.record(z.string(), z.literal(true)).optional(), attributeList: z.array(z.string()).optional(), diff --git a/packages/sdk/src/types/auth.generated.ts b/packages/sdk/src/types/auth.generated.ts index 2fd445add..8edbea0a7 100644 --- a/packages/sdk/src/types/auth.generated.ts +++ b/packages/sdk/src/types/auth.generated.ts @@ -425,19 +425,7 @@ export type AuthConfigInput = publishSessionEvents?: boolean | undefined; userProfile?: | { - type: { - name: string; - fields: any; - metadata: any; - hooks: any; - validate: any; - features: any; - indexes: any; - files: any; - permission: any; - gqlPermission: any; - _output: any; - }; + type: unknown; usernameField: string; namespace?: string | undefined; attributes?: @@ -776,16 +764,954 @@ export type AuthConfig = | { type: { name: string; - fields: any; - metadata: any; - hooks: any; - validate: any; - features: any; - indexes: any; - files: any; - permission: any; - gqlPermission: any; - _output: any; + fields: { + [x: string]: { + type: string; + fields?: any | undefined; + metadata: { + required?: boolean | undefined | undefined; + array?: boolean | undefined | undefined; + description?: string | undefined | undefined; + typeName?: string | undefined | undefined; + allowedValues?: + | { + value: string; + description?: string | undefined | undefined; + }[] + | undefined; + index?: boolean | undefined | undefined; + unique?: boolean | undefined | undefined; + vector?: boolean | undefined | undefined; + foreignKey?: boolean | undefined | undefined; + foreignKeyType?: string | undefined | undefined; + foreignKeyField?: string | undefined | undefined; + hooks?: + | { + create?: Function | undefined; + update?: Function | undefined; + } + | undefined; + validate?: (Function | [Function, string])[] | undefined; + serial?: + | { + start: number; + maxValue?: number | undefined | undefined; + format?: string | undefined | undefined; + } + | undefined; + scale?: number | undefined | undefined; + }; + rawRelation?: + | { + type: "1-1" | "n-1" | "keyOnly" | "oneToOne" | "manyToOne" | "N-1"; + toward: { + type: string; + as?: string | undefined | undefined; + key?: string | undefined | undefined; + }; + backward?: string | undefined | undefined; + } + | undefined; + }; + }; + metadata: { + name: string; + permissions: { + record?: + | { + create: readonly ( + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + boolean, + ] + | readonly ( + | boolean + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + )[] + | { + conditions: + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly (readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ])[]; + description?: string | undefined; + permit?: boolean | undefined; + } + )[]; + read: readonly ( + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + boolean, + ] + | readonly ( + | boolean + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + )[] + | { + conditions: + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly (readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ])[]; + description?: string | undefined; + permit?: boolean | undefined; + } + )[]; + update: readonly ( + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + boolean, + ] + | readonly ( + | boolean + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + )[] + | { + conditions: + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly (readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ])[]; + description?: string | undefined; + permit?: boolean | undefined; + } + )[]; + delete: readonly ( + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + boolean, + ] + | readonly ( + | boolean + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + )[] + | { + conditions: + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly (readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ])[]; + description?: string | undefined; + permit?: boolean | undefined; + } + )[]; + } + | undefined; + gql?: + | readonly { + conditions: readonly (readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + ), + ])[]; + actions: + | "all" + | readonly ( + | "bulkUpsert" + | "create" + | "update" + | "delete" + | "read" + | "aggregate" + )[]; + permit?: boolean | undefined; + description?: string | undefined; + }[] + | undefined; + }; + files: { + [x: string]: string; + }; + description?: string | undefined; + settings?: + | { + pluralForm?: string | undefined; + aggregation?: boolean | undefined; + bulkUpsert?: boolean | undefined; + gqlOperations?: + | { + create?: boolean | undefined; + update?: boolean | undefined; + delete?: boolean | undefined; + read?: boolean | undefined; + } + | undefined; + publishEvents?: boolean | undefined; + } + | undefined; + indexes?: + | { + [x: string]: { + fields: string[]; + unique?: boolean | undefined; + }; + } + | undefined; + }; }; usernameField: string; namespace?: string | undefined; From 9752bc43efcffc98a747f6a42827a29dc403e962 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 17:20:57 +0900 Subject: [PATCH 401/618] test(sdk): cover invalid user profile type fields --- .../sdk/src/parser/service/auth/index.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/sdk/src/parser/service/auth/index.test.ts b/packages/sdk/src/parser/service/auth/index.test.ts index 347f58500..ed0907ee6 100644 --- a/packages/sdk/src/parser/service/auth/index.test.ts +++ b/packages/sdk/src/parser/service/auth/index.test.ts @@ -419,4 +419,34 @@ describe("AuthConfigSchema userProfile/machineUserAttributes validation", () => } expect(JSON.stringify(result.error.issues)).toContain("unknownOption"); }); + + test("rejects invalid TailorDB fields in userProfile.type", () => { + const typeWithInvalidField = brandValue( + { + ...userType, + fields: { + ...userType.fields, + unsupported: { + type: "unsupported", + metadata: {}, + }, + }, + }, + "tailordb-type", + ); + + const result = AuthConfigSchema.safeParse({ + name: "my-auth", + userProfile: { + type: typeWithInvalidField, + usernameField: "email", + }, + }); + + expect(result.success).toBe(false); + if (result.success) { + throw new Error("Expected AuthConfigSchema parsing to fail"); + } + expect(JSON.stringify(result.error.issues)).toContain("unsupported"); + }); }); From c25b0814d441e934b62b4d599bac164be9c87cdf Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 17:43:50 +0900 Subject: [PATCH 402/618] test(sdk): align ERD preview workflow command --- packages/sdk/src/cli/commands/setup/workflow-lint.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts b/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts index 3e54dc802..9c51a9a11 100644 --- a/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts +++ b/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts @@ -109,7 +109,7 @@ describe("repository ERD preview workflow", () => { expect(content).toContain("Base ERD namespace '$NAMESPACE' not found"); expect(content).toContain('diff_args=(--namespace "$NAMESPACE"'); expect(content).toContain('diff_args+=(--base-html "$base_html")'); - expect(content).toContain("pnpm exec tailor-sdk tailordb erd diff"); + expect(content).toContain("pnpm exec tailor tailordb erd diff"); }); test("uploads artifacts with names matched by the sticky comment", () => { From d67eb41cd864b5af715e30904eb0e053e839fa56 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 18:02:35 +0900 Subject: [PATCH 403/618] feat(sdk): tighten Tailor field string types --- .changeset/strict-tailor-field-strings.md | 5 + .../src/configure/services/auth/index.test.ts | 11 +- .../services/tailordb/schema.test.ts | 101 ++++++++++++++---- .../src/configure/services/tailordb/schema.ts | 51 +++++++-- .../sdk/src/configure/types/field.types.ts | 18 +++- packages/sdk/src/configure/types/type.test.ts | 78 ++++++++++---- packages/sdk/src/configure/types/type.ts | 41 ++++++- .../sdk/src/parser/service/auth/index.test.ts | 6 +- 8 files changed, 246 insertions(+), 65 deletions(-) create mode 100644 .changeset/strict-tailor-field-strings.md diff --git a/.changeset/strict-tailor-field-strings.md b/.changeset/strict-tailor-field-strings.md new file mode 100644 index 000000000..63f024d4f --- /dev/null +++ b/.changeset/strict-tailor-field-strings.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Tighten Tailor field output types for UUID, date, datetime, time, and decimal fields. Date and datetime parsing now rejects invalid calendar dates and accepts RFC 3339 datetime offsets. diff --git a/packages/sdk/src/configure/services/auth/index.test.ts b/packages/sdk/src/configure/services/auth/index.test.ts index a83010b39..97e4e7cf4 100644 --- a/packages/sdk/src/configure/services/auth/index.test.ts +++ b/packages/sdk/src/configure/services/auth/index.test.ts @@ -1,5 +1,4 @@ // oxlint-disable vitest/expect-expect -- Type-only assertions are checked by TypeScript. -import { randomUUID } from "node:crypto"; import { describe, expect, test, expectTypeOf } from "vitest"; import { t } from "#/configure/types/type"; import { db } from "../tailordb/schema"; @@ -36,7 +35,9 @@ const attributeMapConfig: Attributes = { }; const attributeListConfig: AttributeList = ["externalId"]; -const machineUserAttributeList: [string] = [randomUUID()]; +const adminExternalId = "00000000-0000-4000-8000-000000000001"; +const workerExternalId = "00000000-0000-4000-8000-000000000002"; +const machineUserAttributeList: [typeof adminExternalId] = [adminExternalId]; describe("defineAuth", () => { test("creates auth configuration with userProfile and machineUsers", () => { @@ -53,7 +54,7 @@ describe("defineAuth", () => { role: "ADMIN", isActive: true, tags: ["root"], - externalId: "admin-external-id", + externalId: adminExternalId, }, attributeList: machineUserAttributeList, }, @@ -93,7 +94,7 @@ describe("defineAuth", () => { role: "ADMIN", isActive: true, tags: ["root"], - externalId: "admin-external-id", + externalId: adminExternalId, }, attributeList: machineUserAttributeList, }, @@ -102,7 +103,7 @@ describe("defineAuth", () => { role: "WORKER", isActive: false, tags: [], - externalId: "worker-external-id", + externalId: workerExternalId, }, }, }, diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 4af15002f..c7dbc1558 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -7,6 +7,14 @@ import type { TailorPrincipal } from "#/runtime/types"; import type { output, TypeLevelError } from "#/types/helpers"; import type { Hook } from "./types"; +type DateString = `${number}-${number}-${number}`; +type TimeString = `${number}:${number}`; +type TimeZoneOffsetString = "Z" | "z" | `${"+" | "-"}${TimeString}`; +type DateTimeString = + `${DateString}${"T" | "t"}${TimeString}:${number}${"" | `.${number}`}${TimeZoneOffsetString}`; +type UUIDString = `${string}-${string}-${string}-${string}-${string}`; +type DecimalString = `${number}`; + describe("TailorDBField basic field type tests", () => { test("string field outputs string type correctly", () => { const _stringType = db.type("Test", { @@ -48,43 +56,57 @@ describe("TailorDBField basic field type tests", () => { }>(); }); - test("uuid field outputs string type correctly", () => { + test("uuid field outputs UUID string type correctly", () => { const _uuidType = db.type("Test", { uuid: db.uuid(), }); expectTypeOf>().toEqualTypeOf<{ id: string; - uuid: string; + uuid: UUIDString; }>(); }); - test("date field outputs string type correctly", () => { + test("date field outputs date string type correctly", () => { const _dateType = db.type("Test", { birthDate: db.date(), }); expectTypeOf>().toEqualTypeOf<{ id: string; - birthDate: string; + birthDate: DateString; }>(); }); - test("datetime field outputs string | Date type correctly", () => { + test("datetime field outputs datetime string | Date type correctly", () => { const _datetimeType = db.type("Test", { timestamp: db.datetime(), }); expectTypeOf>().toMatchObjectType<{ id: string; - timestamp: string | Date; + timestamp: DateTimeString | Date; }>(); }); - test("time field outputs string type correctly", () => { + test("time field outputs time string type correctly", () => { const _timeType = db.type("Test", { openingTime: db.time(), }); expectTypeOf>().toEqualTypeOf<{ id: string; - openingTime: string; + openingTime: TimeString; + }>(); + }); + + test("pickFields preserves the generated id string type", () => { + const _schemaType = t.object({ + ...db + .type("Test", { + name: db.string(), + }) + .pickFields(["id"], { optional: true }), + }); + + expectTypeOf>().toEqualTypeOf<{ + id?: string | null; }>(); }); }); @@ -478,7 +500,7 @@ describe("TailorDBField relation modifier tests", () => { expectTypeOf>().toEqualTypeOf<{ id: string; title: string; - authorId: string; + authorId: UUIDString; }>(); }); @@ -686,8 +708,8 @@ describe("TailorDBType withTimestamps option tests", () => { expectTypeOf>().toEqualTypeOf<{ id: string; name: string; - createdAt: string | Date; - updatedAt: string | Date; + createdAt: DateTimeString | Date; + updatedAt: DateTimeString | Date; }>(); }); @@ -776,9 +798,9 @@ describe("TailorDBType composite type tests", () => { tags: string[]; role: "admin" | "user" | "guest"; score: number; - birthDate: string; - lastLogin?: string | Date | null; - closingTime: string; + birthDate: DateString; + lastLogin?: DateTimeString | Date | null; + closingTime: TimeString; }>(); }); }); @@ -951,8 +973,8 @@ describe("TailorDBType plural form tests", () => { id: string; title: string; content?: string | null; - createdAt: string | Date; - updatedAt: string | Date; + createdAt: DateTimeString | Date; + updatedAt: DateTimeString | Date; }>(); expect(_postType.name).toBe("Post"); @@ -1447,7 +1469,7 @@ describe("TailorDBField fluent API type preservation", () => { .uuid() .description("User reference") .relation({ type: "n-1", toward: { type: User } }); - expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); }); }); @@ -1626,6 +1648,43 @@ describe("TailorDBField runtime validation tests", () => { expect(bad.issues?.[0]?.message).toBe( 'Expected to match "yyyy-MM-dd" format: received 2025/01/01', ); + + const invalidDate = field.parse({ value: "2025-02-30", data, user }); + expect(invalidDate.issues?.[0]?.message).toBe( + 'Expected to match "yyyy-MM-dd" format: received 2025-02-30', + ); + }); + + test("validates datetime format", () => { + const field = db.datetime(); + for (const value of [ + "2025-01-01T10:11:12Z", + "2025-01-01T10:11:12.123456Z", + "2025-01-01T10:11:12+09:00", + "2025-01-01t10:11:12-08:00", + ]) { + const ok = field.parse({ value, data, user }); + expect(ok.issues).toBeUndefined(); + if (ok.issues) { + throw new Error("Unexpected issues"); + } + expect(ok.value).toBe(value); + } + + const bad = field.parse({ value: "2025-01-01T10:11:12+0900", data, user }); + expect(bad.issues?.[0]?.message).toBe( + "Expected to match ISO format: received 2025-01-01T10:11:12+0900", + ); + + const invalidTime = field.parse({ value: "2025-01-01T25:11:12Z", data, user }); + expect(invalidTime.issues?.[0]?.message).toBe( + "Expected to match ISO format: received 2025-01-01T25:11:12Z", + ); + + const invalidOffset = field.parse({ value: "2025-01-01T10:11:12+24:00", data, user }); + expect(invalidOffset.issues?.[0]?.message).toBe( + "Expected to match ISO format: received 2025-01-01T10:11:12+24:00", + ); }); test("validates time format", () => { @@ -2053,23 +2112,23 @@ describe("TailorDBField clone tests", () => { }); describe("TailorDBField decimal type tests", () => { - test("decimal field outputs string type correctly", () => { + test("decimal field outputs decimal string type correctly", () => { const _decimalType = db.type("Test", { price: db.decimal(), }); expectTypeOf>().toEqualTypeOf<{ id: string; - price: string; + price: DecimalString; }>(); }); - test("optional decimal field outputs string | null type correctly", () => { + test("optional decimal field outputs decimal string | null type correctly", () => { const _decimalType = db.type("Test", { discount: db.decimal({ optional: true }), }); expectTypeOf>().toEqualTypeOf<{ id: string; - discount?: string | null; + discount?: DecimalString | null; }>(); }); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 8b4230646..010f0bcfc 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -285,10 +285,7 @@ export interface TailorDBField< */ clone( options?: NewOpt, - ): TailorDBField< - WithDBFieldCloneOptions, - FieldOutput - >; + ): TailorDBField, FieldOutput>; } /** @@ -335,7 +332,7 @@ export interface TailorDBType< Omit & { array: Opt extends { array: true } ? true : D["array"]; }, - FieldOutput + FieldOutput<_O, Opt> > : never; }; @@ -383,10 +380,43 @@ const regex = { date: /^(?\d{4})-(?\d{2})-(?\d{2})$/, time: /^(?\d{2}):(?\d{2})$/, datetime: - /^(?\d{4})-(?\d{2})-(?\d{2})T(?\d{2}):(?\d{2}):(?\d{2})(.(?\d{3}))?Z$/, + /^(?\d{4})-(?\d{2})-(?\d{2})[Tt](?[01]\d|2[0-3]):(?[0-5]\d):(?[0-5]\d|60)(\.(?\d+))?(?[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/, decimal: /^-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/, } as const; +function isValidDateString(value: string): boolean { + const match = regex.date.exec(value); + return match?.groups !== undefined && isValidDateParts(match.groups); +} + +function isValidDateTimeString(value: string): boolean { + const match = regex.datetime.exec(value); + return match?.groups !== undefined && isValidDateParts(match.groups); +} + +function isValidDateParts(groups: Record): boolean { + const { year: yearValue, month: monthValue, day: dayValue } = groups; + if (yearValue === undefined || monthValue === undefined || dayValue === undefined) { + return false; + } + + const year = Number(yearValue); + const month = Number(monthValue); + const day = Number(dayValue); + if (month < 1 || month > 12 || day < 1) { + return false; + } + + const daysInMonth = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][ + month - 1 + ]; + return daysInMonth !== undefined && day <= daysInMonth; +} + +function isLeapYear(year: number): boolean { + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); +} + type FieldParseArgs = { value: unknown; data: unknown; @@ -561,7 +591,7 @@ function createTailorDBFieldRuntime< } break; case "date": - if (typeof value !== "string" || !regex.date.test(value)) { + if (typeof value !== "string" || !isValidDateString(value)) { issues.push({ message: `Expected to match "yyyy-MM-dd" format: received ${String(value)}`, path: pathArray.length > 0 ? pathArray : undefined, @@ -569,7 +599,7 @@ function createTailorDBFieldRuntime< } break; case "datetime": - if (typeof value !== "string" || !regex.datetime.test(value)) { + if (typeof value !== "string" || !isValidDateTimeString(value)) { issues.push({ message: `Expected to match ISO format: received ${String(value)}`, path: pathArray.length > 0 ? pathArray : undefined, @@ -947,7 +977,8 @@ function date(options?: Opt) { /** * Create a datetime field (date and time). - * Format: ISO 8601 "yyyy-MM-ddTHH:mm:ssZ" + * Format: RFC 3339 date-time, such as "yyyy-MM-ddTHH:mm:ssZ" or + * "yyyy-MM-ddTHH:mm:ss+09:00". * @param options - Field configuration options * @returns A datetime field * @example db.datetime() @@ -1192,7 +1223,7 @@ function createTailorDBType< return brandValue(dbType, "tailordb-type"); } -const idField = uuid(); +const idField = createField<"uuid", Record, string>("uuid"); type idField = typeof idField; type DBType> = TailorDBInstance< { id: idField } & F diff --git a/packages/sdk/src/configure/types/field.types.ts b/packages/sdk/src/configure/types/field.types.ts index f7dae4a00..72d77c0a6 100644 --- a/packages/sdk/src/configure/types/field.types.ts +++ b/packages/sdk/src/configure/types/field.types.ts @@ -21,16 +21,24 @@ export type TailorFieldType = | "time" | "nested"; +type DateString = `${number}-${number}-${number}`; +type TimeString = `${number}:${number}`; +type TimeZoneOffsetString = "Z" | "z" | `${"+" | "-"}${TimeString}`; +type DateTimeString = + `${DateString}${"T" | "t"}${TimeString}:${number}${"" | `.${number}`}${TimeZoneOffsetString}`; +type UUIDString = `${string}-${string}-${string}-${string}-${string}`; +type DecimalString = `${number}`; + export type TailorToTs = { string: string; integer: number; float: number; - decimal: string; + decimal: DecimalString; boolean: boolean; - uuid: string; - date: string; - datetime: string | Date; - time: string; + uuid: UUIDString; + date: DateString; + datetime: DateTimeString | Date; + time: TimeString; enum: string; object: Record; nested: Record; diff --git a/packages/sdk/src/configure/types/type.test.ts b/packages/sdk/src/configure/types/type.test.ts index 1de30840a..151e5dc7b 100644 --- a/packages/sdk/src/configure/types/type.test.ts +++ b/packages/sdk/src/configure/types/type.test.ts @@ -5,6 +5,13 @@ import type { TailorPrincipal } from "#/runtime/types"; import type { output } from "#/types/helpers"; import type { AllowedValues } from "./field"; +type DateString = `${number}-${number}-${number}`; +type TimeString = `${number}:${number}`; +type TimeZoneOffsetString = "Z" | "z" | `${"+" | "-"}${TimeString}`; +type DateTimeString = + `${DateString}${"T" | "t"}${TimeString}:${number}${"" | `.${number}`}${TimeZoneOffsetString}`; +type UUIDString = `${string}-${string}-${string}-${string}-${string}`; + describe("TailorType basic field type tests", () => { test("string field outputs string type correctly", () => { const _stringType = t.object({ @@ -42,39 +49,39 @@ describe("TailorType basic field type tests", () => { }>(); }); - test("uuid field outputs string type correctly", () => { + test("uuid field outputs UUID string type correctly", () => { const _uuidType = t.object({ id: t.uuid(), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; }>(); }); - test("date field outputs string type correctly", () => { + test("date field outputs date string type correctly", () => { const _dateType = t.object({ birthDate: t.date(), }); expectTypeOf>().toEqualTypeOf<{ - birthDate: string; + birthDate: DateString; }>(); }); - test("datetime field outputs string | Date type correctly", () => { + test("datetime field outputs datetime string | Date type correctly", () => { const _datetimeType = t.object({ createdAt: t.datetime(), }); expectTypeOf>().toEqualTypeOf<{ - createdAt: string | Date; + createdAt: DateTimeString | Date; }>(); }); - test("time field outputs string type correctly", () => { + test("time field outputs time string type correctly", () => { const _timeType = t.object({ openingTime: t.time(), }); expectTypeOf>().toEqualTypeOf<{ - openingTime: string; + openingTime: TimeString; }>(); }); }); @@ -227,7 +234,7 @@ describe("TailorType composite type tests", () => { role: t.enum(["admin", "user", "guest"]), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; name: string; email: string; age?: number | null; @@ -403,7 +410,7 @@ describe("t.object tests", () => { }); expectTypeOf>().toEqualTypeOf<{ items: { - id: string; + id: UUIDString; name: string; }[]; }>(); @@ -422,7 +429,7 @@ describe("t.object tests", () => { expectTypeOf>().toEqualTypeOf<{ optionalItems?: | { - id: string; + id: UUIDString; value?: string | null; }[] | null; @@ -602,27 +609,60 @@ describe("TailorField runtime validation tests", () => { 'Expected to match "yyyy-MM-dd" format: received 2025/12/21', ); } + + { + const result = t.date().parse({ value: "2025-02-30", data, user }); + expect(result.issues).toBeDefined(); + expect(result.issues?.[0]?.message).toEqual( + 'Expected to match "yyyy-MM-dd" format: received 2025-02-30', + ); + } }); test("validates datetime format", () => { + for (const value of [ + "2025-12-21T10:11:12Z", + "2025-12-21T10:11:12.123456Z", + "2025-12-21T10:11:12+09:00", + "2025-12-21t10:11:12-08:00", + ]) { + const result = t.datetime().parse({ value, data, invoker }); + expect(result.issues).toBeUndefined(); + if (result.issues) { + throw new Error("Unexpected issues"); + } + expect(result.value).toBe(value); + } + { const result = t.datetime().parse({ - value: "2025-12-21T10:11:12.123Z", + value: "2025-12-21T10:11:12+0900", data, invoker, }); - expect(result.issues).toBeUndefined(); - if (result.issues) { - throw new Error("Unexpected issues"); - } - expect(result.value).toBe("2025-12-21T10:11:12.123Z"); + expect(result.issues).toBeDefined(); + expect(result.issues?.[0]?.message).toEqual( + "Expected to match ISO format: received 2025-12-21T10:11:12+0900", + ); + } + + { + const result = t.datetime().parse({ value: "2025-12-21T25:11:12Z", data, invoker }); + expect(result.issues).toBeDefined(); + expect(result.issues?.[0]?.message).toEqual( + "Expected to match ISO format: received 2025-12-21T25:11:12Z", + ); } { - const result = t.datetime().parse({ value: "2025-12-21 10:11:12", data, invoker }); + const result = t.datetime().parse({ + value: "2025-12-21T10:11:12+24:00", + data, + invoker, + }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual( - "Expected to match ISO format: received 2025-12-21 10:11:12", + "Expected to match ISO format: received 2025-12-21T10:11:12+24:00", ); } }); diff --git a/packages/sdk/src/configure/types/type.ts b/packages/sdk/src/configure/types/type.ts index 2c4f5a8e0..14451b76d 100644 --- a/packages/sdk/src/configure/types/type.ts +++ b/packages/sdk/src/configure/types/type.ts @@ -133,10 +133,43 @@ const regex = { date: /^(?\d{4})-(?\d{2})-(?\d{2})$/, time: /^(?\d{2}):(?\d{2})$/, datetime: - /^(?\d{4})-(?\d{2})-(?\d{2})T(?\d{2}):(?\d{2}):(?\d{2})(.(?\d{3}))?Z$/, + /^(?\d{4})-(?\d{2})-(?\d{2})[Tt](?[01]\d|2[0-3]):(?[0-5]\d):(?[0-5]\d|60)(\.(?\d+))?(?[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/, decimal: /^-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/, } as const; +function isValidDateString(value: string): boolean { + const match = regex.date.exec(value); + return match?.groups !== undefined && isValidDateParts(match.groups); +} + +function isValidDateTimeString(value: string): boolean { + const match = regex.datetime.exec(value); + return match?.groups !== undefined && isValidDateParts(match.groups); +} + +function isValidDateParts(groups: Record): boolean { + const { year: yearValue, month: monthValue, day: dayValue } = groups; + if (yearValue === undefined || monthValue === undefined || dayValue === undefined) { + return false; + } + + const year = Number(yearValue); + const month = Number(monthValue); + const day = Number(dayValue); + if (month < 1 || month > 12 || day < 1) { + return false; + } + + const daysInMonth = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][ + month - 1 + ]; + return daysInMonth !== undefined && day <= daysInMonth; +} + +function isLeapYear(year: number): boolean { + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); +} + type FieldParseArgs = { value: unknown; data: unknown; @@ -300,7 +333,7 @@ function createTailorField< } break; case "date": - if (typeof value !== "string" || !regex.date.test(value)) { + if (typeof value !== "string" || !isValidDateString(value)) { issues.push({ message: `Expected to match "yyyy-MM-dd" format: received ${String(value)}`, path, @@ -308,7 +341,7 @@ function createTailorField< } break; case "datetime": - if (typeof value !== "string" || !regex.datetime.test(value)) { + if (typeof value !== "string" || !isValidDateTimeString(value)) { issues.push({ message: `Expected to match ISO format: received ${String(value)}`, path, @@ -622,6 +655,8 @@ function date(options?: Opt) { /** * Create a datetime field for resolver input/output. + * Format: RFC 3339 date-time, such as "yyyy-MM-ddTHH:mm:ssZ" or + * "yyyy-MM-ddTHH:mm:ss+09:00". * @param options - Field configuration options * @returns A datetime field * @example t.datetime() diff --git a/packages/sdk/src/parser/service/auth/index.test.ts b/packages/sdk/src/parser/service/auth/index.test.ts index dbf8f49d8..84c1d3172 100644 --- a/packages/sdk/src/parser/service/auth/index.test.ts +++ b/packages/sdk/src/parser/service/auth/index.test.ts @@ -6,6 +6,8 @@ import type { AuthServiceInput } from "#/configure/services/auth/types"; import type { OptionalKeysOf } from "type-fest"; import type { z } from "zod"; +type UUIDString = `${string}-${string}-${string}-${string}-${string}`; + // Define userType for type inference const userType = db.type("User", { email: db.string().unique(), @@ -90,11 +92,11 @@ describe("AuthServiceInput and AuthConfigSchema type alignment", () => { role: string; isActive: boolean; tags: string[]; - externalId: string; + externalId: UUIDString; }>(); expectTypeOf().toMatchObjectType<{ - attributeList: [string]; + attributeList: [UUIDString]; }>(); }); From 861b8826bb4c2a5d433d28094006db0adf77b64f Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 18:06:21 +0900 Subject: [PATCH 404/618] refactor(sdk): share field date validation --- .../src/configure/services/tailordb/schema.ts | 37 +----------------- .../sdk/src/configure/types/field-format.ts | 38 +++++++++++++++++++ packages/sdk/src/configure/types/type.ts | 37 +----------------- 3 files changed, 40 insertions(+), 72 deletions(-) create mode 100644 packages/sdk/src/configure/types/field-format.ts diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 010f0bcfc..94b243f95 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -4,6 +4,7 @@ import { type AllowedValuesOutput, mapAllowedValues, } from "#/configure/types/field"; +import { isValidDateString, isValidDateTimeString } from "#/configure/types/field-format"; import { brandValue } from "#/utils/brand"; import type { TailorDBField as TailorDBFieldBase, @@ -377,46 +378,10 @@ function isRelationSelfConfig( const regex = { uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, - date: /^(?\d{4})-(?\d{2})-(?\d{2})$/, time: /^(?\d{2}):(?\d{2})$/, - datetime: - /^(?\d{4})-(?\d{2})-(?\d{2})[Tt](?[01]\d|2[0-3]):(?[0-5]\d):(?[0-5]\d|60)(\.(?\d+))?(?[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/, decimal: /^-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/, } as const; -function isValidDateString(value: string): boolean { - const match = regex.date.exec(value); - return match?.groups !== undefined && isValidDateParts(match.groups); -} - -function isValidDateTimeString(value: string): boolean { - const match = regex.datetime.exec(value); - return match?.groups !== undefined && isValidDateParts(match.groups); -} - -function isValidDateParts(groups: Record): boolean { - const { year: yearValue, month: monthValue, day: dayValue } = groups; - if (yearValue === undefined || monthValue === undefined || dayValue === undefined) { - return false; - } - - const year = Number(yearValue); - const month = Number(monthValue); - const day = Number(dayValue); - if (month < 1 || month > 12 || day < 1) { - return false; - } - - const daysInMonth = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][ - month - 1 - ]; - return daysInMonth !== undefined && day <= daysInMonth; -} - -function isLeapYear(year: number): boolean { - return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); -} - type FieldParseArgs = { value: unknown; data: unknown; diff --git a/packages/sdk/src/configure/types/field-format.ts b/packages/sdk/src/configure/types/field-format.ts new file mode 100644 index 000000000..602cba1d0 --- /dev/null +++ b/packages/sdk/src/configure/types/field-format.ts @@ -0,0 +1,38 @@ +const regex = { + date: /^(?\d{4})-(?\d{2})-(?\d{2})$/, + datetime: + /^(?\d{4})-(?\d{2})-(?\d{2})[Tt](?[01]\d|2[0-3]):(?[0-5]\d):(?[0-5]\d|60)(\.(?\d+))?(?[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/, +} as const; + +export function isValidDateString(value: string): boolean { + const match = regex.date.exec(value); + return match?.groups !== undefined && isValidDateParts(match.groups); +} + +export function isValidDateTimeString(value: string): boolean { + const match = regex.datetime.exec(value); + return match?.groups !== undefined && isValidDateParts(match.groups); +} + +function isValidDateParts(groups: Record): boolean { + const { year: yearValue, month: monthValue, day: dayValue } = groups; + if (yearValue === undefined || monthValue === undefined || dayValue === undefined) { + return false; + } + + const year = Number(yearValue); + const month = Number(monthValue); + const day = Number(dayValue); + if (month < 1 || month > 12 || day < 1) { + return false; + } + + const daysInMonth = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][ + month - 1 + ]; + return daysInMonth !== undefined && day <= daysInMonth; +} + +function isLeapYear(year: number): boolean { + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); +} diff --git a/packages/sdk/src/configure/types/type.ts b/packages/sdk/src/configure/types/type.ts index 14451b76d..bcb4d1a72 100644 --- a/packages/sdk/src/configure/types/type.ts +++ b/packages/sdk/src/configure/types/type.ts @@ -1,4 +1,5 @@ import { type AllowedValues, type AllowedValuesOutput, mapAllowedValues } from "./field"; +import { isValidDateString, isValidDateTimeString } from "./field-format"; import type { DefinedFieldMetadata, TailorFieldType, @@ -130,46 +131,10 @@ type CloneableField = { clone(): TailorAnyField }; const regex = { uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, - date: /^(?\d{4})-(?\d{2})-(?\d{2})$/, time: /^(?\d{2}):(?\d{2})$/, - datetime: - /^(?\d{4})-(?\d{2})-(?\d{2})[Tt](?[01]\d|2[0-3]):(?[0-5]\d):(?[0-5]\d|60)(\.(?\d+))?(?[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/, decimal: /^-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/, } as const; -function isValidDateString(value: string): boolean { - const match = regex.date.exec(value); - return match?.groups !== undefined && isValidDateParts(match.groups); -} - -function isValidDateTimeString(value: string): boolean { - const match = regex.datetime.exec(value); - return match?.groups !== undefined && isValidDateParts(match.groups); -} - -function isValidDateParts(groups: Record): boolean { - const { year: yearValue, month: monthValue, day: dayValue } = groups; - if (yearValue === undefined || monthValue === undefined || dayValue === undefined) { - return false; - } - - const year = Number(yearValue); - const month = Number(monthValue); - const day = Number(dayValue); - if (month < 1 || month > 12 || day < 1) { - return false; - } - - const daysInMonth = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][ - month - 1 - ]; - return daysInMonth !== undefined && day <= daysInMonth; -} - -function isLeapYear(year: number): boolean { - return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); -} - type FieldParseArgs = { value: unknown; data: unknown; From 912cbdfdd3b63123593b5249ac25dadb62536176 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 18:28:32 +0900 Subject: [PATCH 405/618] fix(sdk): preserve cloned DB field output types --- .changeset/strict-tailor-field-strings.md | 2 +- .../services/tailordb/schema.test.ts | 59 +++++++ .../src/configure/services/tailordb/schema.ts | 160 ++++++++++-------- packages/sdk/src/configure/types/type.ts | 2 - 4 files changed, 151 insertions(+), 72 deletions(-) diff --git a/.changeset/strict-tailor-field-strings.md b/.changeset/strict-tailor-field-strings.md index 63f024d4f..77702114c 100644 --- a/.changeset/strict-tailor-field-strings.md +++ b/.changeset/strict-tailor-field-strings.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk": major --- -Tighten Tailor field output types for UUID, date, datetime, time, and decimal fields. Date and datetime parsing now rejects invalid calendar dates and accepts RFC 3339 datetime offsets. +Tighten Tailor field output types for UUID, date, datetime, time, and decimal fields. Date and datetime parsing now rejects invalid calendar dates and accepts ISO 8601 datetime offsets. diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index c7dbc1558..2442214d6 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -109,6 +109,39 @@ describe("TailorDBField basic field type tests", () => { id?: string | null; }>(); }); + + test("pickFields recomputes output from the base field type", () => { + const _schemaType = t.object({ + ...db + .type("Test", { + names: db.string({ array: true }), + nickname: db.string({ optional: true }), + }) + .pickFields(["id", "names", "nickname"], { array: false, optional: false }), + }); + + expectTypeOf>().toEqualTypeOf<{ + id: string; + names: string; + nickname: string; + }>(); + }); + + test("pickFields preserves existing options that are not overridden", () => { + const _schemaType = t.object({ + ...db + .type("Test", { + names: db.string({ array: true }), + nickname: db.string({ optional: true }), + }) + .pickFields(["names", "nickname"], { array: true }), + }); + + expectTypeOf>().toEqualTypeOf<{ + names: string[]; + nickname?: string[] | null; + }>(); + }); }); describe("TailorDBField optional option tests", () => { @@ -2109,6 +2142,32 @@ describe("TailorDBField clone tests", () => { expect(cloned.fields.name).not.toBe(original.fields.name); expect(cloned.fields.age).not.toBe(original.fields.age); }); + + test("clone recomputes output from the base field type", () => { + const clonedArray = db.string({ array: true }).clone({ array: true }); + const clonedScalar = db.string({ array: true }).clone({ array: false }); + const clonedRequired = db.string({ optional: true }).clone({ optional: false }); + const clonedUnchanged = db.string({ optional: true, array: true }).clone(); + const clonedOptionalArray = db.string({ optional: true }).clone({ array: true }); + const clonedRequiredArray = db + .string({ optional: true, array: true }) + .clone({ optional: false }); + + expectTypeOf>().not.toBeAny(); + expectTypeOf>().not.toBeAny(); + expectTypeOf>().not.toBeAny(); + expectTypeOf>().not.toBeAny(); + expectTypeOf>().not.toBeAny(); + expectTypeOf>().not.toBeAny(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + + const _indexed = clonedScalar.index(); + }); }); describe("TailorDBField decimal type tests", () => { diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 94b243f95..3478b3778 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -95,8 +95,26 @@ type WithDBFieldCloneOptions< Defined extends DefinedDBFieldMetadata, NewOpt extends FieldOptions, > = Omit & { - array: NewOpt extends { array: true } ? true : Defined["array"]; + array: NewOpt extends { array: infer NewArray extends boolean } ? NewArray : Defined["array"]; }; +type DBFieldCloneFieldOptions< + Defined extends DefinedDBFieldMetadata, + Output, + NewOpt extends FieldOptions, +> = { + optional: NewOpt extends { optional: infer NewOptional extends boolean } + ? NewOptional + : null extends Output + ? true + : false; + array: NewOpt extends { array: infer NewArray extends boolean } ? NewArray : Defined["array"]; +}; +type DBFieldCloneOutput< + Defined extends DefinedDBFieldMetadata, + Output, + OutputBase, + NewOpt extends FieldOptions, +> = FieldOutput>; type FileKeyConflictError< Fields extends Record, User extends object, @@ -106,77 +124,82 @@ type FileKeyConflictError< TypeLevelError<"file keys cannot use existing field names"> > >; -type DBFieldDescriptionFn = ( +type DBFieldDescriptionFn = ( description: string, -) => TailorDBField, Output>; -type DBFieldRelationFn = { +) => TailorDBField, Output, OutputBase>; +type DBFieldRelationFn = { ( config: RelationConfig, - ): TailorDBField, Output>; - (config: S): TailorDBField, Output>; + ): TailorDBField, Output, OutputBase>; + ( + config: S, + ): TailorDBField, Output, OutputBase>; }; -type DBFieldIndexFn = () => TailorDBField< - WithDBFieldIndex, - Output ->; -type DBFieldUniqueFn = () => TailorDBField< - WithDBFieldUnique, - Output ->; -type DBFieldVectorFn = () => TailorDBField< - WithDBFieldVector, - Output ->; -type DBFieldHooksFn = < +type DBFieldIndexFn< + Defined extends DefinedDBFieldMetadata, + Output, + OutputBase, +> = () => TailorDBField, Output, OutputBase>; +type DBFieldUniqueFn< + Defined extends DefinedDBFieldMetadata, + Output, + OutputBase, +> = () => TailorDBField, Output, OutputBase>; +type DBFieldVectorFn< + Defined extends DefinedDBFieldMetadata, + Output, + OutputBase, +> = () => TailorDBField, Output, OutputBase>; +type DBFieldHooksFn = < const H extends Hook, >( hooks: H, -) => TailorDBField, Output>; -type DBFieldValidateFn = ( +) => TailorDBField, Output, OutputBase>; +type DBFieldValidateFn = ( ...validate: FieldValidateInput[] -) => TailorDBField, Output>; -type DBFieldSerialFn = ( +) => TailorDBField, Output, OutputBase>; +type DBFieldSerialFn = ( config: SerialConfig, -) => TailorDBField, Output>; -type DBFieldDescriptionMethod = +) => TailorDBField, Output, OutputBase>; +type DBFieldDescriptionMethod = IsAny extends true - ? DBFieldDescriptionFn + ? DBFieldDescriptionFn : Defined extends { description: unknown } ? TypeLevelError<".description() has already been set"> - : DBFieldDescriptionFn; -type DBFieldRelationMethod = + : DBFieldDescriptionFn; +type DBFieldRelationMethod = IsAny extends true - ? DBFieldRelationFn + ? DBFieldRelationFn : Defined extends { relation: unknown } ? TypeLevelError<".relation() has already been set"> - : DBFieldRelationFn; -type DBFieldIndexMethod = + : DBFieldRelationFn; +type DBFieldIndexMethod = IsAny extends true - ? DBFieldIndexFn + ? DBFieldIndexFn : Defined extends { index: unknown } ? TypeLevelError<".index() has already been set"> : Defined extends { array: true } ? TypeLevelError<"index cannot be set on array fields"> - : DBFieldIndexFn; -type DBFieldUniqueMethod = + : DBFieldIndexFn; +type DBFieldUniqueMethod = IsAny extends true - ? DBFieldUniqueFn + ? DBFieldUniqueFn : Defined extends { unique: unknown } ? TypeLevelError<".unique() has already been set"> : Defined extends { array: true } ? TypeLevelError<"unique cannot be set on array fields"> - : DBFieldUniqueFn; -type DBFieldVectorMethod = + : DBFieldUniqueFn; +type DBFieldVectorMethod = IsAny extends true - ? DBFieldVectorFn + ? DBFieldVectorFn : Defined extends { vector: unknown } ? TypeLevelError<".vector() has already been set"> : Defined extends { type: "string"; array: false } - ? DBFieldVectorFn + ? DBFieldVectorFn : TypeLevelError<"vector can only be set on non-array string fields">; -type DBFieldHooksMethod = +type DBFieldHooksMethod = IsAny extends true - ? DBFieldHooksFn + ? DBFieldHooksFn : Defined extends { serial: true; hooks: { create: false; update: false }; @@ -188,28 +211,28 @@ type DBFieldHooksMethod = ? TypeLevelError<".hooks() has already been set"> : Defined extends { type: "nested" } ? TypeLevelError<"hooks cannot be set on nested type fields"> - : DBFieldHooksFn; -type DBFieldValidateMethod = + : DBFieldHooksFn; +type DBFieldValidateMethod = IsAny extends true - ? DBFieldValidateFn + ? DBFieldValidateFn : Defined extends { validate: unknown } ? TypeLevelError<".validate() has already been set"> - : DBFieldValidateFn; -type DBFieldSerialMethod = + : DBFieldValidateFn; +type DBFieldSerialMethod = IsAny extends true - ? DBFieldSerialFn + ? DBFieldSerialFn : Defined extends { serial: true } ? TypeLevelError<".serial() has already been set"> : Defined extends { serial: false } ? TypeLevelError<"serial cannot be set after hooks"> : IsAny extends true ? Defined extends { type: "integer" | "string"; array: false } - ? DBFieldSerialFn + ? DBFieldSerialFn : TypeLevelError<"serial can only be set on non-array integer or string fields"> : null extends Output ? TypeLevelError<"serial can only be set on non-array integer or string fields"> : Defined extends { type: "integer" | "string"; array: false } - ? DBFieldSerialFn + ? DBFieldSerialFn : TypeLevelError<"serial can only be set on non-array integer or string fields">; /** @@ -220,6 +243,7 @@ export interface TailorDBField< Defined extends DefinedDBFieldMetadata = DefinedDBFieldMetadata, // oxlint-disable-next-line no-explicit-any Output = any, + OutputBase = Output, > extends Omit, "fields"> { readonly fields: Record; _metadata: DBFieldMetadata; @@ -244,49 +268,53 @@ export interface TailorDBField< /** * Set a description for the field */ - description: DBFieldDescriptionMethod; + description: DBFieldDescriptionMethod; /** * Define a relation to another type. */ - relation: DBFieldRelationMethod; + relation: DBFieldRelationMethod; /** * Add an index to the field */ - index: DBFieldIndexMethod; + index: DBFieldIndexMethod; /** * Make the field unique (also adds an index) */ - unique: DBFieldUniqueMethod; + unique: DBFieldUniqueMethod; /** * Enable vector search on the field (string type only) */ - vector: DBFieldVectorMethod; + vector: DBFieldVectorMethod; /** * Add hooks for create/update operations on this field. */ - hooks: DBFieldHooksMethod; + hooks: DBFieldHooksMethod; /** * Add validation functions to the field. */ - validate: DBFieldValidateMethod; + validate: DBFieldValidateMethod; /** * Configure serial/auto-increment behavior */ - serial: DBFieldSerialMethod; + serial: DBFieldSerialMethod; /** * Clone the field with optional overrides for field options */ - clone( + clone>( options?: NewOpt, - ): TailorDBField, FieldOutput>; + ): TailorDBField< + WithDBFieldCloneOptions, + DBFieldCloneOutput, + OutputBase + >; } /** @@ -328,13 +356,8 @@ export interface TailorDBType< keys: K[], options: Opt, ): { - [P in K]: Fields[P] extends TailorDBField - ? TailorDBField< - Omit & { - array: Opt extends { array: true } ? true : D["array"]; - }, - FieldOutput<_O, Opt> - > + [P in K]: Fields[P] extends TailorDBField + ? TailorDBField, DBFieldCloneOutput, OBase> : never; }; omitFields(keys: K[]): Omit; @@ -417,7 +440,7 @@ type TailorDBFieldInstance< T extends TailorFieldType, Opt extends FieldOptions, OutputBase = TailorToTs[T], -> = TailorDBField, DBFieldOutput>; +> = TailorDBField, DBFieldOutput, OutputBase>; type TailorDBFieldRuntimeInstance< T extends TailorFieldType, Opt extends FieldOptions, @@ -942,8 +965,7 @@ function date(options?: Opt) { /** * Create a datetime field (date and time). - * Format: RFC 3339 date-time, such as "yyyy-MM-ddTHH:mm:ssZ" or - * "yyyy-MM-ddTHH:mm:ss+09:00". + * Format: ISO 8601 "yyyy-MM-ddTHH:mm:ssZ" * @param options - Field configuration options * @returns A datetime field * @example db.datetime() diff --git a/packages/sdk/src/configure/types/type.ts b/packages/sdk/src/configure/types/type.ts index bcb4d1a72..d1b1b249e 100644 --- a/packages/sdk/src/configure/types/type.ts +++ b/packages/sdk/src/configure/types/type.ts @@ -620,8 +620,6 @@ function date(options?: Opt) { /** * Create a datetime field for resolver input/output. - * Format: RFC 3339 date-time, such as "yyyy-MM-ddTHH:mm:ssZ" or - * "yyyy-MM-ddTHH:mm:ss+09:00". * @param options - Field configuration options * @returns A datetime field * @example t.datetime() From 9762a29c4759136a0b1976989b7f132eebf54910 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 18:43:28 +0900 Subject: [PATCH 406/618] fix(sdk): preserve enum object clone output types --- .../services/tailordb/schema.test.ts | 32 +++++++++++++++++++ .../src/configure/services/tailordb/schema.ts | 6 ++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 2442214d6..f4c76d38c 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -127,6 +127,22 @@ describe("TailorDBField basic field type tests", () => { }>(); }); + test("pickFields recomputes enum and nested object output from the base field type", () => { + const _schemaType = t.object({ + ...db + .type("Test", { + status: db.enum(["active", "inactive"], { array: true }), + profile: db.object({ name: db.string() }, { array: true }), + }) + .pickFields(["status", "profile"], { array: false }), + }); + + expectTypeOf>().toEqualTypeOf<{ + status: "active" | "inactive"; + profile: { name: string }; + }>(); + }); + test("pickFields preserves existing options that are not overridden", () => { const _schemaType = t.object({ ...db @@ -2152,6 +2168,14 @@ describe("TailorDBField clone tests", () => { const clonedRequiredArray = db .string({ optional: true, array: true }) .clone({ optional: false }); + const clonedEnumArray = db.enum(["active", "inactive"], { array: true }).clone(); + const clonedEnumScalar = db.enum(["active", "inactive"], { array: true }).clone({ + array: false, + }); + const clonedObjectArray = db.object({ name: db.string() }, { array: true }).clone(); + const clonedObjectScalar = db.object({ name: db.string() }, { array: true }).clone({ + array: false, + }); expectTypeOf>().not.toBeAny(); expectTypeOf>().not.toBeAny(); @@ -2159,12 +2183,20 @@ describe("TailorDBField clone tests", () => { expectTypeOf>().not.toBeAny(); expectTypeOf>().not.toBeAny(); expectTypeOf>().not.toBeAny(); + expectTypeOf>().not.toBeAny(); + expectTypeOf>().not.toBeAny(); + expectTypeOf>().not.toBeAny(); + expectTypeOf>().not.toBeAny(); expectTypeOf>().toEqualTypeOf(); expectTypeOf>().toEqualTypeOf(); expectTypeOf>().toEqualTypeOf(); expectTypeOf>().toEqualTypeOf(); expectTypeOf>().toEqualTypeOf(); expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf<("active" | "inactive")[]>(); + expectTypeOf>().toEqualTypeOf<"active" | "inactive">(); + expectTypeOf>().toEqualTypeOf<{ name: string }[]>(); + expectTypeOf>().toEqualTypeOf<{ name: string }>(); const _indexed = clonedScalar.index(); }); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 3478b3778..477c35243 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -998,7 +998,8 @@ function _enum( options?: Opt, ): TailorDBField< { type: "enum"; array: Opt extends { array: true } ? true : false }, - FieldOutput, Opt> + FieldOutput, Opt>, + AllowedValuesOutput > { return createField<"enum", Opt, AllowedValuesOutput>("enum", options, undefined, values); } @@ -1017,7 +1018,8 @@ function object< >(fields: F, options?: Opt) { return createField("nested", options, fields) as unknown as TailorDBField< { type: "nested"; array: Opt extends { array: true } ? true : false }, - FieldOutput, Opt> + FieldOutput, Opt>, + InferFieldsOutput >; } From 29f4d772d0b947cb77e39a58d1065dcffd311bab Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 18:55:32 +0900 Subject: [PATCH 407/618] fix(sdk): keep array clone guards conservative --- .../configure/services/tailordb/schema.test.ts | 15 +++++++++++++++ .../sdk/src/configure/services/tailordb/schema.ts | 12 ++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index f4c76d38c..5377a9e53 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -2200,6 +2200,21 @@ describe("TailorDBField clone tests", () => { const _indexed = clonedScalar.index(); }); + + test("clone preserves array guards for dynamic array overrides", () => { + const maybeArray = true as boolean; + const clonedExistingArray = db.string({ array: true }).clone({ array: maybeArray }); + const clonedExistingScalar = db.string().clone({ array: maybeArray }); + + expectTypeOf>().toEqualTypeOf(); + expectTypeOf(clonedExistingArray.index).toEqualTypeOf< + TypeLevelError<"index cannot be set on array fields"> + >(); + expectTypeOf(clonedExistingArray.unique).toEqualTypeOf< + TypeLevelError<"unique cannot be set on array fields"> + >(); + expectTypeOf>().toEqualTypeOf(); + }); }); describe("TailorDBField decimal type tests", () => { diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 477c35243..2b58c93d8 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -95,8 +95,16 @@ type WithDBFieldCloneOptions< Defined extends DefinedDBFieldMetadata, NewOpt extends FieldOptions, > = Omit & { - array: NewOpt extends { array: infer NewArray extends boolean } ? NewArray : Defined["array"]; + array: DBFieldCloneArrayOption; }; +type DBFieldCloneArrayOption< + Defined extends DefinedDBFieldMetadata, + NewOpt extends FieldOptions, +> = NewOpt extends { array: false } + ? false + : NewOpt extends { array: true } + ? true + : Defined["array"]; type DBFieldCloneFieldOptions< Defined extends DefinedDBFieldMetadata, Output, @@ -107,7 +115,7 @@ type DBFieldCloneFieldOptions< : null extends Output ? true : false; - array: NewOpt extends { array: infer NewArray extends boolean } ? NewArray : Defined["array"]; + array: DBFieldCloneArrayOption; }; type DBFieldCloneOutput< Defined extends DefinedDBFieldMetadata, From 0db61d78104c7ab2890d8e2582f22da01ce131ae Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 19:18:54 +0900 Subject: [PATCH 408/618] docs(sdk): show datetime offset format --- packages/sdk/src/configure/services/tailordb/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 2b58c93d8..c308586e0 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -973,7 +973,7 @@ function date(options?: Opt) { /** * Create a datetime field (date and time). - * Format: ISO 8601 "yyyy-MM-ddTHH:mm:ssZ" + * Format: ISO 8601 "yyyy-MM-ddTHH:mm:ssZ" or "yyyy-MM-ddTHH:mm:ss+09:00" * @param options - Field configuration options * @returns A datetime field * @example db.datetime() From cb14f206ed6db92644162023e2e891a203926b52 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 2 Jul 2026 19:32:29 +0900 Subject: [PATCH 409/618] test(sdk): align validation fixtures with v2 --- .../services/tailordb/schema.test.ts | 22 ++++++++++++++----- packages/sdk/src/configure/types/type.test.ts | 2 +- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 5377a9e53..c242c36ee 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -1698,7 +1698,7 @@ describe("TailorDBField runtime validation tests", () => { 'Expected to match "yyyy-MM-dd" format: received 2025/01/01', ); - const invalidDate = field.parse({ value: "2025-02-30", data, user }); + const invalidDate = field.parse({ value: "2025-02-30", data, invoker }); expect(invalidDate.issues?.[0]?.message).toBe( 'Expected to match "yyyy-MM-dd" format: received 2025-02-30', ); @@ -1712,7 +1712,7 @@ describe("TailorDBField runtime validation tests", () => { "2025-01-01T10:11:12+09:00", "2025-01-01t10:11:12-08:00", ]) { - const ok = field.parse({ value, data, user }); + const ok = field.parse({ value, data, invoker }); expect(ok.issues).toBeUndefined(); if (ok.issues) { throw new Error("Unexpected issues"); @@ -1720,17 +1720,29 @@ describe("TailorDBField runtime validation tests", () => { expect(ok.value).toBe(value); } - const bad = field.parse({ value: "2025-01-01T10:11:12+0900", data, user }); + const bad = field.parse({ + value: "2025-01-01T10:11:12+0900", + data, + invoker, + }); expect(bad.issues?.[0]?.message).toBe( "Expected to match ISO format: received 2025-01-01T10:11:12+0900", ); - const invalidTime = field.parse({ value: "2025-01-01T25:11:12Z", data, user }); + const invalidTime = field.parse({ + value: "2025-01-01T25:11:12Z", + data, + invoker, + }); expect(invalidTime.issues?.[0]?.message).toBe( "Expected to match ISO format: received 2025-01-01T25:11:12Z", ); - const invalidOffset = field.parse({ value: "2025-01-01T10:11:12+24:00", data, user }); + const invalidOffset = field.parse({ + value: "2025-01-01T10:11:12+24:00", + data, + invoker, + }); expect(invalidOffset.issues?.[0]?.message).toBe( "Expected to match ISO format: received 2025-01-01T10:11:12+24:00", ); diff --git a/packages/sdk/src/configure/types/type.test.ts b/packages/sdk/src/configure/types/type.test.ts index 151e5dc7b..8ea891bf6 100644 --- a/packages/sdk/src/configure/types/type.test.ts +++ b/packages/sdk/src/configure/types/type.test.ts @@ -611,7 +611,7 @@ describe("TailorField runtime validation tests", () => { } { - const result = t.date().parse({ value: "2025-02-30", data, user }); + const result = t.date().parse({ value: "2025-02-30", data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual( 'Expected to match "yyyy-MM-dd" format: received 2025-02-30', From c33a8d62b83e58f039d667d0c010153e398da1c2 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 07:30:46 +0900 Subject: [PATCH 410/618] fix(sdk): preserve user profile type input --- .../sdk/src/parser/service/auth/index.test.ts | 31 +- .../sdk/src/parser/service/auth/schema.ts | 6 +- .../service/tailordb/builder-helpers.ts | 30 +- packages/sdk/src/types/auth.generated.ts | 909 +++++++++++++++++- 4 files changed, 946 insertions(+), 30 deletions(-) diff --git a/packages/sdk/src/parser/service/auth/index.test.ts b/packages/sdk/src/parser/service/auth/index.test.ts index ed0907ee6..1f153fc22 100644 --- a/packages/sdk/src/parser/service/auth/index.test.ts +++ b/packages/sdk/src/parser/service/auth/index.test.ts @@ -4,6 +4,7 @@ import { t } from "#/configure/types/type"; import { brandValue } from "#/utils/brand"; import { AuthConfigSchema, OAuth2ClientSchema } from "./schema"; import type { AuthServiceInput } from "#/configure/services/auth/types"; +import type { TailorDBInstance } from "#/configure/services/tailordb/types"; import type { OptionalKeysOf } from "type-fest"; import type { z } from "zod"; @@ -29,6 +30,7 @@ type AuthInput = AuthServiceInput["admin"]; type AuthSchemaInput = Omit, "name">; +type IsUnknown = unknown extends T ? ([keyof T] extends [never] ? true : false) : false; describe("AuthServiceInput and AuthConfigSchema type alignment", () => { test("aligns top-level keys and optionality with the schema", () => { @@ -61,6 +63,8 @@ describe("AuthServiceInput and AuthConfigSchema type alignment", () => { type AlignedSchemaAttributes = Pick; expectTypeOf().toMatchObjectType(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf().toExtend(); expectTypeOf().toExtend(); expectTypeOf().toExtend< SchemaUserProfile["usernameField"] @@ -396,7 +400,24 @@ describe("AuthConfigSchema userProfile/machineUserAttributes validation", () => expect(result.userProfile?.type).not.toHaveProperty("_output"); }); - test("rejects unknown userProfile.type keys after stripping TailorDB builder helpers", () => { + test("strips TailorDB type builder helpers from unbranded userProfile.type copies", () => { + const result = AuthConfigSchema.parse({ + name: "my-auth", + userProfile: { + type: { ...userType }, + usernameField: "email", + }, + }); + + expect(result.userProfile?.type).toMatchObject({ + name: "User", + fields: expect.any(Object), + }); + expect(result.userProfile?.type).not.toHaveProperty("hooks"); + expect(result.userProfile?.type).not.toHaveProperty("_output"); + }); + + test("omits unknown outer userProfile.type keys with TailorDB builder helpers", () => { const typeWithUnknownKey = brandValue( { ...userType, @@ -405,7 +426,7 @@ describe("AuthConfigSchema userProfile/machineUserAttributes validation", () => "tailordb-type", ); - const result = AuthConfigSchema.safeParse({ + const result = AuthConfigSchema.parse({ name: "my-auth", userProfile: { type: typeWithUnknownKey, @@ -413,11 +434,7 @@ describe("AuthConfigSchema userProfile/machineUserAttributes validation", () => }, }); - expect(result.success).toBe(false); - if (result.success) { - throw new Error("Expected AuthConfigSchema parsing to fail"); - } - expect(JSON.stringify(result.error.issues)).toContain("unknownOption"); + expect(result.userProfile?.type).not.toHaveProperty("unknownOption"); }); test("rejects invalid TailorDB fields in userProfile.type", () => { diff --git a/packages/sdk/src/parser/service/auth/schema.ts b/packages/sdk/src/parser/service/auth/schema.ts index 90a570659..1a4a3d1d6 100644 --- a/packages/sdk/src/parser/service/auth/schema.ts +++ b/packages/sdk/src/parser/service/auth/schema.ts @@ -4,6 +4,7 @@ import { TailorFieldSchema } from "#/parser/service/field/schema"; import { stripTailorDBTypeBuilderHelpers } from "#/parser/service/tailordb/builder-helpers"; import { TailorDBTypeSchema } from "#/parser/service/tailordb/index"; import type { ValueOperand } from "#/configure/services/auth/types"; +import type { TailorDBInstance } from "#/configure/services/tailordb/types"; export const AuthInvokerObjectSchema = z.strictObject({ namespace: z.string().describe("Auth namespace"), @@ -199,7 +200,10 @@ export const TenantProviderSchema = z.strictObject({ const UserProfileSchema = z.strictObject({ namespace: z.string().optional().describe("TailorDB namespace where the user type is defined"), - type: z.preprocess(stripTailorDBTypeBuilderHelpers, TailorDBTypeSchema), + type: z + .custom() + .transform(stripTailorDBTypeBuilderHelpers) + .pipe(TailorDBTypeSchema), usernameField: z.string(), attributes: z.record(z.string(), z.literal(true)).optional(), attributeList: z.array(z.string()).optional(), diff --git a/packages/sdk/src/parser/service/tailordb/builder-helpers.ts b/packages/sdk/src/parser/service/tailordb/builder-helpers.ts index 905e03862..675c5e0c3 100644 --- a/packages/sdk/src/parser/service/tailordb/builder-helpers.ts +++ b/packages/sdk/src/parser/service/tailordb/builder-helpers.ts @@ -1,30 +1,18 @@ -import { isSdkBranded } from "#/utils/brand"; +import { TailorDBTypeSchema } from "./schema"; -const TAILORDB_TYPE_BUILDER_HELPER_KEYS = [ - "_output", - "_description", - "hooks", - "validate", - "features", - "indexes", - "files", - "permission", - "gqlPermission", - "description", - "pickFields", - "omitFields", - "plugins", - "plugin", -] as const; +const TAILORDB_TYPE_SCHEMA_KEYS = TailorDBTypeSchema.keyof().options; export function stripTailorDBTypeBuilderHelpers(type: unknown): unknown { - if (!isSdkBranded(type, "tailordb-type")) { + if (typeof type !== "object" || type === null) { return type; } - const config = { ...(type as Record) }; - for (const key of TAILORDB_TYPE_BUILDER_HELPER_KEYS) { - delete config[key]; + const config: Record = {}; + const input = type as Record; + for (const key of TAILORDB_TYPE_SCHEMA_KEYS) { + if (Object.prototype.hasOwnProperty.call(input, key)) { + config[key] = input[key]; + } } return config; } diff --git a/packages/sdk/src/types/auth.generated.ts b/packages/sdk/src/types/auth.generated.ts index 8edbea0a7..efe5d57e8 100644 --- a/packages/sdk/src/types/auth.generated.ts +++ b/packages/sdk/src/types/auth.generated.ts @@ -425,7 +425,914 @@ export type AuthConfigInput = publishSessionEvents?: boolean | undefined; userProfile?: | { - type: unknown; + type: { + readonly name: string; + readonly fields: any; + readonly _output: {}; + readonly metadata: { + name: string; + description?: string | undefined; + settings?: + | { + pluralForm?: string | undefined; + aggregation?: boolean | undefined; + bulkUpsert?: boolean | undefined; + gqlOperations?: + | "query" + | { + create?: boolean | undefined | undefined; + update?: boolean | undefined | undefined; + delete?: boolean | undefined | undefined; + read?: boolean | undefined | undefined; + } + | undefined; + publishEvents?: boolean | undefined; + } + | undefined; + permissions: { + record?: + | { + create: readonly ( + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + boolean, + ] + | readonly ( + | boolean + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + )[] + | { + conditions: + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly (readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ])[]; + description?: string | undefined | undefined; + permit?: boolean | undefined | undefined; + } + )[]; + read: readonly ( + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + boolean, + ] + | readonly ( + | boolean + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + )[] + | { + conditions: + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly (readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ])[]; + description?: string | undefined | undefined; + permit?: boolean | undefined | undefined; + } + )[]; + update: readonly ( + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + boolean, + ] + | readonly ( + | boolean + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + )[] + | { + conditions: + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly (readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ])[]; + description?: string | undefined | undefined; + permit?: boolean | undefined | undefined; + } + )[]; + delete: readonly ( + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + boolean, + ] + | readonly ( + | boolean + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + )[] + | { + conditions: + | readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ] + | readonly (readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + | { + record: string; + } + | { + oldRecord: string; + } + | { + newRecord: string; + } + ), + ])[]; + description?: string | undefined | undefined; + permit?: boolean | undefined | undefined; + } + )[]; + } + | undefined; + gql?: + | readonly { + conditions: readonly (readonly [ + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + ), + "in" | "=" | "!=" | "not in" | "hasAny" | "not hasAny", + ( + | string + | boolean + | string[] + | boolean[] + | { + user: string; + } + ), + ])[]; + actions: + | "all" + | readonly ( + | "bulkUpsert" + | "create" + | "update" + | "delete" + | "read" + | "aggregate" + )[]; + permit?: boolean | undefined | undefined; + description?: string | undefined | undefined; + }[] + | undefined; + }; + files: { + [x: string]: string; + }; + indexes?: + | { + [x: string]: { + fields: string[]; + unique?: boolean | undefined; + }; + } + | undefined; + }; + readonly plugins: { + pluginId: string; + config: unknown; + }[]; + }; usernameField: string; namespace?: string | undefined; attributes?: From b71c5d80e7ebedbd8297ac8f9ea8ddd68dfba018 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 07:34:34 +0900 Subject: [PATCH 411/618] fix: handle auth connection token codemod edge cases --- .../scripts/transform.ts | 59 ++++++++++++++++++- .../tests/bare-arrow-shadowed/input.ts | 3 + .../tests/catch-shadowed/input.ts | 9 +++ .../expected.ts | 11 ++++ .../input.ts | 11 ++++ .../remove-last-import-specifier/expected.ts | 9 +++ .../remove-last-import-specifier/input.ts | 8 +++ packages/sdk-codemod/src/runner.test.ts | 17 ++++++ 8 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/bare-arrow-shadowed/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/catch-shadowed/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/multiline-import-leading-specifier/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/multiline-import-leading-specifier/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/remove-last-import-specifier/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/remove-last-import-specifier/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 9040e4094..db26bbb2e 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -21,7 +21,7 @@ interface TokenCall { } function quickFilter(source: string): boolean { - return source.includes(GET_CONNECTION_TOKEN) && source.includes("tailor.config"); + return source.includes(GET_CONNECTION_TOKEN); } function sourceLang(filePath: string, source: string): Lang { @@ -168,6 +168,14 @@ function firstDeclaratorChild(node: SgNode): SgNode | null { return node.children().find((child) => child.kind() !== "=") ?? null; } +function collectDirectBindingChildren(node: SgNode, names: Set): void { + for (const child of node.children()) { + if (["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind())) { + collectBindingNames(child, names); + } + } +} + function localDeclarationNames(root: SgNode): Set { const names = new Set(); for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { @@ -202,6 +210,22 @@ function localDeclarationNames(root: SgNode): Set { .find((child) => child.kind() === "identifier" || child.kind() === "type_identifier"); if (name) names.add(name.text()); } + for (const catchClause of root.findAll({ rule: { kind: "catch_clause" } })) { + collectDirectBindingChildren(catchClause, names); + } + for (const arrow of root.findAll({ rule: { kind: "arrow_function" } })) { + const children = arrow.children(); + const arrowIndex = children.findIndex((child) => child.kind() === "=>"); + if (arrowIndex === -1) continue; + for (const child of children.slice(0, arrowIndex)) { + if (child.kind() === "=") break; + if ( + ["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind()) + ) { + collectBindingNames(child, names); + } + } + } return names; } @@ -306,6 +330,25 @@ function buildAddRuntimeImportEdit(root: SgNode, source: string, imports: SgNode return { startPos: pos, endPos: pos, insertedText }; } +function lineStartIndex(source: string, index: number): number { + let pos = index; + while (pos > 0 && source[pos - 1] !== "\n" && source[pos - 1] !== "\r") pos--; + return pos; +} + +function consumeLineBreak(source: string, index: number): number { + if (source[index] === "\r") return source[index + 1] === "\n" ? index + 2 : index + 1; + if (source[index] === "\n") return index + 1; + return index; +} + +function isHorizontalWhitespace(source: string, start: number, end: number): boolean { + for (let index = start; index < end; index++) { + if (source[index] !== " " && source[index] !== "\t") return false; + } + return true; +} + function buildImportRemovalEdit(source: string, binding: AuthImport): Edit | null { const allSpecs = binding.importStmt.findAll({ rule: { kind: "import_specifier" } }); if (allSpecs.length === 1) { @@ -318,16 +361,28 @@ function buildImportRemovalEdit(source: string, binding: AuthImport): Edit | nul const range = binding.spec.range(); let start = range.start.index; let end = range.end.index; + const specEnd = end; while (end < source.length && /[ \t]/.test(source[end]!)) end++; if (source[end] === ",") { end++; while (end < source.length && /[ \t]/.test(source[end]!)) end++; + const nextLine = consumeLineBreak(source, end); + const lineStart = lineStartIndex(source, start); + if (nextLine !== end && isHorizontalWhitespace(source, lineStart, start)) { + return { startPos: lineStart, endPos: nextLine, insertedText: "" }; + } return { startPos: start, endPos: end, insertedText: "" }; } + const nextLine = consumeLineBreak(source, end); + const lineStart = lineStartIndex(source, start); + if (nextLine !== end && isHorizontalWhitespace(source, lineStart, start)) { + return { startPos: lineStart, endPos: nextLine, insertedText: "" }; + } + while (start > 0 && /[ \t]/.test(source[start - 1]!)) start--; if (source[start - 1] === ",") start--; - return { startPos: start, endPos, insertedText: "" }; + return { startPos: start, endPos: specEnd, insertedText: "" }; } function applyEdits(source: string, edits: Edit[]): string { diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/bare-arrow-shadowed/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/bare-arrow-shadowed/input.ts new file mode 100644 index 000000000..54de5e380 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/bare-arrow-shadowed/input.ts @@ -0,0 +1,3 @@ +import { auth } from "../tailor.config"; + +export const tokens = ["google"].map(auth => auth.getConnectionToken("google")); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/catch-shadowed/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/catch-shadowed/input.ts new file mode 100644 index 000000000..21e6d2f99 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/catch-shadowed/input.ts @@ -0,0 +1,9 @@ +import { auth } from "../tailor.config"; + +export async function run() { + try { + return "ok"; + } catch (auth) { + return auth.getConnectionToken("google"); + } +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/multiline-import-leading-specifier/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/multiline-import-leading-specifier/expected.ts new file mode 100644 index 000000000..5898d3f2d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/multiline-import-leading-specifier/expected.ts @@ -0,0 +1,11 @@ +import { + other, +} from "../tailor.config"; +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export async function run() { + return { + token: await authconnection.getConnectionToken("google"), + other, + }; +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/multiline-import-leading-specifier/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/multiline-import-leading-specifier/input.ts new file mode 100644 index 000000000..c3a496bc2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/multiline-import-leading-specifier/input.ts @@ -0,0 +1,11 @@ +import { + auth, + other, +} from "../tailor.config"; + +export async function run() { + return { + token: await auth.getConnectionToken("google"), + other, + }; +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/remove-last-import-specifier/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/remove-last-import-specifier/expected.ts new file mode 100644 index 000000000..78370bdda --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/remove-last-import-specifier/expected.ts @@ -0,0 +1,9 @@ +import { other } from "../tailor.config"; +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export async function run() { + return { + token: await authconnection.getConnectionToken("google"), + other, + }; +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/remove-last-import-specifier/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/remove-last-import-specifier/input.ts new file mode 100644 index 000000000..7d6a23893 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/remove-last-import-specifier/input.ts @@ -0,0 +1,8 @@ +import { other, auth } from "../tailor.config"; + +export async function run() { + return { + token: await auth.getConnectionToken("google"), + other, + }; +} diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 799c9c6b5..7e426414f 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1073,6 +1073,17 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "reexported-config.ts"), + [ + 'import { auth } from "../app-config";', + "", + "export async function run() {", + ' return auth.getConnectionToken("google");', + "}", + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "computed.ts"), [ @@ -1140,6 +1151,7 @@ describe("runCodemods", () => { "computed.ts", "default-import.ts", "destructure.ts", + "reexported-config.ts", "shadowed.ts", ], findings: [ @@ -1168,6 +1180,11 @@ describe("runCodemods", () => { line: 3, excerpt: "export const { getConnectionToken } = auth;", }), + expect.objectContaining({ + file: "reexported-config.ts", + line: 4, + excerpt: 'return auth.getConnectionToken("google");', + }), expect.objectContaining({ file: "shadowed.ts", line: 4, From 2f76418833d321ca0bf36523362ca9235bf1ef0c Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 07:40:28 +0900 Subject: [PATCH 412/618] refactor: share codemod ast-grep helpers --- .../scripts/transform.ts | 128 ++------------- .../scripts/transform.ts | 136 +-------------- packages/sdk-codemod/src/ast-grep-helpers.ts | 155 ++++++++++++++++++ 3 files changed, 173 insertions(+), 246 deletions(-) create mode 100644 packages/sdk-codemod/src/ast-grep-helpers.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index db26bbb2e..6e13161fa 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -1,4 +1,12 @@ import { parse, Lang } from "@ast-grep/napi"; +import { + findImportStatements, + importSource, + importSpecNames, + isTypeOnlyImport, + localDeclarationNames, + namedImportsNode, +} from "../../../../src/ast-grep-helpers"; import type { LlmReviewFinding } from "../../../../src/types"; import type { Edit, SgNode } from "@ast-grep/napi"; @@ -43,43 +51,10 @@ function parseRoot(source: string, filePath: string): SgNode | null { } } -function stringValue(node: SgNode | null): string | null { - return node?.text().replace(/^['"]|['"]$/g, "") ?? null; -} - -function importSource(importStmt: SgNode): string | null { - return stringValue(importStmt.find({ rule: { kind: "string" } }) ?? null); -} - function isTailorConfigSource(source: string): boolean { return /(^|\/)tailor\.config(?:\.(?:ts|tsx|js|jsx|mts|cts|mjs|cjs))?$/.test(source); } -function isTypeOnlyImport(importStmt: SgNode): boolean { - return importStmt.children().some((child) => child.kind() === "type"); -} - -function namedImportsNode(importStmt: SgNode): SgNode | null { - return importStmt.find({ rule: { kind: "named_imports" } }) ?? null; -} - -function importSpecNames(spec: SgNode): { importedName: string; localName: string } | null { - if (spec.children().some((child) => child.kind() === "type")) return null; - const ids = spec.children().filter((child) => child.kind() === "identifier"); - if (ids.length === 0) return null; - return { - importedName: ids[0]!.text(), - localName: ids[1]?.text() ?? ids[0]!.text(), - }; -} - -function findImportStatements(root: SgNode): SgNode[] { - return root - .findAll({ rule: { kind: "import_statement" } }) - .filter((stmt) => stmt.parent()?.kind() === "program") - .toSorted((a, b) => a.range().start.index - b.range().start.index); -} - function findAuthImports(imports: SgNode[]): AuthImport[] { const authImports: AuthImport[] = []; for (const importStmt of imports) { @@ -88,7 +63,7 @@ function findAuthImports(imports: SgNode[]): AuthImport[] { for (const spec of importStmt.findAll({ rule: { kind: "import_specifier" } })) { const names = importSpecNames(spec); - if (names?.importedName !== "auth") continue; + if (names?.importedName !== "auth" || names.typeOnly) continue; authImports.push({ importStmt, localName: names.localName, spec }); } } @@ -113,7 +88,7 @@ function importLocalNames(importStmt: SgNode): Set { if (child.kind() !== "named_imports") continue; for (const spec of child.findAll({ rule: { kind: "import_specifier" } })) { const specNames = importSpecNames(spec); - if (specNames) names.add(specNames.localName); + if (specNames && !specNames.typeOnly) names.add(specNames.localName); } } return names; @@ -146,89 +121,6 @@ function runtimeNamedValueImport(imports: SgNode[]): SgNode | null { ); } -function collectBindingNames(node: SgNode, names: Set): void { - const kind = node.kind(); - if ( - kind === "identifier" || - kind === "type_identifier" || - kind === "shorthand_property_identifier_pattern" - ) { - names.add(node.text()); - return; - } - - for (const child of node.children()) { - if (child.kind() === "property_identifier") continue; - if (child.kind() === "=") break; - collectBindingNames(child, names); - } -} - -function firstDeclaratorChild(node: SgNode): SgNode | null { - return node.children().find((child) => child.kind() !== "=") ?? null; -} - -function collectDirectBindingChildren(node: SgNode, names: Set): void { - for (const child of node.children()) { - if (["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind())) { - collectBindingNames(child, names); - } - } -} - -function localDeclarationNames(root: SgNode): Set { - const names = new Set(); - for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { - const binding = firstDeclaratorChild(decl); - if (binding) collectBindingNames(binding, names); - } - for (const param of root.findAll({ - rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] }, - })) { - const binding = param - .children() - .find((child) => - ["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind()), - ); - if (binding) collectBindingNames(binding, names); - } - for (const decl of root.findAll({ - rule: { - any: [ - { kind: "function_declaration" }, - { kind: "class_declaration" }, - { kind: "class" }, - { kind: "enum_declaration" }, - { kind: "interface_declaration" }, - { kind: "type_alias_declaration" }, - { kind: "import_alias" }, - ], - }, - })) { - const name = decl - .children() - .find((child) => child.kind() === "identifier" || child.kind() === "type_identifier"); - if (name) names.add(name.text()); - } - for (const catchClause of root.findAll({ rule: { kind: "catch_clause" } })) { - collectDirectBindingChildren(catchClause, names); - } - for (const arrow of root.findAll({ rule: { kind: "arrow_function" } })) { - const children = arrow.children(); - const arrowIndex = children.findIndex((child) => child.kind() === "=>"); - if (arrowIndex === -1) continue; - for (const child of children.slice(0, arrowIndex)) { - if (child.kind() === "=") break; - if ( - ["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind()) - ) { - collectBindingNames(child, names); - } - } - } - return names; -} - function hasRuntimeImportCollision(root: SgNode, imports: SgNode[]): boolean { if (localDeclarationNames(root).has(AUTHCONNECTION)) return true; return imports.some( diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts index d77690d09..77ecf3543 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts @@ -1,4 +1,12 @@ import { parse, Lang } from "@ast-grep/napi"; +import { + findImportStatements, + importSource, + importSpecNames, + isTypeOnlyImport, + localDeclarationNames, + namedImportsNode, +} from "../../../../src/ast-grep-helpers"; import type { Edit, SgNode } from "@ast-grep/napi"; const RUNTIME_MODULE = "@tailor-platform/sdk/runtime"; @@ -22,35 +30,6 @@ function sourceLang(filePath: string, source: string): Lang { : Lang.TypeScript; } -function stringValue(node: SgNode | null): string | null { - return node?.text().replace(/^['"]|['"]$/g, "") ?? null; -} - -function isTypeOnlyImport(stmt: SgNode): boolean { - return stmt.children().some((child) => child.kind() === "type"); -} - -function importSource(stmt: SgNode): string | null { - const source = stmt.find({ rule: { kind: "string" } }); - return stringValue(source ?? null); -} - -function namedImportsNode(importStmt: SgNode): SgNode | null { - return importStmt.find({ rule: { kind: "named_imports" } }) ?? null; -} - -function importSpecNames( - spec: SgNode, -): { importedName: string; localName: string; typeOnly: boolean } | null { - const ids = spec.children().filter((child) => child.kind() === "identifier"); - if (ids.length === 0) return null; - return { - importedName: ids[0]!.text(), - localName: ids[1]?.text() ?? ids[0]!.text(), - typeOnly: spec.children().some((child) => child.kind() === "type"), - }; -} - function importBindings(importStmt: SgNode): ImportBinding[] { const source = importSource(importStmt); if (!source) return []; @@ -91,105 +70,6 @@ function importBindings(importStmt: SgNode): ImportBinding[] { return bindings; } -function collectBindingNames(node: SgNode, out: Set): void { - const kind = node.kind(); - if ( - kind === "identifier" || - kind === "type_identifier" || - kind === "shorthand_property_identifier_pattern" - ) { - out.add(node.text()); - return; - } - - for (const child of node.children()) { - if (child.kind() === "property_identifier") continue; - collectBindingNames(child, out); - } -} - -function firstDeclaratorChild(node: SgNode): SgNode | null { - return node.children().find((child) => child.kind() !== "=") ?? null; -} - -function localDeclarationNames(root: SgNode): Set { - const names = new Set(); - - for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { - const binding = firstDeclaratorChild(decl); - if (binding) collectBindingNames(binding, names); - } - - for (const param of root.findAll({ - rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] }, - })) { - const binding = param - .children() - .find((child) => - ["identifier", "object_pattern", "array_pattern", "rest_pattern"].includes(child.kind()), - ); - if (binding) collectBindingNames(binding, names); - } - - for (const decl of root.findAll({ - rule: { - any: [ - { kind: "function_declaration" }, - { kind: "function_expression" }, - { kind: "class_declaration" }, - { kind: "class" }, - { kind: "interface_declaration" }, - { kind: "type_alias_declaration" }, - { kind: "enum_declaration" }, - { kind: "internal_module" }, - { kind: "import_alias" }, - ], - }, - })) { - const name = decl - .children() - .find((child) => child.kind() === "identifier" || child.kind() === "type_identifier"); - if (name) names.add(name.text()); - } - - for (const catchClause of root.findAll({ rule: { kind: "catch_clause" } })) { - for (const child of catchClause.children()) { - if (["identifier", "object_pattern", "array_pattern"].includes(child.kind())) { - collectBindingNames(child, names); - } - } - } - - for (const arrow of root.findAll({ rule: { kind: "arrow_function" } })) { - const children = arrow.children(); - const arrowIndex = children.findIndex((child) => child.kind() === "=>"); - if (arrowIndex === -1) continue; - for (const child of children.slice(0, arrowIndex)) { - collectBindingNames(child, names); - } - } - - for (const loop of root.findAll({ rule: { kind: "for_in_statement" } })) { - const children = loop.children(); - const keywordIndex = children.findIndex( - (child) => child.kind() === "in" || child.kind() === "of", - ); - if (keywordIndex === -1) continue; - for (const child of children.slice(0, keywordIndex)) { - collectBindingNames(child, names); - } - } - - return names; -} - -function findImportStatements(root: SgNode): SgNode[] { - return root - .findAll({ rule: { kind: "import_statement" } }) - .filter((stmt) => stmt.parent()?.kind() === "program") - .toSorted((a, b) => a.range().start.index - b.range().start.index); -} - function runtimeIdpLocalName(imports: SgNode[]): string | null { for (const importStmt of imports) { for (const binding of importBindings(importStmt)) { diff --git a/packages/sdk-codemod/src/ast-grep-helpers.ts b/packages/sdk-codemod/src/ast-grep-helpers.ts new file mode 100644 index 000000000..a81e08edc --- /dev/null +++ b/packages/sdk-codemod/src/ast-grep-helpers.ts @@ -0,0 +1,155 @@ +import type { SgNode } from "@ast-grep/napi"; + +const DECLARATION_KINDS = [ + "function_declaration", + "function_expression", + "class_declaration", + "class", + "enum_declaration", + "interface_declaration", + "type_alias_declaration", + "internal_module", + "import_alias", +]; + +export interface ImportSpecifierNames { + importedName: string; + localName: string; + typeOnly: boolean; +} + +function isBindingLeafKind(kind: ReturnType): boolean { + return ( + kind === "identifier" || + kind === "type_identifier" || + kind === "shorthand_property_identifier_pattern" + ); +} + +function isBindingPatternKind(kind: ReturnType): boolean { + return ( + isBindingLeafKind(kind) || + kind === "object_pattern" || + kind === "array_pattern" || + kind === "rest_pattern" + ); +} + +export function stringValue(node: SgNode | null): string | null { + return node?.text().replace(/^['"]|['"]$/g, "") ?? null; +} + +export function importSource(importStmt: SgNode): string | null { + return stringValue(importStmt.find({ rule: { kind: "string" } }) ?? null); +} + +export function isTypeOnlyImport(importStmt: SgNode): boolean { + return importStmt.children().some((child) => child.kind() === "type"); +} + +export function namedImportsNode(importStmt: SgNode): SgNode | null { + return importStmt.find({ rule: { kind: "named_imports" } }) ?? null; +} + +export function importSpecNames(spec: SgNode): ImportSpecifierNames | null { + const ids = spec.children().filter((child) => child.kind() === "identifier"); + if (ids.length === 0) return null; + return { + importedName: ids[0]!.text(), + localName: ids[1]?.text() ?? ids[0]!.text(), + typeOnly: spec.children().some((child) => child.kind() === "type"), + }; +} + +export function findImportStatements(root: SgNode): SgNode[] { + return root + .findAll({ rule: { kind: "import_statement" } }) + .filter((stmt) => stmt.parent()?.kind() === "program") + .toSorted((a, b) => a.range().start.index - b.range().start.index); +} + +export function collectBindingNames(node: SgNode, names: Set): void { + if (isBindingLeafKind(node.kind())) { + names.add(node.text()); + return; + } + + for (const child of node.children()) { + if (child.kind() === "property_identifier") continue; + if (child.kind() === "=") break; + collectBindingNames(child, names); + } +} + +function firstDeclaratorChild(node: SgNode): SgNode | null { + return node.children().find((child) => child.kind() !== "=") ?? null; +} + +function collectDirectBindingChildren(node: SgNode, names: Set): void { + for (const child of node.children()) { + if (isBindingPatternKind(child.kind())) collectBindingNames(child, names); + } +} + +function collectParameters(root: SgNode, names: Set): void { + for (const param of root.findAll({ + rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] }, + })) { + collectDirectBindingChildren(param, names); + } +} + +function collectDeclarationNames(root: SgNode, names: Set): void { + for (const decl of root.findAll({ rule: { any: DECLARATION_KINDS.map((kind) => ({ kind })) } })) { + const name = decl + .children() + .find((child) => child.kind() === "identifier" || child.kind() === "type_identifier"); + if (name) names.add(name.text()); + } +} + +function collectArrowParameters(root: SgNode, names: Set): void { + for (const arrow of root.findAll({ rule: { kind: "arrow_function" } })) { + const children = arrow.children(); + const arrowIndex = children.findIndex((child) => child.kind() === "=>"); + if (arrowIndex === -1) continue; + for (const child of children.slice(0, arrowIndex)) { + if (child.kind() === "=") break; + if (isBindingPatternKind(child.kind())) collectBindingNames(child, names); + } + } +} + +function collectForInBindings(root: SgNode, names: Set): void { + for (const loop of root.findAll({ rule: { kind: "for_in_statement" } })) { + const children = loop.children(); + const keywordIndex = children.findIndex( + (child) => child.kind() === "in" || child.kind() === "of", + ); + if (keywordIndex === -1) continue; + for (const child of children.slice(0, keywordIndex)) { + collectBindingNames(child, names); + } + } +} + +export function localDeclarationNames(root: SgNode): Set { + const names = new Set(); + + for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { + const binding = firstDeclaratorChild(decl); + if (binding) collectBindingNames(binding, names); + } + + collectParameters(root, names); + collectDeclarationNames(root, names); + + for (const catchClause of root.findAll({ rule: { kind: "catch_clause" } })) { + collectDirectBindingChildren(catchClause, names); + } + + collectArrowParameters(root, names); + collectForInBindings(root, names); + + return names; +} From 07ff0e0c480edfb5d09771fea6b1d0ff9641d35a Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 07:50:15 +0900 Subject: [PATCH 413/618] fix: preserve value imports for codemod runtime helpers --- .../scripts/transform.ts | 16 +++++++++++----- .../tests/type-only-runtime-import/expected.ts | 5 +++++ .../tests/type-only-runtime-import/input.ts | 6 ++++++ .../runtime-globals-opt-in/scripts/transform.ts | 10 ++++++---- 4 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-import/input.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 6e13161fa..135375cae 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -99,7 +99,7 @@ function runtimeAuthconnectionReference(imports: SgNode[]): string | null { if (importSource(importStmt) !== RUNTIME_MODULE || isTypeOnlyImport(importStmt)) continue; for (const spec of importStmt.findAll({ rule: { kind: "import_specifier" } })) { const names = importSpecNames(spec); - if (names?.importedName === AUTHCONNECTION) return names.localName; + if (names?.importedName === AUTHCONNECTION && !names.typeOnly) return names.localName; } const clause = importStmt.children().find((child) => child.kind() === "import_clause"); @@ -208,10 +208,16 @@ function buildAddRuntimeImportEdit(root: SgNode, source: string, imports: SgNode const existingRuntimeImport = runtimeNamedValueImport(imports); const namedImports = existingRuntimeImport ? namedImportsNode(existingRuntimeImport) : null; if (namedImports) { - const specTexts = namedImports - .findAll({ rule: { kind: "import_specifier" } }) - .map((spec) => spec.text()); - return namedImports.replace(`{ ${[...specTexts, AUTHCONNECTION].join(", ")} }`); + const specTexts = namedImports.findAll({ rule: { kind: "import_specifier" } }).map((spec) => { + const names = importSpecNames(spec); + return names?.importedName === AUTHCONNECTION && names.localName === AUTHCONNECTION + ? AUTHCONNECTION + : spec.text(); + }); + const nextSpecTexts = specTexts.includes(AUTHCONNECTION) + ? specTexts + : [...specTexts, AUTHCONNECTION]; + return namedImports.replace(`{ ${nextSpecTexts.join(", ")} }`); } const pos = importInsertionIndex(root, imports, source); diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-import/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-import/expected.ts new file mode 100644 index 000000000..ad4c2010e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-import/expected.ts @@ -0,0 +1,5 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export async function run() { + return authconnection.getConnectionToken("google"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-import/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-import/input.ts new file mode 100644 index 000000000..f2b951056 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/type-only-runtime-import/input.ts @@ -0,0 +1,6 @@ +import { type authconnection } from "@tailor-platform/sdk/runtime"; +import { auth } from "../tailor.config"; + +export async function run() { + return auth.getConnectionToken("google"); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts index 77ecf3543..279f27c12 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts @@ -153,10 +153,12 @@ function buildAddRuntimeImportEdit(root: SgNode, source: string, imports: SgNode const existingRuntimeImport = runtimeNamedValueImport(imports); const namedImports = existingRuntimeImport ? namedImportsNode(existingRuntimeImport) : null; if (namedImports) { - const specTexts = namedImports - .findAll({ rule: { kind: "import_specifier" } }) - .map((spec) => spec.text()); - return namedImports.replace(`{ ${[...specTexts, "idp"].join(", ")} }`); + const specTexts = namedImports.findAll({ rule: { kind: "import_specifier" } }).map((spec) => { + const names = importSpecNames(spec); + return names?.importedName === "idp" && names.localName === "idp" ? "idp" : spec.text(); + }); + const nextSpecTexts = specTexts.includes("idp") ? specTexts : [...specTexts, "idp"]; + return namedImports.replace(`{ ${nextSpecTexts.join(", ")} }`); } const pos = importInsertionIndex(root, imports, source); From 31ed5938195016b91103d76c735c1913deee6edb Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 08:03:05 +0900 Subject: [PATCH 414/618] fix(sdk): keep date validation shape-only --- .changeset/strict-tailor-field-strings.md | 2 +- .../services/tailordb/schema.test.ts | 11 ++++--- .../sdk/src/configure/types/field-format.ts | 33 +++---------------- packages/sdk/src/configure/types/type.test.ts | 10 +++--- 4 files changed, 18 insertions(+), 38 deletions(-) diff --git a/.changeset/strict-tailor-field-strings.md b/.changeset/strict-tailor-field-strings.md index 77702114c..3c463ec65 100644 --- a/.changeset/strict-tailor-field-strings.md +++ b/.changeset/strict-tailor-field-strings.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk": major --- -Tighten Tailor field output types for UUID, date, datetime, time, and decimal fields. Date and datetime parsing now rejects invalid calendar dates and accepts ISO 8601 datetime offsets. +Tighten Tailor field output types for UUID, date, datetime, time, and decimal fields. Datetime parsing now accepts ISO 8601 datetime offsets. diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index c242c36ee..dc8c906d2 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -1698,10 +1698,12 @@ describe("TailorDBField runtime validation tests", () => { 'Expected to match "yyyy-MM-dd" format: received 2025/01/01', ); - const invalidDate = field.parse({ value: "2025-02-30", data, invoker }); - expect(invalidDate.issues?.[0]?.message).toBe( - 'Expected to match "yyyy-MM-dd" format: received 2025-02-30', - ); + const calendarDateShape = field.parse({ value: "2025-02-30", data, invoker }); + expect(calendarDateShape.issues).toBeUndefined(); + if (calendarDateShape.issues) { + throw new Error("Unexpected issues"); + } + expect(calendarDateShape.value).toBe("2025-02-30"); }); test("validates datetime format", () => { @@ -1711,6 +1713,7 @@ describe("TailorDBField runtime validation tests", () => { "2025-01-01T10:11:12.123456Z", "2025-01-01T10:11:12+09:00", "2025-01-01t10:11:12-08:00", + "2025-02-30T10:11:12Z", ]) { const ok = field.parse({ value, data, invoker }); expect(ok.issues).toBeUndefined(); diff --git a/packages/sdk/src/configure/types/field-format.ts b/packages/sdk/src/configure/types/field-format.ts index 602cba1d0..bb38724ea 100644 --- a/packages/sdk/src/configure/types/field-format.ts +++ b/packages/sdk/src/configure/types/field-format.ts @@ -1,38 +1,13 @@ const regex = { - date: /^(?\d{4})-(?\d{2})-(?\d{2})$/, + date: /^\d{4}-\d{2}-\d{2}$/, datetime: - /^(?\d{4})-(?\d{2})-(?\d{2})[Tt](?[01]\d|2[0-3]):(?[0-5]\d):(?[0-5]\d|60)(\.(?\d+))?(?[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/, + /^\d{4}-\d{2}-\d{2}[Tt](?[01]\d|2[0-3]):(?[0-5]\d):(?[0-5]\d|60)(\.(?\d+))?(?[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/, } as const; export function isValidDateString(value: string): boolean { - const match = regex.date.exec(value); - return match?.groups !== undefined && isValidDateParts(match.groups); + return regex.date.test(value); } export function isValidDateTimeString(value: string): boolean { - const match = regex.datetime.exec(value); - return match?.groups !== undefined && isValidDateParts(match.groups); -} - -function isValidDateParts(groups: Record): boolean { - const { year: yearValue, month: monthValue, day: dayValue } = groups; - if (yearValue === undefined || monthValue === undefined || dayValue === undefined) { - return false; - } - - const year = Number(yearValue); - const month = Number(monthValue); - const day = Number(dayValue); - if (month < 1 || month > 12 || day < 1) { - return false; - } - - const daysInMonth = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][ - month - 1 - ]; - return daysInMonth !== undefined && day <= daysInMonth; -} - -function isLeapYear(year: number): boolean { - return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + return regex.datetime.test(value); } diff --git a/packages/sdk/src/configure/types/type.test.ts b/packages/sdk/src/configure/types/type.test.ts index 8ea891bf6..44ff36f1f 100644 --- a/packages/sdk/src/configure/types/type.test.ts +++ b/packages/sdk/src/configure/types/type.test.ts @@ -612,10 +612,11 @@ describe("TailorField runtime validation tests", () => { { const result = t.date().parse({ value: "2025-02-30", data, invoker }); - expect(result.issues).toBeDefined(); - expect(result.issues?.[0]?.message).toEqual( - 'Expected to match "yyyy-MM-dd" format: received 2025-02-30', - ); + expect(result.issues).toBeUndefined(); + if (result.issues) { + throw new Error("Unexpected issues"); + } + expect(result.value).toBe("2025-02-30"); } }); @@ -625,6 +626,7 @@ describe("TailorField runtime validation tests", () => { "2025-12-21T10:11:12.123456Z", "2025-12-21T10:11:12+09:00", "2025-12-21t10:11:12-08:00", + "2025-02-30T10:11:12Z", ]) { const result = t.datetime().parse({ value, data, invoker }); expect(result.issues).toBeUndefined(); From 6616674eb41a53619138603405a4498e3f09d70b Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 08:09:57 +0900 Subject: [PATCH 415/618] chore: add sdk-codemod changeset --- .changeset/share-codemod-runtime-imports.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/share-codemod-runtime-imports.md diff --git a/.changeset/share-codemod-runtime-imports.md b/.changeset/share-codemod-runtime-imports.md new file mode 100644 index 000000000..5f1cfa1be --- /dev/null +++ b/.changeset/share-codemod-runtime-imports.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Keep v2 codemods from reusing type-only runtime helper imports when adding runtime value imports. From 1cb6716cecdd141a26d48b01efd5e7eab180972d Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 08:17:14 +0900 Subject: [PATCH 416/618] test(sdk): align CI fixtures with current types --- .../templates/generators/src/resolver/getProduct.test.ts | 8 ++++---- .../multi-application/apps/admin/db/adminNote.ts | 6 +++++- packages/sdk/src/cli/commands/setup/workflow-lint.test.ts | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts b/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts index 598ee996f..0239c0b4e 100644 --- a/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts +++ b/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts @@ -7,7 +7,7 @@ describe("getProduct resolver", () => { using db = mockTailordb(); // Select product db.enqueueResult({ - id: "product-1", + id: "00000000-0000-4000-8000-000000000001", name: "Widget", price: 9.99, status: "ACTIVE", @@ -20,7 +20,7 @@ describe("getProduct resolver", () => { db.enqueueResult({ name: "Gadgets" }); const result = await resolver.body({ - input: { productId: "product-1" }, + input: { productId: "00000000-0000-4000-8000-000000000001" }, caller: null, invoker: null, env: {}, @@ -39,7 +39,7 @@ describe("getProduct resolver", () => { using db = mockTailordb(); // Select product (no categoryId) db.enqueueResult({ - id: "product-2", + id: "00000000-0000-4000-8000-000000000002", name: "Standalone Item", price: 19.99, status: "DRAFT", @@ -50,7 +50,7 @@ describe("getProduct resolver", () => { }); const result = await resolver.body({ - input: { productId: "product-2" }, + input: { productId: "00000000-0000-4000-8000-000000000002" }, caller: null, invoker: null, env: {}, diff --git a/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts b/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts index 0a39ee828..75726d5b6 100644 --- a/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts +++ b/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts @@ -4,11 +4,15 @@ import { unsafeAllowAllTypePermission, } from "@tailor-platform/sdk"; +type UUIDString = `${string}-${string}-${string}-${string}-${string}`; + export const adminNote = db .type("AdminNote", { title: db.string(), content: db.string(), - authorId: db.uuid().hooks({ create: ({ invoker }) => invoker?.id ?? crypto.randomUUID() }), + authorId: db.uuid().hooks({ + create: ({ invoker }) => (invoker?.id ?? crypto.randomUUID()) as UUIDString, + }), ...db.fields.timestamps(), }) // NOTE: This permits all operations for simplicity. diff --git a/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts b/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts index 3e54dc802..9c51a9a11 100644 --- a/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts +++ b/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts @@ -109,7 +109,7 @@ describe("repository ERD preview workflow", () => { expect(content).toContain("Base ERD namespace '$NAMESPACE' not found"); expect(content).toContain('diff_args=(--namespace "$NAMESPACE"'); expect(content).toContain('diff_args+=(--base-html "$base_html")'); - expect(content).toContain("pnpm exec tailor-sdk tailordb erd diff"); + expect(content).toContain("pnpm exec tailor tailordb erd diff"); }); test("uploads artifacts with names matched by the sticky comment", () => { From eddd234d399eb02fee58d752c660904fbab07832 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 09:15:18 +0900 Subject: [PATCH 417/618] feat(sdk): propagate strict field scalar types --- .changeset/strict-tailor-field-strings.md | 2 +- example/generated/tailordb.ts | 47 ++++---- example/migrations/0003/db.ts | 100 +++++++++++----- example/migrations/analyticsdb/0001/db.ts | 12 +- example/resolvers/triggerWorkflow.ts | 2 +- example/tests/bundled_execution.test.ts | 24 ++-- example/tests/fixtures/expected/db.ts | 47 ++++---- example/workflows/jobs/fetch-customer.ts | 11 +- example/workflows/order-processing.ts | 3 +- .../src/executor/onUserCreated.test.ts | 4 +- .../executor/src/executor/shared.test.ts | 2 +- .../templates/executor/src/executor/shared.ts | 3 +- .../templates/executor/src/generated/db.ts | 11 +- .../templates/generators/src/generated/db.ts | 17 +-- .../src/generated/kysely-tailordb.ts | 3 +- .../src/generated/kysely-tailordb.ts | 27 ++--- .../apps/admin/db/adminNote.ts | 4 +- .../templates/resolver/src/generated/db.ts | 3 +- .../src/resolver/showUserInfo.test.ts | 4 +- .../resolver/src/resolver/upsertUsers.test.ts | 4 +- .../templates/tailordb/src/generated/db.ts | 19 +-- .../templates/workflow/src/generated/db.ts | 5 +- .../migrate/db-types-generator.test.ts | 14 ++- .../tailordb/migrate/db-types-generator.ts | 108 +++++++++++++----- .../sdk/src/cli/shared/runtime-exprs.test.ts | 12 +- packages/sdk/src/configure/index.ts | 8 ++ .../services/executor/executor.test.ts | 39 ++++--- .../services/executor/trigger/event.ts | 13 ++- .../services/resolver/resolver.test.ts | 20 ++-- .../services/tailordb/schema.test.ts | 104 ++++++++--------- .../src/configure/services/tailordb/schema.ts | 3 +- .../configure/services/workflow/job.test.ts | 20 ++-- .../services/workflow/test-env-key.ts | 2 +- .../sdk/src/configure/types/field.types.ts | 30 +++-- packages/sdk/src/configure/types/index.ts | 8 ++ .../sdk/src/configure/types/scalar.types.ts | 7 ++ packages/sdk/src/configure/types/type.test.ts | 4 +- packages/sdk/src/kysely/index.test-d.ts | 56 ++++++--- packages/sdk/src/kysely/index.ts | 16 ++- .../plugin/builtin/kysely-type/index.test.ts | 4 +- .../src/plugin/builtin/kysely-type/index.ts | 11 +- .../kysely-type/type-processor.test.ts | 18 +-- .../builtin/kysely-type/type-processor.ts | 88 ++++++++++---- .../src/plugin/builtin/kysely-type/types.ts | 6 + packages/sdk/src/runtime/context.test.ts | 4 +- packages/sdk/src/runtime/context.ts | 3 +- packages/sdk/src/runtime/idp.test.ts | 50 +++++--- packages/sdk/src/runtime/idp.ts | 20 ++-- packages/sdk/src/runtime/types.ts | 4 +- packages/sdk/src/vitest/mock.test.ts | 25 ++-- packages/sdk/src/vitest/mock.ts | 34 +++--- 51 files changed, 689 insertions(+), 396 deletions(-) create mode 100644 packages/sdk/src/configure/types/scalar.types.ts diff --git a/.changeset/strict-tailor-field-strings.md b/.changeset/strict-tailor-field-strings.md index 3c463ec65..5cbac8ed8 100644 --- a/.changeset/strict-tailor-field-strings.md +++ b/.changeset/strict-tailor-field-strings.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk": major --- -Tighten Tailor field output types for UUID, date, datetime, time, and decimal fields. Datetime parsing now accepts ISO 8601 datetime offsets. +Tighten Tailor field output types for UUID, date, datetime, time, and decimal fields, including generated Kysely types and runtime user IDs. Datetime parsing now accepts ISO 8601 datetime offsets. diff --git a/example/generated/tailordb.ts b/example/generated/tailordb.ts index e769dbf3a..ae1e3c5bb 100644 --- a/example/generated/tailordb.ts +++ b/example/generated/tailordb.ts @@ -1,6 +1,7 @@ import { createGetDB, type Generated, + type UUIDString, type Timestamp, type ObjectColumnType, type Serial, @@ -16,7 +17,7 @@ import { export interface Namespace { "tailordb": { Customer: { - id: Generated; + id: Generated; name: string; email: string; phone: string | null; @@ -31,9 +32,9 @@ export interface Namespace { } Invoice: { - id: Generated; + id: Generated; invoiceNumber: Serial; - salesOrderID: string; + salesOrderID: UUIDString; amount: number | null; sequentialId: Serial; status: "draft" | "sent" | "paid" | "cancelled" | null; @@ -42,7 +43,7 @@ export interface Namespace { } NestedProfile: { - id: Generated; + id: Generated; userInfo: ObjectColumnType<{ name: string; age?: number | null; @@ -61,13 +62,13 @@ export interface Namespace { } PurchaseOrder: { - id: Generated; - supplierID: string; + id: Generated; + supplierID: UUIDString; totalPrice: number; discount: number | null; status: string; attachedFiles: { - id: string; + id: UUIDString; name: string; size: number; type: "text" | "image"; @@ -77,9 +78,9 @@ export interface Namespace { } SalesOrder: { - id: Generated; - customerID: string; - approvedByUserIDs: string[] | null; + id: Generated; + customerID: UUIDString; + approvedByUserIDs: UUIDString[] | null; totalPrice: number | null; discount: number | null; status: string | null; @@ -90,22 +91,22 @@ export interface Namespace { } SalesOrderCreated: { - id: Generated; - salesOrderID: string; - customerID: string; + id: Generated; + salesOrderID: UUIDString; + customerID: UUIDString; totalPrice: number | null; status: string | null; } Selfie: { - id: Generated; + id: Generated; name: string; - parentID: string | null; - dependId: string | null; + parentID: UUIDString | null; + dependId: UUIDString | null; } Supplier: { - id: Generated; + id: Generated; name: string; phone: string; fax: string | null; @@ -119,7 +120,7 @@ export interface Namespace { } User: { - id: Generated; + id: Generated; name: string; email: string; status: string | null; @@ -130,24 +131,24 @@ export interface Namespace { } UserLog: { - id: Generated; - userID: string; + id: Generated; + userID: UUIDString; message: string; createdAt: Generated; updatedAt: Generated; } UserSetting: { - id: Generated; + id: Generated; language: "jp" | "en"; - userID: string; + userID: UUIDString; createdAt: Generated; updatedAt: Generated; } }, "analyticsdb": { Event: { - id: Generated; + id: Generated; name: "CLICK" | "VIEW" | "PURCHASE"; createdAt: Generated; updatedAt: Generated; diff --git a/example/migrations/0003/db.ts b/example/migrations/0003/db.ts index 3dc32b41c..9e1b783ec 100644 --- a/example/migrations/0003/db.ts +++ b/example/migrations/0003/db.ts @@ -8,10 +8,12 @@ import { type ColumnType, type Transaction as KyselyTransaction, + type UUIDString, + type DateTimeString, } from "@tailor-platform/sdk/kysely"; import type { Env } from "@tailor-platform/sdk"; -type Timestamp = ColumnType; +type Timestamp = ColumnType; type Generated = T extends ColumnType ? ColumnType @@ -19,7 +21,7 @@ type Generated = interface Database { Customer: { - id: Generated; + id: Generated; name: string; email: string; phone: string | null; @@ -30,63 +32,83 @@ interface Database { fullAddress: string; state: string; createdAt: Timestamp; - updatedAt: ColumnType; + updatedAt: ColumnType< + Date | DateTimeString | null, + Date | DateTimeString, + Date | DateTimeString + >; }; Invoice: { - id: Generated; + id: Generated; invoiceNumber: string; - salesOrderID: string; + salesOrderID: UUIDString; amount: number | null; sequentialId: number; status: "draft" | "sent" | "paid" | "cancelled" | null; createdAt: Timestamp; - updatedAt: ColumnType; + updatedAt: ColumnType< + Date | DateTimeString | null, + Date | DateTimeString, + Date | DateTimeString + >; }; NestedProfile: { - id: Generated; + id: Generated; userInfo: string; metadata: string; archived: boolean | null; createdAt: Timestamp; - updatedAt: ColumnType; + updatedAt: ColumnType< + Date | DateTimeString | null, + Date | DateTimeString, + Date | DateTimeString + >; }; PurchaseOrder: { - id: Generated; - supplierID: string; + id: Generated; + supplierID: UUIDString; totalPrice: number; discount: number | null; status: string; attachedFiles: string[]; createdAt: Timestamp; - updatedAt: ColumnType; + updatedAt: ColumnType< + Date | DateTimeString | null, + Date | DateTimeString, + Date | DateTimeString + >; }; SalesOrder: { - id: Generated; - customerID: string; - approvedByUserIDs: string[] | null; + id: Generated; + customerID: UUIDString; + approvedByUserIDs: UUIDString[] | null; totalPrice: number | null; discount: number | null; status: string | null; cancelReason: string | null; canceledAt: Timestamp | null; createdAt: Timestamp; - updatedAt: ColumnType; + updatedAt: ColumnType< + Date | DateTimeString | null, + Date | DateTimeString, + Date | DateTimeString + >; }; SalesOrderCreated: { - id: Generated; - salesOrderID: string; - customerID: string; + id: Generated; + salesOrderID: UUIDString; + customerID: UUIDString; totalPrice: number | null; status: string | null; }; Selfie: { - id: Generated; + id: Generated; name: string; - parentID: string | null; - dependId: string | null; + parentID: UUIDString | null; + dependId: UUIDString | null; }; Supplier: { - id: Generated; + id: Generated; name: string; phone: string; fax: string | null; @@ -96,31 +118,47 @@ interface Database { state: "Alabama" | "Alaska"; city: string; createdAt: Timestamp; - updatedAt: ColumnType; + updatedAt: ColumnType< + Date | DateTimeString | null, + Date | DateTimeString, + Date | DateTimeString + >; }; User: { - id: Generated; + id: Generated; name: string; email: string; status: string | null; department: string | null; role: "MANAGER" | "STAFF"; createdAt: Timestamp; - updatedAt: ColumnType; + updatedAt: ColumnType< + Date | DateTimeString | null, + Date | DateTimeString, + Date | DateTimeString + >; }; UserLog: { - id: Generated; - userID: string; + id: Generated; + userID: UUIDString; message: string; createdAt: Timestamp; - updatedAt: ColumnType; + updatedAt: ColumnType< + Date | DateTimeString | null, + Date | DateTimeString, + Date | DateTimeString + >; }; UserSetting: { - id: Generated; + id: Generated; language: "jp" | "en"; - userID: string; + userID: UUIDString; createdAt: Timestamp; - updatedAt: ColumnType; + updatedAt: ColumnType< + Date | DateTimeString | null, + Date | DateTimeString, + Date | DateTimeString + >; }; } diff --git a/example/migrations/analyticsdb/0001/db.ts b/example/migrations/analyticsdb/0001/db.ts index e4f20afd9..11cb40173 100644 --- a/example/migrations/analyticsdb/0001/db.ts +++ b/example/migrations/analyticsdb/0001/db.ts @@ -8,10 +8,12 @@ import { type ColumnType, type Transaction as KyselyTransaction, + type UUIDString, + type DateTimeString, } from "@tailor-platform/sdk/kysely"; import type { Env } from "@tailor-platform/sdk"; -type Timestamp = ColumnType; +type Timestamp = ColumnType; type Generated = T extends ColumnType ? ColumnType @@ -19,10 +21,14 @@ type Generated = interface Database { Event: { - id: Generated; + id: Generated; name: "CLICK" | "VIEW" | "PURCHASE"; createdAt: Timestamp; - updatedAt: ColumnType; + updatedAt: ColumnType< + Date | DateTimeString | null, + Date | DateTimeString, + Date | DateTimeString + >; }; } diff --git a/example/resolvers/triggerWorkflow.ts b/example/resolvers/triggerWorkflow.ts index 4fa4447c5..028b493fb 100644 --- a/example/resolvers/triggerWorkflow.ts +++ b/example/resolvers/triggerWorkflow.ts @@ -7,7 +7,7 @@ export default createResolver({ operation: "mutation", input: { orderId: t.string().description("Order ID to process"), - customerId: t.string().description("Customer ID for the order"), + customerId: t.uuid().description("Customer ID for the order"), }, body: async ({ input }) => { // Trigger the workflow with invoker (machine user name is type-narrowed via tailor.d.ts) diff --git a/example/tests/bundled_execution.test.ts b/example/tests/bundled_execution.test.ts index 3d214bf25..4ad303968 100644 --- a/example/tests/bundled_execution.test.ts +++ b/example/tests/bundled_execution.test.ts @@ -134,7 +134,7 @@ describe("bundled execution tests", () => { }, }, user: { - id: "test-user-id", + id: "123e4567-e89b-12d3-a456-426614174000", type: "user", workspaceId: "test-workspace-id", }, @@ -167,15 +167,18 @@ describe("bundled execution tests", () => { }); const main = await importActualMain("executors/user-created.js"); - const payload = { newRecord: { id: "user-1" } }; + const payload = { newRecord: { id: "11111111-1111-4111-8111-111111111111" } }; const result = await main(payload); expect(result).toBeUndefined(); expect(db.executedQueries).toEqual([ - { query: 'select * from "User" where "id" = $1', params: ["user-1"] }, + { + query: 'select * from "User" where "id" = $1', + params: ["11111111-1111-4111-8111-111111111111"], + }, { query: 'insert into "UserLog" ("userID", "message") values ($1, $2)', - params: ["user-1", "User created: undefined (undefined)"], + params: ["11111111-1111-4111-8111-111111111111", "User created: undefined (undefined)"], }, ]); expect(db.createdClients).toMatchObject([{ namespace: "tailordb" }]); @@ -199,19 +202,22 @@ describe("bundled execution tests", () => { const main = await importActualMain("workflow-jobs/process-order.js"); const result = await main({ orderId: "order-123", - customerId: "customer-456", + customerId: "123e4567-e89b-12d3-a456-426614174000", }); expect(result).toEqual({ orderId: "order-123", - customerId: "customer-456", + customerId: "123e4567-e89b-12d3-a456-426614174000", customerEmail: "customer@example.com", notificationSent: true, processedAt: "2025-01-01 12:00:00", }); expect(wf.triggeredJobs).toEqual([ - { jobName: "fetch-customer", args: { customerId: "customer-456" } }, + { + jobName: "fetch-customer", + args: { customerId: "123e4567-e89b-12d3-a456-426614174000" }, + }, { jobName: "send-notification", args: { @@ -231,9 +237,9 @@ describe("bundled execution tests", () => { await expect( main({ orderId: "order-123", - customerId: "non-existent", + customerId: "00000000-0000-0000-0000-000000000000", }), - ).rejects.toThrow("Customer non-existent not found"); + ).rejects.toThrow("Customer 00000000-0000-0000-0000-000000000000 not found"); }); test("workflow-jobs/send-notification.js executes correctly", async () => { diff --git a/example/tests/fixtures/expected/db.ts b/example/tests/fixtures/expected/db.ts index e769dbf3a..ae1e3c5bb 100644 --- a/example/tests/fixtures/expected/db.ts +++ b/example/tests/fixtures/expected/db.ts @@ -1,6 +1,7 @@ import { createGetDB, type Generated, + type UUIDString, type Timestamp, type ObjectColumnType, type Serial, @@ -16,7 +17,7 @@ import { export interface Namespace { "tailordb": { Customer: { - id: Generated; + id: Generated; name: string; email: string; phone: string | null; @@ -31,9 +32,9 @@ export interface Namespace { } Invoice: { - id: Generated; + id: Generated; invoiceNumber: Serial; - salesOrderID: string; + salesOrderID: UUIDString; amount: number | null; sequentialId: Serial; status: "draft" | "sent" | "paid" | "cancelled" | null; @@ -42,7 +43,7 @@ export interface Namespace { } NestedProfile: { - id: Generated; + id: Generated; userInfo: ObjectColumnType<{ name: string; age?: number | null; @@ -61,13 +62,13 @@ export interface Namespace { } PurchaseOrder: { - id: Generated; - supplierID: string; + id: Generated; + supplierID: UUIDString; totalPrice: number; discount: number | null; status: string; attachedFiles: { - id: string; + id: UUIDString; name: string; size: number; type: "text" | "image"; @@ -77,9 +78,9 @@ export interface Namespace { } SalesOrder: { - id: Generated; - customerID: string; - approvedByUserIDs: string[] | null; + id: Generated; + customerID: UUIDString; + approvedByUserIDs: UUIDString[] | null; totalPrice: number | null; discount: number | null; status: string | null; @@ -90,22 +91,22 @@ export interface Namespace { } SalesOrderCreated: { - id: Generated; - salesOrderID: string; - customerID: string; + id: Generated; + salesOrderID: UUIDString; + customerID: UUIDString; totalPrice: number | null; status: string | null; } Selfie: { - id: Generated; + id: Generated; name: string; - parentID: string | null; - dependId: string | null; + parentID: UUIDString | null; + dependId: UUIDString | null; } Supplier: { - id: Generated; + id: Generated; name: string; phone: string; fax: string | null; @@ -119,7 +120,7 @@ export interface Namespace { } User: { - id: Generated; + id: Generated; name: string; email: string; status: string | null; @@ -130,24 +131,24 @@ export interface Namespace { } UserLog: { - id: Generated; - userID: string; + id: Generated; + userID: UUIDString; message: string; createdAt: Generated; updatedAt: Generated; } UserSetting: { - id: Generated; + id: Generated; language: "jp" | "en"; - userID: string; + userID: UUIDString; createdAt: Generated; updatedAt: Generated; } }, "analyticsdb": { Event: { - id: Generated; + id: Generated; name: "CLICK" | "VIEW" | "PURCHASE"; createdAt: Generated; updatedAt: Generated; diff --git a/example/workflows/jobs/fetch-customer.ts b/example/workflows/jobs/fetch-customer.ts index d7891370e..844ce069a 100644 --- a/example/workflows/jobs/fetch-customer.ts +++ b/example/workflows/jobs/fetch-customer.ts @@ -1,9 +1,14 @@ import { createWorkflowJob } from "@tailor-platform/sdk"; import { getDB } from "../../generated/tailordb"; +import type { DateTimeString, UUIDString } from "@tailor-platform/sdk"; + +function serializeDateTime(value: Date | DateTimeString): string { + return value instanceof Date ? value.toISOString() : value; +} export const fetchCustomer = createWorkflowJob({ name: "fetch-customer", - body: async (input: { customerId: string }) => { + body: async (input: { customerId: UUIDString }) => { const db = getDB("tailordb"); const customer = await db .selectFrom("Customer") @@ -13,8 +18,8 @@ export const fetchCustomer = createWorkflowJob({ if (!customer) return undefined; return { ...customer, - createdAt: customer.createdAt.toISOString(), - updatedAt: customer.updatedAt.toISOString(), + createdAt: serializeDateTime(customer.createdAt), + updatedAt: serializeDateTime(customer.updatedAt), }; }, }); diff --git a/example/workflows/order-processing.ts b/example/workflows/order-processing.ts index 375f16099..567caa6dd 100644 --- a/example/workflows/order-processing.ts +++ b/example/workflows/order-processing.ts @@ -1,12 +1,13 @@ import { createWorkflow, createWorkflowJob } from "@tailor-platform/sdk"; import { fetchCustomer } from "./jobs/fetch-customer"; import { sendNotification } from "./jobs/send-notification"; +import type { UUIDString } from "@tailor-platform/sdk"; // Note: We're NOT importing generateReport and archiveData // Those jobs should be completely excluded from the bundle export const processOrder = createWorkflowJob({ name: "process-order", - body: (input: { orderId: string; customerId: string }, { env }) => { + body: (input: { orderId: string; customerId: UUIDString }, { env }) => { // Log env for demonstration console.log("Environment:", env); diff --git a/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts b/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts index 22d1113fc..80d12b2bd 100644 --- a/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts +++ b/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts @@ -11,7 +11,7 @@ describe("onUserCreated executor", () => { } await onUserCreated.operation.body({ newRecord: { - id: "user-1", + id: "11111111-1111-4111-8111-111111111111", name: "Alice", email: "alice@example.com", role: "ADMIN", @@ -23,7 +23,7 @@ describe("onUserCreated executor", () => { expect(createAuditLog).toHaveBeenCalledExactlyOnceWith({ action: "USER_CREATED", entityType: "User", - entityId: "user-1", + entityId: "11111111-1111-4111-8111-111111111111", message: "Admin user created: Alice (alice@example.com)", }); }); diff --git a/packages/create-sdk/templates/executor/src/executor/shared.test.ts b/packages/create-sdk/templates/executor/src/executor/shared.test.ts index b56c0fce8..c7a70ab98 100644 --- a/packages/create-sdk/templates/executor/src/executor/shared.test.ts +++ b/packages/create-sdk/templates/executor/src/executor/shared.test.ts @@ -8,7 +8,7 @@ describe("createAuditLog", () => { await createAuditLog({ action: "USER_CREATED", entityType: "User", - entityId: "test-id", + entityId: "123e4567-e89b-12d3-a456-426614174000", message: "Test audit log", }); diff --git a/packages/create-sdk/templates/executor/src/executor/shared.ts b/packages/create-sdk/templates/executor/src/executor/shared.ts index 0483a944b..d8e5abe1b 100644 --- a/packages/create-sdk/templates/executor/src/executor/shared.ts +++ b/packages/create-sdk/templates/executor/src/executor/shared.ts @@ -1,9 +1,10 @@ import { getDB } from "../generated/db"; +import type { UUIDString } from "@tailor-platform/sdk"; interface AuditLogInput { action: string; entityType: string; - entityId: string; + entityId: UUIDString; message: string; } diff --git a/packages/create-sdk/templates/executor/src/generated/db.ts b/packages/create-sdk/templates/executor/src/generated/db.ts index c50b7fcbe..ecce78ef0 100644 --- a/packages/create-sdk/templates/executor/src/generated/db.ts +++ b/packages/create-sdk/templates/executor/src/generated/db.ts @@ -1,6 +1,7 @@ import { createGetDB, type Generated, + type UUIDString, type Timestamp, type NamespaceDB, type NamespaceInsertable, @@ -14,18 +15,18 @@ import { export interface Namespace { "main-db": { AuditLog: { - id: Generated; + id: Generated; action: string; entityType: string; - entityId: string; + entityId: UUIDString; message: string; createdAt: Generated; updatedAt: Generated; } Notification: { - id: Generated; - userId: string; + id: Generated; + userId: UUIDString; title: string; body: string; isRead: boolean; @@ -34,7 +35,7 @@ export interface Namespace { } User: { - id: Generated; + id: Generated; name: string; email: string; role: "ADMIN" | "MEMBER"; diff --git a/packages/create-sdk/templates/generators/src/generated/db.ts b/packages/create-sdk/templates/generators/src/generated/db.ts index a9b7441d5..fb3814422 100644 --- a/packages/create-sdk/templates/generators/src/generated/db.ts +++ b/packages/create-sdk/templates/generators/src/generated/db.ts @@ -1,6 +1,7 @@ import { createGetDB, type Generated, + type UUIDString, type Timestamp, type NamespaceDB, type NamespaceInsertable, @@ -14,16 +15,16 @@ import { export interface Namespace { "main-db": { Category: { - id: Generated; + id: Generated; name: string; slug: string; - parentCategoryId: string | null; + parentCategoryId: UUIDString | null; } Order: { - id: Generated; - productId: string; - userId: string; + id: Generated; + productId: UUIDString; + userId: UUIDString; quantity: number; totalPrice: number; status: "PENDING" | "CONFIRMED" | "SHIPPED" | "DELIVERED" | "CANCELLED"; @@ -32,18 +33,18 @@ export interface Namespace { } Product: { - id: Generated; + id: Generated; name: string; description: string | null; price: number; status: "DRAFT" | "ACTIVE" | "DISCONTINUED"; - categoryId: string | null; + categoryId: UUIDString | null; createdAt: Generated; updatedAt: Generated; } User: { - id: Generated; + id: Generated; name: string; email: string; role: "ADMIN" | "MEMBER" | "VIEWER"; diff --git a/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts b/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts index 67fefdd51..60cb1acae 100644 --- a/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts +++ b/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts @@ -1,6 +1,7 @@ import { createGetDB, type Generated, + type UUIDString, type Timestamp, type NamespaceDB, type NamespaceInsertable, @@ -14,7 +15,7 @@ import { export interface Namespace { "main-db": { User: { - id: Generated; + id: Generated; name: string; email: string; role: "MANAGER" | "STAFF"; diff --git a/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts b/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts index 6ab154858..11a6cd4fc 100644 --- a/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts +++ b/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts @@ -1,6 +1,7 @@ import { createGetDB, type Generated, + type UUIDString, type Timestamp, type NamespaceDB, type NamespaceInsertable, @@ -14,7 +15,7 @@ import { export interface Namespace { "main-db": { Category: { - id: Generated; + id: Generated; name: string; description: string | null; createdAt: Generated; @@ -22,7 +23,7 @@ export interface Namespace { } Contact: { - id: Generated; + id: Generated; name: string; email: string; phone: string | null; @@ -32,35 +33,35 @@ export interface Namespace { } Inventory: { - id: Generated; - productId: string; + id: Generated; + productId: UUIDString; quantity: number; createdAt: Generated; updatedAt: Generated; } Notification: { - id: Generated; + id: Generated; message: string; createdAt: Generated; updatedAt: Generated; } Order: { - id: Generated; + id: Generated; name: string; description: string | null; orderDate: Timestamp; orderType: "PURCHASE" | "SALES"; - contactId: string; + contactId: UUIDString; createdAt: Generated; updatedAt: Generated; } OrderItem: { - id: Generated; - orderId: string; - productId: string; + id: Generated; + orderId: UUIDString; + productId: UUIDString; quantity: number; unitPrice: number; totalPrice: Generated; @@ -69,16 +70,16 @@ export interface Namespace { } Product: { - id: Generated; + id: Generated; name: string; description: string | null; - categoryId: string; + categoryId: UUIDString; createdAt: Generated; updatedAt: Generated; } User: { - id: Generated; + id: Generated; name: string; email: string; role: "MANAGER" | "STAFF"; diff --git a/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts b/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts index 75726d5b6..c9383e3bf 100644 --- a/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts +++ b/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts @@ -4,14 +4,12 @@ import { unsafeAllowAllTypePermission, } from "@tailor-platform/sdk"; -type UUIDString = `${string}-${string}-${string}-${string}-${string}`; - export const adminNote = db .type("AdminNote", { title: db.string(), content: db.string(), authorId: db.uuid().hooks({ - create: ({ invoker }) => (invoker?.id ?? crypto.randomUUID()) as UUIDString, + create: ({ invoker }) => invoker?.id ?? crypto.randomUUID(), }), ...db.fields.timestamps(), }) diff --git a/packages/create-sdk/templates/resolver/src/generated/db.ts b/packages/create-sdk/templates/resolver/src/generated/db.ts index b892f0e12..870db86c1 100644 --- a/packages/create-sdk/templates/resolver/src/generated/db.ts +++ b/packages/create-sdk/templates/resolver/src/generated/db.ts @@ -1,6 +1,7 @@ import { createGetDB, type Generated, + type UUIDString, type Timestamp, type NamespaceDB, type NamespaceInsertable, @@ -14,7 +15,7 @@ import { export interface Namespace { "main-db": { User: { - id: Generated; + id: Generated; name: string; email: string; age: number; diff --git a/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts b/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts index 991ae9e4f..2db8085d4 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts @@ -19,7 +19,7 @@ describe("showUserInfo resolver", () => { test("returns custom user info", async () => { const customCaller = { - id: "user-123", + id: "123e4567-e89b-12d3-a456-426614174000", type: "machine_user" as const, workspaceId: "ws-456", attributes: { role: "admin" }, @@ -32,7 +32,7 @@ describe("showUserInfo resolver", () => { env: { appName: "Resolver Template", version: 1 }, }); expect(result).toEqual({ - userId: "user-123", + userId: "123e4567-e89b-12d3-a456-426614174000", userType: "machine_user", workspaceId: "ws-456", }); diff --git a/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts b/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts index a09b1fbd8..3ce7d25d2 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts @@ -13,7 +13,9 @@ describe("upsertUsers resolver", () => { mock.setQueryResolver((query) => { switch (query.kind) { case "SelectQueryNode": - return query.parameters.includes("exists@example.com") ? [{ id: "user-1" }] : []; + return query.parameters.includes("exists@example.com") + ? [{ id: "11111111-1111-4111-8111-111111111111" }] + : []; case "InsertQueryNode": case "UpdateQueryNode": return { numAffectedRows: 1 }; diff --git a/packages/create-sdk/templates/tailordb/src/generated/db.ts b/packages/create-sdk/templates/tailordb/src/generated/db.ts index 46840cc83..a8cef984d 100644 --- a/packages/create-sdk/templates/tailordb/src/generated/db.ts +++ b/packages/create-sdk/templates/tailordb/src/generated/db.ts @@ -1,6 +1,7 @@ import { createGetDB, type Generated, + type UUIDString, type Timestamp, type ObjectColumnType, type NamespaceDB, @@ -15,17 +16,17 @@ import { export interface Namespace { "main-db": { Category: { - id: Generated; + id: Generated; name: string; description: string | null; - parentCategoryId: string | null; + parentCategoryId: UUIDString | null; } Comment: { - id: Generated; + id: Generated; body: string; - taskId: string; - authorId: string; + taskId: UUIDString; + authorId: UUIDString; metadata: ObjectColumnType<{ source: string; editedAt?: Timestamp | null; @@ -36,21 +37,21 @@ export interface Namespace { } Task: { - id: Generated; + id: Generated; title: string; description: string | null; status: "TODO" | "IN_PROGRESS" | "DONE" | "CANCELLED"; priority: number; dueDate: Timestamp | null; - assigneeId: string | null; - categoryId: string | null; + assigneeId: UUIDString | null; + categoryId: UUIDString | null; isArchived: Generated; createdAt: Generated; updatedAt: Generated; } User: { - id: Generated; + id: Generated; name: string; email: string; role: "ADMIN" | "MEMBER" | "VIEWER"; diff --git a/packages/create-sdk/templates/workflow/src/generated/db.ts b/packages/create-sdk/templates/workflow/src/generated/db.ts index bf14fa403..e660a08da 100644 --- a/packages/create-sdk/templates/workflow/src/generated/db.ts +++ b/packages/create-sdk/templates/workflow/src/generated/db.ts @@ -1,6 +1,7 @@ import { createGetDB, type Generated, + type UUIDString, type Timestamp, type NamespaceDB, type NamespaceInsertable, @@ -14,7 +15,7 @@ import { export interface Namespace { "main-db": { Order: { - id: Generated; + id: Generated; customerName: string; amount: number; status: "PENDING" | "PROCESSING" | "COMPLETED" | "FAILED"; @@ -23,7 +24,7 @@ export interface Namespace { } User: { - id: Generated; + id: Generated; name: string; email: string; age: number; diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.test.ts index 70a915aa0..2074709ef 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.test.ts @@ -188,8 +188,8 @@ describe("db-types-generator", () => { const filePath = await writeDbTypesFile(snapshot, testDir, 1); const content = fs.readFileSync(filePath, "utf-8"); - expect(content).toContain("externalId: string;"); - expect(content).toContain("referenceId: string | null;"); + expect(content).toContain("externalId: UUIDString;"); + expect(content).toContain("referenceId: UUIDString | null;"); }); test("generates types with date/datetime fields using Timestamp", async () => { @@ -208,8 +208,10 @@ describe("db-types-generator", () => { const content = fs.readFileSync(filePath, "utf-8"); // Should define Timestamp type - expect(content).toContain("type Timestamp = ColumnType;"); - expect(content).toContain("eventDate: Timestamp;"); + expect(content).toContain( + "type Timestamp = ColumnType;", + ); + expect(content).toContain("eventDate: DateString;"); expect(content).toContain("startTime: Timestamp;"); expect(content).toContain("endTime: Timestamp | null;"); }); @@ -308,7 +310,7 @@ describe("db-types-generator", () => { const filePath = await writeDbTypesFile(snapshot, testDir, 1); const content = fs.readFileSync(filePath, "utf-8"); - expect(content).toContain("id: Generated;"); + expect(content).toContain("id: Generated;"); expect(content).toContain( "type Generated = T extends ColumnType", ); @@ -413,7 +415,7 @@ describe("db-types-generator", () => { const content = fs.readFileSync(filePath, "utf-8"); expect(content).toContain( - "updatedAt: ColumnType;", + "updatedAt: ColumnType;", ); }); diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.ts b/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.ts index 4fa61d33f..945478d88 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.ts @@ -15,6 +15,14 @@ import { } from "./snapshot"; import type { MigrationDiff } from "./diff-calculator"; +type UsedUtilityType = + | "Timestamp" + | "Serial" + | "DateString" + | "DateTimeString" + | "DecimalString" + | "TimeString"; + /** * Information about enum value changes */ @@ -132,26 +140,37 @@ function generateDbTypesFromSnapshot(snapshot: SchemaSnapshot, diff?: MigrationD }; // Track which utility types are used - const usedUtilityTypes = new Set<"Timestamp" | "Serial">(); + const usedUtilityTypes = new Set(); // Generate type definitions const typeDefinitions: string[] = []; for (const type of types) { const result = generateTableType(type, breakingChangeFields); - if (result.usedTimestamp) usedUtilityTypes.add("Timestamp"); + for (const utilityType of result.usedUtilityTypes) { + usedUtilityTypes.add(utilityType); + } typeDefinitions.push(result.typeDef); } // Build imports // ColumnType is always needed for Generated and Timestamp utility types - const imports: string[] = ["type ColumnType", "type Transaction as KyselyTransaction"]; + const imports: string[] = [ + "type ColumnType", + "type Transaction as KyselyTransaction", + "type UUIDString", + ]; + if (usedUtilityTypes.has("DateString")) imports.push("type DateString"); + if (usedUtilityTypes.has("DateTimeString")) imports.push("type DateTimeString"); + if (usedUtilityTypes.has("DecimalString")) imports.push("type DecimalString"); + if (usedUtilityTypes.has("TimeString")) imports.push("type TimeString"); // Build utility type declarations const utilityTypeDeclarations: string[] = []; if (usedUtilityTypes.has("Timestamp")) { utilityTypeDeclarations.push( - "type Timestamp = ColumnType;", + "type Timestamp = ColumnType;", ); + if (!usedUtilityTypes.has("DateTimeString")) imports.push("type DateTimeString"); } utilityTypeDeclarations.push( "type Generated = T extends ColumnType\n ? ColumnType\n : ColumnType;", @@ -224,22 +243,22 @@ function generateEmptyDbTypes(namespace: string): string { * Generate table type definition from a snapshot type * @param {TailorDBSnapshotType} type - Snapshot type * @param {BreakingChangeFieldInfo} breakingChangeFields - Breaking change field info - * @returns {{ typeDef: string; usedTimestamp: boolean; usedColumnType: boolean }} Generated type and utility type usage + * @returns Generated type and utility type usage */ function generateTableType( type: TailorDBSnapshotType, breakingChangeFields: BreakingChangeFieldInfo, ): { typeDef: string; - usedTimestamp: boolean; + usedUtilityTypes: Set; usedColumnType: boolean; } { const fieldLines: string[] = []; - let usedTimestamp = false; + const usedUtilityTypes = new Set(); let usedColumnType = false; // Add id field first - fieldLines.push(" id: Generated;"); + fieldLines.push(" id: Generated;"); // Get fields that are changing from optional to required for this type const optionalToRequiredFields = @@ -258,7 +277,9 @@ function generateTableType( const enumValueChange = enumValueChangesForType.get(fieldName); const result = generateFieldType(fieldConfig, isOptionalToRequired, enumValueChange); fieldLines.push(` ${fieldName}: ${result.type};`); - usedTimestamp = usedTimestamp || result.usedTimestamp; + for (const utilityType of result.usedUtilityTypes) { + usedUtilityTypes.add(utilityType); + } usedColumnType = usedColumnType || result.usedColumnType; } @@ -268,36 +289,43 @@ function generateTableType( // Treat as optional→required change (isOptionalToRequired: true) const result = generateFieldType(fieldConfig, true, undefined); fieldLines.push(` ${fieldName}: ${result.type};`); - usedTimestamp = usedTimestamp || result.usedTimestamp; + for (const utilityType of result.usedUtilityTypes) { + usedUtilityTypes.add(utilityType); + } usedColumnType = usedColumnType || result.usedColumnType; } const typeDef = ` ${type.name}: {\n${fieldLines.join("\n")}\n }`; - return { typeDef, usedTimestamp, usedColumnType }; + return { typeDef, usedUtilityTypes, usedColumnType }; } function mapToTsType(fieldType: string): { type: string; - usedTimestamp: boolean; + usedUtilityTypes: Set; } { switch (fieldType) { case "uuid": + return { type: "UUIDString", usedUtilityTypes: new Set() }; case "string": + return { type: "string", usedUtilityTypes: new Set() }; case "decimal": - return { type: "string", usedTimestamp: false }; + return { type: "DecimalString", usedUtilityTypes: new Set(["DecimalString"]) }; case "integer": case "float": case "number": - return { type: "number", usedTimestamp: false }; + return { type: "number", usedUtilityTypes: new Set() }; case "date": + return { type: "DateString", usedUtilityTypes: new Set(["DateString"]) }; case "datetime": - return { type: "Timestamp", usedTimestamp: true }; + return { type: "Timestamp", usedUtilityTypes: new Set(["Timestamp"]) }; + case "time": + return { type: "TimeString", usedUtilityTypes: new Set(["TimeString"]) }; case "bool": case "boolean": - return { type: "boolean", usedTimestamp: false }; + return { type: "boolean", usedUtilityTypes: new Set() }; default: - return { type: "string", usedTimestamp: false }; + return { type: "string", usedUtilityTypes: new Set() }; } } @@ -325,14 +353,36 @@ function generateEnumChangeColumnType( return `ColumnType<${selectType}, ${afterType}, ${afterType}>`; } -function generateOptionalToRequiredDateColumnType(config: SnapshotFieldConfig): string | null { +function generateOptionalToRequiredDateColumnType( + config: SnapshotFieldConfig, +): { type: string; usedUtilityTypes: Set } | null { if (config.type !== "date" && config.type !== "datetime") return null; + if (config.type === "date") { + if (config.array) { + return { + type: "ColumnType", + usedUtilityTypes: new Set(["DateString"]), + }; + } + + return { + type: "ColumnType", + usedUtilityTypes: new Set(["DateString"]), + }; + } + if (config.array) { - return "ColumnType"; + return { + type: "ColumnType<(Date | DateTimeString)[] | null, (Date | DateTimeString)[], (Date | DateTimeString)[]>", + usedUtilityTypes: new Set(["DateTimeString"]), + }; } - return "ColumnType"; + return { + type: "ColumnType", + usedUtilityTypes: new Set(["DateTimeString"]), + }; } /** @@ -340,7 +390,7 @@ function generateOptionalToRequiredDateColumnType(config: SnapshotFieldConfig): * @param {SnapshotFieldConfig} config - Field configuration * @param {boolean} isOptionalToRequired - Whether this field is changing from optional to required * @param {EnumValueChange} [enumValueChange] - Enum value change info if applicable - * @returns {{ type: string; usedTimestamp: boolean; usedColumnType: boolean }} Generated type string and utility type usage + * @returns Generated type string and utility type usage */ function generateFieldType( config: SnapshotFieldConfig, @@ -348,21 +398,21 @@ function generateFieldType( enumValueChange?: EnumValueChange, ): { type: string; - usedTimestamp: boolean; + usedUtilityTypes: Set; usedColumnType: boolean; } { // Handle enum value changes specially if (enumValueChange) { return { type: generateEnumChangeColumnType(enumValueChange, config), - usedTimestamp: false, + usedUtilityTypes: new Set(), usedColumnType: true, }; } // Get base type let baseType: string; - let usedTimestamp = false; + let usedUtilityTypes = new Set(); if (config.type === "enum") { const enumValues = config.allowedValues?.map((v) => v.value) ?? []; @@ -370,7 +420,7 @@ function generateFieldType( } else { const mapped = mapToTsType(config.type); baseType = mapped.type; - usedTimestamp = mapped.usedTimestamp; + usedUtilityTypes = mapped.usedUtilityTypes; } // Apply array modifier @@ -386,8 +436,8 @@ function generateFieldType( const dateColumnType = generateOptionalToRequiredDateColumnType(config); if (dateColumnType) { return { - type: dateColumnType, - usedTimestamp: false, + type: dateColumnType.type, + usedUtilityTypes: dateColumnType.usedUtilityTypes, usedColumnType: true, }; } @@ -397,7 +447,7 @@ function generateFieldType( // INSERT/UPDATE requires T (must provide a value) return { type: `ColumnType<${type} | null, ${type}, ${type}>`, - usedTimestamp, + usedUtilityTypes, usedColumnType: true, }; } @@ -406,7 +456,7 @@ function generateFieldType( type = `${type} | null`; } - return { type, usedTimestamp, usedColumnType: false }; + return { type, usedUtilityTypes, usedColumnType: false }; } /** diff --git a/packages/sdk/src/cli/shared/runtime-exprs.test.ts b/packages/sdk/src/cli/shared/runtime-exprs.test.ts index c127955d7..97a39eee6 100644 --- a/packages/sdk/src/cli/shared/runtime-exprs.test.ts +++ b/packages/sdk/src/cli/shared/runtime-exprs.test.ts @@ -69,14 +69,14 @@ describe("buildExecutorArgsExpr", () => { namespaceName: "app", actor: { userType: "USER_TYPE_USER", - userId: "user-1", + userId: "11111111-1111-4111-8111-111111111111", workspaceId: "workspace-1", attributeMap: { role: "admin" }, attributes: ["role"], }, }).actor, ).toEqual({ - id: "user-1", + id: "11111111-1111-4111-8111-111111111111", type: "user", workspaceId: "workspace-1", attributes: { role: "admin" }, @@ -89,7 +89,7 @@ describe("buildExecutorArgsExpr", () => { const actors = [ null, undefined, - { userType: "USER_TYPE_UNSPECIFIED", userId: "user-1" }, + { userType: "USER_TYPE_UNSPECIFIED", userId: "11111111-1111-4111-8111-111111111111" }, { userType: "USER_TYPE_USER", userId: nilUuid }, { userType: "USER_TYPE_USER" }, ]; @@ -210,7 +210,7 @@ describe("buildResolverOperationHookExpr", () => { const users = [ null, undefined, - { type: "USER_TYPE_UNSPECIFIED", id: "user-1" }, + { type: "USER_TYPE_UNSPECIFIED", id: "11111111-1111-4111-8111-111111111111" }, { type: "USER_TYPE_USER", id: nilUuid }, ]; for (const user of users) { @@ -223,14 +223,14 @@ describe("INVOKER_EXPR", () => { test("maps invoker payloads to TailorPrincipal shape", () => { expect( runInvokerExpr({ - id: "user-1", + id: "11111111-1111-4111-8111-111111111111", type: "user", workspaceId: "workspace-1", attributeMap: { role: "member" }, attributes: ["role"], }), ).toEqual({ - id: "user-1", + id: "11111111-1111-4111-8111-111111111111", type: "user", workspaceId: "workspace-1", attributes: { role: "member" }, diff --git a/packages/sdk/src/configure/index.ts b/packages/sdk/src/configure/index.ts index 30356ab34..9f45829b6 100644 --- a/packages/sdk/src/configure/index.ts +++ b/packages/sdk/src/configure/index.ts @@ -16,6 +16,14 @@ export namespace t { } export { type TailorField } from "#/configure/types/type"; +export type { + DateString, + DateTimeString, + DecimalString, + TimeString, + TimeZoneOffsetString, + UUIDString, +} from "#/configure/types/scalar.types"; export { type TailorPrincipal, type Attributes, diff --git a/packages/sdk/src/configure/services/executor/executor.test.ts b/packages/sdk/src/configure/services/executor/executor.test.ts index b376a1c88..f3035d4f3 100644 --- a/packages/sdk/src/configure/services/executor/executor.test.ts +++ b/packages/sdk/src/configure/services/executor/executor.test.ts @@ -16,6 +16,7 @@ import { } from "./trigger/event"; import { scheduleTrigger } from "./trigger/schedule"; import { incomingWebhookTrigger } from "./trigger/webhook"; +import type { UUIDString } from "#/configure/types/scalar.types"; import type { TailorPrincipal } from "#/runtime/types"; import type { Operation } from "./operation"; @@ -310,7 +311,7 @@ describe("recordCreatedTrigger", () => { appNamespace: string; typeName: string; newRecord: { - id: string; + id: UUIDString; name: string; age: number; }; @@ -326,7 +327,7 @@ describe("recordCreatedTrigger", () => { appNamespace: string; typeName: string; newRecord: { - id: string; + id: UUIDString; name: string; age: number; }; @@ -388,12 +389,12 @@ describe("recordUpdatedTrigger", () => { appNamespace: string; typeName: string; newRecord: { - id: string; + id: UUIDString; name: string; age: number; }; oldRecord: { - id: string; + id: UUIDString; name: string; age: number; }; @@ -409,12 +410,12 @@ describe("recordUpdatedTrigger", () => { appNamespace: string; typeName: string; newRecord: { - id: string; + id: UUIDString; name: string; age: number; }; oldRecord: { - id: string; + id: UUIDString; name: string; age: number; }; @@ -476,7 +477,7 @@ describe("recordDeletedTrigger", () => { appNamespace: string; typeName: string; oldRecord: { - id: string; + id: UUIDString; name: string; age: number; }; @@ -492,7 +493,7 @@ describe("recordDeletedTrigger", () => { appNamespace: string; typeName: string; oldRecord: { - id: string; + id: UUIDString; name: string; age: number; }; @@ -726,7 +727,7 @@ describe("resolverExecutedTrigger", () => { const resolver = createResolver({ name: "test", operation: "query", - body: () => ({ userId: "user-123" }), + body: () => ({ userId: "123e4567-e89b-12d3-a456-426614174000" }), output: t.object({ userId: t.string(), }), @@ -872,19 +873,19 @@ describe("recordTrigger (multi-event)", () => { // Can narrow by kind if (args.event === "created") { expectTypeOf(args.newRecord).toExtend<{ - id: string; + id: UUIDString; name: string; age: number; }>(); } if (args.event === "updated") { expectTypeOf(args.newRecord).toExtend<{ - id: string; + id: UUIDString; name: string; age: number; }>(); expectTypeOf(args.oldRecord).toExtend<{ - id: string; + id: UUIDString; name: string; age: number; }>(); @@ -927,7 +928,7 @@ describe("recordTrigger (multi-event)", () => { body: (args) => { if (args.event === "deleted") { expectTypeOf(args.oldRecord).toExtend<{ - id: string; + id: UUIDString; name: string; age: number; }>(); @@ -962,7 +963,7 @@ describe("idpUserTrigger (multi-event)", () => { workspaceId: string; appNamespace: string; namespaceName: string; - userId: string; + userId: UUIDString; }>(); if (args.event === "created") { expectTypeOf(args.event).toEqualTypeOf<"created">(); @@ -991,7 +992,7 @@ describe("authAccessTokenTrigger (multi-event)", () => { workspaceId: string; appNamespace: string; namespaceName: string; - userId: string; + userId: UUIDString; }>(); if (args.event === "issued") { expectTypeOf(args.event).toEqualTypeOf<"issued">(); @@ -1110,7 +1111,7 @@ describe("gqlTarget", () => { } `, variables: () => ({ - id: "test-id", + id: "123e4567-e89b-12d3-a456-426614174000", }), }, }); @@ -1291,7 +1292,7 @@ describe("workflowTarget", () => { operation: { kind: "workflow", workflow: testWorkflow, - args: { orderId: "test-id" }, + args: { orderId: "123e4567-e89b-12d3-a456-426614174000" }, }, }); expect(executor.operation.kind).toBe("workflow"); @@ -1372,7 +1373,7 @@ describe("workflowTarget", () => { operation: { kind: "workflow", workflow: testWorkflow, - args: { orderId: "test-id" }, + args: { orderId: "123e4567-e89b-12d3-a456-426614174000" }, invoker: "admin", }, }); @@ -1408,7 +1409,7 @@ describe("workflowTarget", () => { workflow: testWorkflow, args: (args) => { expectTypeOf(args).not.toHaveProperty("invoker"); - return { orderId: "test-id" }; + return { orderId: "123e4567-e89b-12d3-a456-426614174000" }; }, }, }); diff --git a/packages/sdk/src/configure/services/executor/trigger/event.ts b/packages/sdk/src/configure/services/executor/trigger/event.ts index 8e5469281..d3fabe194 100644 --- a/packages/sdk/src/configure/services/executor/trigger/event.ts +++ b/packages/sdk/src/configure/services/executor/trigger/event.ts @@ -1,6 +1,7 @@ import type { ResolverConfig } from "#/configure/services/resolver/resolver"; import type { TailorDBType } from "#/configure/services/tailordb/schema"; import type { IdpName } from "#/configure/types/idp-name"; +import type { UUIDString } from "#/configure/types/scalar.types"; import type { TailorEnv, TailorPrincipal } from "#/runtime/types"; import type { TailorDBTrigger as ParserTailorDBTrigger, @@ -76,21 +77,21 @@ export interface IdpUserCreatedArgs extends EventArgs { event: "created"; rawEvent: "idp.user.created"; namespaceName: string; - userId: string; + userId: UUIDString; } export interface IdpUserUpdatedArgs extends EventArgs { event: "updated"; rawEvent: "idp.user.updated"; namespaceName: string; - userId: string; + userId: UUIDString; } export interface IdpUserDeletedArgs extends EventArgs { event: "deleted"; rawEvent: "idp.user.deleted"; namespaceName: string; - userId: string; + userId: UUIDString; } export type IdpUserArgs = IdpUserCreatedArgs | IdpUserUpdatedArgs | IdpUserDeletedArgs; @@ -100,21 +101,21 @@ export interface AuthAccessTokenIssuedArgs extends EventArgs { event: "issued"; rawEvent: "auth.access_token.issued"; namespaceName: string; - userId: string; + userId: UUIDString; } export interface AuthAccessTokenRefreshedArgs extends EventArgs { event: "refreshed"; rawEvent: "auth.access_token.refreshed"; namespaceName: string; - userId: string; + userId: UUIDString; } export interface AuthAccessTokenRevokedArgs extends EventArgs { event: "revoked"; rawEvent: "auth.access_token.revoked"; namespaceName: string; - userId: string; + userId: UUIDString; } export type AuthAccessTokenArgs = diff --git a/packages/sdk/src/configure/services/resolver/resolver.test.ts b/packages/sdk/src/configure/services/resolver/resolver.test.ts index 8e37dc0d7..115f34438 100644 --- a/packages/sdk/src/configure/services/resolver/resolver.test.ts +++ b/packages/sdk/src/configure/services/resolver/resolver.test.ts @@ -2,6 +2,12 @@ import { describe, expectTypeOf, test, expect } from "vitest"; import { db } from "#/configure/services/tailordb/index"; import { t } from "#/configure/types/index"; import { createResolver } from "./resolver"; +import type { + DateString, + DateTimeString, + TimeString, + UUIDString, +} from "#/configure/types/scalar.types"; import type { TailorPrincipal } from "#/runtime/types"; import type { output } from "#/types/helpers"; import type { ResolverInput } from "#/types/resolver.generated"; @@ -186,12 +192,12 @@ describe("createResolver", () => { success: t.bool(), }), body: (context) => { - expectTypeOf(context.input.id).toBeString(); + expectTypeOf(context.input.id).toEqualTypeOf(); expectTypeOf(context.input.name).toBeString(); expectTypeOf(context.input.active).toBeBoolean(); expectTypeOf(context.input.count).toBeNumber(); expectTypeOf(context.input.score).toBeNumber(); - expectTypeOf(context.input.createdAt).toExtend(); + expectTypeOf(context.input.createdAt).toEqualTypeOf(); expectTypeOf(context.input.tags).toBeArray(); expectTypeOf(context.input.metadata.key).toBeString(); return { success: true }; @@ -276,7 +282,7 @@ describe("createResolver", () => { body: (context) => { expectTypeOf(context.caller).toEqualTypeOf(); if (!context.caller) return { userId: "anonymous" }; - expectTypeOf(context.caller.id).toBeString(); + expectTypeOf(context.caller.id).toEqualTypeOf(); expectTypeOf(context.caller.type).toEqualTypeOf<"user" | "machine_user">(); expectTypeOf(context.caller.workspaceId).toBeString(); return { userId: context.caller.id }; @@ -341,14 +347,14 @@ describe("createResolver", () => { summary: t.string(), }), body: (context) => { - expectTypeOf(context.input.uuid).toBeString(); + expectTypeOf(context.input.uuid).toEqualTypeOf(); expectTypeOf(context.input.string).toBeString(); expectTypeOf(context.input.bool).toBeBoolean(); expectTypeOf(context.input.int).toBeNumber(); expectTypeOf(context.input.float).toBeNumber(); - expectTypeOf(context.input.date).toExtend(); - expectTypeOf(context.input.datetime).toExtend(); - expectTypeOf(context.input.time).toBeString(); + expectTypeOf(context.input.date).toEqualTypeOf(); + expectTypeOf(context.input.datetime).toEqualTypeOf(); + expectTypeOf(context.input.time).toEqualTypeOf(); return { summary: "ok" }; }, }); diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index dc8c906d2..e17af480d 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -21,7 +21,7 @@ describe("TailorDBField basic field type tests", () => { name: db.string(), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; name: string; }>(); }); @@ -31,7 +31,7 @@ describe("TailorDBField basic field type tests", () => { age: db.int(), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; age: number; }>(); }); @@ -41,7 +41,7 @@ describe("TailorDBField basic field type tests", () => { active: db.bool(), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; active: boolean; }>(); }); @@ -51,7 +51,7 @@ describe("TailorDBField basic field type tests", () => { price: db.float(), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; price: number; }>(); }); @@ -61,7 +61,7 @@ describe("TailorDBField basic field type tests", () => { uuid: db.uuid(), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; uuid: UUIDString; }>(); }); @@ -71,7 +71,7 @@ describe("TailorDBField basic field type tests", () => { birthDate: db.date(), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; birthDate: DateString; }>(); }); @@ -81,7 +81,7 @@ describe("TailorDBField basic field type tests", () => { timestamp: db.datetime(), }); expectTypeOf>().toMatchObjectType<{ - id: string; + id: UUIDString; timestamp: DateTimeString | Date; }>(); }); @@ -91,12 +91,12 @@ describe("TailorDBField basic field type tests", () => { openingTime: db.time(), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; openingTime: TimeString; }>(); }); - test("pickFields preserves the generated id string type", () => { + test("pickFields preserves the generated id UUID type", () => { const _schemaType = t.object({ ...db .type("Test", { @@ -106,7 +106,7 @@ describe("TailorDBField basic field type tests", () => { }); expectTypeOf>().toEqualTypeOf<{ - id?: string | null; + id?: UUIDString | null; }>(); }); @@ -121,7 +121,7 @@ describe("TailorDBField basic field type tests", () => { }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; names: string; nickname: string; }>(); @@ -166,7 +166,7 @@ describe("TailorDBField optional option tests", () => { description: db.string({ optional: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; description?: string | null; }>(); }); @@ -178,7 +178,7 @@ describe("TailorDBField optional option tests", () => { count: db.int({ optional: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; title: string; description?: string | null; count?: number | null; @@ -192,7 +192,7 @@ describe("TailorDBField array option tests", () => { tags: db.string({ array: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; tags: string[]; }>(); }); @@ -202,7 +202,7 @@ describe("TailorDBField array option tests", () => { items: db.string({ optional: true, array: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; items?: string[] | null; }>(); }); @@ -214,7 +214,7 @@ describe("TailorDBField array option tests", () => { flags: db.bool({ array: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; tags: string[]; numbers: number[]; flags: boolean[]; @@ -269,7 +269,7 @@ describe("TailorDBField enum field tests", () => { priority: db.enum(["high", "medium", "low"], { optional: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; priority?: "high" | "medium" | "low" | null; }>(); }); @@ -290,7 +290,7 @@ describe("TailorDBField enum field tests", () => { categories: db.enum(["a", "b", "c"], { array: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; categories: ("a" | "b" | "c")[]; }>(); }); @@ -432,7 +432,7 @@ describe("TailorDBField modifier chain tests", () => { email: db.string().index(), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; email: string; }>(); }); @@ -442,7 +442,7 @@ describe("TailorDBField modifier chain tests", () => { username: db.string().unique(), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; username: string; }>(); }); @@ -547,7 +547,7 @@ describe("TailorDBField relation modifier tests", () => { }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; title: string; authorId: UUIDString; }>(); @@ -579,7 +579,7 @@ describe("TailorDBField hooks modifier tests", () => { }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; name: string; }>(); }); @@ -610,7 +610,7 @@ describe("TailorDBField validate modifier tests", () => { email: db.string().validate(() => true), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; email: string; }>(); }); @@ -620,7 +620,7 @@ describe("TailorDBField validate modifier tests", () => { email: db.string().validate([({ value }) => value.includes("@"), "Email must contain @"]), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; email: string; }>(); @@ -755,7 +755,7 @@ describe("TailorDBType withTimestamps option tests", () => { ...db.fields.timestamps(), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; name: string; createdAt: DateTimeString | Date; updatedAt: DateTimeString | Date; @@ -839,7 +839,7 @@ describe("TailorDBType composite type tests", () => { closingTime: db.time(), }); expectTypeOf>().toMatchObjectType<{ - id: string; + id: UUIDString; name: string; email: string; age?: number | null; @@ -860,7 +860,7 @@ describe("TailorDBType edge case tests", () => { value: db.string(), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; value: string; }>(); }); @@ -872,7 +872,7 @@ describe("TailorDBType edge case tests", () => { c: db.bool({ optional: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; a?: string | null; b?: number | null; c?: boolean | null; @@ -886,7 +886,7 @@ describe("TailorDBType edge case tests", () => { booleans: db.bool({ array: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; strings: string[]; numbers: number[]; booleans: boolean[]; @@ -912,7 +912,7 @@ describe("TailorDBType type consistency tests", () => { name: db.string(), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; name: string; }>(); }); @@ -1019,7 +1019,7 @@ describe("TailorDBType plural form tests", () => { }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; title: string; content?: string | null; createdAt: DateTimeString | Date; @@ -1107,7 +1107,7 @@ describe("TailorDBType hooks modifier tests", () => { }, }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; name: string; }>(); }); @@ -1146,7 +1146,7 @@ describe("TailorDBType hooks modifier tests", () => { expectTypeOf().toEqualTypeOf< Hook< { - id: string; + id: UUIDString; readonly name: string; }, string @@ -1165,7 +1165,7 @@ describe("TailorDBType hooks modifier tests", () => { expectTypeOf().toEqualTypeOf< Hook< { - id: string; + id: UUIDString; name?: string | null; }, string | null @@ -1185,7 +1185,7 @@ describe("TailorDBType validate modifier tests", () => { }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; email: string; }>(); const fieldMetadata = _validateType.fields.email.metadata; @@ -1247,7 +1247,7 @@ describe("TailorDBType validate modifier tests", () => { test("validate modifier on string field receives string", () => { const _validate = db.type("Test", { name: db.string() }).validate; - expectTypeOf>().toExtend< + expectTypeOf>().toExtend< Parameters[0]["name"] >(); }); @@ -1256,9 +1256,9 @@ describe("TailorDBType validate modifier tests", () => { const _validate = db.type("Test", { name: db.string({ optional: true }), }).validate; - expectTypeOf>().toExtend< - Parameters[0]["name"] - >(); + expectTypeOf< + ValidateConfig + >().toExtend[0]["name"]>(); }); }); @@ -1271,7 +1271,7 @@ describe("db.object tests", () => { }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; user: { name: string; age: number; @@ -1298,7 +1298,7 @@ describe("db.object tests", () => { }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; user: { name: string; age?: number | null; @@ -1318,7 +1318,7 @@ describe("db.object tests", () => { ), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; user?: { name: string; avatar?: string | null; @@ -1337,7 +1337,7 @@ describe("db.object tests", () => { ), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; users: { name: string; age: number; @@ -1354,7 +1354,7 @@ describe("db.object tests", () => { }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; user: { name: string; tags: string[]; @@ -1375,7 +1375,7 @@ describe("db.object tests", () => { ), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; optionalUsers?: | { name: string; @@ -1394,7 +1394,7 @@ describe("db.object tests", () => { }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; settings: { enabled: boolean; push?: boolean | null; @@ -1412,7 +1412,7 @@ describe("db.object tests", () => { }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; product: { name: string; price: number; @@ -1571,7 +1571,7 @@ describe("TailorDBType files method tests", () => { describe("TailorDBField runtime validation tests", () => { const invoker: TailorPrincipal = { - id: "test", + id: "123e4567-e89b-12d3-a456-426614174000", type: "user", workspaceId: "workspace-test", attributes: {}, @@ -2238,7 +2238,7 @@ describe("TailorDBField decimal type tests", () => { price: db.decimal(), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; price: DecimalString; }>(); }); @@ -2248,7 +2248,7 @@ describe("TailorDBField decimal type tests", () => { discount: db.decimal({ optional: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: string; + id: UUIDString; discount?: DecimalString | null; }>(); }); @@ -2277,7 +2277,7 @@ describe("TailorDBField decimal type tests", () => { test("decimal parse validates valid decimal strings", () => { const field = db.decimal(); const invoker: TailorPrincipal = { - id: "test", + id: "123e4567-e89b-12d3-a456-426614174000", type: "user", workspaceId: "workspace-test", attributes: {}, @@ -2298,7 +2298,7 @@ describe("TailorDBField decimal type tests", () => { test("decimal parse rejects invalid decimal strings", () => { const field = db.decimal(); const invoker: TailorPrincipal = { - id: "test", + id: "123e4567-e89b-12d3-a456-426614174000", type: "user", workspaceId: "workspace-test", attributes: {}, diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index c308586e0..ca08cdf73 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -26,6 +26,7 @@ import type { ValidateConfig, Validators, } from "#/configure/types/field.types"; +import type { UUIDString } from "#/configure/types/scalar.types"; import type { PluginAttachment, PluginConfigs } from "#/plugin/types"; import type { InferredAttributes, TailorPrincipal } from "#/runtime/types"; import type { output, InferFieldsOutput, TypeLevelError } from "#/types/helpers"; @@ -1220,7 +1221,7 @@ function createTailorDBType< return brandValue(dbType, "tailordb-type"); } -const idField = createField<"uuid", Record, string>("uuid"); +const idField = createField<"uuid", Record, UUIDString>("uuid"); type idField = typeof idField; type DBType> = TailorDBInstance< { id: idField } & F diff --git a/packages/sdk/src/configure/services/workflow/job.test.ts b/packages/sdk/src/configure/services/workflow/job.test.ts index 0376883e2..8a8ac3994 100644 --- a/packages/sdk/src/configure/services/workflow/job.test.ts +++ b/packages/sdk/src/configure/services/workflow/job.test.ts @@ -106,7 +106,7 @@ describe("WorkflowJob type inference", () => { }); try { const invoker: TailorPrincipal = { - id: "principal-1", + id: "11111111-1111-4111-8111-111111111111", type: "user", workspaceId: "workspace-1", attributes: {}, @@ -122,7 +122,9 @@ describe("WorkflowJob type inference", () => { }); await withRegisteredJobRuntime(async () => { - await expect(parent.body(undefined, { env: {}, invoker })).resolves.toBe("principal-1"); + await expect(parent.body(undefined, { env: {}, invoker })).resolves.toBe( + "11111111-1111-4111-8111-111111111111", + ); }); } finally { if (descriptor) { @@ -135,7 +137,7 @@ describe("WorkflowJob type inference", () => { test("direct body calls propagate invoker to triggered child jobs", async () => { const invoker: TailorPrincipal = { - id: "principal-1", + id: "11111111-1111-4111-8111-111111111111", type: "user", workspaceId: "workspace-1", attributes: { role: "ADMIN" }, @@ -151,20 +153,22 @@ describe("WorkflowJob type inference", () => { }); await withRegisteredJobRuntime(async () => { - await expect(parent.body(undefined, { env: {}, invoker })).resolves.toBe("principal-1"); + await expect(parent.body(undefined, { env: {}, invoker })).resolves.toBe( + "11111111-1111-4111-8111-111111111111", + ); }); }); test("concurrent direct body calls isolate invokers for child triggers", async () => { const firstInvoker: TailorPrincipal = { - id: "principal-1", + id: "11111111-1111-4111-8111-111111111111", type: "user", workspaceId: "workspace-1", attributes: {}, attributeList: [], }; const secondInvoker: TailorPrincipal = { - id: "principal-2", + id: "22222222-2222-4222-8222-222222222222", type: "machine_user", workspaceId: "workspace-1", attributes: {}, @@ -199,9 +203,9 @@ describe("WorkflowJob type inference", () => { const second = parent.body({ gate: "second" }, { env: {}, invoker: secondInvoker }); releaseFirst(); - await expect(first).resolves.toBe("principal-1"); + await expect(first).resolves.toBe("11111111-1111-4111-8111-111111111111"); releaseSecond(); - await expect(second).resolves.toBe("principal-2"); + await expect(second).resolves.toBe("22222222-2222-4222-8222-222222222222"); }); }); diff --git a/packages/sdk/src/configure/services/workflow/test-env-key.ts b/packages/sdk/src/configure/services/workflow/test-env-key.ts index 4dbaa0982..5ce588702 100644 --- a/packages/sdk/src/configure/services/workflow/test-env-key.ts +++ b/packages/sdk/src/configure/services/workflow/test-env-key.ts @@ -59,7 +59,7 @@ export function withWorkflowTestInvoker(invoker: TailorPrincipal | null, run: } type RuntimeInvoker = { - id: string; + id: TailorPrincipal["id"]; type: "user" | "machine_user"; workspaceId: string; attributes?: string[] | TailorPrincipal["attributes"]; diff --git a/packages/sdk/src/configure/types/field.types.ts b/packages/sdk/src/configure/types/field.types.ts index 72d77c0a6..5a90597ba 100644 --- a/packages/sdk/src/configure/types/field.types.ts +++ b/packages/sdk/src/configure/types/field.types.ts @@ -3,6 +3,17 @@ // This is a pure type module: type declarations only, no zod/schema // references, importable type-only from any layer. +import type { TailorPrincipal } from "#/runtime/types"; +import type { output, InferFieldsOutput } from "#/types/helpers"; +import type { + DateString, + DateTimeString, + DecimalString, + TimeString, + UUIDString, +} from "./scalar.types"; +import type { NonEmptyObject } from "type-fest"; + export interface EnumValue { value: string; description?: string; @@ -21,13 +32,14 @@ export type TailorFieldType = | "time" | "nested"; -type DateString = `${number}-${number}-${number}`; -type TimeString = `${number}:${number}`; -type TimeZoneOffsetString = "Z" | "z" | `${"+" | "-"}${TimeString}`; -type DateTimeString = - `${DateString}${"T" | "t"}${TimeString}:${number}${"" | `.${number}`}${TimeZoneOffsetString}`; -type UUIDString = `${string}-${string}-${string}-${string}-${string}`; -type DecimalString = `${number}`; +export type { + DateString, + DateTimeString, + DecimalString, + TimeString, + TimeZoneOffsetString, + UUIDString, +} from "./scalar.types"; export type TailorToTs = { string: string; @@ -82,10 +94,6 @@ export type ArrayFieldOutput = [O] extends [ ? T[] : T; -import type { TailorPrincipal } from "#/runtime/types"; -import type { output, InferFieldsOutput } from "#/types/helpers"; -import type { NonEmptyObject } from "type-fest"; - /** * Validation function type */ diff --git a/packages/sdk/src/configure/types/index.ts b/packages/sdk/src/configure/types/index.ts index 7521bddc3..7c1d8774d 100644 --- a/packages/sdk/src/configure/types/index.ts +++ b/packages/sdk/src/configure/types/index.ts @@ -1,3 +1,11 @@ export * from "./machine-user"; export * from "./type"; export * from "./field"; +export type { + DateString, + DateTimeString, + DecimalString, + TimeString, + TimeZoneOffsetString, + UUIDString, +} from "./scalar.types"; diff --git a/packages/sdk/src/configure/types/scalar.types.ts b/packages/sdk/src/configure/types/scalar.types.ts new file mode 100644 index 000000000..5b2653349 --- /dev/null +++ b/packages/sdk/src/configure/types/scalar.types.ts @@ -0,0 +1,7 @@ +export type DateString = `${number}-${number}-${number}`; +export type TimeString = `${number}:${number}`; +export type TimeZoneOffsetString = "Z" | "z" | `${"+" | "-"}${TimeString}`; +export type DateTimeString = + `${DateString}${"T" | "t"}${TimeString}:${number}${"" | `.${number}`}${TimeZoneOffsetString}`; +export type UUIDString = `${string}-${string}-${string}-${string}-${string}`; +export type DecimalString = `${number}`; diff --git a/packages/sdk/src/configure/types/type.test.ts b/packages/sdk/src/configure/types/type.test.ts index 44ff36f1f..9fb5978fd 100644 --- a/packages/sdk/src/configure/types/type.test.ts +++ b/packages/sdk/src/configure/types/type.test.ts @@ -479,7 +479,7 @@ describe("t.object tests", () => { describe("TailorField runtime validation tests", () => { const invoker: TailorPrincipal = { - id: "test", + id: "123e4567-e89b-12d3-a456-426614174000", type: "user", workspaceId: "workspace-test", attributes: {}, @@ -866,7 +866,7 @@ describe("TailorField runtime validation tests", () => { describe("TailorField clone-on-write / no aliasing", () => { const invoker: TailorPrincipal = { - id: "test", + id: "123e4567-e89b-12d3-a456-426614174000", type: "user", workspaceId: "workspace-test", attributes: {}, diff --git a/packages/sdk/src/kysely/index.test-d.ts b/packages/sdk/src/kysely/index.test-d.ts index 4b6d0d89f..4210155f1 100644 --- a/packages/sdk/src/kysely/index.test-d.ts +++ b/packages/sdk/src/kysely/index.test-d.ts @@ -4,8 +4,11 @@ import type { ObjectColumnType, ArrayColumnType, Timestamp, + DateString, + DateTimeString, NamespaceInsertable, NamespaceSelectable, + UUIDString, } from "./index"; // Sanity check: verify typecheck catches errors @@ -20,15 +23,17 @@ describe("typecheck sanity", () => { type TestNamespace = { testNs: { Receipt: { - id: Generated; - // 1. plain timestamp - receiptDate: Timestamp; - // 2. timestamp inside object + id: Generated; + // 1. plain date + receiptDate: DateString; + // 2. plain timestamp + createdAt: Timestamp; + // 3. timestamp inside object dueSchedule: ObjectColumnType<{ dueDate: Timestamp; reminderAt?: Timestamp | null; }>; - // 3. timestamp inside object x array + // 4. timestamp inside object x array metadata: ArrayColumnType< ObjectColumnType<{ created: Timestamp; @@ -36,18 +41,19 @@ type TestNamespace = { version: number; }> >; - // 4. timestamp array + // 5. timestamp array eventDates: ArrayColumnType; }; }; }; describe("NamespaceInsertable", () => { - test("should accept Date and string for nested datetime on insert", () => { + test("should accept strict date strings and datetimes on insert", () => { type ReceiptInsertable = NamespaceInsertable; assertType({ - receiptDate: new Date(), + receiptDate: "2024-01-01", + createdAt: new Date(), dueSchedule: { dueDate: new Date(), }, @@ -57,37 +63,49 @@ describe("NamespaceInsertable", () => { assertType({ receiptDate: "2024-01-01", + createdAt: "2024-01-01T00:00:00Z", dueSchedule: { - dueDate: "2024-01-01", + dueDate: "2024-01-01T00:00:00Z", }, - metadata: [{ created: "2024-01-01", version: 1 }], - eventDates: ["2024-01-01"], + metadata: [{ created: "2024-01-01T00:00:00Z", version: 1 }], + eventDates: ["2024-01-01T00:00:00Z"], }); }); }); describe("NamespaceSelectable", () => { - test("should return Date for both top-level and nested datetime", () => { + test("should return strict date strings and datetimes", () => { type ReceiptSelectable = NamespaceSelectable; - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + Date | DateTimeString + >(); // Nullable nested fields should be required in select - expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + Date | DateTimeString | null + >(); }); test("should return array of resolved objects for ObjectArrayColumnType", () => { type ReceiptSelectable = NamespaceSelectable; expectTypeOf().toEqualTypeOf< - { created: Date; lastUpdated: Date | null; version: number }[] + { + created: Date | DateTimeString; + lastUpdated: Date | DateTimeString | null; + version: number; + }[] + >(); + expectTypeOf().toEqualTypeOf< + Date | DateTimeString >(); - expectTypeOf().toEqualTypeOf(); }); - test("should return Date[] for timestamp array", () => { + test("should return datetime arrays for timestamp array", () => { type ReceiptSelectable = NamespaceSelectable; - expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<(Date | DateTimeString)[]>(); }); }); diff --git a/packages/sdk/src/kysely/index.ts b/packages/sdk/src/kysely/index.ts index e063a33de..10288b3d5 100644 --- a/packages/sdk/src/kysely/index.ts +++ b/packages/sdk/src/kysely/index.ts @@ -16,6 +16,7 @@ import { type Transaction as KyselyTransaction, type Updateable, } from "kysely"; +import type { DateTimeString } from "#/configure/types/scalar.types"; export { type ColumnType, @@ -30,7 +31,20 @@ export { export { TailordbDialect } from "@tailor-platform/function-kysely-tailordb"; -export type Timestamp = ColumnType; +export type { + DateString, + DateTimeString, + DecimalString, + TimeString, + TimeZoneOffsetString, + UUIDString, +} from "#/configure/types/scalar.types"; + +export type Timestamp = ColumnType< + Date | DateTimeString, + Date | DateTimeString, + Date | DateTimeString +>; type ResolveSelect = T extends ColumnType ? S : T; type ResolveInsert = T extends ColumnType ? I : T; type ResolveUpdate = T extends ColumnType ? U : T; diff --git a/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts index 9f64dc419..2e392d7bb 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts @@ -71,13 +71,13 @@ describe("KyselyTypePlugin integration tests", () => { expect(result.name).toBe("User"); expect(result.typeDef).toContain("User: {"); - expect(result.typeDef).toContain("id: Generated;"); + expect(result.typeDef).toContain("id: Generated;"); expect(result.typeDef).toContain("name: string;"); expect(result.typeDef).toContain("email: string;"); expect(result.typeDef).toContain("age: number | null;"); expect(result.typeDef).toContain("isActive: boolean;"); expect(result.typeDef).toContain("score: number | null;"); - expect(result.typeDef).toContain("birthDate: Timestamp | null;"); + expect(result.typeDef).toContain("birthDate: DateString | null;"); expect(result.typeDef).toContain("lastLogin: Timestamp | null;"); expect(result.typeDef).toContain("tags: string[];"); expect(result.typeDef).toContain("createdAt: Generated;"); diff --git a/packages/sdk/src/plugin/builtin/kysely-type/index.ts b/packages/sdk/src/plugin/builtin/kysely-type/index.ts index 9d47c9a88..c5da7764e 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/index.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/index.ts @@ -42,8 +42,17 @@ export function kyselyTypePlugin( (acc, type) => ({ Timestamp: acc.Timestamp || type.usedUtilityTypes.Timestamp, Serial: acc.Serial || type.usedUtilityTypes.Serial, + DateString: acc.DateString || type.usedUtilityTypes.DateString, + DecimalString: acc.DecimalString || type.usedUtilityTypes.DecimalString, + TimeString: acc.TimeString || type.usedUtilityTypes.TimeString, }), - { Timestamp: false, Serial: false }, + { + Timestamp: false, + Serial: false, + DateString: false, + DecimalString: false, + TimeString: false, + }, ); allNamespaceData.push({ diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts index 4a22b4eb1..7dfe31156 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts @@ -61,7 +61,7 @@ describe("Kysely TypeProcessor", () => { const result = await processKyselyType(parseTailorDBType(toSchemaOutput(type))); - expect(result.typeDef).toContain("startDate: Timestamp;"); + expect(result.typeDef).toContain("startDate: DateString;"); expect(result.typeDef).toContain("endDate: Timestamp;"); expect(result.typeDef).toContain("cancelledAt: Timestamp | null;"); }); @@ -74,8 +74,8 @@ describe("Kysely TypeProcessor", () => { const result = await processKyselyType(parseTailorDBType(toSchemaOutput(type))); - expect(result.typeDef).toContain("userId: string;"); - expect(result.typeDef).toContain("deviceId: string | null;"); + expect(result.typeDef).toContain("userId: UUIDString;"); + expect(result.typeDef).toContain("deviceId: UUIDString | null;"); }); }); @@ -101,7 +101,7 @@ describe("Kysely TypeProcessor", () => { const result = await processKyselyType(parseTailorDBType(toSchemaOutput(type))); expect(result.typeDef).toContain("eventDates: ArrayColumnType;"); - expect(result.typeDef).toContain("optionalDates: ArrayColumnType | null;"); + expect(result.typeDef).toContain("optionalDates: DateString[] | null;"); }); }); @@ -181,7 +181,7 @@ describe("Kysely TypeProcessor", () => { expect(result.typeDef).toContain("phone?: string | null"); }); - test("should use Date | string instead of Timestamp for date fields inside nested objects", async () => { + test("should use DateString for date fields inside nested objects", async () => { const type = db.type("Receipt", { receiptDate: db.date(), dueSchedule: db.object({ @@ -192,10 +192,10 @@ describe("Kysely TypeProcessor", () => { const result = await processKyselyType(parseTailorDBType(toSchemaOutput(type))); - expect(result.typeDef).toContain("receiptDate: Timestamp;"); + expect(result.typeDef).toContain("receiptDate: DateString;"); // Nested object with datetime is wrapped in ObjectColumnType expect(result.typeDef).toContain("ObjectColumnType<"); - expect(result.typeDef).toContain("dueDate: Timestamp"); + expect(result.typeDef).toContain("dueDate: DateString"); expect(result.typeDef).toContain("reminderAt?: Timestamp | null"); expect(result.usedUtilityTypes.Timestamp).toBe(true); }); @@ -287,14 +287,14 @@ describe("Kysely TypeProcessor", () => { expect(result.typeDef).toContain("updatedAt: Generated;"); }); - test("should always include Generated for id field", async () => { + test("should always include Generated for id field", async () => { const type = db.type("User", { name: db.string(), }); const result = await processKyselyType(parseTailorDBType(toSchemaOutput(type))); - expect(result.typeDef).toContain("id: Generated;"); + expect(result.typeDef).toContain("id: Generated;"); }); test("should correctly track used utility types - basic types only", async () => { diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts index 024487e05..56690938e 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts @@ -2,7 +2,38 @@ import multiline from "#/utils/multiline"; import { type KyselyNamespaceMetadata, type KyselyTypeMetadata } from "./types"; import type { OperatorFieldConfig, TailorDBType } from "#/parser/service/tailordb/types"; -type UsedUtilityTypes = { Timestamp: boolean; Serial: boolean }; +type UsedUtilityTypes = { + Timestamp: boolean; + Serial: boolean; + DateString: boolean; + DecimalString: boolean; + TimeString: boolean; +}; + +function createUsedUtilityTypes(): UsedUtilityTypes { + return { + Timestamp: false, + Serial: false, + DateString: false, + DecimalString: false, + TimeString: false, + }; +} + +function mergeUsedUtilityTypes( + results: { usedUtilityTypes: UsedUtilityTypes }[], +): UsedUtilityTypes { + return results.reduce( + (acc, result) => ({ + Timestamp: acc.Timestamp || result.usedUtilityTypes.Timestamp, + Serial: acc.Serial || result.usedUtilityTypes.Serial, + DateString: acc.DateString || result.usedUtilityTypes.DateString, + DecimalString: acc.DecimalString || result.usedUtilityTypes.DecimalString, + TimeString: acc.TimeString || result.usedUtilityTypes.TimeString, + }), + createUsedUtilityTypes(), + ); +} type FieldTypeResult = { type: string; @@ -38,7 +69,7 @@ function getNestedType(fieldConfig: OperatorFieldConfig): FieldTypeResult { if (!fields || typeof fields !== "object") { return { type: "string", - usedUtilityTypes: { Timestamp: false, Serial: false }, + usedUtilityTypes: createUsedUtilityTypes(), }; } @@ -51,13 +82,7 @@ function getNestedType(fieldConfig: OperatorFieldConfig): FieldTypeResult { }; }); - const aggregatedUtilityTypes = fieldResults.reduce( - (acc, result) => ({ - Timestamp: acc.Timestamp || result.usedUtilityTypes.Timestamp, - Serial: acc.Serial || result.usedUtilityTypes.Serial, - }), - { Timestamp: false, Serial: false }, - ); + const aggregatedUtilityTypes = mergeUsedUtilityTypes(fieldResults); const fieldTypes = fieldResults.map((r) => r.fieldType); const obj = `{\n ${fieldTypes.join(";\n ")}${fieldTypes.length > 0 ? ";" : ""}\n}`; @@ -76,24 +101,36 @@ function getNestedType(fieldConfig: OperatorFieldConfig): FieldTypeResult { */ function getBaseType(fieldConfig: OperatorFieldConfig): FieldTypeResult { const fieldType = fieldConfig.type; - const usedUtilityTypes = { Timestamp: false, Serial: false }; + const usedUtilityTypes = createUsedUtilityTypes(); let type: string; switch (fieldType) { case "uuid": + type = "UUIDString"; + break; case "string": - case "decimal": type = "string"; break; + case "decimal": + usedUtilityTypes.DecimalString = true; + type = "DecimalString"; + break; case "integer": case "float": type = "number"; break; - case "date": case "datetime": usedUtilityTypes.Timestamp = true; type = "Timestamp"; break; + case "date": + usedUtilityTypes.DateString = true; + type = "DateString"; + break; + case "time": + usedUtilityTypes.TimeString = true; + type = "TimeString"; + break; case "bool": case "boolean": type = "boolean"; @@ -173,18 +210,11 @@ function generateTableInterface(type: TailorDBType): { })); const fields = [ - "id: Generated;", + "id: Generated;", ...fieldResults.map((result) => `${result.fieldName}: ${result.type};`), ]; - const aggregatedUtilityTypes = fieldResults.reduce( - (acc, result) => ({ - Timestamp: acc.Timestamp || result.usedUtilityTypes.Timestamp, - - Serial: acc.Serial || result.usedUtilityTypes.Serial, - }), - { Timestamp: false, Serial: false }, - ); + const aggregatedUtilityTypes = mergeUsedUtilityTypes(fieldResults); const typeDef = multiline /* ts */ ` ${type.name}: { @@ -225,14 +255,26 @@ export function generateUnifiedKyselyTypes(namespaceData: KyselyNamespaceMetadat (acc, ns) => ({ Timestamp: acc.Timestamp || ns.usedUtilityTypes.Timestamp, Serial: acc.Serial || ns.usedUtilityTypes.Serial, + DateString: acc.DateString || ns.usedUtilityTypes.DateString, + DecimalString: acc.DecimalString || ns.usedUtilityTypes.DecimalString, + TimeString: acc.TimeString || ns.usedUtilityTypes.TimeString, }), - { Timestamp: false, Serial: false }, + createUsedUtilityTypes(), ); - const utilityTypeImports: string[] = ["type Generated"]; + const utilityTypeImports: string[] = ["type Generated", "type UUIDString"]; if (globalUsedUtilityTypes.Timestamp) { utilityTypeImports.push("type Timestamp"); } + if (globalUsedUtilityTypes.DateString) { + utilityTypeImports.push("type DateString"); + } + if (globalUsedUtilityTypes.DecimalString) { + utilityTypeImports.push("type DecimalString"); + } + if (globalUsedUtilityTypes.TimeString) { + utilityTypeImports.push("type TimeString"); + } const hasObjectColumnType = namespaceData.some((ns) => ns.types.some((t) => t.typeDef.includes("ObjectColumnType<")), ); diff --git a/packages/sdk/src/plugin/builtin/kysely-type/types.ts b/packages/sdk/src/plugin/builtin/kysely-type/types.ts index fd1cd3c0e..d238a2114 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/types.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/types.ts @@ -8,6 +8,9 @@ export interface KyselyTypeMetadata { usedUtilityTypes: { Timestamp: boolean; Serial: boolean; + DateString: boolean; + DecimalString: boolean; + TimeString: boolean; }; } @@ -17,5 +20,8 @@ export interface KyselyNamespaceMetadata { usedUtilityTypes: { Timestamp: boolean; Serial: boolean; + DateString: boolean; + DecimalString: boolean; + TimeString: boolean; }; } diff --git a/packages/sdk/src/runtime/context.test.ts b/packages/sdk/src/runtime/context.test.ts index 3de72d57e..7153f2b46 100644 --- a/packages/sdk/src/runtime/context.test.ts +++ b/packages/sdk/src/runtime/context.test.ts @@ -23,7 +23,7 @@ describe("@tailor-platform/sdk/runtime/context", () => { test("getInvoker exposes SDK shape (attributes map + attributeList array)", () => { using _invokerSpy = vi.spyOn(globalThis.tailor.context, "getInvoker").mockReturnValue({ - id: "u-1", + id: "11111111-1111-4111-8111-111111111111", type: "machine_user", workspaceId: "ws-1", attributes: ["role"], @@ -33,7 +33,7 @@ describe("@tailor-platform/sdk/runtime/context", () => { const invoker = context.getInvoker(); expect(invoker).toEqual({ - id: "u-1", + id: "11111111-1111-4111-8111-111111111111", type: "machine_user", workspaceId: "ws-1", attributes: { role: "MANAGER" }, diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index 203ae6863..805a851d5 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -12,6 +12,7 @@ * } */ +import type { UUIDString } from "#/configure/types/scalar.types"; import type { TailorPrincipal } from "#/runtime/types"; /** @@ -29,7 +30,7 @@ export type Invoker = TailorPrincipal; */ export interface ContextInvoker { /** The invoker's ID */ - id: string; + id: UUIDString; /** The invoker's type */ type: "user" | "machine_user"; /** The workspace ID */ diff --git a/packages/sdk/src/runtime/idp.test.ts b/packages/sdk/src/runtime/idp.test.ts index 45fb90c54..5b521bee1 100644 --- a/packages/sdk/src/runtime/idp.test.ts +++ b/packages/sdk/src/runtime/idp.test.ts @@ -19,18 +19,32 @@ describe("@tailor-platform/sdk/runtime/idp", () => { test("Client.user forwards args and namespace", async () => { using idpM = mockIdp(); - idpM.enqueueResult({ id: "u-1", name: "alice", disabled: false }); + idpM.enqueueResult({ + id: "11111111-1111-4111-8111-111111111111", + name: "alice", + disabled: false, + }); const client = new idp.Client({ namespace: "ns" }); - const result = await client.user("u-1"); + const result = await client.user("11111111-1111-4111-8111-111111111111"); - expect(result).toEqual({ id: "u-1", name: "alice", disabled: false }); - expect(idpM.calls).toEqual([{ method: "user", args: ["u-1"], namespace: "ns" }]); + expect(result).toEqual({ + id: "11111111-1111-4111-8111-111111111111", + name: "alice", + disabled: false, + }); + expect(idpM.calls).toEqual([ + { method: "user", args: ["11111111-1111-4111-8111-111111111111"], namespace: "ns" }, + ]); }); test("Client.userByName forwards", async () => { using idpM = mockIdp(); - idpM.enqueueResult({ id: "u-1", name: "alice", disabled: false }); + idpM.enqueueResult({ + id: "11111111-1111-4111-8111-111111111111", + name: "alice", + disabled: false, + }); const client = new idp.Client({ namespace: "ns" }); await client.userByName("alice"); @@ -41,7 +55,7 @@ describe("@tailor-platform/sdk/runtime/idp", () => { test("Client.users forwards options", async () => { using idpM = mockIdp(); idpM.enqueueResult({ - users: [{ id: "u-1", name: "alice", disabled: false }], + users: [{ id: "11111111-1111-4111-8111-111111111111", name: "alice", disabled: false }], nextPageToken: null, totalCount: 1, }); @@ -56,15 +70,15 @@ describe("@tailor-platform/sdk/runtime/idp", () => { test("Client.createUser / updateUser / deleteUser forward", async () => { using idpM = mockIdp(); idpM.enqueueResults( - { id: "u-2", name: "bob", disabled: false }, - { id: "u-2", name: "bob2", disabled: false }, + { id: "22222222-2222-4222-8222-222222222222", name: "bob", disabled: false }, + { id: "22222222-2222-4222-8222-222222222222", 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"); + await client.updateUser({ id: "22222222-2222-4222-8222-222222222222", name: "bob2" }); + const removed = await client.deleteUser("22222222-2222-4222-8222-222222222222"); expect(removed).toBe(true); expect(idpM.calls.map((c) => c.method)).toEqual(["createUser", "updateUser", "deleteUser"]); @@ -74,7 +88,7 @@ describe("@tailor-platform/sdk/runtime/idp", () => { using idpM = mockIdp(); const client = new idp.Client({ namespace: "ns" }); const ok = await client.sendPasswordResetEmail({ - userId: "u-1", + userId: "11111111-1111-4111-8111-111111111111", redirectUri: "https://example.com/reset", }); @@ -82,7 +96,12 @@ describe("@tailor-platform/sdk/runtime/idp", () => { expect(idpM.calls).toEqual([ { method: "sendPasswordResetEmail", - args: [{ userId: "u-1", redirectUri: "https://example.com/reset" }], + args: [ + { + userId: "11111111-1111-4111-8111-111111111111", + redirectUri: "https://example.com/reset", + }, + ], namespace: "ns", }, ]); @@ -91,13 +110,16 @@ describe("@tailor-platform/sdk/runtime/idp", () => { test("Client.unenrollMfa forwards", async () => { using idpM = mockIdp(); const client = new idp.Client({ namespace: "ns" }); - const ok = await client.unenrollMfa({ userId: "u-1", mfaFactorId: "f-1" }); + const ok = await client.unenrollMfa({ + userId: "11111111-1111-4111-8111-111111111111", + mfaFactorId: "f-1", + }); expect(ok).toBe(true); expect(idpM.calls).toEqual([ { method: "unenrollMfa", - args: [{ userId: "u-1", mfaFactorId: "f-1" }], + args: [{ userId: "11111111-1111-4111-8111-111111111111", mfaFactorId: "f-1" }], namespace: "ns", }, ]); diff --git a/packages/sdk/src/runtime/idp.ts b/packages/sdk/src/runtime/idp.ts index c707de793..c6e64c41e 100644 --- a/packages/sdk/src/runtime/idp.ts +++ b/packages/sdk/src/runtime/idp.ts @@ -11,6 +11,8 @@ * const { users } = await client.users({ first: 10 }); */ +import type { UUIDString } from "#/configure/types/scalar.types"; + /** Configuration object for {@link Client}. */ export interface ClientConfig { namespace: string; @@ -18,7 +20,7 @@ export interface ClientConfig { /** User record returned by IDP operations. */ export interface User { - id: string; + id: UUIDString; name: string; disabled: boolean; createdAt?: string; @@ -37,7 +39,7 @@ export interface User { /** Filter options for {@link Client.users}. */ export interface UserQuery { /** Filter by user IDs */ - ids?: string[]; + ids?: UUIDString[]; /** Filter by user names */ names?: string[]; } @@ -72,7 +74,7 @@ export interface CreateUserInput { /** Input for {@link Client.updateUser}. */ export interface UpdateUserInput { /** The user's ID */ - id: string; + id: UUIDString; /** New name for the user */ name?: string; /** New password for the user. Cannot be used with clearPassword. */ @@ -86,7 +88,7 @@ export interface UpdateUserInput { /** Input for {@link Client.sendPasswordResetEmail}. */ export interface SendPasswordResetEmailInput { /** The ID of the user */ - userId: string; + userId: UUIDString; /** The URI to redirect to after password reset */ redirectUri: string; /** The sender display name. Defaults to 'Tailor Platform IdP'. */ @@ -98,7 +100,7 @@ export interface SendPasswordResetEmailInput { /** Input for {@link Client.unenrollMfa}. */ export interface UnenrollMfaInput { /** The ID of the user whose factor will be unenrolled. */ - userId: string; + userId: UUIDString; /** * The ID of the factor to unenroll. Factor IDs are exposed on the user * record (see {@link User.mfaFactorIds}). @@ -109,11 +111,11 @@ export interface UnenrollMfaInput { /** Instance methods exposed by `tailor.idp.Client`. */ export interface IdpClientInstance { users(options?: ListUsersOptions): Promise; - user(userId: string): Promise; + user(userId: UUIDString): Promise; userByName(name: string): Promise; createUser(input: CreateUserInput): Promise; updateUser(input: UpdateUserInput): Promise; - deleteUser(userId: string): Promise; + deleteUser(userId: UUIDString): Promise; sendPasswordResetEmail(input: SendPasswordResetEmailInput): Promise; unenrollMfa(input: UnenrollMfaInput): Promise; } @@ -161,7 +163,7 @@ export class Client { * @param userId - IDP user ID * @returns The matching user */ - user(userId: string): Promise { + user(userId: UUIDString): Promise { return this.#impl.user(userId); } @@ -197,7 +199,7 @@ export class Client { * @param userId - IDP user ID * @returns `true` when the user was deleted */ - deleteUser(userId: string): Promise { + deleteUser(userId: UUIDString): Promise { return this.#impl.deleteUser(userId); } diff --git a/packages/sdk/src/runtime/types.ts b/packages/sdk/src/runtime/types.ts index 8aa961e2e..9e54d1798 100644 --- a/packages/sdk/src/runtime/types.ts +++ b/packages/sdk/src/runtime/types.ts @@ -4,6 +4,8 @@ // not reference zod or schema modules, so every layer can import it type-only // without pulling any runtime dependency. +import type { UUIDString } from "#/configure/types/scalar.types"; + // Interfaces for module augmentation // Users can extend these via: declare module "@tailor-platform/sdk" { interface Attributes { ... } } // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -23,7 +25,7 @@ export type InferredAttributeList = AttributeList["__tuple"] extends [] /** Represents a user or machine user principal in the Tailor Platform. */ export type TailorPrincipal = { /** The ID of the principal. */ - id: string; + id: UUIDString; /** The type of the principal. */ type: "user" | "machine_user"; /** The ID of the workspace the principal belongs to. */ diff --git a/packages/sdk/src/vitest/mock.test.ts b/packages/sdk/src/vitest/mock.test.ts index 7b51d1122..8c48c7c3f 100644 --- a/packages/sdk/src/vitest/mock.test.ts +++ b/packages/sdk/src/vitest/mock.test.ts @@ -583,19 +583,28 @@ describe("mock", () => { test("records calls with method, args, namespace", async () => { using idp = mockIdp(); const client = new (globalThis as any).tailor.idp.Client({ namespace: "ns" }); - await client.user("u-1"); - expect(idp.calls).toEqual([{ method: "user", args: ["u-1"], namespace: "ns" }]); + await client.user("11111111-1111-4111-8111-111111111111"); + expect(idp.calls).toEqual([ + { method: "user", args: ["11111111-1111-4111-8111-111111111111"], namespace: "ns" }, + ]); }); test("enqueueResults provides ordered responses", async () => { using idp = mockIdp(); - idp.enqueueResults({ id: "u-1", name: "alice", disabled: false }, true); + idp.enqueueResults( + { id: "11111111-1111-4111-8111-111111111111", name: "alice", disabled: false }, + true, + ); const client = new (globalThis as any).tailor.idp.Client({ namespace: "ns" }); - const user = await client.user("u-1"); - expect(user).toEqual({ id: "u-1", name: "alice", disabled: false }); + const user = await client.user("11111111-1111-4111-8111-111111111111"); + expect(user).toEqual({ + id: "11111111-1111-4111-8111-111111111111", + name: "alice", + disabled: false, + }); - const deleted = await client.deleteUser("u-1"); + const deleted = await client.deleteUser("11111111-1111-4111-8111-111111111111"); expect(deleted).toBe(true); }); @@ -604,7 +613,7 @@ describe("mock", () => { idp.setResolver((method) => { if (method === "users") return { - users: [{ id: "u-1", name: "bob", disabled: false }], + users: [{ id: "11111111-1111-4111-8111-111111111111", name: "bob", disabled: false }], nextPageToken: null, totalCount: 1, }; @@ -619,7 +628,7 @@ describe("mock", () => { test("reset clears state", async () => { using idp = mockIdp(); const client = new (globalThis as any).tailor.idp.Client({ namespace: "ns" }); - await client.user("u-1"); + await client.user("11111111-1111-4111-8111-111111111111"); idp.reset(); expect(idp.calls).toHaveLength(0); }); diff --git a/packages/sdk/src/vitest/mock.ts b/packages/sdk/src/vitest/mock.ts index 1af1ab399..97f4e44f3 100644 --- a/packages/sdk/src/vitest/mock.ts +++ b/packages/sdk/src/vitest/mock.ts @@ -36,7 +36,7 @@ import type { User as IdpUser } from "../runtime/idp"; // `@tailor-platform/sdk` externally instead of inlining the registry — the same // generated `declare module "@tailor-platform/sdk"` that narrows // `authconnection.getConnectionToken` then also narrows this mock's API. -import type { AuthConnectionTokenResult, ConnectionName } from "@tailor-platform/sdk"; +import type { AuthConnectionTokenResult, ConnectionName, UUIDString } from "@tailor-platform/sdk"; export { RUNTIME_FLAG_KEY } from "./globals"; @@ -701,23 +701,29 @@ export function mockAuthconnection() { const IDP_DEFAULTS: Record = { users: { users: [], nextPageToken: null, totalCount: 0 }, - user: { id: "mock-id", name: "mock-user", disabled: false, mfaEnrolled: false, mfaFactorIds: [] }, + user: { + id: "123e4567-e89b-12d3-a456-426614174000", + name: "mock-user", + disabled: false, + mfaEnrolled: false, + mfaFactorIds: [], + }, userByName: { - id: "mock-id", + id: "123e4567-e89b-12d3-a456-426614174000", name: "mock-user", disabled: false, mfaEnrolled: false, mfaFactorIds: [], }, createUser: { - id: "mock-id", + id: "123e4567-e89b-12d3-a456-426614174000", name: "mock-user", disabled: false, mfaEnrolled: false, mfaFactorIds: [], }, updateUser: { - id: "mock-id", + id: "123e4567-e89b-12d3-a456-426614174000", name: "mock-user", disabled: false, mfaEnrolled: false, @@ -738,7 +744,7 @@ const IDP_DEFAULTS: Record = { * test("resolver-based", async () => { * using idp = mockIdp(); * idp.setResolver((method) => - * method === "user" ? { id: "u-1", name: "alice", disabled: false } : null, + * method === "user" ? { id: "11111111-1111-4111-8111-111111111111", name: "alice", disabled: false } : null, * ); * // … * }); @@ -771,11 +777,11 @@ export function mockIdp() { ) { const namespace = config.namespace; this.users = async (options?: unknown) => handle("users", [options], namespace); - this.user = async (userId: string) => handle("user", [userId], namespace); + this.user = async (userId: UUIDString) => handle("user", [userId], namespace); this.userByName = async (name: string) => handle("userByName", [name], namespace); this.createUser = async (input: unknown) => handle("createUser", [input], namespace); this.updateUser = async (input: unknown) => handle("updateUser", [input], namespace); - this.deleteUser = async (userId: string) => handle("deleteUser", [userId], namespace); + this.deleteUser = async (userId: UUIDString) => handle("deleteUser", [userId], namespace); this.sendPasswordResetEmail = async (input: unknown) => handle("sendPasswordResetEmail", [input], namespace); this.unenrollMfa = async (input: unknown) => handle("unenrollMfa", [input], namespace); @@ -783,21 +789,21 @@ export function mockIdp() { users(options?: { first?: number; after?: string; - query?: { ids?: string[]; names?: string[] }; + query?: { ids?: UUIDString[]; names?: string[] }; }): Promise<{ users: IdpUser[]; nextPageToken: string | null; totalCount: number }>; - user(userId: string): Promise; + user(userId: UUIDString): Promise; userByName(name: string): Promise; createUser(input: { name: string; password?: string; disabled?: boolean }): Promise; updateUser(input: { - id: string; + id: UUIDString; name?: string; password?: string; clearPassword?: boolean; disabled?: boolean; }): Promise; - deleteUser(userId: string): Promise; - sendPasswordResetEmail(input: { userId: string; redirectUri: string }): Promise; - unenrollMfa(input: { userId: string; mfaFactorId: string }): Promise; + deleteUser(userId: UUIDString): Promise; + sendPasswordResetEmail(input: { userId: UUIDString; redirectUri: string }): Promise; + unenrollMfa(input: { userId: UUIDString; mfaFactorId: string }): Promise; }; root.idp = { Client }; From 23c062d3029a5233856e743f60efea13428fc842 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 09:20:47 +0900 Subject: [PATCH 418/618] test(sdk): assert strict scalar exports directly --- .../configure/services/tailordb/schema.test.ts | 15 +++++++-------- packages/sdk/src/configure/types/type.test.ts | 8 +------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index e17af480d..b321788b9 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -3,18 +3,17 @@ import { describe, expectTypeOf, expect, test } from "vitest"; import { t } from "#/configure/types/index"; import { db, type TailorAnyDBField } from "./schema"; import type { FieldValidateInput, ValidateConfig } from "#/configure/types/field.types"; +import type { + DateString, + DateTimeString, + DecimalString, + TimeString, + UUIDString, +} from "#/configure/types/scalar.types"; import type { TailorPrincipal } from "#/runtime/types"; import type { output, TypeLevelError } from "#/types/helpers"; import type { Hook } from "./types"; -type DateString = `${number}-${number}-${number}`; -type TimeString = `${number}:${number}`; -type TimeZoneOffsetString = "Z" | "z" | `${"+" | "-"}${TimeString}`; -type DateTimeString = - `${DateString}${"T" | "t"}${TimeString}:${number}${"" | `.${number}`}${TimeZoneOffsetString}`; -type UUIDString = `${string}-${string}-${string}-${string}-${string}`; -type DecimalString = `${number}`; - describe("TailorDBField basic field type tests", () => { test("string field outputs string type correctly", () => { const _stringType = db.type("Test", { diff --git a/packages/sdk/src/configure/types/type.test.ts b/packages/sdk/src/configure/types/type.test.ts index 9fb5978fd..2787fc947 100644 --- a/packages/sdk/src/configure/types/type.test.ts +++ b/packages/sdk/src/configure/types/type.test.ts @@ -4,13 +4,7 @@ import { t } from "./type"; import type { TailorPrincipal } from "#/runtime/types"; import type { output } from "#/types/helpers"; import type { AllowedValues } from "./field"; - -type DateString = `${number}-${number}-${number}`; -type TimeString = `${number}:${number}`; -type TimeZoneOffsetString = "Z" | "z" | `${"+" | "-"}${TimeString}`; -type DateTimeString = - `${DateString}${"T" | "t"}${TimeString}:${number}${"" | `.${number}`}${TimeZoneOffsetString}`; -type UUIDString = `${string}-${string}-${string}-${string}-${string}`; +import type { DateString, DateTimeString, TimeString, UUIDString } from "./scalar.types"; describe("TailorType basic field type tests", () => { test("string field outputs string type correctly", () => { From 672aaafad396144c914197afafcdfdb5c8773879 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 09:41:06 +0900 Subject: [PATCH 419/618] fix(sdk): preserve auth scalar attribute types --- .../sdk/src/cli/shared/type-generator.test.ts | 42 +++++++++ packages/sdk/src/cli/shared/type-generator.ts | 85 +++++++++++++++---- 2 files changed, 111 insertions(+), 16 deletions(-) diff --git a/packages/sdk/src/cli/shared/type-generator.test.ts b/packages/sdk/src/cli/shared/type-generator.test.ts index 95156aa87..848bb8282 100644 --- a/packages/sdk/src/cli/shared/type-generator.test.ts +++ b/packages/sdk/src/cli/shared/type-generator.test.ts @@ -1,6 +1,7 @@ import * as path from "pathe"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { defineAuth } from "#/configure/services/auth/index"; +import { db } from "#/configure/services/tailordb/schema"; import { t } from "#/configure/types/type"; import { extractAttributesFromConfig, @@ -179,6 +180,8 @@ describe("extractAttributesFromConfig + generateTypeDefinition", () => { auth: defineAuth("auth", { machineUserAttributes: { role: t.enum(["ADMIN", "WORKER"]), + externalId: t.uuid(), + balance: t.decimal(), isActive: t.bool(), tags: t.string({ array: true }), }, @@ -186,6 +189,8 @@ describe("extractAttributesFromConfig + generateTypeDefinition", () => { admin: { attributes: { role: "ADMIN", + externalId: "123e4567-e89b-12d3-a456-426614174000", + balance: "123.45", isActive: true, tags: ["root"], }, @@ -198,10 +203,47 @@ describe("extractAttributesFromConfig + generateTypeDefinition", () => { const content = generateTypeDefinition(attributes, undefined); expect(content).toContain('role: "ADMIN" | "WORKER";'); + expect(content).toContain( + 'import type { DecimalString, UUIDString } from "@tailor-platform/sdk";', + ); + expect(content).toContain("externalId: UUIDString;"); + expect(content).toContain("balance: DecimalString;"); expect(content).toContain("isActive: boolean;"); expect(content).toContain("tags: string[];"); }); + test("preserves scalar types from userProfile attributes and attributeList", () => { + const userType = db.type("User", { + email: db.string().unique(), + externalId: db.uuid(), + balance: db.decimal(), + }); + const config = { + name: "test-app", + auth: defineAuth("auth", { + userProfile: { + type: userType, + usernameField: "email", + attributes: { + externalId: true, + balance: true, + }, + attributeList: ["externalId"] as ["externalId"], + }, + }), + }; + + const { attributes, attributeList } = extractAttributesFromConfig(config); + const content = generateTypeDefinition(attributes, attributeList); + + expect(content).toContain( + 'import type { DecimalString, UUIDString } from "@tailor-platform/sdk";', + ); + expect(content).toContain("externalId: UUIDString;"); + expect(content).toContain("balance: DecimalString;"); + expect(content).toContain("__tuple?: [UUIDString];"); + }); + test("extracts machine user names into MachineUserNameRegistry", () => { const config = { name: "test-app", diff --git a/packages/sdk/src/cli/shared/type-generator.ts b/packages/sdk/src/cli/shared/type-generator.ts index 22a8a27f5..1a9f45954 100644 --- a/packages/sdk/src/cli/shared/type-generator.ts +++ b/packages/sdk/src/cli/shared/type-generator.ts @@ -10,6 +10,28 @@ export interface AttributesConfig { export type AttributeListConfig = readonly string[]; +type ScalarTypeName = + | "DateString" + | "DateTimeString" + | "DecimalString" + | "TimeString" + | "UUIDString"; + +const scalarTypeNames: ScalarTypeName[] = [ + "DateString", + "DateTimeString", + "DecimalString", + "TimeString", + "UUIDString", +]; + +const knownAttributeListTypes = new Set([ + "boolean", + "number", + "string", + ...scalarTypeNames, +]); + interface ExtractedAttributes { attributes?: AttributesConfig; attributeList?: AttributeListConfig; @@ -55,6 +77,16 @@ export function generateTypeDefinition( idpNames?: readonly string[], connectionNames?: readonly string[], ): string { + const attributeListTypes = attributeList?.map(normalizeAttributeListType); + const scalarTypeImports = collectScalarTypeImports([ + ...Object.values(attributes ?? {}), + ...(attributeListTypes ?? []), + ]); + const scalarTypeImportSource = + scalarTypeImports.length > 0 + ? `import type { ${scalarTypeImports.join(", ")} } from "@tailor-platform/sdk";\n\n` + : ""; + // Generate Attributes interface // attributes values are type string representations (e.g., "string", "boolean", "string[]") const attributeFields = attributes @@ -71,7 +103,7 @@ ${attributeFields} }`; // Generate AttributeList type as a tuple of strings based on the length - const listType = attributeList ? `[${attributeList.map(() => "string").join(", ")}]` : "[]"; + const listType = attributeListTypes ? `[${attributeListTypes.join(", ")}]` : "[]"; // Use interface with __tuple marker for declaration merging and tuple type support const listBody = `{ @@ -140,7 +172,7 @@ ${idpNameFields} ${connectionNameFields} }`; - return ml /* ts */ ` + return `${scalarTypeImportSource}${ml /* ts */ ` // This file is auto-generated by @tailor-platform/sdk // Do not edit this file manually // Regenerated automatically when running 'tailor deploy' or 'tailor generate' @@ -156,7 +188,17 @@ declare module "@tailor-platform/sdk" { export {}; -`; +`}`; +} + +function collectScalarTypeImports(types: readonly string[]): ScalarTypeName[] { + return scalarTypeNames.filter((typeName) => + types.some((type) => new RegExp(`\\b${typeName}\\b`).test(type)), + ); +} + +function normalizeAttributeListType(typeOrKey: string): string { + return knownAttributeListTypes.has(typeOrKey) ? typeOrKey : "string"; } function collectAttributesFromConfig(config: AppConfig): ExtractedAttributes { @@ -185,23 +227,32 @@ function collectAttributesFromConfig(config: AppConfig): ExtractedAttributes { const type = field?.type; const metadata = field?.metadata; - // Default to string if no metadata - if (!metadata) { + if (!type && !metadata) { return "string"; } - let typeStr = "string"; - - if (type === "boolean") { - typeStr = "boolean"; - } else if (type === "enum" && metadata.allowedValues) { - // Generate union type from enum values - typeStr = metadata.allowedValues.map((v) => `"${v.value}"`).join(" | "); - } + const typeStr = + type === "boolean" || type === "bool" + ? "boolean" + : type === "enum" && metadata?.allowedValues + ? metadata.allowedValues.map((v) => `"${v.value}"`).join(" | ") + : type === "uuid" + ? "UUIDString" + : type === "date" + ? "DateString" + : type === "datetime" + ? "DateTimeString | Date" + : type === "time" + ? "TimeString" + : type === "decimal" + ? "DecimalString" + : type === "integer" || type === "float" || type === "number" + ? "number" + : "string"; // Add array suffix if needed - if (metadata.array) { - typeStr += "[]"; + if (metadata?.array) { + return typeStr.includes("|") ? `(${typeStr})[]` : `${typeStr}[]`; } return typeStr; @@ -223,7 +274,9 @@ function collectAttributesFromConfig(config: AppConfig): ExtractedAttributes { const selectedAttributes = userProfile?.attributes; const fields = userProfile?.type?.fields; - const attributeList = userProfile?.attributeList; + const attributeList = userProfile?.attributeList?.map((key) => + inferAttributeType(fields?.[key]), + ); // Convert attributes to AttributesConfig by inferring types from field metadata const attributes: AttributesConfig | undefined = selectedAttributes From 31d0812753406fec386a2277b4fbdf6b92ea5100 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 12:55:43 +0900 Subject: [PATCH 420/618] fix(sdk): reject unbranded user profile type keys --- .../sdk/src/parser/service/auth/index.test.ts | 17 ++++++++--------- .../parser/service/tailordb/builder-helpers.ts | 3 ++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/sdk/src/parser/service/auth/index.test.ts b/packages/sdk/src/parser/service/auth/index.test.ts index 1f153fc22..7d32e309b 100644 --- a/packages/sdk/src/parser/service/auth/index.test.ts +++ b/packages/sdk/src/parser/service/auth/index.test.ts @@ -400,21 +400,20 @@ describe("AuthConfigSchema userProfile/machineUserAttributes validation", () => expect(result.userProfile?.type).not.toHaveProperty("_output"); }); - test("strips TailorDB type builder helpers from unbranded userProfile.type copies", () => { - const result = AuthConfigSchema.parse({ + test("rejects unbranded userProfile.type copies with unknown keys", () => { + const result = AuthConfigSchema.safeParse({ name: "my-auth", userProfile: { - type: { ...userType }, + type: { ...userType, unknownOption: true }, usernameField: "email", }, }); - expect(result.userProfile?.type).toMatchObject({ - name: "User", - fields: expect.any(Object), - }); - expect(result.userProfile?.type).not.toHaveProperty("hooks"); - expect(result.userProfile?.type).not.toHaveProperty("_output"); + expect(result.success).toBe(false); + if (result.success) { + throw new Error("Expected AuthConfigSchema parsing to fail"); + } + expect(JSON.stringify(result.error.issues)).toContain("unknownOption"); }); test("omits unknown outer userProfile.type keys with TailorDB builder helpers", () => { diff --git a/packages/sdk/src/parser/service/tailordb/builder-helpers.ts b/packages/sdk/src/parser/service/tailordb/builder-helpers.ts index 675c5e0c3..bcd6cca64 100644 --- a/packages/sdk/src/parser/service/tailordb/builder-helpers.ts +++ b/packages/sdk/src/parser/service/tailordb/builder-helpers.ts @@ -1,9 +1,10 @@ +import { isSdkBranded } from "#/utils/brand"; import { TailorDBTypeSchema } from "./schema"; const TAILORDB_TYPE_SCHEMA_KEYS = TailorDBTypeSchema.keyof().options; export function stripTailorDBTypeBuilderHelpers(type: unknown): unknown { - if (typeof type !== "object" || type === null) { + if (!isSdkBranded(type, "tailordb-type")) { return type; } From 43ec8044e32426aa1f669d83823eb62805aebbdf Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 12:58:06 +0900 Subject: [PATCH 421/618] test(example): expect uuid resolver input as ID --- example/e2e/resolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/e2e/resolver.test.ts b/example/e2e/resolver.test.ts index 1bde34561..423ea61b2 100644 --- a/example/e2e/resolver.test.ts +++ b/example/e2e/resolver.test.ts @@ -498,7 +498,7 @@ describe("dataplane", () => { const customerIdInput = triggerResolver?.inputs?.find((i) => i.name === "customerId"); expect(customerIdInput?.description).toBe("Customer ID for the order"); expect(customerIdInput?.type?.kind).toBe("ScalarType"); - expect(customerIdInput?.type?.name).toBe("String"); + expect(customerIdInput?.type?.name).toBe("ID"); // Verify response const responseFields = triggerResolver?.response?.type?.fields ?? []; From 52d25934f7c1aba32b9dc548aa16cad58c3fb49e Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 13:23:54 +0900 Subject: [PATCH 422/618] feat(sdk): add scalar string helpers --- .changeset/strict-tailor-field-strings.md | 2 +- packages/sdk/src/configure/index.ts | 17 ++ .../src/configure/services/tailordb/schema.ts | 20 +-- .../sdk/src/configure/types/field-format.ts | 15 ++ packages/sdk/src/configure/types/index.ts | 1 + .../sdk/src/configure/types/scalar.test.ts | 119 ++++++++++++ packages/sdk/src/configure/types/scalar.ts | 169 ++++++++++++++++++ packages/sdk/src/configure/types/type.ts | 20 +-- 8 files changed, 342 insertions(+), 21 deletions(-) create mode 100644 packages/sdk/src/configure/types/scalar.test.ts create mode 100644 packages/sdk/src/configure/types/scalar.ts diff --git a/.changeset/strict-tailor-field-strings.md b/.changeset/strict-tailor-field-strings.md index 5cbac8ed8..2b12bbe2c 100644 --- a/.changeset/strict-tailor-field-strings.md +++ b/.changeset/strict-tailor-field-strings.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk": major --- -Tighten Tailor field output types for UUID, date, datetime, time, and decimal fields, including generated Kysely types and runtime user IDs. Datetime parsing now accepts ISO 8601 datetime offsets. +Tighten Tailor field output types for UUID, date, datetime, time, and decimal fields, including generated Kysely types and runtime user IDs. Add scalar string guard, parse, and assertion helpers. Datetime parsing now accepts ISO 8601 datetime offsets. diff --git a/packages/sdk/src/configure/index.ts b/packages/sdk/src/configure/index.ts index 9f45829b6..a536604d8 100644 --- a/packages/sdk/src/configure/index.ts +++ b/packages/sdk/src/configure/index.ts @@ -24,6 +24,23 @@ export type { TimeZoneOffsetString, UUIDString, } from "#/configure/types/scalar.types"; +export { + assertDateString, + assertDateTimeString, + assertDecimalString, + assertTimeString, + assertUUIDString, + isDateString, + isDateTimeString, + isDecimalString, + isTimeString, + isUUIDString, + parseDateString, + parseDateTimeString, + parseDecimalString, + parseTimeString, + parseUUIDString, +} from "#/configure/types/scalar"; export { type TailorPrincipal, type Attributes, diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index ca08cdf73..2fad5f02d 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -4,7 +4,13 @@ import { type AllowedValuesOutput, mapAllowedValues, } from "#/configure/types/field"; -import { isValidDateString, isValidDateTimeString } from "#/configure/types/field-format"; +import { + isValidDateString, + isValidDateTimeString, + isValidDecimalString, + isValidTimeString, + isValidUUIDString, +} from "#/configure/types/field-format"; import { brandValue } from "#/utils/brand"; import type { TailorDBField as TailorDBFieldBase, @@ -408,12 +414,6 @@ function isRelationSelfConfig( return config.toward.type === "self"; } -const regex = { - uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, - time: /^(?\d{2}):(?\d{2})$/, - decimal: /^-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/, -} as const; - type FieldParseArgs = { value: unknown; data: unknown; @@ -580,7 +580,7 @@ function createTailorDBFieldRuntime< break; case "uuid": - if (typeof value !== "string" || !regex.uuid.test(value)) { + if (typeof value !== "string" || !isValidUUIDString(value)) { issues.push({ message: `Expected a valid UUID: received ${String(value)}`, path: pathArray.length > 0 ? pathArray : undefined, @@ -604,7 +604,7 @@ function createTailorDBFieldRuntime< } break; case "time": - if (typeof value !== "string" || !regex.time.test(value)) { + if (typeof value !== "string" || !isValidTimeString(value)) { issues.push({ message: `Expected to match "HH:mm" format: received ${String(value)}`, path: pathArray.length > 0 ? pathArray : undefined, @@ -612,7 +612,7 @@ function createTailorDBFieldRuntime< } break; case "decimal": - if (typeof value !== "string" || !regex.decimal.test(value)) { + if (typeof value !== "string" || !isValidDecimalString(value)) { issues.push({ message: `Expected a decimal string: received ${String(value)}`, path: pathArray.length > 0 ? pathArray : undefined, diff --git a/packages/sdk/src/configure/types/field-format.ts b/packages/sdk/src/configure/types/field-format.ts index bb38724ea..147cd3e6e 100644 --- a/packages/sdk/src/configure/types/field-format.ts +++ b/packages/sdk/src/configure/types/field-format.ts @@ -1,9 +1,16 @@ const regex = { + uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, date: /^\d{4}-\d{2}-\d{2}$/, datetime: /^\d{4}-\d{2}-\d{2}[Tt](?[01]\d|2[0-3]):(?[0-5]\d):(?[0-5]\d|60)(\.(?\d+))?(?[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/, + time: /^(?\d{2}):(?\d{2})$/, + decimal: /^-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/, } as const; +export function isValidUUIDString(value: string): boolean { + return regex.uuid.test(value); +} + export function isValidDateString(value: string): boolean { return regex.date.test(value); } @@ -11,3 +18,11 @@ export function isValidDateString(value: string): boolean { export function isValidDateTimeString(value: string): boolean { return regex.datetime.test(value); } + +export function isValidTimeString(value: string): boolean { + return regex.time.test(value); +} + +export function isValidDecimalString(value: string): boolean { + return regex.decimal.test(value); +} diff --git a/packages/sdk/src/configure/types/index.ts b/packages/sdk/src/configure/types/index.ts index 7c1d8774d..398ec2c08 100644 --- a/packages/sdk/src/configure/types/index.ts +++ b/packages/sdk/src/configure/types/index.ts @@ -1,6 +1,7 @@ export * from "./machine-user"; export * from "./type"; export * from "./field"; +export * from "./scalar"; export type { DateString, DateTimeString, diff --git a/packages/sdk/src/configure/types/scalar.test.ts b/packages/sdk/src/configure/types/scalar.test.ts new file mode 100644 index 000000000..fde6d23c9 --- /dev/null +++ b/packages/sdk/src/configure/types/scalar.test.ts @@ -0,0 +1,119 @@ +// oxlint-disable vitest/expect-expect -- Type-only assertions are checked by TypeScript. +import { describe, expect, expectTypeOf, test } from "vitest"; +import { + assertDateString, + assertDateTimeString, + assertDecimalString, + assertTimeString, + assertUUIDString, + isDateString, + isDateTimeString, + isDecimalString, + isTimeString, + isUUIDString, + parseDateString, + parseDateTimeString, + parseDecimalString, + parseTimeString, + parseUUIDString, +} from "../index"; +import type { DateString, DateTimeString, DecimalString, TimeString, UUIDString } from "../index"; + +describe("scalar string helpers", () => { + test("type guard helpers narrow unknown values", () => { + const uuid: unknown = "550e8400-e29b-41d4-a716-446655440000"; + const date: unknown = "2026-07-03"; + const datetime: unknown = "2026-07-03T12:34:56+09:00"; + const time: unknown = "12:34"; + const decimal: unknown = "123.45"; + + if (isUUIDString(uuid)) { + expectTypeOf(uuid).toEqualTypeOf(); + } + if (isDateString(date)) { + expectTypeOf(date).toEqualTypeOf(); + } + if (isDateTimeString(datetime)) { + expectTypeOf(datetime).toEqualTypeOf(); + } + if (isTimeString(time)) { + expectTypeOf(time).toEqualTypeOf(); + } + if (isDecimalString(decimal)) { + expectTypeOf(decimal).toEqualTypeOf(); + } + + expect(isUUIDString(uuid)).toBe(true); + expect(isDateString(date)).toBe(true); + expect(isDateTimeString(datetime)).toBe(true); + expect(isTimeString(time)).toBe(true); + expect(isDecimalString(decimal)).toBe(true); + }); + + test("type guard helpers reject invalid values", () => { + expect(isUUIDString("not-a-uuid")).toBe(false); + expect(isDateString("2026/07/03")).toBe(false); + expect(isDateTimeString("2026-07-03T12:34:56+0900")).toBe(false); + expect(isTimeString("12:34:56")).toBe(false); + expect(isDecimalString("1_000")).toBe(false); + expect(isUUIDString(123)).toBe(false); + }); + + test("parse helpers return typed scalar strings", () => { + const uuid = parseUUIDString("550e8400-e29b-41d4-a716-446655440000"); + const date = parseDateString("2026-07-03"); + const datetime = parseDateTimeString("2026-07-03T12:34:56Z"); + const time = parseTimeString("12:34"); + const decimal = parseDecimalString("123.45"); + + expectTypeOf(uuid).toEqualTypeOf(); + expectTypeOf(date).toEqualTypeOf(); + expectTypeOf(datetime).toEqualTypeOf(); + expectTypeOf(time).toEqualTypeOf(); + expectTypeOf(decimal).toEqualTypeOf(); + + expect(uuid).toBe("550e8400-e29b-41d4-a716-446655440000"); + expect(date).toBe("2026-07-03"); + expect(datetime).toBe("2026-07-03T12:34:56Z"); + expect(time).toBe("12:34"); + expect(decimal).toBe("123.45"); + }); + + test("parse helpers throw TypeError with the given label", () => { + expect(() => parseUUIDString("not-a-uuid", "customerId")).toThrow( + new TypeError("customerId must be a UUID string"), + ); + expect(() => parseDateString("2026/07/03", "businessDate")).toThrow( + new TypeError("businessDate must be a date string"), + ); + expect(() => parseDateTimeString("2026-07-03T12:34:56+0900", "scheduledAt")).toThrow( + new TypeError("scheduledAt must be a datetime string"), + ); + expect(() => parseTimeString("12:34:56", "openingTime")).toThrow( + new TypeError("openingTime must be a time string"), + ); + expect(() => parseDecimalString("1_000", "amount")).toThrow( + new TypeError("amount must be a decimal string"), + ); + }); + + test("assert helpers narrow unknown values", () => { + const uuid: unknown = "550e8400-e29b-41d4-a716-446655440000"; + const date: unknown = "2026-07-03"; + const datetime: unknown = "2026-07-03T12:34:56Z"; + const time: unknown = "12:34"; + const decimal: unknown = "123.45"; + + assertUUIDString(uuid); + assertDateString(date); + assertDateTimeString(datetime); + assertTimeString(time); + assertDecimalString(decimal); + + expectTypeOf(uuid).toEqualTypeOf(); + expectTypeOf(date).toEqualTypeOf(); + expectTypeOf(datetime).toEqualTypeOf(); + expectTypeOf(time).toEqualTypeOf(); + expectTypeOf(decimal).toEqualTypeOf(); + }); +}); diff --git a/packages/sdk/src/configure/types/scalar.ts b/packages/sdk/src/configure/types/scalar.ts new file mode 100644 index 000000000..86a9fdaa8 --- /dev/null +++ b/packages/sdk/src/configure/types/scalar.ts @@ -0,0 +1,169 @@ +import { + isValidDateString, + isValidDateTimeString, + isValidDecimalString, + isValidTimeString, + isValidUUIDString, +} from "./field-format"; +import type { + DateString, + DateTimeString, + DecimalString, + TimeString, + UUIDString, +} from "./scalar.types"; + +function scalarTypeError(label: string, expected: string): TypeError { + return new TypeError(`${label} must be a ${expected}`); +} + +/** + * Check whether a value is a UUID string accepted by Tailor fields. + * @param value - Value to check + * @returns True when the value is a UUID string + */ +export function isUUIDString(value: unknown): value is UUIDString { + return typeof value === "string" && isValidUUIDString(value); +} + +/** + * Check whether a value is a date string accepted by Tailor fields. + * @param value - Value to check + * @returns True when the value matches `yyyy-MM-dd` + */ +export function isDateString(value: unknown): value is DateString { + return typeof value === "string" && isValidDateString(value); +} + +/** + * Check whether a value is a datetime string accepted by Tailor fields. + * @param value - Value to check + * @returns True when the value matches the supported ISO datetime format + */ +export function isDateTimeString(value: unknown): value is DateTimeString { + return typeof value === "string" && isValidDateTimeString(value); +} + +/** + * Check whether a value is a time string accepted by Tailor fields. + * @param value - Value to check + * @returns True when the value matches `HH:mm` + */ +export function isTimeString(value: unknown): value is TimeString { + return typeof value === "string" && isValidTimeString(value); +} + +/** + * Check whether a value is a decimal string accepted by Tailor fields. + * @param value - Value to check + * @returns True when the value is a decimal string + */ +export function isDecimalString(value: unknown): value is DecimalString { + return typeof value === "string" && isValidDecimalString(value); +} + +/** + * Parse a value as a UUID string. + * @param value - Value to parse + * @param label - Name used in the error message + * @returns The original value typed as `UUIDString` + */ +export function parseUUIDString(value: unknown, label = "value"): UUIDString { + if (isUUIDString(value)) return value; + throw scalarTypeError(label, "UUID string"); +} + +/** + * Parse a value as a date string. + * @param value - Value to parse + * @param label - Name used in the error message + * @returns The original value typed as `DateString` + */ +export function parseDateString(value: unknown, label = "value"): DateString { + if (isDateString(value)) return value; + throw scalarTypeError(label, "date string"); +} + +/** + * Parse a value as a datetime string. + * @param value - Value to parse + * @param label - Name used in the error message + * @returns The original value typed as `DateTimeString` + */ +export function parseDateTimeString(value: unknown, label = "value"): DateTimeString { + if (isDateTimeString(value)) return value; + throw scalarTypeError(label, "datetime string"); +} + +/** + * Parse a value as a time string. + * @param value - Value to parse + * @param label - Name used in the error message + * @returns The original value typed as `TimeString` + */ +export function parseTimeString(value: unknown, label = "value"): TimeString { + if (isTimeString(value)) return value; + throw scalarTypeError(label, "time string"); +} + +/** + * Parse a value as a decimal string. + * @param value - Value to parse + * @param label - Name used in the error message + * @returns The original value typed as `DecimalString` + */ +export function parseDecimalString(value: unknown, label = "value"): DecimalString { + if (isDecimalString(value)) return value; + throw scalarTypeError(label, "decimal string"); +} + +/** + * Assert that a value is a UUID string. + * @param value - Value to check + * @param label - Name used in the error message + */ +export function assertUUIDString(value: unknown, label?: string): asserts value is UUIDString { + parseUUIDString(value, label); +} + +/** + * Assert that a value is a date string. + * @param value - Value to check + * @param label - Name used in the error message + */ +export function assertDateString(value: unknown, label?: string): asserts value is DateString { + parseDateString(value, label); +} + +/** + * Assert that a value is a datetime string. + * @param value - Value to check + * @param label - Name used in the error message + */ +export function assertDateTimeString( + value: unknown, + label?: string, +): asserts value is DateTimeString { + parseDateTimeString(value, label); +} + +/** + * Assert that a value is a time string. + * @param value - Value to check + * @param label - Name used in the error message + */ +export function assertTimeString(value: unknown, label?: string): asserts value is TimeString { + parseTimeString(value, label); +} + +/** + * Assert that a value is a decimal string. + * @param value - Value to check + * @param label - Name used in the error message + */ +export function assertDecimalString( + value: unknown, + label?: string, +): asserts value is DecimalString { + parseDecimalString(value, label); +} diff --git a/packages/sdk/src/configure/types/type.ts b/packages/sdk/src/configure/types/type.ts index d1b1b249e..802e4b993 100644 --- a/packages/sdk/src/configure/types/type.ts +++ b/packages/sdk/src/configure/types/type.ts @@ -1,5 +1,11 @@ import { type AllowedValues, type AllowedValuesOutput, mapAllowedValues } from "./field"; -import { isValidDateString, isValidDateTimeString } from "./field-format"; +import { + isValidDateString, + isValidDateTimeString, + isValidDecimalString, + isValidTimeString, + isValidUUIDString, +} from "./field-format"; import type { DefinedFieldMetadata, TailorFieldType, @@ -129,12 +135,6 @@ export interface TailorField< */ type CloneableField = { clone(): TailorAnyField }; -const regex = { - uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, - time: /^(?\d{2}):(?\d{2})$/, - decimal: /^-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/, -} as const; - type FieldParseArgs = { value: unknown; data: unknown; @@ -290,7 +290,7 @@ function createTailorField< break; case "uuid": - if (typeof value !== "string" || !regex.uuid.test(value)) { + if (typeof value !== "string" || !isValidUUIDString(value)) { issues.push({ message: `Expected a valid UUID: received ${String(value)}`, path, @@ -314,7 +314,7 @@ function createTailorField< } break; case "time": - if (typeof value !== "string" || !regex.time.test(value)) { + if (typeof value !== "string" || !isValidTimeString(value)) { issues.push({ message: `Expected to match "HH:mm" format: received ${String(value)}`, path, @@ -322,7 +322,7 @@ function createTailorField< } break; case "decimal": - if (typeof value !== "string" || !regex.decimal.test(value)) { + if (typeof value !== "string" || !isValidDecimalString(value)) { issues.push({ message: `Expected a decimal string: received ${String(value)}`, path, From 81e2ce804a9732a3f998f20b15e7fd16a4ecde78 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 14:21:12 +0900 Subject: [PATCH 423/618] fix(codemod): preserve runtime helper imports --- .../scripts/transform.ts | 80 ++++---------- .../expected.ts | 5 + .../default-value-runtime-reference/input.ts | 5 + .../scripts/transform.ts | 100 +++--------------- .../expected.ts | 7 ++ .../type-only-idp-with-runtime-value/input.ts | 7 ++ .../tests/type-only-idp/expected.ts | 6 ++ packages/sdk-codemod/src/ast-grep-helpers.ts | 88 ++++++++++++++- 8 files changed, 152 insertions(+), 146 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-value-runtime-reference/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-value-runtime-reference/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp-with-runtime-value/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp-with-runtime-value/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp/expected.ts diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts index 135375cae..d67776af5 100644 --- a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/scripts/transform.ts @@ -1,11 +1,12 @@ import { parse, Lang } from "@ast-grep/napi"; import { + buildAddNamedImportEdit, findImportStatements, + importBindings, importSource, importSpecNames, isTypeOnlyImport, localDeclarationNames, - namedImportsNode, } from "../../../../src/ast-grep-helpers"; import type { LlmReviewFinding } from "../../../../src/types"; import type { Edit, SgNode } from "@ast-grep/napi"; @@ -70,36 +71,13 @@ function findAuthImports(imports: SgNode[]): AuthImport[] { return authImports; } -function importLocalNames(importStmt: SgNode): Set { - const names = new Set(); - const clause = importStmt.children().find((child) => child.kind() === "import_clause"); - if (!clause) return names; - - for (const child of clause.children()) { - if (child.kind() === "identifier") { - names.add(child.text()); - continue; - } - if (child.kind() === "namespace_import") { - const local = child.children().find((grandchild) => grandchild.kind() === "identifier"); - if (local) names.add(local.text()); - continue; - } - if (child.kind() !== "named_imports") continue; - for (const spec of child.findAll({ rule: { kind: "import_specifier" } })) { - const specNames = importSpecNames(spec); - if (specNames && !specNames.typeOnly) names.add(specNames.localName); - } - } - return names; -} - function runtimeAuthconnectionReference(imports: SgNode[]): string | null { for (const importStmt of imports) { if (importSource(importStmt) !== RUNTIME_MODULE || isTypeOnlyImport(importStmt)) continue; - for (const spec of importStmt.findAll({ rule: { kind: "import_specifier" } })) { - const names = importSpecNames(spec); - if (names?.importedName === AUTHCONNECTION && !names.typeOnly) return names.localName; + for (const binding of importBindings(importStmt)) { + if (binding.importedName === AUTHCONNECTION && !binding.typeOnly) { + return binding.localName; + } } const clause = importStmt.children().find((child) => child.kind() === "import_clause"); @@ -110,23 +88,14 @@ function runtimeAuthconnectionReference(imports: SgNode[]): string | null { return null; } -function runtimeNamedValueImport(imports: SgNode[]): SgNode | null { - return ( - imports.find( - (importStmt) => - importSource(importStmt) === RUNTIME_MODULE && - !isTypeOnlyImport(importStmt) && - namedImportsNode(importStmt), - ) ?? null - ); -} - function hasRuntimeImportCollision(root: SgNode, imports: SgNode[]): boolean { if (localDeclarationNames(root).has(AUTHCONNECTION)) return true; return imports.some( (importStmt) => - importLocalNames(importStmt).has(AUTHCONNECTION) && - importSource(importStmt) !== RUNTIME_MODULE, + importSource(importStmt) !== RUNTIME_MODULE && + importBindings(importStmt).some( + (binding) => binding.localName === AUTHCONNECTION && !binding.typeOnly, + ), ); } @@ -205,27 +174,14 @@ function importInsertionIndex(root: SgNode, imports: SgNode[], source: string): } function buildAddRuntimeImportEdit(root: SgNode, source: string, imports: SgNode[]): Edit { - const existingRuntimeImport = runtimeNamedValueImport(imports); - const namedImports = existingRuntimeImport ? namedImportsNode(existingRuntimeImport) : null; - if (namedImports) { - const specTexts = namedImports.findAll({ rule: { kind: "import_specifier" } }).map((spec) => { - const names = importSpecNames(spec); - return names?.importedName === AUTHCONNECTION && names.localName === AUTHCONNECTION - ? AUTHCONNECTION - : spec.text(); - }); - const nextSpecTexts = specTexts.includes(AUTHCONNECTION) - ? specTexts - : [...specTexts, AUTHCONNECTION]; - return namedImports.replace(`{ ${nextSpecTexts.join(", ")} }`); - } - - const pos = importInsertionIndex(root, imports, source); - const insertedText = - pos === 0 || source[pos - 1] === "\n" - ? `import { ${AUTHCONNECTION} } from "${RUNTIME_MODULE}";\n\n` - : `\nimport { ${AUTHCONNECTION} } from "${RUNTIME_MODULE}";`; - return { startPos: pos, endPos: pos, insertedText }; + return buildAddNamedImportEdit({ + importName: AUTHCONNECTION, + imports, + insertionIndex: importInsertionIndex, + moduleName: RUNTIME_MODULE, + root, + source, + }); } function lineStartIndex(source: string, index: number): number { diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-value-runtime-reference/expected.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-value-runtime-reference/expected.ts new file mode 100644 index 000000000..ef6b7212d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-value-runtime-reference/expected.ts @@ -0,0 +1,5 @@ +import { authconnection } from "@tailor-platform/sdk/runtime"; + +export async function run(token = authconnection) { + return authconnection.getConnectionToken("google"); +} diff --git a/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-value-runtime-reference/input.ts b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-value-runtime-reference/input.ts new file mode 100644 index 000000000..6cb415f9b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-connection-token-helper/tests/default-value-runtime-reference/input.ts @@ -0,0 +1,5 @@ +import { auth } from "../tailor.config"; + +export async function run(token = authconnection) { + return auth.getConnectionToken("google"); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts index 279f27c12..7e5d6586d 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/scripts/transform.ts @@ -1,11 +1,9 @@ import { parse, Lang } from "@ast-grep/napi"; import { + buildAddNamedImportEdit, findImportStatements, - importSource, - importSpecNames, - isTypeOnlyImport, + importBindings, localDeclarationNames, - namedImportsNode, } from "../../../../src/ast-grep-helpers"; import type { Edit, SgNode } from "@ast-grep/napi"; @@ -13,13 +11,6 @@ const RUNTIME_MODULE = "@tailor-platform/sdk/runtime"; const TAILOR_IDP_CLIENT = "tailor.idp.Client"; const NON_ARGUMENT_KINDS = new Set(["(", ")", ",", "comment"]); -interface ImportBinding { - localName: string; - importedName?: string; - source: string; - typeOnly: boolean; -} - function quickFilter(source: string): boolean { return source.includes(TAILOR_IDP_CLIENT); } @@ -30,46 +21,6 @@ function sourceLang(filePath: string, source: string): Lang { : Lang.TypeScript; } -function importBindings(importStmt: SgNode): ImportBinding[] { - const source = importSource(importStmt); - if (!source) return []; - - const requireClause = importStmt - .children() - .find((child) => child.kind() === "import_require_clause"); - if (requireClause) { - const local = requireClause.children().find((child) => child.kind() === "identifier"); - return local ? [{ localName: local.text(), source, typeOnly: false }] : []; - } - - const typeOnly = isTypeOnlyImport(importStmt); - const clause = importStmt.children().find((child) => child.kind() === "import_clause"); - if (!clause) return []; - - const bindings: ImportBinding[] = []; - for (const child of clause.children()) { - if (child.kind() === "identifier") { - bindings.push({ localName: child.text(), source, typeOnly }); - continue; - } - - if (child.kind() === "namespace_import") { - const local = child.children().find((c) => c.kind() === "identifier"); - if (local) bindings.push({ localName: local.text(), source, typeOnly }); - continue; - } - - if (child.kind() !== "named_imports") continue; - for (const spec of child.findAll({ rule: { kind: "import_specifier" } })) { - const names = importSpecNames(spec); - if (!names) continue; - bindings.push({ ...names, source, typeOnly: typeOnly || names.typeOnly }); - } - } - - return bindings; -} - function runtimeIdpLocalName(imports: SgNode[]): string | null { for (const importStmt of imports) { for (const binding of importBindings(importStmt)) { @@ -85,6 +36,10 @@ function runtimeIdpLocalName(imports: SgNode[]): string | null { return null; } +function isRuntimeIdpBinding(binding: ReturnType[number]): boolean { + return binding.source === RUNTIME_MODULE && binding.importedName === "idp"; +} + function hasCollision( imports: SgNode[], localNames: Set, @@ -101,27 +56,15 @@ function hasCollision( for (const importStmt of imports) { for (const binding of importBindings(importStmt)) { if (binding.localName === "tailor") return true; - if ( - binding.localName === "idp" && - !(binding.source === RUNTIME_MODULE && binding.importedName === "idp" && !binding.typeOnly) - ) { - return true; - } + if (binding.localName !== "idp") continue; + if (isRuntimeIdpBinding(binding)) continue; + return true; } } return false; } -function runtimeNamedValueImport(imports: SgNode[]): SgNode | null { - return ( - imports.find( - (stmt) => - importSource(stmt) === RUNTIME_MODULE && !isTypeOnlyImport(stmt) && namedImportsNode(stmt), - ) ?? null - ); -} - function isDirectiveStatement(node: SgNode): boolean { return node.kind() === "expression_statement" && node.children()[0]?.kind() === "string"; } @@ -150,23 +93,14 @@ function importInsertionIndex(root: SgNode, imports: SgNode[], source: string): } function buildAddRuntimeImportEdit(root: SgNode, source: string, imports: SgNode[]): Edit { - const existingRuntimeImport = runtimeNamedValueImport(imports); - const namedImports = existingRuntimeImport ? namedImportsNode(existingRuntimeImport) : null; - if (namedImports) { - const specTexts = namedImports.findAll({ rule: { kind: "import_specifier" } }).map((spec) => { - const names = importSpecNames(spec); - return names?.importedName === "idp" && names.localName === "idp" ? "idp" : spec.text(); - }); - const nextSpecTexts = specTexts.includes("idp") ? specTexts : [...specTexts, "idp"]; - return namedImports.replace(`{ ${nextSpecTexts.join(", ")} }`); - } - - const pos = importInsertionIndex(root, imports, source); - const insertedText = - pos === 0 || (pos > 0 && source[pos - 1] === "\n") - ? `import { idp } from "${RUNTIME_MODULE}";\n\n` - : `\nimport { idp } from "${RUNTIME_MODULE}";`; - return { startPos: pos, endPos: pos, insertedText }; + return buildAddNamedImportEdit({ + importName: "idp", + imports, + insertionIndex: importInsertionIndex, + moduleName: RUNTIME_MODULE, + root, + source, + }); } function argumentExpressions(args: SgNode): SgNode[] { diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp-with-runtime-value/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp-with-runtime-value/expected.ts new file mode 100644 index 000000000..025c22600 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp-with-runtime-value/expected.ts @@ -0,0 +1,7 @@ +import { workflow, idp } from "@tailor-platform/sdk/runtime"; + +export async function run() { + await workflow.wait("ready"); + const client = new idp.Client({ namespace: "default" }); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp-with-runtime-value/input.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp-with-runtime-value/input.ts new file mode 100644 index 000000000..0fbe749e5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp-with-runtime-value/input.ts @@ -0,0 +1,7 @@ +import { workflow, type idp } from "@tailor-platform/sdk/runtime"; + +export async function run() { + await workflow.wait("ready"); + const client = new tailor.idp.Client({ namespace: "default" }); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp/expected.ts new file mode 100644 index 000000000..2f61259b2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-globals-opt-in/tests/type-only-idp/expected.ts @@ -0,0 +1,6 @@ +import { idp } from "@tailor-platform/sdk/runtime"; + +export async function run() { + const client = new idp.Client({ namespace: "default" }); + return client.listUsers(); +} diff --git a/packages/sdk-codemod/src/ast-grep-helpers.ts b/packages/sdk-codemod/src/ast-grep-helpers.ts index a81e08edc..007a6d051 100644 --- a/packages/sdk-codemod/src/ast-grep-helpers.ts +++ b/packages/sdk-codemod/src/ast-grep-helpers.ts @@ -1,4 +1,4 @@ -import type { SgNode } from "@ast-grep/napi"; +import type { Edit, SgNode } from "@ast-grep/napi"; const DECLARATION_KINDS = [ "function_declaration", @@ -18,6 +18,22 @@ export interface ImportSpecifierNames { typeOnly: boolean; } +export interface ImportBinding { + localName: string; + importedName?: string; + source: string; + typeOnly: boolean; +} + +export interface AddNamedImportEditOptions { + importName: string; + imports: SgNode[]; + insertionIndex: (root: SgNode, imports: SgNode[], source: string) => number; + moduleName: string; + root: SgNode; + source: string; +} + function isBindingLeafKind(kind: ReturnType): boolean { return ( kind === "identifier" || @@ -68,6 +84,75 @@ export function findImportStatements(root: SgNode): SgNode[] { .toSorted((a, b) => a.range().start.index - b.range().start.index); } +export function importBindings(importStmt: SgNode): ImportBinding[] { + const source = importSource(importStmt); + if (!source) return []; + + const requireClause = importStmt + .children() + .find((child) => child.kind() === "import_require_clause"); + if (requireClause) { + const local = requireClause.children().find((child) => child.kind() === "identifier"); + return local ? [{ localName: local.text(), source, typeOnly: false }] : []; + } + + const typeOnly = isTypeOnlyImport(importStmt); + const clause = importStmt.children().find((child) => child.kind() === "import_clause"); + if (!clause) return []; + + const bindings: ImportBinding[] = []; + for (const child of clause.children()) { + if (child.kind() === "identifier") { + bindings.push({ localName: child.text(), source, typeOnly }); + continue; + } + + if (child.kind() === "namespace_import") { + const local = child.children().find((grandchild) => grandchild.kind() === "identifier"); + if (local) bindings.push({ localName: local.text(), source, typeOnly }); + continue; + } + + if (child.kind() !== "named_imports") continue; + for (const spec of child.findAll({ rule: { kind: "import_specifier" } })) { + const names = importSpecNames(spec); + if (!names) continue; + bindings.push({ ...names, source, typeOnly: typeOnly || names.typeOnly }); + } + } + + return bindings; +} + +export function buildAddNamedImportEdit(options: AddNamedImportEditOptions): Edit { + const { importName, imports, insertionIndex, moduleName, root, source } = options; + const existingImport = + imports.find( + (importStmt) => + importSource(importStmt) === moduleName && + !isTypeOnlyImport(importStmt) && + namedImportsNode(importStmt), + ) ?? null; + const namedImports = existingImport ? namedImportsNode(existingImport) : null; + if (namedImports) { + const specTexts = namedImports.findAll({ rule: { kind: "import_specifier" } }).map((spec) => { + const names = importSpecNames(spec); + return names?.importedName === importName && names.localName === importName + ? importName + : spec.text(); + }); + const nextSpecTexts = specTexts.includes(importName) ? specTexts : [...specTexts, importName]; + return namedImports.replace(`{ ${nextSpecTexts.join(", ")} }`); + } + + const pos = insertionIndex(root, imports, source); + const insertedText = + pos === 0 || source[pos - 1] === "\n" + ? `import { ${importName} } from "${moduleName}";\n\n` + : `\nimport { ${importName} } from "${moduleName}";`; + return { startPos: pos, endPos: pos, insertedText }; +} + export function collectBindingNames(node: SgNode, names: Set): void { if (isBindingLeafKind(node.kind())) { names.add(node.text()); @@ -87,6 +172,7 @@ function firstDeclaratorChild(node: SgNode): SgNode | null { function collectDirectBindingChildren(node: SgNode, names: Set): void { for (const child of node.children()) { + if (child.kind() === "=") break; if (isBindingPatternKind(child.kind())) collectBindingNames(child, names); } } From f0d5d396160bf66f9292b68908aefd2e5580d5fb Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 3 Jul 2026 17:33:13 +0900 Subject: [PATCH 424/618] fix: tighten scalar and type helpers --- packages/sdk/src/cli/shared/type-generator.ts | 35 +++++---- .../sdk/src/configure/types/field-format.ts | 2 +- .../sdk/src/configure/types/scalar.test.ts | 3 + packages/sdk/src/configure/types/scalar.ts | 78 ++++++++++++++----- .../sdk/src/parser/service/auth/index.test.ts | 3 +- 5 files changed, 81 insertions(+), 40 deletions(-) diff --git a/packages/sdk/src/cli/shared/type-generator.ts b/packages/sdk/src/cli/shared/type-generator.ts index 1a9f45954..86be7d122 100644 --- a/packages/sdk/src/cli/shared/type-generator.ts +++ b/packages/sdk/src/cli/shared/type-generator.ts @@ -32,6 +32,19 @@ const knownAttributeListTypes = new Set([ ...scalarTypeNames, ]); +const fieldTypeScriptTypes: Record = { + boolean: "boolean", + bool: "boolean", + uuid: "UUIDString", + date: "DateString", + datetime: "DateTimeString | Date", + time: "TimeString", + decimal: "DecimalString", + integer: "number", + float: "number", + number: "number", +}; + interface ExtractedAttributes { attributes?: AttributesConfig; attributeList?: AttributeListConfig; @@ -232,23 +245,11 @@ function collectAttributesFromConfig(config: AppConfig): ExtractedAttributes { } const typeStr = - type === "boolean" || type === "bool" - ? "boolean" - : type === "enum" && metadata?.allowedValues - ? metadata.allowedValues.map((v) => `"${v.value}"`).join(" | ") - : type === "uuid" - ? "UUIDString" - : type === "date" - ? "DateString" - : type === "datetime" - ? "DateTimeString | Date" - : type === "time" - ? "TimeString" - : type === "decimal" - ? "DecimalString" - : type === "integer" || type === "float" || type === "number" - ? "number" - : "string"; + type === "enum" && metadata?.allowedValues + ? metadata.allowedValues.map((v) => `"${v.value}"`).join(" | ") + : type + ? (fieldTypeScriptTypes[type] ?? "string") + : "string"; // Add array suffix if needed if (metadata?.array) { diff --git a/packages/sdk/src/configure/types/field-format.ts b/packages/sdk/src/configure/types/field-format.ts index 147cd3e6e..05b571cfc 100644 --- a/packages/sdk/src/configure/types/field-format.ts +++ b/packages/sdk/src/configure/types/field-format.ts @@ -3,7 +3,7 @@ const regex = { date: /^\d{4}-\d{2}-\d{2}$/, datetime: /^\d{4}-\d{2}-\d{2}[Tt](?[01]\d|2[0-3]):(?[0-5]\d):(?[0-5]\d|60)(\.(?\d+))?(?[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/, - time: /^(?\d{2}):(?\d{2})$/, + time: /^(?[01]\d|2[0-3]):(?[0-5]\d)$/, decimal: /^-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/, } as const; diff --git a/packages/sdk/src/configure/types/scalar.test.ts b/packages/sdk/src/configure/types/scalar.test.ts index fde6d23c9..46e61ae73 100644 --- a/packages/sdk/src/configure/types/scalar.test.ts +++ b/packages/sdk/src/configure/types/scalar.test.ts @@ -55,6 +55,9 @@ describe("scalar string helpers", () => { expect(isDateString("2026/07/03")).toBe(false); expect(isDateTimeString("2026-07-03T12:34:56+0900")).toBe(false); expect(isTimeString("12:34:56")).toBe(false); + expect(isTimeString("24:00")).toBe(false); + expect(isTimeString("23:60")).toBe(false); + expect(isTimeString("99:99")).toBe(false); expect(isDecimalString("1_000")).toBe(false); expect(isUUIDString(123)).toBe(false); }); diff --git a/packages/sdk/src/configure/types/scalar.ts b/packages/sdk/src/configure/types/scalar.ts index 86a9fdaa8..3eccb9ffd 100644 --- a/packages/sdk/src/configure/types/scalar.ts +++ b/packages/sdk/src/configure/types/scalar.ts @@ -17,13 +17,56 @@ function scalarTypeError(label: string, expected: string): TypeError { return new TypeError(`${label} must be a ${expected}`); } +type ScalarHelpers = { + isString: (value: unknown) => value is T; + parseString: (value: unknown, label?: string) => T; + assertString: (value: unknown, label?: string) => asserts value is T; +}; + +function makeScalarHelpers( + isValid: (value: string) => boolean, + expected: string, +): ScalarHelpers { + const isString = (value: unknown): value is T => typeof value === "string" && isValid(value); + const parseString = (value: unknown, label = "value"): T => { + if (isString(value)) return value; + throw scalarTypeError(label, expected); + }; + const assertString = (value: unknown, label?: string): asserts value is T => { + parseString(value, label); + }; + + return { isString, parseString, assertString }; +} + +const uuidString: ScalarHelpers = makeScalarHelpers( + isValidUUIDString, + "UUID string", +); +const dateString: ScalarHelpers = makeScalarHelpers( + isValidDateString, + "date string", +); +const dateTimeString: ScalarHelpers = makeScalarHelpers( + isValidDateTimeString, + "datetime string", +); +const timeString: ScalarHelpers = makeScalarHelpers( + isValidTimeString, + "time string", +); +const decimalString: ScalarHelpers = makeScalarHelpers( + isValidDecimalString, + "decimal string", +); + /** * Check whether a value is a UUID string accepted by Tailor fields. * @param value - Value to check * @returns True when the value is a UUID string */ export function isUUIDString(value: unknown): value is UUIDString { - return typeof value === "string" && isValidUUIDString(value); + return uuidString.isString(value); } /** @@ -32,7 +75,7 @@ export function isUUIDString(value: unknown): value is UUIDString { * @returns True when the value matches `yyyy-MM-dd` */ export function isDateString(value: unknown): value is DateString { - return typeof value === "string" && isValidDateString(value); + return dateString.isString(value); } /** @@ -41,7 +84,7 @@ export function isDateString(value: unknown): value is DateString { * @returns True when the value matches the supported ISO datetime format */ export function isDateTimeString(value: unknown): value is DateTimeString { - return typeof value === "string" && isValidDateTimeString(value); + return dateTimeString.isString(value); } /** @@ -50,7 +93,7 @@ export function isDateTimeString(value: unknown): value is DateTimeString { * @returns True when the value matches `HH:mm` */ export function isTimeString(value: unknown): value is TimeString { - return typeof value === "string" && isValidTimeString(value); + return timeString.isString(value); } /** @@ -59,7 +102,7 @@ export function isTimeString(value: unknown): value is TimeString { * @returns True when the value is a decimal string */ export function isDecimalString(value: unknown): value is DecimalString { - return typeof value === "string" && isValidDecimalString(value); + return decimalString.isString(value); } /** @@ -69,8 +112,7 @@ export function isDecimalString(value: unknown): value is DecimalString { * @returns The original value typed as `UUIDString` */ export function parseUUIDString(value: unknown, label = "value"): UUIDString { - if (isUUIDString(value)) return value; - throw scalarTypeError(label, "UUID string"); + return uuidString.parseString(value, label); } /** @@ -80,8 +122,7 @@ export function parseUUIDString(value: unknown, label = "value"): UUIDString { * @returns The original value typed as `DateString` */ export function parseDateString(value: unknown, label = "value"): DateString { - if (isDateString(value)) return value; - throw scalarTypeError(label, "date string"); + return dateString.parseString(value, label); } /** @@ -91,8 +132,7 @@ export function parseDateString(value: unknown, label = "value"): DateString { * @returns The original value typed as `DateTimeString` */ export function parseDateTimeString(value: unknown, label = "value"): DateTimeString { - if (isDateTimeString(value)) return value; - throw scalarTypeError(label, "datetime string"); + return dateTimeString.parseString(value, label); } /** @@ -102,8 +142,7 @@ export function parseDateTimeString(value: unknown, label = "value"): DateTimeSt * @returns The original value typed as `TimeString` */ export function parseTimeString(value: unknown, label = "value"): TimeString { - if (isTimeString(value)) return value; - throw scalarTypeError(label, "time string"); + return timeString.parseString(value, label); } /** @@ -113,8 +152,7 @@ export function parseTimeString(value: unknown, label = "value"): TimeString { * @returns The original value typed as `DecimalString` */ export function parseDecimalString(value: unknown, label = "value"): DecimalString { - if (isDecimalString(value)) return value; - throw scalarTypeError(label, "decimal string"); + return decimalString.parseString(value, label); } /** @@ -123,7 +161,7 @@ export function parseDecimalString(value: unknown, label = "value"): DecimalStri * @param label - Name used in the error message */ export function assertUUIDString(value: unknown, label?: string): asserts value is UUIDString { - parseUUIDString(value, label); + uuidString.assertString(value, label); } /** @@ -132,7 +170,7 @@ export function assertUUIDString(value: unknown, label?: string): asserts value * @param label - Name used in the error message */ export function assertDateString(value: unknown, label?: string): asserts value is DateString { - parseDateString(value, label); + dateString.assertString(value, label); } /** @@ -144,7 +182,7 @@ export function assertDateTimeString( value: unknown, label?: string, ): asserts value is DateTimeString { - parseDateTimeString(value, label); + dateTimeString.assertString(value, label); } /** @@ -153,7 +191,7 @@ export function assertDateTimeString( * @param label - Name used in the error message */ export function assertTimeString(value: unknown, label?: string): asserts value is TimeString { - parseTimeString(value, label); + timeString.assertString(value, label); } /** @@ -165,5 +203,5 @@ export function assertDecimalString( value: unknown, label?: string, ): asserts value is DecimalString { - parseDecimalString(value, label); + decimalString.assertString(value, label); } diff --git a/packages/sdk/src/parser/service/auth/index.test.ts b/packages/sdk/src/parser/service/auth/index.test.ts index 84c1d3172..90979a41f 100644 --- a/packages/sdk/src/parser/service/auth/index.test.ts +++ b/packages/sdk/src/parser/service/auth/index.test.ts @@ -3,11 +3,10 @@ import { db } from "#/configure/services/tailordb/schema"; import { t } from "#/configure/types/type"; import { AuthConfigSchema, OAuth2ClientSchema } from "./schema"; import type { AuthServiceInput } from "#/configure/services/auth/types"; +import type { UUIDString } from "#/configure/types/scalar.types"; import type { OptionalKeysOf } from "type-fest"; import type { z } from "zod"; -type UUIDString = `${string}-${string}-${string}-${string}-${string}`; - // Define userType for type inference const userType = db.type("User", { email: db.string().unique(), From 1e34d7a07acb5792f8e41b92b90cc339bf8cc73a Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 25 Jun 2026 14:30:11 +0900 Subject: [PATCH 425/618] feat(cli): add plugin dispatch with shared auth Running `tailor ` for an unknown subcommand execs an external `tailor-` binary resolved from the project's node_modules/.bin (nearest first) or PATH, forwarding all following args. Works for unknown subcommands nested under a known command too (`tailor tailordb erd` -> `tailor-tailordb-erd`). Builtins always take precedence, matching stops at the first unknown segment, and a command that defines its own run is never replaced. Before dispatching, the CLI injects the current platform context as env so plugins do not re-implement auth or re-resolve the active workspace: TAILOR_PLATFORM_TOKEN, TAILOR_PLATFORM_URL, TAILOR_PLATFORM_OAUTH2_CLIENT_ID, TAILOR_PLATFORM_WORKSPACE_ID, TAILOR_PLATFORM_USER, TAILOR_CONFIG_PATH, TAILOR_VERSION, and TAILOR_BIN. Token, workspace, and user are best-effort so auth-free plugins still run when not logged in. Adds `tailor auth token` (print a valid access token, refreshing it if expired, for plugins/scripts) and `tailor plugin list` (list discovered plugins and where they resolve from). The root --help notes point users to `tailor plugin list`. --- .changeset/cli-plugins.md | 10 + packages/sdk/docs/cli-reference.md | 95 ++++- packages/sdk/docs/cli-reference.template.md | 58 +++ packages/sdk/docs/cli/plugin.md | 29 ++ packages/sdk/docs/cli/plugin.template.md | 8 + packages/sdk/docs/cli/user.md | 36 ++ packages/sdk/docs/cli/user.template.md | 1 + packages/sdk/src/cli/commands/auth/index.ts | 10 + packages/sdk/src/cli/commands/auth/token.ts | 18 + packages/sdk/src/cli/commands/plugin/index.ts | 13 + packages/sdk/src/cli/commands/plugin/list.ts | 57 +++ packages/sdk/src/cli/docs.test.ts | 6 +- packages/sdk/src/cli/index.ts | 17 + packages/sdk/src/cli/options.test.ts | 11 + .../sdk/src/cli/shared/builtin-commands.ts | 39 ++ packages/sdk/src/cli/shared/client.ts | 2 +- packages/sdk/src/cli/shared/plugin.test.ts | 280 ++++++++++++++ packages/sdk/src/cli/shared/plugin.ts | 342 ++++++++++++++++++ .../sdk/src/cli/shared/readonly-guard.test.ts | 5 + 19 files changed, 1022 insertions(+), 15 deletions(-) create mode 100644 .changeset/cli-plugins.md create mode 100644 packages/sdk/docs/cli/plugin.md create mode 100644 packages/sdk/docs/cli/plugin.template.md create mode 100644 packages/sdk/src/cli/commands/auth/index.ts create mode 100644 packages/sdk/src/cli/commands/auth/token.ts create mode 100644 packages/sdk/src/cli/commands/plugin/index.ts create mode 100644 packages/sdk/src/cli/commands/plugin/list.ts create mode 100644 packages/sdk/src/cli/shared/builtin-commands.ts create mode 100644 packages/sdk/src/cli/shared/plugin.test.ts create mode 100644 packages/sdk/src/cli/shared/plugin.ts diff --git a/.changeset/cli-plugins.md b/.changeset/cli-plugins.md new file mode 100644 index 000000000..5d1bc3099 --- /dev/null +++ b/.changeset/cli-plugins.md @@ -0,0 +1,10 @@ +--- +"@tailor-platform/sdk": minor +--- + +Add CLI plugin support. Running `tailor ` for an unknown subcommand now executes an external `tailor-` executable found on your PATH or in `node_modules/.bin` (project-local takes precedence), forwarding all following arguments. This also works for unknown subcommands nested under a known command — e.g. `tailor tailordb erd` dispatches to `tailor-tailordb-erd`. Builtins always take precedence, matching stops at the first unknown segment, and a command that takes its own arguments is never replaced by a plugin. The plugin receives the current Tailor Platform context via environment variables (`TAILOR_PLATFORM_TOKEN`, `TAILOR_PLATFORM_URL`, `TAILOR_PLATFORM_OAUTH2_CLIENT_ID`, `TAILOR_PLATFORM_WORKSPACE_ID`, `TAILOR_PLATFORM_USER`, `TAILOR_CONFIG_PATH`, `TAILOR_VERSION`, `TAILOR_BIN`); token, workspace, and user are best-effort, so auth-free plugins still run when you are not logged in. + +Also adds: + +- `tailor auth token` — print a valid access token (refreshing it if expired) for use by plugins and scripts. +- `tailor plugin list` — list discovered plugins and their executable paths. diff --git a/packages/sdk/docs/cli-reference.md b/packages/sdk/docs/cli-reference.md index bf4aa7f67..a8f2aa775 100644 --- a/packages/sdk/docs/cli-reference.md +++ b/packages/sdk/docs/cli-reference.md @@ -97,6 +97,64 @@ Workspace ID resolution follows this priority order: 2. `TAILOR_PLATFORM_WORKSPACE_ID` environment variable 3. Profile specified via `--profile` option or `TAILOR_PLATFORM_PROFILE` +## CLI Plugins + +You can extend the CLI with external plugins, similar to `gh` extensions. When you run a command that +is not a built-in, the CLI looks for an executable named `tailor-` and runs it, forwarding the +remaining arguments: + +```bash +# Runs the `tailor-hello` executable with: world --loud +tailor hello world --loud +``` + +This also works under a built-in command group. The command path is joined with hyphens, so a plugin +nested under `tailordb` is named `tailor-tailordb-erd`: + +```bash +# Runs `tailor-tailordb-erd` with: export +tailor tailordb erd export +``` + +Resolution rules: + +- **Built-ins always win.** A plugin is only used when no built-in command matches. +- **A command that takes its own arguments is never replaced.** Plugin dispatch applies only to command + _groups_ (commands that just route to subcommands). A command that performs its own action — including + one that accepts a positional argument — always runs itself, so a plugin can never shadow an argument value. +- **Lookup order:** the project's `node_modules/.bin` (nearest first, walking up from the current + directory), then your `PATH`. So a plugin installed as a project dev-dependency takes precedence over a + globally installed one. + +Because resolution is based on `node_modules/.bin` and `PATH`, any package manager that populates +`node_modules/.bin` works for project-local plugins — npm, pnpm (its content-addressable store is +transparent here), Bun, and Yarn Classic. The exception is **Yarn Plug'n'Play**, which does not create a +`node_modules` directory: install such plugins globally so they resolve via `PATH`, or use Yarn's +`nodeLinker: node-modules` setting. + +Run `tailor plugin list` to see which plugins are discovered and where they resolve from. + +### Context passed to plugins + +Before running a plugin, the CLI injects the current Tailor Platform context as environment variables so +the plugin does not need to re-implement authentication or re-resolve the active workspace: + +| Variable | Description | +| ---------------------------------- | ------------------------------------------------------------------------ | +| `TAILOR_PLATFORM_TOKEN` | A valid access token (refreshed if needed). Omitted when not logged in. | +| `TAILOR_PLATFORM_URL` | The Tailor Platform endpoint in effect | +| `TAILOR_PLATFORM_OAUTH2_CLIENT_ID` | The OAuth2 client ID in effect, for plugins that run their own auth flow | +| `TAILOR_PLATFORM_WORKSPACE_ID` | The resolved workspace ID, when one can be determined | +| `TAILOR_PLATFORM_USER` | The active user (email when known), when logged in | +| `TAILOR_CONFIG_PATH` | Path to the resolved Tailor config file, when found | +| `TAILOR_VERSION` | The `tailor` version that invoked the plugin | +| `TAILOR_BIN` | Path to the `tailor` executable, for calling back into the CLI | + +The token, workspace ID, and user are best-effort: whatever the current context can resolve is injected, +and auth-free plugins still run when you are not logged in. A long-running plugin (or one started on its +own) can obtain a fresh token at any time with `tailor auth token`, which prints a valid access token to +stdout, refreshing it first if it has expired. + ## Commands ### [Application Commands](./cli/application.md) @@ -146,19 +204,21 @@ Run ad-hoc SQL/GraphQL queries or enter the interactive REPL. Commands for authentication and user management. -| Command | Description | -| ------------------------------------------------ | ----------------------------------------------------- | -| [login](./cli/user.md#login) | Login to Tailor Platform. | -| [logout](./cli/user.md#logout) | Logout from Tailor Platform. | -| [user](./cli/user.md#user) | Manage Tailor Platform users. | -| [user current](./cli/user.md#user-current) | Show current user. | -| [user list](./cli/user.md#user-list) | List all users. | -| [user pat](./cli/user.md#user-pat) | Manage personal access tokens. | -| [user pat create](./cli/user.md#user-pat-create) | Create a new personal access token. | -| [user pat delete](./cli/user.md#user-pat-delete) | Delete a personal access token. | -| [user pat list](./cli/user.md#user-pat-list) | List all personal access tokens. | -| [user pat update](./cli/user.md#user-pat-update) | Update a personal access token (delete and recreate). | -| [user switch](./cli/user.md#user-switch) | Set current user. | +| Command | Description | +| ------------------------------------------------ | ------------------------------------------------------------------------------------- | +| [login](./cli/user.md#login) | Login to Tailor Platform. | +| [logout](./cli/user.md#logout) | Logout from Tailor Platform. | +| [auth](./cli/user.md#auth) | Authentication helpers for scripts and plugins. | +| [auth token](./cli/user.md#auth-token) | Print a valid Tailor Platform access token to stdout, refreshing it first if expired. | +| [user](./cli/user.md#user) | Manage Tailor Platform users. | +| [user current](./cli/user.md#user-current) | Show current user. | +| [user list](./cli/user.md#user-list) | List all users. | +| [user pat](./cli/user.md#user-pat) | Manage personal access tokens. | +| [user pat create](./cli/user.md#user-pat-create) | Create a new personal access token. | +| [user pat delete](./cli/user.md#user-pat-delete) | Delete a personal access token. | +| [user pat list](./cli/user.md#user-pat-list) | List all personal access tokens. | +| [user pat update](./cli/user.md#user-pat-update) | Update a personal access token (delete and recreate). | +| [user switch](./cli/user.md#user-switch) | Set current user. | ### [Organization Commands](./cli/organization.md) @@ -329,6 +389,15 @@ Commands for installing Tailor SDK agent skills. | [skills](./cli/skills.md#skills) | Manage Tailor SDK agent skills. | | [skills install](./cli/skills.md#skills-install) | Install the tailor agent skill from the installed SDK package. | +### [Plugin Commands](./cli/plugin.md) + +Discover and inspect CLI plugins (external `tailor-` executables). + +| Command | Description | +| ------------------------------------------ | ---------------------------------------------------------------------------------------- | +| [plugin](./cli/plugin.md#plugin) | Manage and inspect CLI plugins. | +| [plugin list](./cli/plugin.md#plugin-list) | List discovered plugins (executables named `-` on PATH or node_modules/.bin). | + ### [Completion](./cli/completion.md) Generate shell completion scripts for bash, zsh, and fish. diff --git a/packages/sdk/docs/cli-reference.template.md b/packages/sdk/docs/cli-reference.template.md index 9a532dfc1..4976bfd3f 100644 --- a/packages/sdk/docs/cli-reference.template.md +++ b/packages/sdk/docs/cli-reference.template.md @@ -91,6 +91,64 @@ Workspace ID resolution follows this priority order: 2. `TAILOR_PLATFORM_WORKSPACE_ID` environment variable 3. Profile specified via `--profile` option or `TAILOR_PLATFORM_PROFILE` +## CLI Plugins + +You can extend the CLI with external plugins, similar to `gh` extensions. When you run a command that +is not a built-in, the CLI looks for an executable named `tailor-` and runs it, forwarding the +remaining arguments: + +```bash +# Runs the `tailor-hello` executable with: world --loud +tailor hello world --loud +``` + +This also works under a built-in command group. The command path is joined with hyphens, so a plugin +nested under `tailordb` is named `tailor-tailordb-erd`: + +```bash +# Runs `tailor-tailordb-erd` with: export +tailor tailordb erd export +``` + +Resolution rules: + +- **Built-ins always win.** A plugin is only used when no built-in command matches. +- **A command that takes its own arguments is never replaced.** Plugin dispatch applies only to command + _groups_ (commands that just route to subcommands). A command that performs its own action — including + one that accepts a positional argument — always runs itself, so a plugin can never shadow an argument value. +- **Lookup order:** the project's `node_modules/.bin` (nearest first, walking up from the current + directory), then your `PATH`. So a plugin installed as a project dev-dependency takes precedence over a + globally installed one. + +Because resolution is based on `node_modules/.bin` and `PATH`, any package manager that populates +`node_modules/.bin` works for project-local plugins — npm, pnpm (its content-addressable store is +transparent here), Bun, and Yarn Classic. The exception is **Yarn Plug'n'Play**, which does not create a +`node_modules` directory: install such plugins globally so they resolve via `PATH`, or use Yarn's +`nodeLinker: node-modules` setting. + +Run `tailor plugin list` to see which plugins are discovered and where they resolve from. + +### Context passed to plugins + +Before running a plugin, the CLI injects the current Tailor Platform context as environment variables so +the plugin does not need to re-implement authentication or re-resolve the active workspace: + +| Variable | Description | +| ---------------------------------- | ------------------------------------------------------------------------ | +| `TAILOR_PLATFORM_TOKEN` | A valid access token (refreshed if needed). Omitted when not logged in. | +| `TAILOR_PLATFORM_URL` | The Tailor Platform endpoint in effect | +| `TAILOR_PLATFORM_OAUTH2_CLIENT_ID` | The OAuth2 client ID in effect, for plugins that run their own auth flow | +| `TAILOR_PLATFORM_WORKSPACE_ID` | The resolved workspace ID, when one can be determined | +| `TAILOR_PLATFORM_USER` | The active user (email when known), when logged in | +| `TAILOR_CONFIG_PATH` | Path to the resolved Tailor config file, when found | +| `TAILOR_VERSION` | The `tailor` version that invoked the plugin | +| `TAILOR_BIN` | Path to the `tailor` executable, for calling back into the CLI | + +The token, workspace ID, and user are best-effort: whatever the current context can resolve is injected, +and auth-free plugins still run when you are not logged in. A long-running plugin (or one started on its +own) can obtain a fresh token at any time with `tailor auth token`, which prints a valid access token to +stdout, refreshing it first if it has expired. + ## Commands {{politty:index}} diff --git a/packages/sdk/docs/cli/plugin.md b/packages/sdk/docs/cli/plugin.md new file mode 100644 index 000000000..b849e2bee --- /dev/null +++ b/packages/sdk/docs/cli/plugin.md @@ -0,0 +1,29 @@ +## plugin + +Manage and inspect CLI plugins. + +**Usage** + +``` +tailor plugin [command] +``` + +See [Global Options](../cli-reference.md#global-options) for options available to all commands. + +**Commands** + +| Command | Description | +| ----------------------------- | ---------------------------------------------------------------------------------------- | +| [`plugin list`](#plugin-list) | List discovered plugins (executables named `-` on PATH or node_modules/.bin). | + +### plugin list + +List discovered plugins (executables named `-` on PATH or node_modules/.bin). + +**Usage** + +``` +tailor plugin list +``` + +See [Global Options](../cli-reference.md#global-options) for options available to all commands. diff --git a/packages/sdk/docs/cli/plugin.template.md b/packages/sdk/docs/cli/plugin.template.md new file mode 100644 index 000000000..01b67cfdd --- /dev/null +++ b/packages/sdk/docs/cli/plugin.template.md @@ -0,0 +1,8 @@ +--- +politty: + index: + title: "Plugin Commands" + description: "Discover and inspect CLI plugins (external `tailor-` executables)." +--- + +{{politty:command:plugin}} diff --git a/packages/sdk/docs/cli/user.md b/packages/sdk/docs/cli/user.md index 55cff8722..aa09f238b 100644 --- a/packages/sdk/docs/cli/user.md +++ b/packages/sdk/docs/cli/user.md @@ -42,6 +42,42 @@ tailor logout See [Global Options](../cli-reference.md#global-options) for options available to all commands. +## auth + +Authentication helpers for scripts and plugins. + +**Usage** + +``` +tailor auth +``` + +See [Global Options](../cli-reference.md#global-options) for options available to all commands. + +**Commands** + +| Command | Description | +| --------------------------- | ------------------------------------------------------------------------------------- | +| [`auth token`](#auth-token) | Print a valid Tailor Platform access token to stdout, refreshing it first if expired. | + +### auth token + +Print a valid Tailor Platform access token to stdout, refreshing it first if expired. + +**Usage** + +``` +tailor auth token [options] +``` + +**Options** + +| Option | Alias | Description | Required | Default | Env | +| --------------------- | ----- | ----------------- | -------- | ------- | ------------------------- | +| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | + +See [Global Options](../cli-reference.md#global-options) for options available to all commands. + ## user Manage Tailor Platform users. diff --git a/packages/sdk/docs/cli/user.template.md b/packages/sdk/docs/cli/user.template.md index a78a0ffbd..2530b03ff 100644 --- a/packages/sdk/docs/cli/user.template.md +++ b/packages/sdk/docs/cli/user.template.md @@ -11,6 +11,7 @@ Commands for authentication and user management. {{politty:command:login}} {{politty:command:logout}} +{{politty:command:auth}} {{politty:command:user}} When no subcommand is provided, defaults to `list`. diff --git a/packages/sdk/src/cli/commands/auth/index.ts b/packages/sdk/src/cli/commands/auth/index.ts new file mode 100644 index 000000000..34d32fb14 --- /dev/null +++ b/packages/sdk/src/cli/commands/auth/index.ts @@ -0,0 +1,10 @@ +import { defineCommand } from "politty"; +import { tokenCommand } from "./token"; + +export const authCommand = defineCommand({ + name: "auth", + description: "Authentication helpers for scripts and plugins.", + subCommands: { + token: tokenCommand, + }, +}); diff --git a/packages/sdk/src/cli/commands/auth/token.ts b/packages/sdk/src/cli/commands/auth/token.ts new file mode 100644 index 000000000..f0af9cea1 --- /dev/null +++ b/packages/sdk/src/cli/commands/auth/token.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; +import { workspaceArgs } from "#/cli/shared/args"; +import { defineAppCommand } from "#/cli/shared/command"; +import { loadAccessToken } from "#/cli/shared/context"; +import { logger } from "#/cli/shared/logger"; + +export const tokenCommand = defineAppCommand({ + name: "token", + description: + "Print a valid Tailor Platform access token to stdout, refreshing it first if expired.", + args: z.strictObject({ + profile: workspaceArgs.profile, + }), + run: async ({ profile }) => { + const token = await loadAccessToken({ profile }); + logger.out(token); + }, +}); diff --git a/packages/sdk/src/cli/commands/plugin/index.ts b/packages/sdk/src/cli/commands/plugin/index.ts new file mode 100644 index 000000000..babbedf4a --- /dev/null +++ b/packages/sdk/src/cli/commands/plugin/index.ts @@ -0,0 +1,13 @@ +import { defineCommand, runCommand } from "politty"; +import { listCommand } from "./list"; + +export const pluginCommand = defineCommand({ + name: "plugin", + description: "Manage and inspect CLI plugins.", + subCommands: { + list: listCommand, + }, + async run() { + await runCommand(listCommand, []); + }, +}); diff --git a/packages/sdk/src/cli/commands/plugin/list.ts b/packages/sdk/src/cli/commands/plugin/list.ts new file mode 100644 index 000000000..3fe2133e5 --- /dev/null +++ b/packages/sdk/src/cli/commands/plugin/list.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; +import { BUILTIN_COMMAND_NAMES } from "#/cli/shared/builtin-commands"; +import { defineAppCommand } from "#/cli/shared/command"; +import { logger } from "#/cli/shared/logger"; +import { readPackageJson } from "#/cli/shared/package-json"; +import { listPlugins } from "#/cli/shared/plugin"; +import ml from "#/utils/multiline"; + +interface PluginListItem { + name: string; + source: "node_modules" | "path"; + path: string; + /** True when a builtin command of the same name shadows this plugin. */ + shadowed: boolean; +} + +export const listCommand = defineAppCommand({ + name: "list", + description: + "List discovered plugins (executables named `-` on PATH or node_modules/.bin).", + args: z.strictObject({}), + run: async () => { + const pkg = await readPackageJson(); + const cliName = Object.keys(pkg.bin ?? {})[0] || "tailor"; + const builtins = new Set(BUILTIN_COMMAND_NAMES); + + const plugins = listPlugins(cliName); + if (plugins.length === 0) { + logger.info(ml` + No plugins found. + Install an executable named "${cliName}-" on your PATH or in node_modules/.bin, + then run it with "${cliName} ". + `); + if (logger.jsonMode) { + logger.out([]); + } + return; + } + + const items: PluginListItem[] = plugins.map((plugin) => ({ + name: plugin.name, + source: plugin.source, + path: plugin.path, + shadowed: builtins.has(plugin.name), + })); + + logger.out(items); + + for (const item of items) { + if (item.shadowed) { + logger.warn( + `Plugin "${cliName}-${item.name}" is shadowed by the builtin "${cliName} ${item.name}" command and will not be dispatched.`, + ); + } + } + }, +}); diff --git a/packages/sdk/src/cli/docs.test.ts b/packages/sdk/src/cli/docs.test.ts index f163778bd..07f8ba3a9 100644 --- a/packages/sdk/src/cli/docs.test.ts +++ b/packages/sdk/src/cli/docs.test.ts @@ -35,7 +35,7 @@ const templateFiles: Record = { tpl: "docs/cli/query.template.md", }, "docs/cli/user.md": { - commands: ["login", "logout", "user"], + commands: ["login", "logout", "auth", "user"], tpl: "docs/cli/user.template.md", }, "docs/cli/organization.md": { @@ -86,6 +86,10 @@ const templateFiles: Record = { commands: ["skills"], tpl: "docs/cli/skills.template.md", }, + "docs/cli/plugin.md": { + commands: ["plugin"], + tpl: "docs/cli/plugin.template.md", + }, "docs/cli/completion.md": { commands: ["completion"], tpl: "docs/cli/completion.template.md", diff --git a/packages/sdk/src/cli/index.ts b/packages/sdk/src/cli/index.ts index 1f428949d..3a7978d81 100644 --- a/packages/sdk/src/cli/index.ts +++ b/packages/sdk/src/cli/index.ts @@ -4,6 +4,7 @@ import { defineCommand, runMain } from "politty"; import { withCompletionCommand } from "politty/completion"; import { z } from "zod"; import { apiCommand } from "./commands/api"; +import { authCommand } from "./commands/auth"; import { authconnectionCommand } from "./commands/authconnection"; import { crashReportCommand } from "./commands/crashreport"; import { deployCommand } from "./commands/deploy"; @@ -17,6 +18,7 @@ import { machineuserCommand } from "./commands/machineuser"; import { oauth2clientCommand } from "./commands/oauth2client"; import { openCommand } from "./commands/open"; import { organizationCommand } from "./commands/organization"; +import { pluginCommand } from "./commands/plugin"; import { profileCommand } from "./commands/profile"; import { removeCommand } from "./commands/remove"; import { secretCommand } from "./commands/secret"; @@ -35,6 +37,7 @@ import { commonArgs, isVerbose } from "./shared/args"; import { isCLIError } from "./shared/errors"; import { logger } from "./shared/logger"; import { readPackageJson } from "./shared/package-json"; +import { dispatchPlugin } from "./shared/plugin"; import { registerTsHook } from "./shared/register-ts-hook"; await registerTsHook(new URL("./ts-hook.mjs", import.meta.url)); @@ -53,8 +56,11 @@ export const mainCommand = withCompletionCommand( name: cliName, description: packageJson.description || "Tailor CLI for managing Tailor Platform SDK applications", + notes: `An unknown subcommand is dispatched to an external plugin executable named \`${cliName}-\` (found on your PATH or in node_modules/.bin), similar to \`gh\` extensions. +Run \`${cliName} plugin list\` to see which plugins are installed and where they resolve from.`, subCommands: { api: apiCommand, + auth: authCommand, authconnection: authconnectionCommand, crashreport: crashReportCommand, deploy: deployCommand, @@ -68,6 +74,7 @@ export const mainCommand = withCompletionCommand( oauth2client: oauth2clientCommand, open: openCommand, organization: organizationCommand, + plugin: pluginCommand, profile: profileCommand, query: queryCommand, remove: removeCommand, @@ -90,6 +97,16 @@ runMain(mainCommand, { // strip unknown keys globalArgs: z.object(commonArgs), displayErrors: false, + // CLI plugin dispatch: an unknown subcommand at any level execs the external + // `tailor--` binary, forwarding args and injecting context. + onUnknownSubcommand: ({ commandPath, name, args }) => + dispatchPlugin({ + commandPath, + name, + args, + cliName, + profile: process.env.TAILOR_PLATFORM_PROFILE, + }), cleanup: async ({ error }) => { if (error) { if (isCLIError(error)) { diff --git a/packages/sdk/src/cli/options.test.ts b/packages/sdk/src/cli/options.test.ts index 9899f89cb..3d4e72bdc 100644 --- a/packages/sdk/src/cli/options.test.ts +++ b/packages/sdk/src/cli/options.test.ts @@ -1,5 +1,6 @@ import { extractFields, isLazyCommand } from "politty"; import { describe, expect, test, vi } from "vitest"; +import { BUILTIN_COMMAND_NAMES } from "./shared/builtin-commands"; import { mainCommand } from "./index"; import type { AnyCommand, ExtractedFields, SubCommandValue } from "politty"; @@ -73,4 +74,14 @@ describe("CLI options", () => { await walkCommand(cmd, [name]); } }); + + test("keeps BUILTIN_COMMAND_NAMES in sync with the registered subcommands", () => { + // `plugin list` uses BUILTIN_COMMAND_NAMES (a leaf module, to avoid an + // import cycle) to flag shadowed plugins. Exclude the wrapper-added + // `completion` command and any internal `__`-prefixed commands. + const registered = Object.keys(mainCommand.subCommands ?? {}).filter( + (name) => !name.startsWith("__") && name !== "completion", + ); + expect(new Set(registered)).toEqual(new Set(BUILTIN_COMMAND_NAMES)); + }); }); diff --git a/packages/sdk/src/cli/shared/builtin-commands.ts b/packages/sdk/src/cli/shared/builtin-commands.ts new file mode 100644 index 000000000..9b4c219ee --- /dev/null +++ b/packages/sdk/src/cli/shared/builtin-commands.ts @@ -0,0 +1,39 @@ +/** + * Top-level builtin command names. A plugin named the same as one of these is + * shadowed by the builtin and can never be dispatched. + * + * This list is the single source of truth used by `plugin list` to flag + * shadowed plugins without importing the (cyclic) command tree. It is kept in + * sync with `main-command.ts` by a drift test in `options.test.ts`. + */ +export const BUILTIN_COMMAND_NAMES = [ + "api", + "auth", + "authconnection", + "crashreport", + "deploy", + "executor", + "function", + "generate", + "init", + "login", + "logout", + "machineuser", + "oauth2client", + "open", + "organization", + "plugin", + "profile", + "query", + "remove", + "secret", + "setup", + "show", + "skills", + "staticwebsite", + "tailordb", + "upgrade", + "user", + "workflow", + "workspace", +] as const; diff --git a/packages/sdk/src/cli/shared/client.ts b/packages/sdk/src/cli/shared/client.ts index 27d766499..60cb76927 100644 --- a/packages/sdk/src/cli/shared/client.ts +++ b/packages/sdk/src/cli/shared/client.ts @@ -19,7 +19,7 @@ import type { OperatorService } from "@tailor-platform/tailor-proto/service_pb"; export const platformBaseUrl = process.env.TAILOR_PLATFORM_URL ?? "https://api.tailor.tech"; -const oauth2ClientId = +export const oauth2ClientId = process.env.TAILOR_PLATFORM_OAUTH2_CLIENT_ID ?? "cpoc_0Iudir72fqSpqC6GQ58ri1cLAqcq5vJl"; const oauth2DiscoveryEndpoint = "/.well-known/oauth-authorization-server/oauth2/platform"; diff --git a/packages/sdk/src/cli/shared/plugin.test.ts b/packages/sdk/src/cli/shared/plugin.test.ts new file mode 100644 index 000000000..4e5c3942d --- /dev/null +++ b/packages/sdk/src/cli/shared/plugin.test.ts @@ -0,0 +1,280 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "pathe"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { oauth2ClientId, platformBaseUrl } from "./client"; +import { dispatchPlugin, listPlugins, resolvePlugin } from "./plugin"; + +const contextMocks = vi.hoisted(() => ({ + loadAccessToken: vi.fn(), + loadWorkspaceId: vi.fn(), + loadConfigPath: vi.fn(), + readPlatformConfig: vi.fn(), +})); + +vi.mock("./context", () => contextMocks); + +const isWindows = process.platform === "win32"; +const CLI = "tailor-sdk"; + +/** + * Create an executable fake plugin file. + * @param dir - Directory to create the file in + * @param name - File name + * @returns Absolute path to the created file + */ +function writeExecutable(dir: string, name: string): string { + fs.mkdirSync(dir, { recursive: true }); + const full = path.join(dir, name); + fs.writeFileSync(full, "#!/bin/sh\necho hi\n"); + if (!isWindows) fs.chmodSync(full, 0o755); + return full; +} + +describe.skipIf(isWindows)("resolvePlugin / listPlugins", () => { + let tempDir: string; + let originalCwd: string; + let originalPath: string | undefined; + + beforeEach(() => { + tempDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "tailor-plugin-"))); + originalCwd = process.cwd(); + originalPath = process.env.PATH; + }); + + afterEach(() => { + process.chdir(originalCwd); + process.env.PATH = originalPath; + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + test("resolves a plugin from the nearest node_modules/.bin", () => { + const project = path.join(tempDir, "project"); + const binDir = path.join(project, "node_modules", ".bin"); + const exe = writeExecutable(binDir, `${CLI}-hello`); + process.chdir(project); + process.env.PATH = ""; + + const resolved = resolvePlugin("hello", CLI); + expect(resolved).toEqual({ name: "hello", path: exe, source: "node_modules" }); + }); + + test("resolves a plugin from PATH when not in node_modules", () => { + const project = path.join(tempDir, "project"); + fs.mkdirSync(project, { recursive: true }); + const pathDir = path.join(tempDir, "bin"); + const exe = writeExecutable(pathDir, `${CLI}-frompath`); + process.chdir(project); + process.env.PATH = pathDir; + + const resolved = resolvePlugin("frompath", CLI); + expect(resolved).toEqual({ name: "frompath", path: exe, source: "path" }); + }); + + test("prefers node_modules/.bin over PATH on collision", () => { + const project = path.join(tempDir, "project"); + const binDir = path.join(project, "node_modules", ".bin"); + const nmExe = writeExecutable(binDir, `${CLI}-dup`); + const pathDir = path.join(tempDir, "bin"); + writeExecutable(pathDir, `${CLI}-dup`); + process.chdir(project); + process.env.PATH = pathDir; + + const resolved = resolvePlugin("dup", CLI); + expect(resolved?.source).toBe("node_modules"); + expect(resolved?.path).toBe(nmExe); + }); + + test("returns null when no matching plugin exists", () => { + const project = path.join(tempDir, "project"); + fs.mkdirSync(project, { recursive: true }); + process.chdir(project); + process.env.PATH = ""; + + expect(resolvePlugin("missing", CLI)).toBeNull(); + }); + + test("lists discovered plugins, deduping by name with node_modules precedence", () => { + const project = path.join(tempDir, "project"); + const binDir = path.join(project, "node_modules", ".bin"); + writeExecutable(binDir, `${CLI}-alpha`); + writeExecutable(binDir, `${CLI}-dup`); + const pathDir = path.join(tempDir, "bin"); + writeExecutable(pathDir, `${CLI}-beta`); + writeExecutable(pathDir, `${CLI}-dup`); + // A non-plugin executable must be ignored. + writeExecutable(pathDir, "unrelated-tool"); + process.chdir(project); + process.env.PATH = pathDir; + + const plugins = listPlugins(CLI); + const byName = Object.fromEntries(plugins.map((p) => [p.name, p])); + + expect(Object.keys(byName).toSorted()).toEqual(["alpha", "beta", "dup"]); + expect(byName.alpha?.source).toBe("node_modules"); + expect(byName.beta?.source).toBe("path"); + expect(byName.dup?.source).toBe("node_modules"); + }); +}); + +/** + * Write an executable node plugin that dumps its env and forwarded argv to + * `outFile` as JSON, then exits with `exitCode`. + * @param dir - Directory to create the plugin in + * @param name - Plugin file name + * @param outFile - Path the plugin writes its captured context to + * @param exitCode - Exit code the plugin terminates with + * @returns Absolute path to the created plugin + */ +function writeCapturePlugin(dir: string, name: string, outFile: string, exitCode = 0): string { + fs.mkdirSync(dir, { recursive: true }); + const full = path.join(dir, name); + const script = `#!${process.execPath} +const fs = require("node:fs"); +fs.writeFileSync(${JSON.stringify(outFile)}, JSON.stringify({ env: process.env, argv: process.argv.slice(2) })); +process.exit(${exitCode}); +`; + fs.writeFileSync(full, script); + fs.chmodSync(full, 0o755); + return full; +} + +// Context vars that may be present in the ambient environment; cleared so the +// dispatched child only sees what buildPluginEnv injects. +const CONTEXT_ENV_KEYS = [ + "TAILOR_PLATFORM_TOKEN", + "TAILOR_PLATFORM_WORKSPACE_ID", + "TAILOR_PLATFORM_USER", + "TAILOR_CONFIG_PATH", + "TAILOR_PLATFORM_PROFILE", +] as const; + +describe.skipIf(isWindows)("dispatchPlugin", () => { + let tempDir: string; + let originalCwd: string; + let originalPath: string | undefined; + let originalContextEnv: Record; + let outFile: string; + + beforeEach(() => { + tempDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "tailor-dispatch-"))); + originalCwd = process.cwd(); + originalPath = process.env.PATH; + originalContextEnv = Object.fromEntries(CONTEXT_ENV_KEYS.map((k) => [k, process.env[k]])); + for (const k of CONTEXT_ENV_KEYS) delete process.env[k]; + outFile = path.join(tempDir, "capture.json"); + + contextMocks.loadAccessToken.mockResolvedValue("tok-123"); + contextMocks.loadWorkspaceId.mockResolvedValue("ws-456"); + contextMocks.loadConfigPath.mockReturnValue("/proj/tailor.config.ts"); + contextMocks.readPlatformConfig.mockResolvedValue({ + users: { u1: { email: "me@example.com" } }, + profiles: {}, + current_user: "u1", + }); + }); + + afterEach(() => { + process.chdir(originalCwd); + process.env.PATH = originalPath; + for (const [k, v] of Object.entries(originalContextEnv)) { + if (v === undefined) delete process.env[k]; + else process.env[k] = v; + } + vi.clearAllMocks(); + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + /** + * Read the JSON context captured by the dispatched plugin. + * @returns The plugin's env and forwarded argv + */ + function readCapture(): { env: Record; argv: string[] } { + return JSON.parse(fs.readFileSync(outFile, "utf-8")); + } + + test("forwards args and injects the resolved platform context", async () => { + const project = path.join(tempDir, "project"); + writeCapturePlugin(path.join(project, "node_modules", ".bin"), `${CLI}-hello`, outFile); + process.chdir(project); + process.env.PATH = ""; + + const code = await dispatchPlugin({ + name: "hello", + args: ["world", "--loud"], + cliName: CLI, + }); + + expect(code).toBe(0); + const { env, argv } = readCapture(); + expect(argv).toEqual(["world", "--loud"]); + expect(env.TAILOR_PLATFORM_URL).toBe(platformBaseUrl); + expect(env.TAILOR_PLATFORM_OAUTH2_CLIENT_ID).toBe(oauth2ClientId); + expect(env.TAILOR_PLATFORM_TOKEN).toBe("tok-123"); + expect(env.TAILOR_PLATFORM_WORKSPACE_ID).toBe("ws-456"); + expect(env.TAILOR_PLATFORM_USER).toBe("me@example.com"); + expect(env.TAILOR_CONFIG_PATH).toBe("/proj/tailor.config.ts"); + expect(env.TAILOR_BIN).toBeTruthy(); + expect(env.TAILOR_VERSION).toBeTruthy(); + }); + + test("omits best-effort context when it cannot be resolved but still dispatches", async () => { + contextMocks.loadAccessToken.mockRejectedValue(new Error("not logged in")); + contextMocks.loadWorkspaceId.mockRejectedValue(new Error("no workspace")); + contextMocks.readPlatformConfig.mockResolvedValue({ + users: {}, + profiles: {}, + current_user: null, + }); + + const project = path.join(tempDir, "project"); + writeCapturePlugin(path.join(project, "node_modules", ".bin"), `${CLI}-hello`, outFile); + process.chdir(project); + process.env.PATH = ""; + + const code = await dispatchPlugin({ name: "hello", args: [], cliName: CLI }); + + expect(code).toBe(0); + const { env } = readCapture(); + expect(env.TAILOR_PLATFORM_TOKEN).toBeUndefined(); + expect(env.TAILOR_PLATFORM_WORKSPACE_ID).toBeUndefined(); + expect(env.TAILOR_PLATFORM_USER).toBeUndefined(); + expect(env.TAILOR_PLATFORM_URL).toBe(platformBaseUrl); + }); + + test("builds the plugin slug from the command path for nested dispatch", async () => { + const project = path.join(tempDir, "project"); + writeCapturePlugin(path.join(project, "node_modules", ".bin"), `${CLI}-tailordb-erd`, outFile); + process.chdir(project); + process.env.PATH = ""; + + const code = await dispatchPlugin({ + commandPath: ["tailordb"], + name: "erd", + args: ["export"], + cliName: CLI, + }); + + expect(code).toBe(0); + expect(readCapture().argv).toEqual(["export"]); + }); + + test("propagates a non-zero exit code", async () => { + const project = path.join(tempDir, "project"); + writeCapturePlugin(path.join(project, "node_modules", ".bin"), `${CLI}-boom`, outFile, 3); + process.chdir(project); + process.env.PATH = ""; + + const code = await dispatchPlugin({ name: "boom", args: [], cliName: CLI }); + expect(code).toBe(3); + }); + + test("returns undefined when no matching plugin exists", async () => { + const project = path.join(tempDir, "project"); + fs.mkdirSync(project, { recursive: true }); + process.chdir(project); + process.env.PATH = ""; + + expect(await dispatchPlugin({ name: "missing", args: [], cliName: CLI })).toBeUndefined(); + }); +}); diff --git a/packages/sdk/src/cli/shared/plugin.ts b/packages/sdk/src/cli/shared/plugin.ts new file mode 100644 index 000000000..ebc1870b5 --- /dev/null +++ b/packages/sdk/src/cli/shared/plugin.ts @@ -0,0 +1,342 @@ +import { spawn } from "node:child_process"; +import { accessSync, constants, readdirSync } from "node:fs"; +import * as os from "node:os"; +import * as path from "pathe"; +import { oauth2ClientId, platformBaseUrl } from "./client"; +import { loadAccessToken, loadConfigPath, loadWorkspaceId, readPlatformConfig } from "./context"; +import { logger } from "./logger"; +import { readPackageJson } from "./package-json"; + +/** + * A plugin discovered on the filesystem. `tailor ` dispatches to the + * external `tailor-` executable. + */ +export interface DiscoveredPlugin { + /** Plugin name (the part after the `-` prefix). */ + name: string; + /** Absolute path to the executable. */ + path: string; + /** Where the executable was found. */ + source: "node_modules" | "path"; +} + +/** PATH entry separator (`;` on Windows, `:` elsewhere). */ +const pathDelimiter = process.platform === "win32" ? ";" : ":"; + +/** + * Yield `start` and each ancestor directory up to the filesystem root. + * @param start - Directory to start from + * @yields Each ancestor directory, starting with `start` + */ +function* ancestorDirs(start: string): Generator { + let dir = path.resolve(start); + // Walk up until `dirname` stops changing (the filesystem root), yielding the + // root on the final iteration. + let parent = path.dirname(dir); + while (parent !== dir) { + yield dir; + dir = parent; + parent = path.dirname(dir); + } + yield dir; +} + +/** + * Check whether a path exists and is executable (X_OK on POSIX, existence on Windows). + * @param filePath - Path to check + * @returns Whether the path is an executable file + */ +function isExecutable(filePath: string): boolean { + try { + accessSync(filePath, process.platform === "win32" ? constants.F_OK : constants.X_OK); + return true; + } catch { + return false; + } +} + +/** + * Candidate file names for a plugin binary, accounting for Windows extensions. + * @param binName - Base binary name (e.g. `tailor-foo`) + * @returns Ordered candidate file names + */ +function binaryCandidates(binName: string): string[] { + if (process.platform !== "win32") { + return [binName]; + } + const exts = (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.PS1") + .split(";") + .map((e) => e.trim()) + .filter(Boolean); + // Bare name first (shebang scripts), then PATHEXT variants. + return [binName, ...exts.map((ext) => `${binName}${ext}`)]; +} + +/** + * Directories on the user's PATH. + * @returns PATH entries + */ +function pathDirs(): string[] { + return (process.env.PATH ?? "").split(pathDelimiter).filter(Boolean); +} + +/** + * Nearest `node_modules/.bin` directories, walking up from the current working + * directory. Project-local bins take precedence over those higher in the tree. + * @returns Ordered `node_modules/.bin` directories + */ +function nodeModulesBinDirs(): string[] { + const dirs: string[] = []; + for (const dir of ancestorDirs(process.cwd())) { + dirs.push(path.join(dir, "node_modules", ".bin")); + } + return dirs; +} + +/** + * Find the first existing executable for the given base name across a set of + * directories. + * @param dirs - Directories to search + * @param binName - Base binary name + * @returns Absolute path, or undefined when not found + */ +function findExecutableIn(dirs: string[], binName: string): string | undefined { + const candidates = binaryCandidates(binName); + for (const dir of dirs) { + for (const candidate of candidates) { + const full = path.join(dir, candidate); + if (isExecutable(full)) return full; + } + } + return undefined; +} + +/** + * Resolve a plugin executable by name. + * Search order: project-local `node_modules/.bin` (nearest first), then PATH. + * @param name - Plugin name (without the `-` prefix) + * @param cliName - The host CLI name (e.g. `tailor`) + * @returns The discovered plugin, or null when not found + */ +export function resolvePlugin(name: string, cliName: string): DiscoveredPlugin | null { + const binName = `${cliName}-${name}`; + + const fromNodeModules = findExecutableIn(nodeModulesBinDirs(), binName); + if (fromNodeModules) { + return { name, path: fromNodeModules, source: "node_modules" }; + } + + const fromPath = findExecutableIn(pathDirs(), binName); + if (fromPath) { + return { name, path: fromPath, source: "path" }; + } + + return null; +} + +/** + * Strip a known binary extension from a file name (Windows). + * @param fileName - File name + * @returns File name without a recognized executable extension + */ +function stripBinExt(fileName: string): string { + if (process.platform !== "win32") return fileName; + const ext = path.extname(fileName); + return ext ? fileName.slice(0, -ext.length) : fileName; +} + +/** + * Discover all plugins reachable from `node_modules/.bin` and PATH. + * Earlier entries win on name collisions (project-local over PATH). + * @param cliName - The host CLI name (e.g. `tailor`) + * @returns Discovered plugins, deduped by name + */ +export function listPlugins(cliName: string): DiscoveredPlugin[] { + const prefix = `${cliName}-`; + const seen = new Map(); + + const scan = (dirs: string[], source: DiscoveredPlugin["source"]) => { + for (const dir of dirs) { + let entries: string[]; + try { + entries = readdirSync(dir); + } catch { + continue; + } + for (const entry of entries) { + const base = stripBinExt(entry); + if (!base.startsWith(prefix) || base.length === prefix.length) continue; + const name = base.slice(prefix.length); + if (seen.has(name)) continue; + const full = path.join(dir, entry); + if (!isExecutable(full)) continue; + seen.set(name, { name, path: full, source }); + } + } + }; + + scan(nodeModulesBinDirs(), "node_modules"); + scan(pathDirs(), "path"); + + return [...seen.values()]; +} + +/** + * Options for {@link buildPluginEnv}. + */ +interface PluginContextOptions { + /** Active profile used to resolve the workspace, user, and token. */ + profile?: string | undefined; +} + +/** + * Resolve the active user identifier, preferring the stored email over the + * internal user key. + * @param profile - Active profile name, if any + * @returns The resolved user (email when known), or undefined when not logged in + */ +async function resolveActiveUser(profile?: string): Promise { + const config = await readPlatformConfig(); + const user = profile ? config.profiles[profile]?.user : config.current_user; + if (!user) return undefined; + return config.users[user]?.email ?? user; +} + +/** + * Build the environment variables injected into a dispatched plugin. + * Workspace, user, and token are resolved best-effort: whichever the current + * context can provide is injected, and the rest are omitted so auth-free + * plugins still run. + * @param options - Plugin context options + * @returns Environment variables to merge into the child process env + */ +async function buildPluginEnv(options: PluginContextOptions = {}): Promise> { + const { profile } = options; + const env: Record = { + TAILOR_PLATFORM_URL: platformBaseUrl, + TAILOR_PLATFORM_OAUTH2_CLIENT_ID: oauth2ClientId, + }; + + const binPath = process.argv[1]; + if (binPath) { + env.TAILOR_BIN = binPath; + } + + try { + const { version } = await readPackageJson(); + if (version) env.TAILOR_VERSION = version; + } catch { + // Version is informational; skip when unavailable. + } + + const configPath = loadConfigPath(); + if (configPath) { + env.TAILOR_CONFIG_PATH = configPath; + } + + try { + env.TAILOR_PLATFORM_WORKSPACE_ID = await loadWorkspaceId({ profile }); + } catch { + // No workspace resolvable; plugins that need it surface their own error. + } + + try { + const user = await resolveActiveUser(profile); + if (user) env.TAILOR_PLATFORM_USER = user; + } catch { + // User context is best-effort; skip when the config is unreadable. + } + + try { + const token = await loadAccessToken({ profile }); + if (token) env.TAILOR_PLATFORM_TOKEN = token; + } catch { + // Not logged in (or no token): dispatch without a token so auth-free + // plugins still work. Auth-requiring plugins surface their own error + // (e.g. via `tailor auth token`). + } + + return env; +} + +/** + * Map an exit signal to a conventional exit code (128 + signal number). + * @param signal - Signal name + * @returns Exit code + */ +function signalToExitCode(signal: NodeJS.Signals): number { + const num = os.constants.signals[signal]; + return typeof num === "number" ? 128 + num : 1; +} + +/** + * Build the command + args for spawning a plugin, handling Windows shebang + * scripts via `sh` (following the `gh` extension dispatcher). + * @param pluginPath - Resolved plugin executable path + * @param args - Args to forward to the plugin + * @returns Spawn command, args, and whether a shell is required + */ +function buildSpawnTarget( + pluginPath: string, + args: readonly string[], +): { command: string; args: string[]; shell: boolean } { + if (process.platform !== "win32") { + return { command: pluginPath, args: [...args], shell: false }; + } + + const ext = path.extname(pluginPath).toLowerCase(); + if (ext === ".exe") { + return { command: pluginPath, args: [...args], shell: false }; + } + if (ext === ".cmd" || ext === ".bat") { + // .cmd/.bat must run through the shell on Windows. + return { command: pluginPath, args: [...args], shell: true }; + } + + // Extension-less shebang script: dispatch through `sh` (Git for Windows). + const sh = findExecutableIn(pathDirs(), "sh") ?? "sh"; + return { command: sh, args: ["-c", 'command "$@"', "--", pluginPath, ...args], shell: false }; +} + +/** + * Resolve and execute a plugin, forwarding stdio and propagating its exit code. + * @param params - Dispatch parameters + * @param params.name - Plugin name (without the `-` prefix) + * @param params.args - Args to forward to the plugin + * @param params.cliName - The host CLI name + * @param params.profile - Active profile name, if any + * @returns The plugin's exit code, or undefined when no matching plugin exists + */ +export async function dispatchPlugin(params: { + name: string; + args: readonly string[]; + cliName: string; + commandPath?: readonly string[] | undefined; + profile?: string | undefined; +}): Promise { + // Build the plugin slug from the known command path plus the unknown name, + // so `tailor tailordb erd` resolves `tailor-tailordb-erd`. + const slug = [...(params.commandPath ?? []), params.name].join("-"); + const plugin = resolvePlugin(slug, params.cliName); + if (!plugin) { + return undefined; + } + + const env = { ...process.env, ...(await buildPluginEnv({ profile: params.profile })) }; + const { command, args, shell } = buildSpawnTarget(plugin.path, params.args); + + return await new Promise((resolve) => { + const child = spawn(command, args, { stdio: "inherit", env, shell }); + child.on("error", (error) => { + logger.error(`Failed to run plugin "${params.cliName}-${slug}": ${error.message}`); + resolve(1); + }); + child.on("close", (code, signal) => { + if (signal) { + resolve(signalToExitCode(signal)); + } else { + resolve(code ?? 0); + } + }); + }); +} diff --git a/packages/sdk/src/cli/shared/readonly-guard.test.ts b/packages/sdk/src/cli/shared/readonly-guard.test.ts index 4c2408cfb..d680c032c 100644 --- a/packages/sdk/src/cli/shared/readonly-guard.test.ts +++ b/packages/sdk/src/cli/shared/readonly-guard.test.ts @@ -37,6 +37,8 @@ const READ_OR_LOCAL_COMMAND_PATHS = new Set([ // (including Create*/Update*/Delete*) and must guard. "api/inspect.ts", "api/list.ts", + // Auth token retrieval (reads/refreshes local credentials only) + "auth/token.ts", // Auth connections (read-only) "authconnection/index.ts", "authconnection/list.ts", @@ -80,6 +82,9 @@ const READ_OR_LOCAL_COMMAND_PATHS = new Set([ "organization/folder/index.ts", "organization/folder/get.ts", "organization/folder/list.ts", + // Plugin discovery (local filesystem listing only) + "plugin/index.ts", + "plugin/list.ts", // Profile management (local config only, never platform state) "profile/index.ts", "profile/create.ts", From 2fb79c623481466430ab44612fd58fc3d1d60021 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sat, 4 Jul 2026 06:01:02 +0900 Subject: [PATCH 426/618] fix(cli): resolve plugin platform context and harden Windows dispatch buildPluginEnv now resolves the active profile's platform URL and OAuth2 client ID (via loadPlatformClientConfig) so a plugin dispatched under a profile targeting a non-default platform receives a consistent URL/token/workspace instead of the default endpoint. Also hardens Windows executable handling: buildSpawnTarget runs .com like a native .exe and dispatches .ps1 through PowerShell, isExecutable rejects files whose extension is not in PATHEXT (so data files are not listed as plugins), and the PATHEXT fallback includes .COM. --- packages/sdk/src/cli/shared/plugin.test.ts | 25 ++++++++ packages/sdk/src/cli/shared/plugin.ts | 66 ++++++++++++++++++---- 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/packages/sdk/src/cli/shared/plugin.test.ts b/packages/sdk/src/cli/shared/plugin.test.ts index 08f9ef600..625129995 100644 --- a/packages/sdk/src/cli/shared/plugin.test.ts +++ b/packages/sdk/src/cli/shared/plugin.test.ts @@ -9,6 +9,7 @@ const contextMocks = vi.hoisted(() => ({ loadAccessToken: vi.fn(), loadWorkspaceId: vi.fn(), loadConfigPath: vi.fn(), + loadPlatformClientConfig: vi.fn(), readPlatformConfig: vi.fn(), })); @@ -167,6 +168,7 @@ describe.skipIf(isWindows)("dispatchPlugin", () => { contextMocks.loadAccessToken.mockResolvedValue("tok-123"); contextMocks.loadWorkspaceId.mockResolvedValue("ws-456"); contextMocks.loadConfigPath.mockReturnValue("/proj/tailor.config.ts"); + contextMocks.loadPlatformClientConfig.mockResolvedValue(undefined); contextMocks.readPlatformConfig.mockResolvedValue({ users: { u1: { email: "me@example.com" } }, profiles: {}, @@ -218,6 +220,29 @@ describe.skipIf(isWindows)("dispatchPlugin", () => { expect(env.TAILOR_VERSION).toBeTruthy(); }); + test("injects the active profile's platform URL and OAuth client", async () => { + contextMocks.loadPlatformClientConfig.mockResolvedValue({ + platformUrl: "https://api.staging.example.com", + oauth2ClientId: "cpoc_staging", + }); + const project = path.join(tempDir, "project"); + writeCapturePlugin(path.join(project, "node_modules", ".bin"), `${CLI}-hello`, outFile); + process.chdir(project); + process.env.PATH = ""; + + const code = await dispatchPlugin({ + name: "hello", + args: [], + cliName: CLI, + profile: "staging", + }); + + expect(code).toBe(0); + const { env } = readCapture(); + expect(env.TAILOR_PLATFORM_URL).toBe("https://api.staging.example.com"); + expect(env.TAILOR_PLATFORM_OAUTH2_CLIENT_ID).toBe("cpoc_staging"); + }); + test("omits best-effort context when it cannot be resolved but still dispatches", async () => { contextMocks.loadAccessToken.mockRejectedValue(new Error("not logged in")); contextMocks.loadWorkspaceId.mockRejectedValue(new Error("no workspace")); diff --git a/packages/sdk/src/cli/shared/plugin.ts b/packages/sdk/src/cli/shared/plugin.ts index 1171d1b84..d5a82f798 100644 --- a/packages/sdk/src/cli/shared/plugin.ts +++ b/packages/sdk/src/cli/shared/plugin.ts @@ -2,8 +2,14 @@ import { spawn } from "node:child_process"; import { accessSync, constants, readdirSync } from "node:fs"; import * as os from "node:os"; import * as path from "pathe"; -import { getOAuth2ClientId, getPlatformBaseUrl } from "./client"; -import { loadAccessToken, loadConfigPath, loadWorkspaceId, readPlatformConfig } from "./context"; +import { getOAuth2ClientId, getPlatformBaseUrl, type PlatformClientConfig } from "./client"; +import { + loadAccessToken, + loadConfigPath, + loadPlatformClientConfig, + loadWorkspaceId, + readPlatformConfig, +} from "./context"; import { logger } from "./logger"; import { readPackageJson } from "./package-json"; @@ -41,18 +47,38 @@ function* ancestorDirs(start: string): Generator { yield dir; } +/** Fallback executable extensions when `PATHEXT` is unset (Windows). */ +const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD;.PS1"; + +/** + * Lowercased executable extensions from `PATHEXT` (Windows). + * @returns Recognized executable extensions (e.g. `.exe`, `.cmd`) + */ +function windowsExecutableExts(): string[] { + return (process.env.PATHEXT ?? DEFAULT_PATHEXT) + .split(";") + .map((e) => e.trim().toLowerCase()) + .filter(Boolean); +} + /** - * Check whether a path exists and is executable (X_OK on POSIX, existence on Windows). + * Check whether a path exists and is runnable: X_OK on POSIX; on Windows, the + * file must exist and either be extension-less (shebang script) or carry a + * `PATHEXT` extension, so plain data files are not mistaken for plugins. * @param filePath - Path to check * @returns Whether the path is an executable file */ function isExecutable(filePath: string): boolean { try { accessSync(filePath, process.platform === "win32" ? constants.F_OK : constants.X_OK); - return true; } catch { return false; } + if (process.platform === "win32") { + const ext = path.extname(filePath).toLowerCase(); + return ext === "" || windowsExecutableExts().includes(ext); + } + return true; } /** @@ -64,12 +90,8 @@ function binaryCandidates(binName: string): string[] { if (process.platform !== "win32") { return [binName]; } - const exts = (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.PS1") - .split(";") - .map((e) => e.trim()) - .filter(Boolean); // Bare name first (shebang scripts), then PATHEXT variants. - return [binName, ...exts.map((ext) => `${binName}${ext}`)]; + return [binName, ...windowsExecutableExts().map((ext) => `${binName}${ext}`)]; } /** @@ -212,9 +234,17 @@ async function resolveActiveUser(profile?: string): Promise */ async function buildPluginEnv(options: PluginContextOptions = {}): Promise> { const { profile } = options; + // Resolve the active profile's platform settings so the injected URL and + // OAuth client match the profile the token and workspace belong to. + let platformConfig: PlatformClientConfig | undefined; + try { + platformConfig = await loadPlatformClientConfig({ profile, allowMissingProfile: true }); + } catch { + // Fall back to env/default platform settings when the profile is unreadable. + } const env: Record = { - TAILOR_PLATFORM_URL: getPlatformBaseUrl(), - TAILOR_PLATFORM_OAUTH2_CLIENT_ID: getOAuth2ClientId(), + TAILOR_PLATFORM_URL: getPlatformBaseUrl(platformConfig), + TAILOR_PLATFORM_OAUTH2_CLIENT_ID: getOAuth2ClientId(platformConfig), }; const binPath = process.argv[1]; @@ -285,13 +315,25 @@ function buildSpawnTarget( } const ext = path.extname(pluginPath).toLowerCase(); - if (ext === ".exe") { + if (ext === ".exe" || ext === ".com") { return { command: pluginPath, args: [...args], shell: false }; } if (ext === ".cmd" || ext === ".bat") { // .cmd/.bat must run through the shell on Windows. return { command: pluginPath, args: [...args], shell: true }; } + if (ext === ".ps1") { + // PowerShell scripts are not directly executable via spawn. + const pwsh = + findExecutableIn(pathDirs(), "pwsh") ?? + findExecutableIn(pathDirs(), "powershell") ?? + "powershell"; + return { + command: pwsh, + args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", pluginPath, ...args], + shell: false, + }; + } // Extension-less shebang script: dispatch through `sh` (Git for Windows). const sh = findExecutableIn(pathDirs(), "sh") ?? "sh"; From 7fed5b095c5b418a186c434252df4cbb5df42afc Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 6 Jul 2026 10:20:53 +0900 Subject: [PATCH 427/618] fix(cli): guard plugin name traversal and cover Windows dispatch in CI resolvePlugin now rejects plugin names containing path separators or NUL before joining them into a filesystem path, so a name like `../evil` cannot escape the `-` prefix and resolve an unintended executable. Also enables the plugin test suites on Windows (cross-platform capture fixture using a `.cmd` wrapper) and adds a dedicated `Plugin tests (Windows)` CI job, since the main SDK unit suite only runs on Linux and the Windows PATHEXT/`.cmd`/`.ps1` dispatch branches were previously untested. Corrects the auth/token readonly-guard comment to note it may refresh via the OAuth server and persist tokens locally, without mutating workspace/platform state. --- .github/workflows/test.yml | 37 ++++++++++++++++++- packages/sdk/src/cli/shared/plugin.test.ts | 34 ++++++++++++++--- packages/sdk/src/cli/shared/plugin.ts | 5 +++ .../sdk/src/cli/shared/readonly-guard.test.ts | 3 +- 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd4511b0b..2fcb60cad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: "false" - - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 id: filter with: filters: | @@ -77,9 +77,37 @@ jobs: - name: Run tests run: pnpm -r run test --project 'unit*' + plugin-windows: + needs: changes + if: needs.changes.outputs.should_run == 'true' + runs-on: windows-latest + name: Plugin tests (Windows) + timeout-minutes: 15 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Install deps + uses: ./.github/actions/install-deps + + - name: Build packages + run: pnpm -r --filter '!@tailor-platform/sdk' run build + shell: bash + + # Windows-specific PATHEXT resolution and .cmd/.ps1 spawn branches are + # only exercised here; the main unit suite runs on Linux. + - name: Run plugin tests + run: pnpm --filter @tailor-platform/sdk exec vitest run --project 'unit*' src/cli/shared/plugin.test.ts + shell: bash + test-summary: name: test-summary # required status check context (ruleset status_check_main) — keep equal to the job id - needs: [changes, test] + needs: [changes, test, plugin-windows] if: >- always() && (github.event.action != 'synchronize' || github.event.sender.login != 'tailor-pr-trigger[bot]') @@ -92,6 +120,7 @@ jobs: CHANGES_RESULT: ${{ needs.changes.result }} SHOULD_RUN: ${{ needs.changes.outputs.should_run }} TEST_RESULT: ${{ needs.test.result }} + PLUGIN_WINDOWS_RESULT: ${{ needs.plugin-windows.result }} run: | # Fail closed: if the paths-filter job itself failed, should_run is # empty and must not be mistaken for "no relevant changes". @@ -107,4 +136,8 @@ jobs: echo "Test jobs failed" exit 1 fi + if [[ "$PLUGIN_WINDOWS_RESULT" != "success" ]]; then + echo "Plugin tests (Windows) failed" + exit 1 + fi echo "All test jobs succeeded" diff --git a/packages/sdk/src/cli/shared/plugin.test.ts b/packages/sdk/src/cli/shared/plugin.test.ts index 625129995..1059915a6 100644 --- a/packages/sdk/src/cli/shared/plugin.test.ts +++ b/packages/sdk/src/cli/shared/plugin.test.ts @@ -32,7 +32,7 @@ function writeExecutable(dir: string, name: string): string { return full; } -describe.skipIf(isWindows)("resolvePlugin / listPlugins", () => { +describe("resolvePlugin / listPlugins", () => { let tempDir: string; let originalCwd: string; let originalPath: string | undefined; @@ -95,6 +95,19 @@ describe.skipIf(isWindows)("resolvePlugin / listPlugins", () => { expect(resolvePlugin("missing", CLI)).toBeNull(); }); + test("rejects names containing path separators or NUL", () => { + // A traversal-shaped name must never resolve, even if such a file exists. + const binDir = path.join(tempDir, "project", "node_modules", ".bin"); + writeExecutable(binDir, `${CLI}-evil`); + process.chdir(path.join(tempDir, "project")); + process.env.PATH = ""; + + expect(resolvePlugin("../evil", CLI)).toBeNull(); + expect(resolvePlugin("a/b", CLI)).toBeNull(); + expect(resolvePlugin("a\\b", CLI)).toBeNull(); + expect(resolvePlugin("a\0b", CLI)).toBeNull(); + }); + test("lists discovered plugins, deduping by name with node_modules precedence", () => { const project = path.join(tempDir, "project"); const binDir = path.join(project, "node_modules", ".bin"); @@ -129,13 +142,22 @@ describe.skipIf(isWindows)("resolvePlugin / listPlugins", () => { */ function writeCapturePlugin(dir: string, name: string, outFile: string, exitCode = 0): string { fs.mkdirSync(dir, { recursive: true }); - const full = path.join(dir, name); - const script = `#!${process.execPath} -const fs = require("node:fs"); + const jsBody = `const fs = require("node:fs"); fs.writeFileSync(${JSON.stringify(outFile)}, JSON.stringify({ env: process.env, argv: process.argv.slice(2) })); process.exit(${exitCode}); `; - fs.writeFileSync(full, script); + if (isWindows) { + // Windows can't execute a shebang script directly, so ship a `.cmd` wrapper + // that runs Node (by absolute path, so PATH need not contain node) on a + // companion `.js` file. This exercises the `.cmd` dispatch branch. + const jsPath = path.join(dir, `${name}.js`); + fs.writeFileSync(jsPath, jsBody); + const cmdPath = path.join(dir, `${name}.cmd`); + fs.writeFileSync(cmdPath, `@"${process.execPath}" "${jsPath}" %*\r\n`); + return cmdPath; + } + const full = path.join(dir, name); + fs.writeFileSync(full, `#!${process.execPath}\n${jsBody}`); fs.chmodSync(full, 0o755); return full; } @@ -150,7 +172,7 @@ const CONTEXT_ENV_KEYS = [ "TAILOR_PLATFORM_PROFILE", ] as const; -describe.skipIf(isWindows)("dispatchPlugin", () => { +describe("dispatchPlugin", () => { let tempDir: string; let originalCwd: string; let originalPath: string | undefined; diff --git a/packages/sdk/src/cli/shared/plugin.ts b/packages/sdk/src/cli/shared/plugin.ts index d5a82f798..a0f40929b 100644 --- a/packages/sdk/src/cli/shared/plugin.ts +++ b/packages/sdk/src/cli/shared/plugin.ts @@ -141,6 +141,11 @@ function findExecutableIn(dirs: string[], binName: string): string | undefined { * @returns The discovered plugin, or null when not found */ export function resolvePlugin(name: string, cliName: string): DiscoveredPlugin | null { + // Reject names that could escape the `-` prefix via path traversal + // (e.g. `../evil`) or embed a NUL, before joining them into a filesystem path. + if (name.includes("/") || name.includes("\\") || name.includes("\0")) { + return null; + } const binName = `${cliName}-${name}`; const fromNodeModules = findExecutableIn(nodeModulesBinDirs(), binName); diff --git a/packages/sdk/src/cli/shared/readonly-guard.test.ts b/packages/sdk/src/cli/shared/readonly-guard.test.ts index 320cb9c08..8204f8d24 100644 --- a/packages/sdk/src/cli/shared/readonly-guard.test.ts +++ b/packages/sdk/src/cli/shared/readonly-guard.test.ts @@ -37,7 +37,8 @@ const READ_OR_LOCAL_COMMAND_PATHS = new Set([ // (including Create*/Update*/Delete*) and must guard. "api/inspect.ts", "api/list.ts", - // Auth token retrieval (reads/refreshes local credentials only) + // Auth token retrieval (may refresh via the OAuth server and persist tokens + // locally, but mutates no workspace/platform state) "auth/token.ts", // Auth connections (read-only) "authconnection/index.ts", From c37907cb9013b1a1d1c433fa4af6ba0054579757 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 6 Jul 2026 10:37:39 +0900 Subject: [PATCH 428/618] test: normalize vitest include paths to forward slashes for Windows node:fs globSync yields backslash paths on Windows, which vitest treats as invalid include globs (backslash is an escape in the matcher), so the Plugin tests (Windows) job resolved zero files and exited 1. Normalize the classified unit-test paths to forward slashes so discovery and the vitest run filter work on win32 (no-op on POSIX). --- packages/sdk/vitest.config.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/sdk/vitest.config.ts b/packages/sdk/vitest.config.ts index 0d3d2043f..923d383dc 100644 --- a/packages/sdk/vitest.config.ts +++ b/packages/sdk/vitest.config.ts @@ -51,8 +51,15 @@ const integrationTestIncludes = [ // isolation; the rest reuse module evaluation across files, which roughly // halves their import time. // Classification is by file content so new tests are routed automatically. +// `node:fs` globSync yields OS-native separators (backslashes on Windows). +// vitest matches include globs and CLI file filters with forward slashes, so +// normalize here to keep discovery and `vitest run ` working on Windows. +const toPosix = (file: string): string => file.split(path.sep).join("/"); + const classifyUnitTests = (): { isolated: string[]; shared: string[] } => { - const integrationTestFiles = new Set(globSync(integrationTestIncludes, { cwd: __dirname })); + const integrationTestFiles = new Set( + globSync(integrationTestIncludes, { cwd: __dirname }).map(toPosix), + ); const isExcludedUnitTest = (file: string): boolean => file.includes("/node_modules/") || file.includes("/__test_fixtures__/") || @@ -69,7 +76,7 @@ const classifyUnitTests = (): { isolated: string[]; shared: string[] } => { const shared: string[] = []; for (const file of globSync(["src/**/*.{test,spec}.ts", "scripts/**/*.{test,spec}.ts"], { cwd: __dirname, - })) { + }).map(toPosix)) { if (isExcludedUnitTest(file)) continue; (needsIsolation(file) ? isolated : shared).push(file); } From 7758b00ad01516d03a4db6b207b5ae73f189fd85 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 6 Jul 2026 10:50:59 +0900 Subject: [PATCH 429/618] test: run CLI plugin tests in a dedicated vitest project Replace the separator-sensitive positional file filter (which resolved zero files on Windows) with a dedicated "unit-plugin" vitest project that includes only the CLI plugin test. The Windows CI job selects it with --project unit-plugin, and the unit* glob still runs it on Linux; it is excluded from the general unit split so it does not run twice. Reverts the earlier forward-slash include normalization, now unnecessary. --- .github/workflows/test.yml | 2 +- packages/sdk/vitest.config.ts | 30 +++++++++++++++++++++--------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2fcb60cad..3a7da1877 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -102,7 +102,7 @@ jobs: # Windows-specific PATHEXT resolution and .cmd/.ps1 spawn branches are # only exercised here; the main unit suite runs on Linux. - name: Run plugin tests - run: pnpm --filter @tailor-platform/sdk exec vitest run --project 'unit*' src/cli/shared/plugin.test.ts + run: pnpm --filter @tailor-platform/sdk exec vitest run --project unit-plugin shell: bash test-summary: diff --git a/packages/sdk/vitest.config.ts b/packages/sdk/vitest.config.ts index 923d383dc..7b0f560b5 100644 --- a/packages/sdk/vitest.config.ts +++ b/packages/sdk/vitest.config.ts @@ -43,6 +43,13 @@ const integrationTestIncludes = [ "src/plugin/compat.test.ts", ]; +// The CLI plugin test exercises Windows-specific PATHEXT/`.cmd`/`.ps1` spawn +// branches, so it gets its own project ("unit-plugin", which the `unit*` glob +// still picks up on Linux) that a Windows CI job can run via `--project +// unit-plugin` — no separator-sensitive path filter needed. Excluded from the +// general unit split below so it does not run twice. +const pluginTestInclude = "src/cli/shared/plugin.test.ts"; + // Split unit tests by whether they mutate worker-global state. With // `isolate: false` a worker shares one module registry and one global object // across files, so per-file partial module mocks (e.g. `vi.mock("node:fs", ...)`) @@ -51,19 +58,14 @@ const integrationTestIncludes = [ // isolation; the rest reuse module evaluation across files, which roughly // halves their import time. // Classification is by file content so new tests are routed automatically. -// `node:fs` globSync yields OS-native separators (backslashes on Windows). -// vitest matches include globs and CLI file filters with forward slashes, so -// normalize here to keep discovery and `vitest run ` working on Windows. -const toPosix = (file: string): string => file.split(path.sep).join("/"); - const classifyUnitTests = (): { isolated: string[]; shared: string[] } => { - const integrationTestFiles = new Set( - globSync(integrationTestIncludes, { cwd: __dirname }).map(toPosix), - ); + const integrationTestFiles = new Set(globSync(integrationTestIncludes, { cwd: __dirname })); const isExcludedUnitTest = (file: string): boolean => file.includes("/node_modules/") || file.includes("/__test_fixtures__/") || integrationTestFiles.has(file) || + // Carved into its own "unit-plugin" project (see below). + file === pluginTestInclude || // Self-contained nested vitest project with its own config. file.startsWith("src/vitest/integration/"); @@ -76,7 +78,7 @@ const classifyUnitTests = (): { isolated: string[]; shared: string[] } => { const shared: string[] = []; for (const file of globSync(["src/**/*.{test,spec}.ts", "scripts/**/*.{test,spec}.ts"], { cwd: __dirname, - }).map(toPosix)) { + })) { if (isExcludedUnitTest(file)) continue; (needsIsolation(file) ? isolated : shared).push(file); } @@ -124,6 +126,16 @@ export default defineConfig({ include: sharedUnitTests, }, }, + { + extends: true, + test: { + // Carved out so a Windows CI job can run just the plugin test via + // `--project unit-plugin`; the `unit*` glob still runs it on Linux. + name: "unit-plugin", + include: [pluginTestInclude], + typecheck: { enabled: false }, + }, + }, { extends: true, test: { From 4b90435efeabe468af18603f59658eacef037c73 Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 6 Jul 2026 11:51:49 +0900 Subject: [PATCH 430/618] feat(codemod): add v2 strict scalar strings migration entry Register the v2/strict-scalar-strings migration guidance for the strict UUID/date/datetime/time/decimal field string types introduced on v2. The entry documents the new scalar shapes and is*/parse*/assert*String helpers in the generated migration guide, and flags likely-affected files (getDB / toISOString / mockIdp usage and old "mock-id" fixtures) for LLM-assisted review during tailor upgrade. --- .changeset/strict-scalar-strings-codemod.md | 5 ++ packages/sdk-codemod/src/registry.ts | 57 ++++++++++++ packages/sdk/docs/migration/v2.md | 97 +++++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 .changeset/strict-scalar-strings-codemod.md diff --git a/.changeset/strict-scalar-strings-codemod.md b/.changeset/strict-scalar-strings-codemod.md new file mode 100644 index 000000000..d6a245d0e --- /dev/null +++ b/.changeset/strict-scalar-strings-codemod.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": minor +--- + +Add the `v2/strict-scalar-strings` migration entry for the strict UUID/date/datetime/time/decimal field string types. The migration guide now documents the new scalar shapes and `is*String` / `parse*String` / `assert*String` helpers, and the runner flags likely-affected files (`getDB` / `toISOString` / `mockIdp` usage and old `"mock-id"` fixtures) for LLM-assisted review. diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 81862ce1e..155a011d5 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -96,6 +96,7 @@ const RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN = new RegExp( ); const V2_NEXT_1 = "2.0.0-next.1"; const V2_NEXT_2 = "2.0.0-next.2"; +const V2_NEXT_3 = "2.0.0-next.3"; /** All registered codemods, in registration order. */ export const allCodemods: CodemodPackage[] = [ @@ -712,6 +713,62 @@ export const allCodemods: CodemodPackage[] = [ "trigger result as the job output directly (no Promise wrapper to unwrap).", ].join("\n"), }, + { + id: "v2/strict-scalar-strings", + name: "Strict scalar string types for UUID/date/datetime/time/decimal fields", + description: + "Tailor field outputs infer strict string shapes instead of plain `string`: UUID fields are `UUIDString`, date fields `DateString`, datetime fields `DateTimeString | Date`, time fields `TimeString`, and decimal fields `DecimalString` (all exported from `@tailor-platform/sdk`). Generated Kysely types, migration DB helper types, auth `tailor.d.ts` attributes, and runtime principal / IdP user ids use the same aliases, and the Kysely `Timestamp` columns now select as `Date | DateTimeString` instead of `Date`. String values that already match a shape keep typechecking unchanged; plain `string` values must be narrowed with the new `is*String` / `parse*String` / `assert*String` helpers or have their source types tightened.", + since: "1.0.0", + until: "2.0.0", + prereleaseUntil: V2_NEXT_3, + // No scriptPath: this is a codemod-less ("manual") migration. + suspiciousPatterns: ["getDB", "toISOString", "mockIdp"], + sourceStringSuspiciousPatterns: ["mock-id"], + examples: [ + { + caption: + "Tighten the source type — or narrow at an untyped boundary with the scalar helpers — instead of passing plain `string` to a strict scalar API:", + before: + 'async function loadCustomer(customerId: string) {\n return getDB("tailordb")\n .selectFrom("Customer")\n .where("id", "=", customerId)\n .selectAll()\n .executeTakeFirstOrThrow();\n}', + after: + 'import { type UUIDString, parseUUIDString } from "@tailor-platform/sdk";\n\nasync function loadCustomer(customerId: UUIDString) {\n return getDB("tailordb")\n .selectFrom("Customer")\n .where("id", "=", customerId)\n .selectAll()\n .executeTakeFirstOrThrow();\n}\n\n// At an untyped boundary:\nawait loadCustomer(parseUUIDString(value, "customerId"));', + }, + { + caption: + "Kysely `Timestamp` columns now select as `Date | DateTimeString`, so guard `Date` methods:", + before: "return { createdAt: customer.createdAt.toISOString() };", + after: + "return {\n createdAt:\n customer.createdAt instanceof Date\n ? customer.createdAt.toISOString()\n : customer.createdAt,\n};", + }, + { + caption: + "Record and principal ids are `UUIDString`, so test fixtures need UUID-shaped literals:", + before: 'const payload = { newRecord: { id: "user-1" } };', + after: 'const payload = { newRecord: { id: "11111111-1111-4111-8111-111111111111" } };', + }, + ], + prompt: [ + "The v2 SDK types UUID, date, datetime, time, and decimal Tailor field values as", + "strict string shapes (UUIDString, DateString, DateTimeString, TimeString,", + "DecimalString from @tailor-platform/sdk) instead of plain string. Generated", + "Kysely types, auth attributes, and runtime principal / IdP user ids use the same", + "aliases, and Kysely Timestamp columns now select as Date | DateTimeString.", + "Regenerate the generated types (tailor generate), then typecheck the project and", + "fix the remaining errors:", + "- String literals that already match a shape typecheck unchanged.", + "- For string or unknown values, tighten the source type (e.g. declare the", + " parameter as UUIDString, use t.uuid() for resolver inputs that carry record", + " ids), or narrow with the isUUIDString / parseUUIDString / assertUUIDString", + " helper families (same variants exist for date, datetime, time, and decimal).", + "- Guard Date methods on selected timestamp columns:", + " value instanceof Date ? value.toISOString() : value.", + '- Give test fixtures UUID-shaped ids (e.g. "11111111-1111-4111-8111-111111111111").', + ' The mockIdp default user id changed from "mock-id" to', + ' "123e4567-e89b-12d3-a456-426614174000".', + "- Existing migration db.ts snapshots keep compiling; leave them unchanged. New", + " migrations are generated with the strict shapes automatically.", + ].join("\n"), + }, { id: "v2/cli-token-keyring-storage", name: "CLI tokens stored in the OS keyring", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index c04f240fa..afeeed4b2 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -665,6 +665,103 @@ trigger result as the job output directly (no Promise wrapper to unwrap).
+## Strict scalar string types for UUID/date/datetime/time/decimal fields + +**Migration:** Manual + +Tailor field outputs infer strict string shapes instead of plain `string`: UUID fields are `UUIDString`, date fields `DateString`, datetime fields `DateTimeString | Date`, time fields `TimeString`, and decimal fields `DecimalString` (all exported from `@tailor-platform/sdk`). Generated Kysely types, migration DB helper types, auth `tailor.d.ts` attributes, and runtime principal / IdP user ids use the same aliases, and the Kysely `Timestamp` columns now select as `Date | DateTimeString` instead of `Date`. String values that already match a shape keep typechecking unchanged; plain `string` values must be narrowed with the new `is*String` / `parse*String` / `assert*String` helpers or have their source types tightened. + +Tighten the source type — or narrow at an untyped boundary with the scalar helpers — instead of passing plain `string` to a strict scalar API: + +Before: + +```ts +async function loadCustomer(customerId: string) { + return getDB("tailordb") + .selectFrom("Customer") + .where("id", "=", customerId) + .selectAll() + .executeTakeFirstOrThrow(); +} +``` + +After: + +```ts +import { type UUIDString, parseUUIDString } from "@tailor-platform/sdk"; + +async function loadCustomer(customerId: UUIDString) { + return getDB("tailordb") + .selectFrom("Customer") + .where("id", "=", customerId) + .selectAll() + .executeTakeFirstOrThrow(); +} + +// At an untyped boundary: +await loadCustomer(parseUUIDString(value, "customerId")); +``` + +Kysely `Timestamp` columns now select as `Date | DateTimeString`, so guard `Date` methods: + +Before: + +```ts +return { createdAt: customer.createdAt.toISOString() }; +``` + +After: + +```ts +return { + createdAt: + customer.createdAt instanceof Date + ? customer.createdAt.toISOString() + : customer.createdAt, +}; +``` + +Record and principal ids are `UUIDString`, so test fixtures need UUID-shaped literals: + +Before: + +```ts +const payload = { newRecord: { id: "user-1" } }; +``` + +After: + +```ts +const payload = { newRecord: { id: "11111111-1111-4111-8111-111111111111" } }; +``` + +
+Prompt for an AI agent (to perform this migration) + +```text +The v2 SDK types UUID, date, datetime, time, and decimal Tailor field values as +strict string shapes (UUIDString, DateString, DateTimeString, TimeString, +DecimalString from @tailor-platform/sdk) instead of plain string. Generated +Kysely types, auth attributes, and runtime principal / IdP user ids use the same +aliases, and Kysely Timestamp columns now select as Date | DateTimeString. +Regenerate the generated types (tailor generate), then typecheck the project and +fix the remaining errors: +- String literals that already match a shape typecheck unchanged. +- For string or unknown values, tighten the source type (e.g. declare the + parameter as UUIDString, use t.uuid() for resolver inputs that carry record + ids), or narrow with the isUUIDString / parseUUIDString / assertUUIDString + helper families (same variants exist for date, datetime, time, and decimal). +- Guard Date methods on selected timestamp columns: + value instanceof Date ? value.toISOString() : value. +- Give test fixtures UUID-shaped ids (e.g. "11111111-1111-4111-8111-111111111111"). + The mockIdp default user id changed from "mock-id" to + "123e4567-e89b-12d3-a456-426614174000". +- Existing migration db.ts snapshots keep compiling; leave them unchanged. New + migrations are generated with the strict shapes automatically. +``` + +
+ ## tailor-sdk binary → tailor **Migration:** Partially automatic From 3d3f54e5cbb523099f90cf1effb3630e5d0f49cf Mon Sep 17 00:00:00 2001 From: dqn Date: Mon, 6 Jul 2026 12:00:26 +0900 Subject: [PATCH 431/618] fix(codemod): surface strict scalar guidance project-wide Pattern-scoped review flags miss projects whose only breakage does not mention getDB / toISOString / mockIdp (e.g. invalid id fixtures), so drop the suspicious patterns and let the runner emit the prompt as project-wide guidance. --- .changeset/strict-scalar-strings-codemod.md | 2 +- packages/sdk-codemod/src/registry.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.changeset/strict-scalar-strings-codemod.md b/.changeset/strict-scalar-strings-codemod.md index d6a245d0e..8ce1ab50b 100644 --- a/.changeset/strict-scalar-strings-codemod.md +++ b/.changeset/strict-scalar-strings-codemod.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk-codemod": minor --- -Add the `v2/strict-scalar-strings` migration entry for the strict UUID/date/datetime/time/decimal field string types. The migration guide now documents the new scalar shapes and `is*String` / `parse*String` / `assert*String` helpers, and the runner flags likely-affected files (`getDB` / `toISOString` / `mockIdp` usage and old `"mock-id"` fixtures) for LLM-assisted review. +Add the `v2/strict-scalar-strings` migration entry for the strict UUID/date/datetime/time/decimal field string types. The migration guide now documents the new scalar shapes and `is*String` / `parse*String` / `assert*String` helpers, and the runner surfaces the regenerate-and-typecheck migration steps as project-wide LLM guidance. diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 155a011d5..c1a609f73 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -721,9 +721,8 @@ export const allCodemods: CodemodPackage[] = [ since: "1.0.0", until: "2.0.0", prereleaseUntil: V2_NEXT_3, - // No scriptPath: this is a codemod-less ("manual") migration. - suspiciousPatterns: ["getDB", "toISOString", "mockIdp"], - sourceStringSuspiciousPatterns: ["mock-id"], + // No scriptPath and no scoping patterns: the migration is type-driven, so + // the prompt is surfaced as project-wide guidance instead of per-file. examples: [ { caption: From e0e768d77470d13806ed7b2ee2117fe374d51d40 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 14:07:40 +0900 Subject: [PATCH 432/618] feat(tailordb): aggregate hooks and validators into type-level scripts Field and type-level hooks and validators are now compiled into a single per-type create/update script. Hooks receive a `now` (Date) shared across every field hooked in the same operation, so multiple fields can be stamped with one identical timestamp. --- .changeset/tailordb-shared-now-hook.md | 7 + packages/sdk/docs/services/tailordb.md | 21 ++- .../commands/deploy/tailordb/index.test.ts | 29 ++-- .../src/cli/commands/deploy/tailordb/index.ts | 15 +- .../migrate/snapshot-manifest.test.ts | 50 ++++-- .../tailordb/migrate/snapshot-manifest.ts | 50 ++---- .../tailordb/hooks-validate-bundler.ts | 12 +- .../src/configure/services/tailordb/types.ts | 1 + .../sdk/src/parser/service/tailordb/field.ts | 2 +- .../service/tailordb/type-script.test.ts | 89 +++++++++++ .../parser/service/tailordb/type-script.ts | 151 ++++++++++++++++++ 11 files changed, 350 insertions(+), 77 deletions(-) create mode 100644 .changeset/tailordb-shared-now-hook.md create mode 100644 packages/sdk/src/parser/service/tailordb/type-script.test.ts create mode 100644 packages/sdk/src/parser/service/tailordb/type-script.ts diff --git a/.changeset/tailordb-shared-now-hook.md b/.changeset/tailordb-shared-now-hook.md new file mode 100644 index 000000000..58c467137 --- /dev/null +++ b/.changeset/tailordb-shared-now-hook.md @@ -0,0 +1,7 @@ +--- +"@tailor-platform/sdk": minor +--- + +Add a `now` argument to TailorDB hooks. `now` is the operation timestamp and is shared across every field hooked in the same create/update, so multiple fields can be stamped with an identical `Date`. Hooks and validators are now applied per type rather than per field, which is what makes the shared timestamp possible. + +As part of this, all of a type's hooks now run together and observe the same submitted input: a hook's `data` reflects what the client sent and does not include other fields' hook results. diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index 36dc0ea9a..16519eac2 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -279,11 +279,14 @@ field, files entry, or relation on the same type. ### Hooks -Add hooks to execute functions during data creation or update. Hooks receive three arguments: +Add hooks to execute functions during data creation or update. Hooks receive four arguments: - `value`: User input if provided, otherwise existing value on update or null on create -- `data`: Entire record data (for accessing other field values) +- `data`: The submitted record data (for accessing other field values) - `invoker`: Principal performing the operation +- `now`: Operation timestamp (`Date`). The same instant is shared by every field's hook in the same create/update, so multiple fields can be stamped with an identical timestamp. + +All of a type's hooks run together as one operation and observe the same submitted input: `data` reflects what the client sent, so a hook does not see another field's hook result. Order between fields is not significant. #### Field-level Hooks @@ -317,6 +320,20 @@ export const customer = db }); ``` +Use `now` to stamp several fields with the exact same instant: + +```typescript +export const order = db + .type("Order", { + createdAt: db.datetime(), + updatedAt: db.datetime(), + }) + .hooks({ + createdAt: { create: ({ now }) => now }, + updatedAt: { create: ({ now }) => now, update: ({ now }) => now }, + }); +``` + **Important:** Field-level and type-level hooks cannot coexist on the same field. TypeScript will prevent this at compile time: ```typescript diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts b/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts index 8c630a408..67804bb60 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts @@ -437,20 +437,21 @@ describe("planTailorDB (service level)", () => { const displayNameField = profileField?.fields?.displayName; const contactEmailField = profileField?.fields?.contact!.fields?.email; - expect(displayNameField?.validate).toHaveLength(1); - expect(displayNameField?.validate?.[0]?.errorMessage).toBe("Display name is required"); - expect(displayNameField?.validate?.[0]?.script?.expr).toContain( - "!((_value ?? '').length > 0)", - ); - expect(displayNameField?.hooks?.create?.expr).toBe("(_value ?? '').trim()"); - expect(displayNameField?.hooks?.update?.expr).toBe("(_value ?? '').trim()"); - - expect(contactEmailField?.validate).toHaveLength(1); - expect(contactEmailField?.validate?.[0]?.errorMessage).toBe("Email must contain @"); - expect(contactEmailField?.validate?.[0]?.script?.expr).toContain( - "!((_value ?? '').includes('@'))", - ); - expect(contactEmailField?.hooks?.create?.expr).toBe("(_value ?? '').toLowerCase()"); + // Nested field hooks/validators are aggregated into type-level scripts, + // never emitted per field. + expect(displayNameField?.hooks).toBeUndefined(); + expect(displayNameField?.validate ?? []).toHaveLength(0); + expect(contactEmailField?.hooks).toBeUndefined(); + expect(contactEmailField?.validate ?? []).toHaveLength(0); + + const hookExpr = createdType?.schema?.typeHook?.create?.expr ?? ""; + expect(hookExpr).toContain('"profile": Object.assign({}, _input["profile"], {'); + expect(hookExpr).toContain("(_value ?? '').trim()"); + expect(hookExpr).toContain("(_value ?? '').toLowerCase()"); + + const validateExpr = createdType?.schema?.typeValidate?.create?.expr ?? ""; + expect(validateExpr).toContain('__errs["profile.displayName"] = "Display name is required"'); + expect(validateExpr).toContain('__errs["profile.contact.email"] = "Email must contain @"'); }); }); diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/index.ts b/packages/sdk/src/cli/commands/deploy/tailordb/index.ts index 7c5409a8b..88280602b 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/index.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/index.ts @@ -1758,6 +1758,8 @@ function normalizeComparableTailorDBType(type: unknown) { indexes?: Record; files?: Record; permission?: Record; + typeHook?: Record; + typeValidate?: Record; }; } | null; return normalizeTailorDBCompareValue( @@ -1771,6 +1773,10 @@ function normalizeComparableTailorDBType(type: unknown) { indexes: normalized?.schema?.indexes ?? {}, files: normalized?.schema?.files ?? {}, permission: normalized?.schema?.permission ?? {}, + // Hooks/validators are sent as type-level scripts; include them so a + // changed hook or validator is detected as an update. + typeHook: normalized?.schema?.typeHook ?? {}, + typeValidate: normalized?.schema?.typeValidate ?? {}, }, }, [], @@ -1802,9 +1808,16 @@ function normalizeTailorDBCompareValue( } if (Array.isArray(value)) { - return value + const items = value .map((item, index) => normalizeTailorDBCompareValue(item, [...path, index])) .filter((item) => item !== undefined); + // Field-level validators are no longer emitted by the SDK (they are aggregated + // into type-level type_validate). The platform still returns an empty `validate` + // array per field; treat it as unset so it matches the omitted local value. + if (items.length === 0 && path.at(-1) === "validate") { + return undefined; + } + return items; } if (!isPlainObject(value)) { diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts index 304ffedfe..9d9de07c4 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts @@ -294,7 +294,7 @@ describe("snapshot-manifest", () => { expect(manifest.schema?.settings?.disableGqlOperations?.delete).toBe(true); }); - test("handles hooks configuration", () => { + test("aggregates field hooks into a type-level hook script", () => { const snapshotType = createTestSnapshotType("User", { fields: { id: { type: "uuid", required: true }, @@ -311,11 +311,19 @@ describe("snapshot-manifest", () => { const manifest = generateTailorDBTypeManifestFromSnapshot(snapshotType); - expect(manifest.schema?.fields?.updatedAt?.hooks?.create?.expr).toBe("now()"); - expect(manifest.schema?.fields?.updatedAt?.hooks?.update?.expr).toBe("now()"); + // Field-level hooks are no longer emitted per field. + expect(manifest.schema?.fields?.updatedAt?.hooks).toBeUndefined(); + + // They are aggregated into a single type-level script that binds a shared + // timestamp once and dispatches each field's hook. + const createHook = manifest.schema?.typeHook?.create?.expr ?? ""; + expect(createHook).toContain("const _now = new Date()"); + expect(createHook).toContain('"updatedAt": ((_value) => (now()))(_input["updatedAt"])'); + expect(manifest.schema?.typeHook?.update?.expr).toContain('_input["updatedAt"]'); + expect(manifest.schema?.typeValidate).toBeUndefined(); }); - test("keeps validate and hooks in nested fields", () => { + test("aggregates nested field hooks and validators into type-level scripts", () => { const snapshotType = createTestSnapshotType("User", { fields: { id: { type: "uuid", required: true }, @@ -366,16 +374,30 @@ describe("snapshot-manifest", () => { const displayNameField = profileField?.fields?.displayName; const emailField = profileField?.fields?.contact?.fields?.email; - expect(displayNameField?.validate).toHaveLength(1); - expect(displayNameField?.validate?.[0]?.errorMessage).toBe("Display name is required"); - expect(displayNameField?.validate?.[0]?.script?.expr).toBe("!((_value ?? '').length > 0)"); - expect(displayNameField?.hooks?.create?.expr).toBe("(_value ?? '').trim()"); - expect(displayNameField?.hooks?.update?.expr).toBe("(_value ?? '').trim()"); - - expect(emailField?.validate).toHaveLength(1); - expect(emailField?.validate?.[0]?.errorMessage).toBe("Email must contain @"); - expect(emailField?.validate?.[0]?.script?.expr).toBe("!((_value ?? '').includes('@'))"); - expect(emailField?.hooks?.create?.expr).toBe("(_value ?? '').toLowerCase()"); + // Nested field hooks/validators are not emitted per field. + expect(displayNameField?.hooks).toBeUndefined(); + expect(displayNameField?.validate ?? []).toHaveLength(0); + expect(emailField?.hooks).toBeUndefined(); + expect(emailField?.validate ?? []).toHaveLength(0); + + // Hooks are aggregated into a type-level script that reconstructs nested + // objects so unhooked siblings are preserved. + const hookExpr = manifest.schema?.typeHook?.create?.expr ?? ""; + expect(hookExpr).toContain('"profile": Object.assign({}, _input["profile"], {'); + expect(hookExpr).toContain("(_value ?? '').trim()"); + expect(hookExpr).toContain('(_input["profile"] || {})["displayName"]'); + expect(hookExpr).toContain( + '"contact": Object.assign({}, (_input["profile"] || {})["contact"], {', + ); + expect(hookExpr).toContain("(_value ?? '').toLowerCase()"); + + // Validators are aggregated into a type-level validate script keyed by the + // dotted field path, with the boolean expression negated into a failure. + const validateExpr = manifest.schema?.typeValidate?.create?.expr ?? ""; + expect(validateExpr).toContain('__errs["profile.displayName"] = "Display name is required"'); + expect(validateExpr).toContain("if (!(((_value ?? '').length > 0)))"); + expect(validateExpr).toContain('__errs["profile.contact.email"] = "Email must contain @"'); + expect(manifest.schema?.typeValidate?.update?.expr).toBe(validateExpr); }); test("handles serial configuration", () => { diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts index af6b8b36c..6d87409d0 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts @@ -18,7 +18,6 @@ import { type TailorDBGQLPermissionSchema, TailorDBType_Permission_Operator, TailorDBType_Permission_Permit, - TailorDBType_PermitAction, type TailorDBType_FieldConfigSchema, type TailorDBType_FileConfigSchema, type TailorDBType_IndexSchema, @@ -30,6 +29,7 @@ import { type TailorDBTypeSchema, } from "@tailor-platform/tailor-proto/tailordb_resource_pb"; import * as inflection from "inflection"; +import { buildTypeScripts } from "@/parser/service/tailordb/type-script"; import { isSnapshotFieldRefOperand } from "./snapshot"; import type { SchemaSnapshot, @@ -163,6 +163,10 @@ export function generateTailorDBTypeManifestFromSnapshot( ? convertRecordPermissionToProto(snapshotType.permissions.record) : defaultPermission; + // Field hooks/validators are aggregated into type-level scripts so that a + // single shared timestamp is observed across every field in one operation. + const { typeHook, typeValidate } = buildTypeScripts(snapshotType.fields); + return { name: snapshotType.name, schema: { @@ -175,6 +179,8 @@ export function generateTailorDBTypeManifestFromSnapshot( indexes, files, permission, + ...(typeHook && { typeHook }), + ...(typeValidate && { typeValidate }), }, }; } @@ -187,6 +193,8 @@ export function generateTailorDBTypeManifestFromSnapshot( export function convertFieldConfigToProto( config: SnapshotFieldConfig, ): MessageInitShape { + // Field-level hooks/validators are not sent per field: they are aggregated + // into type-level type_hook/type_validate scripts (see buildTypeScripts). const fieldEntry: MessageInitShape = { type: config.type, allowedValues: @@ -194,7 +202,6 @@ export function convertFieldConfigToProto( ? (config.allowedValues?.map((v: SnapshotEnumValue) => ({ ...v })) ?? []) : [], description: config.description || "", - validate: toProtoSnapshotFieldValidate(config), array: config.array ?? false, index: config.index ?? false, unique: config.unique ?? false, @@ -203,7 +210,6 @@ export function convertFieldConfigToProto( foreignKeyField: config.foreignKeyField, required: config.required, vector: config.vector ?? false, - ...toProtoSnapshotFieldHooks(config), ...(config.serial && { serial: { start: BigInt(config.serial.start), @@ -226,40 +232,6 @@ export function convertFieldConfigToProto( return fieldEntry; } -function toProtoSnapshotFieldValidate( - config: SnapshotFieldConfig, -): MessageInitShape["validate"] { - return (config.validate ?? []).map((val) => ({ - action: TailorDBType_PermitAction.DENY, - errorMessage: val.errorMessage || "", - script: { - expr: val.script && val.script.expr ? `!${val.script.expr}` : "", - }, - })); -} - -function toProtoSnapshotFieldHooks( - config: SnapshotFieldConfig, -): Pick, "hooks"> | Record { - if (!config.hooks) { - return {}; - } - return { - hooks: { - create: config.hooks.create - ? { - expr: config.hooks.create.expr || "", - } - : undefined, - update: config.hooks.update - ? { - expr: config.hooks.update.expr || "", - } - : undefined, - }, - }; -} - /** * Process nested fields from snapshot format to proto format * @param {Record} fields - Nested fields @@ -277,14 +249,12 @@ function processNestedFieldsFromSnapshot( type: "nested", allowedValues: fieldConfig.allowedValues?.map((v: SnapshotEnumValue) => ({ ...v })) ?? [], description: fieldConfig.description || "", - validate: toProtoSnapshotFieldValidate(fieldConfig), required: fieldConfig.required, array: fieldConfig.array ?? false, index: false, unique: false, foreignKey: false, vector: false, - ...toProtoSnapshotFieldHooks(fieldConfig), fields: deepNestedFields, ...(fieldConfig.scale !== undefined && { scale: fieldConfig.scale }), }; @@ -296,14 +266,12 @@ function processNestedFieldsFromSnapshot( ? (fieldConfig.allowedValues?.map((v: SnapshotEnumValue) => ({ ...v })) ?? []) : [], description: fieldConfig.description || "", - validate: toProtoSnapshotFieldValidate(fieldConfig), required: fieldConfig.required, array: fieldConfig.array ?? false, index: false, unique: false, foreignKey: false, vector: false, - ...toProtoSnapshotFieldHooks(fieldConfig), ...(fieldConfig.serial && { serial: { start: BigInt(fieldConfig.serial.start), diff --git a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts index 894a68063..c4ef0503c 100644 --- a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts +++ b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts @@ -386,13 +386,13 @@ export function resolveNeededBindings( }; } -function buildPrecompiledExpr(bundleCode: string): string { +function buildPrecompiledExpr(bundleCode: string, argsObject: string): string { return ( "(() => {\n" + " const module = { exports: {} };\n" + " const exports = module.exports;\n" + `${bundleCode}\n` + - ` return module.exports.main({ value: _value, data: _data, invoker: ${tailorPrincipalMap} });\n` + + ` return module.exports.main(${argsObject});\n` + "})()" ); } @@ -441,8 +441,12 @@ async function bundleScriptTarget(args: { const { fn, kind, sourceFilePath, sourceBindings, tempDir, targetIndex, tsconfig } = args; const context = `${kind} in ${sourceFilePath}`; const fnSource = stringifyFunction(fn); + const argsObject = + kind === "hooks" + ? `{ value: _value, data: _data, invoker: ${tailorPrincipalMap}, now: _now }` + : `{ value: _value, data: _data, invoker: ${tailorPrincipalMap} }`; const inlineExpr = assertParsableExpression( - `(${fnSource})({ value: _value, data: _data, invoker: ${tailorPrincipalMap} })`, + `(${fnSource})(${argsObject})`, context, ); @@ -492,7 +496,7 @@ async function bundleScriptTarget(args: { } as rolldown.BuildOptions); const bundledCode = buildResult.output[0].code; - return assertParsableExpression(buildPrecompiledExpr(bundledCode), context); + return assertParsableExpression(buildPrecompiledExpr(bundledCode, argsObject), context); } /** diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index bbde489da..d0d8b88df 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -149,6 +149,7 @@ type HookFn = (args: { ? { readonly [K in keyof TData]?: TData[K] | null | undefined } : unknown; invoker: TailorPrincipal | null; + now: Date; }) => TReturn; export type Hook = { diff --git a/packages/sdk/src/parser/service/tailordb/field.ts b/packages/sdk/src/parser/service/tailordb/field.ts index 207530a97..3f4de1150 100644 --- a/packages/sdk/src/parser/service/tailordb/field.ts +++ b/packages/sdk/src/parser/service/tailordb/field.ts @@ -199,7 +199,7 @@ const convertToScriptExpr = ( } const normalized = stringifyFunction(fn); return assertParsableExpression( - `(${normalized})({ value: _value, data: _data, invoker: ${tailorPrincipalMap} })`, + `(${normalized})({ value: _value, data: _data, invoker: ${tailorPrincipalMap}, now: _now })`, formatScriptContext(kind, context), ); }; diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts new file mode 100644 index 000000000..886aedffa --- /dev/null +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, test } from "vitest"; +import { buildTypeScripts, type ScriptFieldConfig } from "./type-script"; + +describe("buildTypeScripts", () => { + test("returns empty result when no field has hooks or validators", () => { + const fields: Record = { + id: { type: "uuid" }, + name: { type: "string" }, + }; + expect(buildTypeScripts(fields)).toEqual({}); + }); + + test("aggregates field hooks into one script binding a shared timestamp", () => { + const fields: Record = { + createdAt: { + type: "datetime", + hooks: { create: { expr: "_now" } }, + }, + updatedAt: { + type: "datetime", + hooks: { create: { expr: "_now" }, update: { expr: "_now" } }, + }, + }; + + const { typeHook, typeValidate } = buildTypeScripts(fields); + expect(typeValidate).toBeUndefined(); + + // A single `new Date()` is bound once and dispatched to every field. + const createExpr = typeHook?.create?.expr ?? ""; + expect(createExpr.match(/new Date\(\)/g)).toHaveLength(1); + expect(createExpr).toContain('"createdAt": ((_value) => (_now))(_input["createdAt"])'); + expect(createExpr).toContain('"updatedAt": ((_value) => (_now))(_input["updatedAt"])'); + + // createdAt has no update hook, so the update script only touches updatedAt. + const updateExpr = typeHook?.update?.expr ?? ""; + expect(updateExpr).toContain('"updatedAt":'); + expect(updateExpr).not.toContain('"createdAt":'); + }); + + test("reconstructs nested objects so unhooked siblings are preserved", () => { + const fields: Record = { + profile: { + type: "nested", + fields: { + displayName: { type: "string", hooks: { create: { expr: "_value.trim()" } } }, + contact: { + type: "nested", + fields: { + email: { type: "string", hooks: { create: { expr: "_value.toLowerCase()" } } }, + }, + }, + }, + }, + }; + + const createExpr = buildTypeScripts(fields).typeHook?.create?.expr ?? ""; + expect(createExpr).toContain('"profile": Object.assign({}, _input["profile"], {'); + expect(createExpr).toContain('(_input["profile"] || {})["displayName"]'); + expect(createExpr).toContain( + '"contact": Object.assign({}, (_input["profile"] || {})["contact"], {', + ); + expect(createExpr).toContain('((_input["profile"] || {})["contact"] || {})["email"]'); + }); + + test("builds a validate script keyed by dotted path with negated boolean checks", () => { + const fields: Record = { + age: { + type: "integer", + validate: [ + { script: { expr: "_value >= 0" }, errorMessage: "must be >= 0" }, + { script: { expr: "_value < 200" }, errorMessage: "must be < 200" }, + ], + }, + }; + + const { typeValidate, typeHook } = buildTypeScripts(fields); + expect(typeHook).toBeUndefined(); + + const createExpr = typeValidate?.create?.expr ?? ""; + // Same rules apply to create and update. + expect(typeValidate?.update?.expr).toBe(createExpr); + expect(createExpr).toContain("const __errs = {}"); + expect(createExpr).toContain('if (!(_value >= 0)) { __errs["age"] = "must be >= 0"; }'); + expect(createExpr).toContain('else if (!(_value < 200)) { __errs["age"] = "must be < 200"; }'); + expect(createExpr).toContain("return __errs"); + // `now` is a hook-only concept; validators must not bind a timestamp. + expect(createExpr).not.toContain("new Date()"); + }); +}); diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts new file mode 100644 index 000000000..76ed10c20 --- /dev/null +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -0,0 +1,151 @@ +// Platform-injected record map for type-level hook/validate scripts. +const INPUT = "_input"; +// Shared operation timestamp bound once per script execution. +const NOW = "_now"; + +type HookOperation = "create" | "update"; + +interface ScriptRef { + expr: string; +} + +/** + * Minimal structural shape shared by parser `OperatorFieldConfig` and migration + * `SnapshotFieldConfig`. Only the parts needed to aggregate type-level scripts. + */ +export interface ScriptFieldConfig { + type: string; + hooks?: { + create?: ScriptRef; + update?: ScriptRef; + }; + validate?: { script?: ScriptRef; errorMessage: string }[]; + fields?: Record; +} + +export interface TypeScripts { + typeHook?: { create?: ScriptRef; update?: ScriptRef }; + typeValidate?: { create?: ScriptRef; update?: ScriptRef }; +} + +const key = (name: string) => JSON.stringify(name); + +const isNestedType = (config: ScriptFieldConfig): boolean => + config.type === "nested" && config.fields !== undefined; + +/** + * Build the object literal that reconstructs one record level with hook overrides applied. + * Returns null when no field under this level has a hook for the operation. + * @param fields - Field configs at the current level. + * @param accessExpr - JavaScript expression for the current object level. + * @param operation - "create" or "update". + * @returns Object-literal source, or null when nothing under this level is hooked. + */ +function buildHookObject( + fields: Record, + accessExpr: string, + operation: HookOperation, +): string | null { + const parts: string[] = []; + + for (const [name, config] of Object.entries(fields)) { + const access = `${accessExpr}[${key(name)}]`; + if (isNestedType(config) && config.fields) { + const inner = buildHookObject(config.fields, `(${access} || {})`, operation); + if (inner !== null) { + parts.push(`${key(name)}: Object.assign({}, ${access}, ${inner})`); + } + continue; + } + + const hook = config.hooks?.[operation]; + if (hook) { + parts.push(`${key(name)}: ((_value) => (${hook.expr}))(${access})`); + } + } + + if (parts.length === 0) return null; + return `{ ${parts.join(", ")} }`; +} + +/** + * Build validation statements for one record level. + * Each leaf field with validators contributes a block that records the first + * failing message keyed by its dotted field path. + * @param fields - Field configs at the current level. + * @param accessExpr - JavaScript expression for the current object level. + * @param keyPrefix - Dotted path prefix for nested fields. + * @returns One statement per leaf field that has validators. + */ +function buildValidateStatements( + fields: Record, + accessExpr: string, + keyPrefix: string, +): string[] { + const statements: string[] = []; + + for (const [name, config] of Object.entries(fields)) { + const access = `${accessExpr}[${key(name)}]`; + const fieldPath = keyPrefix ? `${keyPrefix}.${name}` : name; + + if (isNestedType(config) && config.fields) { + statements.push(...buildValidateStatements(config.fields, `(${access} || {})`, fieldPath)); + continue; + } + + const validators = (config.validate ?? []).filter((v) => v.script?.expr); + if (validators.length > 0) { + const chain = validators + .map( + (v) => + `if (!(${v.script?.expr})) { __errs[${key(fieldPath)}] = ${key(v.errorMessage)}; }`, + ) + .join(" else "); + statements.push(`{ const _value = ${access}; ${chain} }`); + } + } + + return statements; +} + +function wrapHook(objectExpr: string): string { + return `(() => { const ${NOW} = new Date(); const _data = ${INPUT}; return ${objectExpr}; })()`; +} + +function wrapValidate(statements: string[]): string { + return ( + `(() => { const _data = ${INPUT}; const __errs = {}; ` + + `${statements.join(" ")} return __errs; })()` + ); +} + +/** + * Aggregate every field's create/update hook and validate into type-level scripts. + * Hooks compute a single shared timestamp (`now`) per operation, so all fields + * touched in one create/update observe the same instant. Validators run with the + * same rules on create and update. + * @param fields - Field configs keyed by field name (parser or snapshot shape). + * @returns Type-level hook/validate scripts, omitting empties. + */ +export function buildTypeScripts(fields: Record): TypeScripts { + const result: TypeScripts = {}; + + const hook: { create?: ScriptRef; update?: ScriptRef } = {}; + for (const operation of ["create", "update"] as const) { + const objectExpr = buildHookObject(fields, INPUT, operation); + if (objectExpr !== null) { + hook[operation] = { expr: wrapHook(objectExpr) }; + } + } + if (hook.create || hook.update) { + result.typeHook = hook; + } + + const statements = buildValidateStatements(fields, INPUT, ""); + if (statements.length > 0) { + const expr = wrapValidate(statements); + result.typeValidate = { create: { expr }, update: { expr } }; + } + + return result; +} From 077a6a321be18aa43934a7a5dbb3c32823412f87 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 17 Jun 2026 15:39:45 +0900 Subject: [PATCH 433/618] fix(tailordb): keep field create-hook placeholder so type-level hooks preserve Create-input optionality The platform derives GraphQL Create-input optionality from the presence of a field-level create hook, not from type_hook. Emit a passthrough create hook (`_value`) per field so required hooked fields stay optional in input; the real shared-now logic in type_hook runs afterward and overwrites the value. --- .../commands/deploy/tailordb/index.test.ts | 8 +++---- .../migrate/snapshot-manifest.test.ts | 13 ++++++---- .../tailordb/migrate/snapshot-manifest.ts | 24 +++++++++++++++++-- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts b/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts index 67804bb60..5e30befe3 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts @@ -437,11 +437,11 @@ describe("planTailorDB (service level)", () => { const displayNameField = profileField?.fields?.displayName; const contactEmailField = profileField?.fields?.contact!.fields?.email; - // Nested field hooks/validators are aggregated into type-level scripts, - // never emitted per field. - expect(displayNameField?.hooks).toBeUndefined(); + // Nested fields keep only a create-hook placeholder (to preserve Create-input + // optionality); the real logic is aggregated into type-level scripts. + expect(displayNameField?.hooks?.create?.expr).toBe("_value"); expect(displayNameField?.validate ?? []).toHaveLength(0); - expect(contactEmailField?.hooks).toBeUndefined(); + expect(contactEmailField?.hooks?.create?.expr).toBe("_value"); expect(contactEmailField?.validate ?? []).toHaveLength(0); const hookExpr = createdType?.schema?.typeHook?.create?.expr ?? ""; diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts index 9d9de07c4..a8b57f544 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts @@ -311,8 +311,10 @@ describe("snapshot-manifest", () => { const manifest = generateTailorDBTypeManifestFromSnapshot(snapshotType); - // Field-level hooks are no longer emitted per field. - expect(manifest.schema?.fields?.updatedAt?.hooks).toBeUndefined(); + // Per field, only a create-hook placeholder remains (to keep the field + // optional in the Create input); the real logic moves to type_hook. + expect(manifest.schema?.fields?.updatedAt?.hooks?.create?.expr).toBe("_value"); + expect(manifest.schema?.fields?.updatedAt?.hooks?.update).toBeUndefined(); // They are aggregated into a single type-level script that binds a shared // timestamp once and dispatches each field's hook. @@ -374,10 +376,11 @@ describe("snapshot-manifest", () => { const displayNameField = profileField?.fields?.displayName; const emailField = profileField?.fields?.contact?.fields?.email; - // Nested field hooks/validators are not emitted per field. - expect(displayNameField?.hooks).toBeUndefined(); + // Nested fields keep only a create-hook placeholder; validators are not + // emitted per field. + expect(displayNameField?.hooks?.create?.expr).toBe("_value"); expect(displayNameField?.validate ?? []).toHaveLength(0); - expect(emailField?.hooks).toBeUndefined(); + expect(emailField?.hooks?.create?.expr).toBe("_value"); expect(emailField?.validate ?? []).toHaveLength(0); // Hooks are aggregated into a type-level script that reconstructs nested diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts index 6d87409d0..4c9a152e2 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts @@ -185,6 +185,23 @@ export function generateTailorDBTypeManifestFromSnapshot( }; } +/** + * Field-level create-hook placeholder kept solely so the platform treats a + * required field as optional in the GraphQL Create input (it derives that from + * the presence of a field-level create hook, not from type_hook). The real hook + * logic runs in type_hook, which executes after field hooks and overwrites the + * value — so this passthrough's result is always replaced. + */ +const CREATE_INPUT_OPTIONAL_PLACEHOLDER = "_value"; + +function createInputOptionalHook( + config: Pick, +): Pick, "hooks"> | Record { + return config.hooks?.create + ? { hooks: { create: { expr: CREATE_INPUT_OPTIONAL_PLACEHOLDER } } } + : {}; +} + /** * Convert a snapshot field config to proto format * @param {SnapshotFieldConfig} config - Snapshot field config @@ -193,8 +210,9 @@ export function generateTailorDBTypeManifestFromSnapshot( export function convertFieldConfigToProto( config: SnapshotFieldConfig, ): MessageInitShape { - // Field-level hooks/validators are not sent per field: they are aggregated - // into type-level type_hook/type_validate scripts (see buildTypeScripts). + // Field hook/validator logic is aggregated into type-level type_hook/type_validate + // scripts (see buildTypeScripts). Only a create-hook placeholder is kept per field, + // to preserve GraphQL Create-input optionality. const fieldEntry: MessageInitShape = { type: config.type, allowedValues: @@ -210,6 +228,7 @@ export function convertFieldConfigToProto( foreignKeyField: config.foreignKeyField, required: config.required, vector: config.vector ?? false, + ...createInputOptionalHook(config), ...(config.serial && { serial: { start: BigInt(config.serial.start), @@ -272,6 +291,7 @@ function processNestedFieldsFromSnapshot( unique: false, foreignKey: false, vector: false, + ...createInputOptionalHook(fieldConfig), ...(fieldConfig.serial && { serial: { start: BigInt(fieldConfig.serial.start), From 9ffc3fb3685f1fc7dc3346f4e80d8aa0e00e8095 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 30 Jun 2026 19:39:47 +0900 Subject: [PATCH 434/618] feat(tailordb): add default value support and use optionalOnCreate proto field Add .default() builder method for TailorDBField with TypeLevelError type guards. Regenerate proto bindings to use optionalOnCreate instead of field-level hook placeholder, resolving type_hook coexistence conflict. --- .../commands/deploy/tailordb/index.test.ts | 8 +-- .../migrate/snapshot-manifest.test.ts | 14 ++--- .../tailordb/migrate/snapshot-manifest.ts | 28 +++------- .../tailordb/migrate/snapshot-schema.ts | 1 + .../tailordb/migrate/snapshot-types.ts | 1 + .../cli/commands/tailordb/migrate/snapshot.ts | 2 + .../services/tailordb/schema.test.ts | 13 +++++ .../src/configure/services/tailordb/schema.ts | 31 +++++++++++ .../src/configure/services/tailordb/types.ts | 10 +++- .../sdk/src/parser/service/tailordb/schema.ts | 1 + .../service/tailordb/type-script.test.ts | 41 ++++++++++++++ .../parser/service/tailordb/type-script.ts | 55 +++++++++++++------ .../sdk/src/parser/service/tailordb/types.ts | 1 + packages/sdk/src/types/tailordb.generated.ts | 2 + 14 files changed, 155 insertions(+), 53 deletions(-) diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts b/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts index 5e30befe3..4933c60ef 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts @@ -437,11 +437,11 @@ describe("planTailorDB (service level)", () => { const displayNameField = profileField?.fields?.displayName; const contactEmailField = profileField?.fields?.contact!.fields?.email; - // Nested fields keep only a create-hook placeholder (to preserve Create-input - // optionality); the real logic is aggregated into type-level scripts. - expect(displayNameField?.hooks?.create?.expr).toBe("_value"); + expect(displayNameField?.optionalOnCreate).toBe(true); + expect(displayNameField?.hooks).toBeUndefined(); expect(displayNameField?.validate ?? []).toHaveLength(0); - expect(contactEmailField?.hooks?.create?.expr).toBe("_value"); + expect(contactEmailField?.optionalOnCreate).toBe(true); + expect(contactEmailField?.hooks).toBeUndefined(); expect(contactEmailField?.validate ?? []).toHaveLength(0); const hookExpr = createdType?.schema?.typeHook?.create?.expr ?? ""; diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts index a8b57f544..25731ea79 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts @@ -311,10 +311,8 @@ describe("snapshot-manifest", () => { const manifest = generateTailorDBTypeManifestFromSnapshot(snapshotType); - // Per field, only a create-hook placeholder remains (to keep the field - // optional in the Create input); the real logic moves to type_hook. - expect(manifest.schema?.fields?.updatedAt?.hooks?.create?.expr).toBe("_value"); - expect(manifest.schema?.fields?.updatedAt?.hooks?.update).toBeUndefined(); + expect(manifest.schema?.fields?.updatedAt?.optionalOnCreate).toBe(true); + expect(manifest.schema?.fields?.updatedAt?.hooks).toBeUndefined(); // They are aggregated into a single type-level script that binds a shared // timestamp once and dispatches each field's hook. @@ -376,11 +374,11 @@ describe("snapshot-manifest", () => { const displayNameField = profileField?.fields?.displayName; const emailField = profileField?.fields?.contact?.fields?.email; - // Nested fields keep only a create-hook placeholder; validators are not - // emitted per field. - expect(displayNameField?.hooks?.create?.expr).toBe("_value"); + expect(displayNameField?.optionalOnCreate).toBe(true); + expect(displayNameField?.hooks).toBeUndefined(); expect(displayNameField?.validate ?? []).toHaveLength(0); - expect(emailField?.hooks?.create?.expr).toBe("_value"); + expect(emailField?.optionalOnCreate).toBe(true); + expect(emailField?.hooks).toBeUndefined(); expect(emailField?.validate ?? []).toHaveLength(0); // Hooks are aggregated into a type-level script that reconstructs nested diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts index 4c9a152e2..44da1cc11 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts @@ -29,7 +29,7 @@ import { type TailorDBTypeSchema, } from "@tailor-platform/tailor-proto/tailordb_resource_pb"; import * as inflection from "inflection"; -import { buildTypeScripts } from "@/parser/service/tailordb/type-script"; +import { buildTypeScripts } from "#/parser/service/tailordb/type-script"; import { isSnapshotFieldRefOperand } from "./snapshot"; import type { SchemaSnapshot, @@ -185,21 +185,10 @@ export function generateTailorDBTypeManifestFromSnapshot( }; } -/** - * Field-level create-hook placeholder kept solely so the platform treats a - * required field as optional in the GraphQL Create input (it derives that from - * the presence of a field-level create hook, not from type_hook). The real hook - * logic runs in type_hook, which executes after field hooks and overwrites the - * value — so this passthrough's result is always replaced. - */ -const CREATE_INPUT_OPTIONAL_PLACEHOLDER = "_value"; - -function createInputOptionalHook( - config: Pick, -): Pick, "hooks"> | Record { - return config.hooks?.create - ? { hooks: { create: { expr: CREATE_INPUT_OPTIONAL_PLACEHOLDER } } } - : {}; +function optionalOnCreate( + config: Pick, +): Pick, "optionalOnCreate"> { + return config.hooks?.create || config.default !== undefined ? { optionalOnCreate: true } : {}; } /** @@ -210,9 +199,6 @@ function createInputOptionalHook( export function convertFieldConfigToProto( config: SnapshotFieldConfig, ): MessageInitShape { - // Field hook/validator logic is aggregated into type-level type_hook/type_validate - // scripts (see buildTypeScripts). Only a create-hook placeholder is kept per field, - // to preserve GraphQL Create-input optionality. const fieldEntry: MessageInitShape = { type: config.type, allowedValues: @@ -228,7 +214,7 @@ export function convertFieldConfigToProto( foreignKeyField: config.foreignKeyField, required: config.required, vector: config.vector ?? false, - ...createInputOptionalHook(config), + ...optionalOnCreate(config), ...(config.serial && { serial: { start: BigInt(config.serial.start), @@ -291,7 +277,7 @@ function processNestedFieldsFromSnapshot( unique: false, foreignKey: false, vector: false, - ...createInputOptionalHook(fieldConfig), + ...optionalOnCreate(fieldConfig), ...(fieldConfig.serial && { serial: { start: BigInt(fieldConfig.serial.start), diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-schema.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-schema.ts index 5820180c5..8e040c0c5 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-schema.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-schema.ts @@ -124,6 +124,7 @@ export const snapshotFieldConfigSchema: z.ZodType = z.loose validate: z.array(snapshotValidationSchema).optional(), serial: snapshotSerialSchema.optional(), scale: z.number().optional(), + default: z.unknown().optional(), fields: z.lazy(() => snapshotRecordSchema(snapshotFieldConfigSchema)).optional(), }) as z.ZodType; diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-types.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-types.ts index 50a2576a6..fca607864 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-types.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-types.ts @@ -65,6 +65,7 @@ export interface SnapshotFieldConfig { validate?: SnapshotValidation[]; serial?: SnapshotSerial; scale?: number; + default?: unknown; /** Nested fields (recursive) */ fields?: Record; } diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts index 1b2290e89..47b648325 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts @@ -357,6 +357,7 @@ function createSnapshotFieldConfig(field: ParsedField): SnapshotFieldConfig { } if (field.config.scale !== undefined) config.scale = field.config.scale; + if (field.config.default !== undefined) config.default = field.config.default; if (field.config.fields && Object.keys(field.config.fields).length > 0) { config.fields = {}; @@ -427,6 +428,7 @@ function createSnapshotFieldConfigFromOperatorConfig( } if (fieldConfig.scale !== undefined) config.scale = fieldConfig.scale; + if (fieldConfig.default !== undefined) config.default = fieldConfig.default; // Recursive for nested fields if (fieldConfig.fields && Object.keys(fieldConfig.fields).length > 0) { diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 84a86e33b..fbc51fc8a 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -530,6 +530,19 @@ describe("TailorDBField type error message tests", () => { expectTypeOf(db.string({ optional: true }).serial).toEqualTypeOf< TypeLevelError<"serial can only be set on non-array integer or string fields"> >(); + + const defaulted = db.string().default("hello"); + expectTypeOf(defaulted.default).toEqualTypeOf< + TypeLevelError<".default() has already been set"> + >(); + + expectTypeOf(db.object({ x: db.string() }).default).toEqualTypeOf< + TypeLevelError<"default cannot be set on nested type fields"> + >(); + + expectTypeOf(db.string().serial({ start: 0 }).default).toEqualTypeOf< + TypeLevelError<"default cannot be set on serial fields"> + >(); }); }); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 246256ca5..00397a616 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -57,6 +57,7 @@ export type TailorAnyDBField = Omit< index: AnyBuilderMethod; unique: AnyBuilderMethod; vector: AnyBuilderMethod; + default: AnyBuilderMethod; hooks: AnyBuilderMethod; validate: AnyBuilderMethod; serial: AnyBuilderMethod; @@ -91,6 +92,7 @@ type WithDBFieldHooks = Defined & { }; serial: false; }; +type WithDBFieldDefault = Defined & { default: true }; type WithDBFieldValidate = Defined & { validate: true }; type WithDBFieldSerial = Defined & { serial: true; @@ -247,6 +249,19 @@ type DBFieldSerialMethod : TypeLevelError<"serial can only be set on non-array integer or string fields">; +type DBFieldDefaultFn = ( + value: Output extends null ? NonNullable : Output, +) => TailorDBField, Output>; +type DBFieldDefaultMethod = + IsAny extends true + ? DBFieldDefaultFn + : Defined extends { default: unknown } + ? TypeLevelError<".default() has already been set"> + : Defined extends { type: "nested" } + ? TypeLevelError<"default cannot be set on nested type fields"> + : Defined extends { serial: true } + ? TypeLevelError<"default cannot be set on serial fields"> + : DBFieldDefaultFn; /** * Full TailorDBField interface with builder methods. @@ -303,6 +318,15 @@ export interface TailorDBField< */ vector: DBFieldVectorMethod; + /** + * Set a default value for the field on create. When the field is required, + * this makes it optional in the Create input — the default fills in when + * no value (or a nullish hook result) is provided. + * + * For datetime/date/time fields, pass `"now"` to use the operation timestamp. + */ + default: DBFieldDefaultMethod; + /** * Add hooks for create/update operations on this field. */ @@ -444,6 +468,7 @@ type TailorDBFieldRuntime = Omit index(): object; unique(): object; vector(): object; + default(value: unknown): object; hooks(hooks: Hook): object; serial(config: SerialConfig): object; clone(options?: FieldOptions): TailorDBFieldRuntime; @@ -586,6 +611,12 @@ function createTailorDBFieldRuntime< return cloneWith({ vector: true }); }, + // oxlint-disable-next-line no-explicit-any + default(value: any) { + // oxlint-disable-next-line no-explicit-any + return cloneWith({ default: value }) as any; + }, + hooks(hooks: Hook) { return cloneWith({ hooks }); }, diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index d0d8b88df..eb500edb7 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -40,6 +40,7 @@ export interface DBFieldMetadata extends FieldMetadata { serial?: SerialConfig; relation?: boolean; scale?: number; + default?: unknown; } export interface DefinedDBFieldMetadata extends DefinedFieldMetadata { @@ -55,6 +56,7 @@ export interface DefinedDBFieldMetadata extends DefinedFieldMetadata { }; serial?: boolean; relation?: boolean; + default?: boolean; } export type GqlOperationsConfig = GqlOperationsInput; @@ -152,8 +154,8 @@ type HookFn = (args: { now: Date; }) => TReturn; -export type Hook = { - create?: HookFn; +export type Hook = { + create?: HookFn; update?: HookFn; }; @@ -167,7 +169,9 @@ export type Hooks< ? never : F[K]["_defined"] extends { type: "nested" } ? never - : K]?: Hook>; + : K]?: F[K]["_defined"] extends { default: unknown } + ? Hook, output | null | undefined> + : Hook>; }>; // --- Field helper types --- diff --git a/packages/sdk/src/parser/service/tailordb/schema.ts b/packages/sdk/src/parser/service/tailordb/schema.ts index c1de82633..11f5f3fbb 100644 --- a/packages/sdk/src/parser/service/tailordb/schema.ts +++ b/packages/sdk/src/parser/service/tailordb/schema.ts @@ -100,6 +100,7 @@ export const DBFieldMetadataSchema = z.strictObject({ .max(12) .optional() .describe("Decimal scale (number of digits after decimal point, 0-12)"), + default: z.unknown().optional().describe("Default value for the field on create"), }); const RelationTypeSchema = z.enum(relationTypesKeys); diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index 886aedffa..730a78ef5 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -62,6 +62,47 @@ describe("buildTypeScripts", () => { expect(createExpr).toContain('((_input["profile"] || {})["contact"] || {})["email"]'); }); + test("applies default as ?? fallback after hook on create only", () => { + const fields: Record = { + status: { + type: "enum", + default: "active", + hooks: { create: { expr: "_value" } }, + }, + name: { + type: "string", + default: "unnamed", + }, + }; + + const { typeHook } = buildTypeScripts(fields); + const createExpr = typeHook?.create?.expr ?? ""; + + // hook + default: hookResult ?? defaultValue + expect(createExpr).toContain('"status": ((_value) => (_value))(_input["status"]) ?? "active"'); + // default only: input ?? defaultValue + expect(createExpr).toContain('"name": _input["name"] ?? "unnamed"'); + + // defaults are create-only — update script should not include them + expect(typeHook?.update).toBeUndefined(); + }); + + test("uses _now for datetime/date/time defaults with 'now'", () => { + const fields: Record = { + createdAt: { type: "datetime", default: "now" }, + startDate: { type: "date", default: "now" }, + startTime: { type: "time", default: "now" }, + label: { type: "string", default: "now" }, + }; + + const createExpr = buildTypeScripts(fields).typeHook?.create?.expr ?? ""; + expect(createExpr).toContain('"createdAt": _input["createdAt"] ?? _now'); + expect(createExpr).toContain('"startDate": _input["startDate"] ?? _now'); + expect(createExpr).toContain('"startTime": _input["startTime"] ?? _now'); + // "now" on a string field is just a literal string, not _now + expect(createExpr).toContain('"label": _input["label"] ?? "now"'); + }); + test("builds a validate script keyed by dotted path with negated boolean checks", () => { const fields: Record = { age: { diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index 76ed10c20..6d252e960 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -3,6 +3,8 @@ const INPUT = "_input"; // Shared operation timestamp bound once per script execution. const NOW = "_now"; +const TIME_TYPES = new Set(["datetime", "date", "time"]); + type HookOperation = "create" | "update"; interface ScriptRef { @@ -20,6 +22,7 @@ export interface ScriptFieldConfig { update?: ScriptRef; }; validate?: { script?: ScriptRef; errorMessage: string }[]; + default?: unknown; fields?: Record; } @@ -33,13 +36,22 @@ const key = (name: string) => JSON.stringify(name); const isNestedType = (config: ScriptFieldConfig): boolean => config.type === "nested" && config.fields !== undefined; +function serializeDefault(value: unknown, fieldType: string): string { + if (value === "now" && TIME_TYPES.has(fieldType)) return NOW; + if (value instanceof Date) return `new Date(${JSON.stringify(value.toISOString())})`; + return JSON.stringify(value); +} + /** - * Build the object literal that reconstructs one record level with hook overrides applied. - * Returns null when no field under this level has a hook for the operation. - * @param fields - Field configs at the current level. - * @param accessExpr - JavaScript expression for the current object level. - * @param operation - "create" or "update". - * @returns Object-literal source, or null when nothing under this level is hooked. + * Build the object literal that reconstructs one record level with hook + * overrides and defaults applied. For create, defaults are appended as + * `?? defaultValue` after the hook expression (field hook → default). + * Returns null when no field under this level has a hook or default for + * the operation. + * @param {Record} fields - Field configurations + * @param {string} accessExpr - JS expression to access the parent object + * @param {HookOperation} operation - Hook operation type + * @returns {string | null} Object literal expression or null */ function buildHookObject( fields: Record, @@ -59,8 +71,16 @@ function buildHookObject( } const hook = config.hooks?.[operation]; - if (hook) { + const hasDefault = operation === "create" && config.default !== undefined; + + if (hook && hasDefault) { + parts.push( + `${key(name)}: ((_value) => (${hook.expr}))(${access}) ?? ${serializeDefault(config.default, config.type)}`, + ); + } else if (hook) { parts.push(`${key(name)}: ((_value) => (${hook.expr}))(${access})`); + } else if (hasDefault) { + parts.push(`${key(name)}: ${access} ?? ${serializeDefault(config.default, config.type)}`); } } @@ -72,10 +92,10 @@ function buildHookObject( * Build validation statements for one record level. * Each leaf field with validators contributes a block that records the first * failing message keyed by its dotted field path. - * @param fields - Field configs at the current level. - * @param accessExpr - JavaScript expression for the current object level. - * @param keyPrefix - Dotted path prefix for nested fields. - * @returns One statement per leaf field that has validators. + * @param {Record} fields - Field configurations + * @param {string} accessExpr - JS expression to access the parent object + * @param {string} keyPrefix - Dotted path prefix for error keys + * @returns {string[]} Array of validation statement strings */ function buildValidateStatements( fields: Record, @@ -120,12 +140,13 @@ function wrapValidate(statements: string[]): string { } /** - * Aggregate every field's create/update hook and validate into type-level scripts. - * Hooks compute a single shared timestamp (`now`) per operation, so all fields - * touched in one create/update observe the same instant. Validators run with the - * same rules on create and update. - * @param fields - Field configs keyed by field name (parser or snapshot shape). - * @returns Type-level hook/validate scripts, omitting empties. + * Aggregate every field's create/update hook, default, and validate into + * type-level scripts. Hooks compute a single shared timestamp (`now`) per + * operation, so all fields touched in one create/update observe the same + * instant. Defaults are applied after hooks on create only. Validators + * run with the same rules on create and update. + * @param {Record} fields - Field configurations + * @returns {TypeScripts} Aggregated type-level scripts */ export function buildTypeScripts(fields: Record): TypeScripts { const result: TypeScripts = {}; diff --git a/packages/sdk/src/parser/service/tailordb/types.ts b/packages/sdk/src/parser/service/tailordb/types.ts index c478ca7e0..347900578 100644 --- a/packages/sdk/src/parser/service/tailordb/types.ts +++ b/packages/sdk/src/parser/service/tailordb/types.ts @@ -79,6 +79,7 @@ export interface OperatorFieldConfig { format?: string; }; scale?: number; + default?: unknown; fields?: Record; } diff --git a/packages/sdk/src/types/tailordb.generated.ts b/packages/sdk/src/types/tailordb.generated.ts index 40984ceb8..6c9e498e2 100644 --- a/packages/sdk/src/types/tailordb.generated.ts +++ b/packages/sdk/src/types/tailordb.generated.ts @@ -68,6 +68,8 @@ export type DBFieldMetadata = { | undefined; /** Decimal scale (number of digits after decimal point, 0-12) */ scale?: number | undefined; + /** Default value for the field on create */ + default?: unknown; }; export type DBFieldMetadataInput = DBFieldMetadata; From 7189ba230ecf915922d49d7ac645b4f75ff53ec5 Mon Sep 17 00:00:00 2001 From: "tailor-platform-pr-trigger[bot]" <247949890+tailor-platform-pr-trigger[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 10:42:04 +0000 Subject: [PATCH 435/618] chore: run generate --- packages/sdk/src/types/tailordb.generated.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sdk/src/types/tailordb.generated.ts b/packages/sdk/src/types/tailordb.generated.ts index 6c9e498e2..e3fdf576b 100644 --- a/packages/sdk/src/types/tailordb.generated.ts +++ b/packages/sdk/src/types/tailordb.generated.ts @@ -1064,6 +1064,7 @@ export type TailorDBTypeRaw = { } | undefined; scale?: number | undefined | undefined; + default?: unknown; }; rawRelation?: | { From 890c6d042ad19c9aff344d82620652ec6c75950f Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 1 Jul 2026 12:48:07 +0900 Subject: [PATCH 436/618] feat(tailordb): add type-level validate with issues() API and simplify field validators - Add type-level validate function form to .validate() accepting (args, issues) callback - Field validators now return string (error) | void (pass) instead of [fn, message] tuple - Type-safe field path autocomplete for issues() via DottedPaths utility type - Full pipeline: configure -> parser -> bundler -> snapshot -> manifest - Hook functions use newRecord/oldRecord args matching platform _input/_oldRecord variables - Field validators receive { newValue, oldValue } only --- example/resolvers/add.ts | 10 +- example/resolvers/stepChain.ts | 14 +- example/tailordb/customer.ts | 18 +- example/tailordb/file.ts | 4 +- .../inventory-management/src/db/inventory.ts | 2 +- .../inventory-management/src/db/orderItem.ts | 8 +- .../templates/tailordb/src/db/comment.ts | 4 +- .../templates/tailordb/src/db/task.ts | 33 +-- .../perf/features/tailordb-validate.ts | 100 +++++--- .../deploy/__test_fixtures__/resolvers/add.ts | 10 +- .../commands/deploy/tailordb/index.test.ts | 5 +- .../migrate/snapshot-manifest.test.ts | 10 +- .../tailordb/migrate/snapshot-manifest.ts | 5 +- .../tailordb/migrate/snapshot-types.ts | 1 + .../cli/commands/tailordb/migrate/snapshot.ts | 4 + .../tailordb/hooks-validate-bundler.ts | 31 ++- .../services/tailordb/schema.test.ts | 231 ++++++++++++------ .../src/configure/services/tailordb/schema.ts | 37 ++- .../src/configure/services/tailordb/types.ts | 31 ++- .../sdk/src/configure/types/field.types.ts | 36 +-- .../src/configure/types/type-typename.test.ts | 2 +- packages/sdk/src/configure/types/type.test.ts | 16 +- .../tailordb/field.precompiled.test.ts | 3 +- .../sdk/src/parser/service/tailordb/field.ts | 34 +-- .../sdk/src/parser/service/tailordb/schema.ts | 6 +- .../parser/service/tailordb/type-parser.ts | 5 +- .../service/tailordb/type-script.test.ts | 48 +++- .../parser/service/tailordb/type-script.ts | 44 ++-- .../sdk/src/parser/service/tailordb/types.ts | 1 + packages/sdk/src/types/tailordb.generated.ts | 6 +- packages/sdk/src/utils/test/index.test.ts | 10 +- packages/sdk/src/utils/test/index.ts | 4 +- 32 files changed, 487 insertions(+), 286 deletions(-) diff --git a/example/resolvers/add.ts b/example/resolvers/add.ts index 2656931ca..b5eb462f9 100644 --- a/example/resolvers/add.ts +++ b/example/resolvers/add.ts @@ -1,9 +1,11 @@ import { createResolver, t } from "@tailor-platform/sdk"; -const validators: [(a: { value: number }) => boolean, string][] = [ - [({ value }) => value >= 0, "Value must be non-negative"], - [({ value }) => value < 10, "Value must be less than 10"], -]; +const validators = [ + ({ newValue }: { newValue: number }) => + newValue >= 0 ? undefined : "Value must be non-negative", + ({ newValue }: { newValue: number }) => + newValue < 10 ? undefined : "Value must be less than 10", +] as const; export default createResolver({ name: "add", description: "Addition operation", diff --git a/example/resolvers/stepChain.ts b/example/resolvers/stepChain.ts index 123cdc864..ac3f4de28 100644 --- a/example/resolvers/stepChain.ts +++ b/example/resolvers/stepChain.ts @@ -14,17 +14,15 @@ export default createResolver({ first: t .string() .description("User's first name") - .validate([ - ({ value }) => value.length >= 2, - "First name must be at least 2 characters", - ]), + .validate(({ newValue }) => + newValue.length >= 2 ? undefined : "First name must be at least 2 characters", + ), last: t .string() .description("User's last name") - .validate([ - ({ value }) => value.length >= 2, - "Last name must be at least 2 characters", - ]), + .validate(({ newValue }) => + newValue.length >= 2 ? undefined : "Last name must be at least 2 characters", + ), }) .description("User's full name"), activatedAt: t.datetime({ optional: true }).description("User activation timestamp"), diff --git a/example/tailordb/customer.ts b/example/tailordb/customer.ts index efde41cb1..ef6e8cfb3 100644 --- a/example/tailordb/customer.ts +++ b/example/tailordb/customer.ts @@ -10,8 +10,10 @@ export const customer = db postalCode: db.string(), address: db.string({ optional: true }), city: db.string({ optional: true }).validate( - ({ value }) => (value ? value.length > 1 : true), - ({ value }) => (value ? value.length < 100 : true), + ({ newValue }) => + newValue && newValue.length <= 1 ? "City must be longer than 1 character" : undefined, + ({ newValue }) => + newValue && newValue.length >= 100 ? "City must be shorter than 100 characters" : undefined, ), fullAddress: db.string(), state: db.string(), @@ -19,12 +21,18 @@ export const customer = db }) .hooks({ fullAddress: { - create: ({ data }) => `${data.postalCode} ${data.address} ${data.city}`, - update: ({ data }) => `${data.postalCode} ${data.address} ${data.city}`, + create: ({ newRecord }) => `${newRecord.postalCode} ${newRecord.address} ${newRecord.city}`, + update: ({ newRecord }) => `${newRecord.postalCode} ${newRecord.address} ${newRecord.city}`, }, }) .validate({ - name: [({ value }) => value.length > 5, "Name must be longer than 5 characters"], + name: ({ newValue }) => + newValue.length <= 5 ? "Name must be longer than 5 characters" : undefined, + }) + .validate(({ newRecord }, issues) => { + if (newRecord.country === "JP" && !newRecord.postalCode) { + issues("postalCode", "Postal code is required for Japan"); + } }) .permission(defaultPermission) .gqlPermission(defaultGqlPermission); diff --git a/example/tailordb/file.ts b/example/tailordb/file.ts index 38726367c..25a8a1809 100644 --- a/example/tailordb/file.ts +++ b/example/tailordb/file.ts @@ -4,7 +4,9 @@ export const attachedFiles = db.object( { id: db.uuid(), name: db.string(), - size: db.int().validate(({ value }) => value > 0), + size: db + .int() + .validate(({ newValue }) => (newValue <= 0 ? "Size must be positive" : undefined)), type: db.enum(["text", "image"]), }, { array: true }, diff --git a/packages/create-sdk/templates/inventory-management/src/db/inventory.ts b/packages/create-sdk/templates/inventory-management/src/db/inventory.ts index 6deb5d780..a708e73ff 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/inventory.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/inventory.ts @@ -11,7 +11,7 @@ export const inventory = db quantity: db .int() .description("Quantity of the product in inventory") - .validate(({ value }) => value >= 0), + .validate(({ newValue }) => (newValue < 0 ? "Quantity must be non-negative" : undefined)), ...db.fields.timestamps(), }) .permission(permissionLoggedIn) diff --git a/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts b/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts index 2fc8c572e..b6b81686b 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts @@ -16,18 +16,18 @@ export const orderItem = db quantity: db .int() .description("Quantity of the product") - .validate(({ value }) => value >= 0), + .validate(({ newValue }) => (newValue < 0 ? "Quantity must be non-negative" : undefined)), unitPrice: db .float() .description("Unit price of the product") - .validate(({ value }) => value >= 0), + .validate(({ newValue }) => (newValue < 0 ? "Unit price must be non-negative" : undefined)), totalPrice: db.float({ optional: true }).description("Total price of the order item"), ...db.fields.timestamps(), }) .hooks({ totalPrice: { - create: ({ data }) => (data?.quantity ?? 0) * (data.unitPrice ?? 0), - update: ({ data }) => (data?.quantity ?? 0) * (data.unitPrice ?? 0), + create: ({ newRecord }) => (newRecord?.quantity ?? 0) * (newRecord.unitPrice ?? 0), + update: ({ newRecord }) => (newRecord?.quantity ?? 0) * (newRecord.unitPrice ?? 0), }, }) .permission(permissionLoggedIn) diff --git a/packages/create-sdk/templates/tailordb/src/db/comment.ts b/packages/create-sdk/templates/tailordb/src/db/comment.ts index 5f8f18067..028ba2b06 100644 --- a/packages/create-sdk/templates/tailordb/src/db/comment.ts +++ b/packages/create-sdk/templates/tailordb/src/db/comment.ts @@ -5,7 +5,9 @@ import { user } from "./user"; export const comment = db .type("Comment", "A comment on a task", { - body: db.string().validate([({ value }) => value.length >= 1, "Comment must not be empty"]), + body: db + .string() + .validate(({ newValue }) => (newValue.length < 1 ? "Comment must not be empty" : undefined)), taskId: db.uuid().relation({ type: "n-1", toward: { type: task }, diff --git a/packages/create-sdk/templates/tailordb/src/db/task.ts b/packages/create-sdk/templates/tailordb/src/db/task.ts index 4e4a56143..13dd98b66 100644 --- a/packages/create-sdk/templates/tailordb/src/db/task.ts +++ b/packages/create-sdk/templates/tailordb/src/db/task.ts @@ -5,12 +5,11 @@ import { user } from "./user"; export const task = db .type("Task", "A task with comprehensive features", { - title: db - .string() - .validate( - [({ value }) => value.length >= 3, "Title must be at least 3 characters"], - [({ value }) => value.length <= 200, "Title must be at most 200 characters"], - ), + title: db.string().validate( + ({ newValue }) => (newValue.length >= 3 ? undefined : "Title must be at least 3 characters"), + ({ newValue }) => + newValue.length <= 200 ? undefined : "Title must be at most 200 characters", + ), description: db.string({ optional: true }), status: db.enum([ { value: "TODO", description: "Not started" }, @@ -18,12 +17,10 @@ export const task = db { value: "DONE", description: "Completed" }, { value: "CANCELLED", description: "No longer needed" }, ]), - priority: db - .int() - .validate( - [({ value }) => value >= 0, "Priority must be non-negative"], - [({ value }) => value <= 4, "Priority must be at most 4"], - ), + priority: db.int().validate( + ({ newValue }) => (newValue >= 0 ? undefined : "Priority must be non-negative"), + ({ newValue }) => (newValue <= 4 ? undefined : "Priority must be at most 4"), + ), dueDate: db.datetime({ optional: true }), assigneeId: db.uuid({ optional: true }).relation({ type: "n-1", @@ -45,14 +42,10 @@ export const task = db { fields: ["status", "priority"], unique: false }, { fields: ["assigneeId", "status"], unique: false, name: "task_assignee_status_idx" }, ) - .validate({ - status: [ - ({ value, data }) => { - const d = data as { dueDate: string | null }; - return !(value === "DONE" && d.dueDate === null); - }, - "Completed tasks must have a due date", - ], + .validate(({ newRecord }, issues) => { + if (newRecord.status === "DONE" && !newRecord.dueDate) { + issues("status", "Completed tasks must have a due date"); + } }) .permission(rolePermission) .gqlPermission(roleGqlPermission); diff --git a/packages/sdk/scripts/perf/features/tailordb-validate.ts b/packages/sdk/scripts/perf/features/tailordb-validate.ts index e4eacf505..bf24b0c77 100644 --- a/packages/sdk/scripts/perf/features/tailordb-validate.ts +++ b/packages/sdk/scripts/perf/features/tailordb-validate.ts @@ -6,61 +6,101 @@ import { db } from "../../../src/configure"; export const type0 = db.type("Type0", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), + name: db + .string() + .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + email: db + .string() + .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), }); export const type1 = db.type("Type1", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), + name: db + .string() + .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + email: db + .string() + .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), }); export const type2 = db.type("Type2", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), + name: db + .string() + .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + email: db + .string() + .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), }); export const type3 = db.type("Type3", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), + name: db + .string() + .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + email: db + .string() + .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), }); export const type4 = db.type("Type4", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), + name: db + .string() + .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + email: db + .string() + .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), }); export const type5 = db.type("Type5", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), + name: db + .string() + .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + email: db + .string() + .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), }); export const type6 = db.type("Type6", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), + name: db + .string() + .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + email: db + .string() + .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), }); export const type7 = db.type("Type7", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), + name: db + .string() + .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + email: db + .string() + .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), }); export const type8 = db.type("Type8", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), + name: db + .string() + .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + email: db + .string() + .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), }); export const type9 = db.type("Type9", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), + name: db + .string() + .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + email: db + .string() + .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), }); diff --git a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/add.ts b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/add.ts index 2656931ca..b5eb462f9 100644 --- a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/add.ts +++ b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/add.ts @@ -1,9 +1,11 @@ import { createResolver, t } from "@tailor-platform/sdk"; -const validators: [(a: { value: number }) => boolean, string][] = [ - [({ value }) => value >= 0, "Value must be non-negative"], - [({ value }) => value < 10, "Value must be less than 10"], -]; +const validators = [ + ({ newValue }: { newValue: number }) => + newValue >= 0 ? undefined : "Value must be non-negative", + ({ newValue }: { newValue: number }) => + newValue < 10 ? undefined : "Value must be less than 10", +] as const; export default createResolver({ name: "add", description: "Addition operation", diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts b/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts index 4933c60ef..9d721817d 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts @@ -450,8 +450,9 @@ describe("planTailorDB (service level)", () => { expect(hookExpr).toContain("(_value ?? '').toLowerCase()"); const validateExpr = createdType?.schema?.typeValidate?.create?.expr ?? ""; - expect(validateExpr).toContain('__errs["profile.displayName"] = "Display name is required"'); - expect(validateExpr).toContain('__errs["profile.contact.email"] = "Email must contain @"'); + expect(validateExpr).toContain('__errs["profile.displayName"]'); + expect(validateExpr).toContain('__errs["profile.contact.email"]'); + expect(validateExpr).toContain('if (typeof __r === "string")'); }); }); diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts index 25731ea79..ee35bce05 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts @@ -392,12 +392,12 @@ describe("snapshot-manifest", () => { ); expect(hookExpr).toContain("(_value ?? '').toLowerCase()"); - // Validators are aggregated into a type-level validate script keyed by the - // dotted field path, with the boolean expression negated into a failure. + // Validators are aggregated into a type-level validate script using ?? chain. const validateExpr = manifest.schema?.typeValidate?.create?.expr ?? ""; - expect(validateExpr).toContain('__errs["profile.displayName"] = "Display name is required"'); - expect(validateExpr).toContain("if (!(((_value ?? '').length > 0)))"); - expect(validateExpr).toContain('__errs["profile.contact.email"] = "Email must contain @"'); + expect(validateExpr).toContain('__errs["profile.displayName"]'); + expect(validateExpr).toContain("((_value ?? '').length > 0)"); + expect(validateExpr).toContain('if (typeof __r === "string")'); + expect(validateExpr).toContain('__errs["profile.contact.email"]'); expect(manifest.schema?.typeValidate?.update?.expr).toBe(validateExpr); }); diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts index 44da1cc11..efac3b739 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts @@ -165,7 +165,10 @@ export function generateTailorDBTypeManifestFromSnapshot( // Field hooks/validators are aggregated into type-level scripts so that a // single shared timestamp is observed across every field in one operation. - const { typeHook, typeValidate } = buildTypeScripts(snapshotType.fields); + const { typeHook, typeValidate } = buildTypeScripts( + snapshotType.fields, + snapshotType.typeValidateExpr, + ); return { name: snapshotType.name, diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-types.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-types.ts index fca607864..1e4c6501f 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-types.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-types.ts @@ -219,6 +219,7 @@ export interface TailorDBSnapshotType { record?: SnapshotRecordPermission; gql?: SnapshotGqlPermission; }; + typeValidateExpr?: string; } export type SnapshotSettings = NonNullable; diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts index 47b648325..9ba5f1ee8 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts @@ -503,6 +503,10 @@ export function createSnapshotType(type: TailorDBType): TailorDBSnapshotType { snapshotType.files = { ...type.files }; } + if (type.typeValidateExpr) { + snapshotType.typeValidateExpr = type.typeValidateExpr; + } + if (Object.keys(type.forwardRelationships).length > 0) { snapshotType.forwardRelationships = {}; for (const [relName, rel] of Object.entries(type.forwardRelationships)) { diff --git a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts index c4ef0503c..d05c33e3e 100644 --- a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts +++ b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts @@ -24,7 +24,7 @@ type ScriptFunction = (...args: unknown[]) => unknown; type ScriptTarget = { fn: ScriptFunction; - kind: "hooks" | "validate"; + kind: "hooks" | "validate" | "typeValidate"; }; /** Binding found in the source file: either an import or a top-level declaration */ @@ -105,13 +105,8 @@ function collectScriptTargets(type: TailorDBTypeSchemaOutput): ScriptTarget[] { } for (const validateInput of metadata.validate ?? []) { - if (typeof validateInput === "function") { - const validateFn = toScriptFunction(validateInput); - if (validateFn) targets.push({ fn: validateFn, kind: "validate" }); - } else { - const validateFn = toScriptFunction(validateInput[0]); - if (validateFn) targets.push({ fn: validateFn, kind: "validate" }); - } + const validateFn = toScriptFunction(validateInput); + if (validateFn) targets.push({ fn: validateFn, kind: "validate" }); } if (field.type === "nested" && field.fields) { @@ -125,6 +120,11 @@ function collectScriptTargets(type: TailorDBTypeSchemaOutput): ScriptTarget[] { collectFieldTargets(field); } + const typeValidateFn = toScriptFunction(type.metadata.typeValidate); + if (typeValidateFn) { + targets.push({ fn: typeValidateFn, kind: "typeValidate" }); + } + return targets; } @@ -403,6 +403,7 @@ function buildPrecompiledExpr(bundleCode: string, argsObject: string): string { * @param declarations - Declaration statement texts. * @param fnSource - The function source code. * @param sourceFilePath - Path to the source file for resolving relative imports. + * @param multiArg - Whether the function accepts multiple arguments (spread via `...args`). * @returns Entry file content string. */ export function buildMinimalEntryFromResolved( @@ -410,6 +411,7 @@ export function buildMinimalEntryFromResolved( declarations: string[], fnSource: string, sourceFilePath: string, + multiArg = false, ): string { const sourceDir = resolve(sourceFilePath, "..").replace(/\\/g, "/"); @@ -424,14 +426,16 @@ export function buildMinimalEntryFromResolved( const lines = [ ...resolvedImports, ...declarations, - `export function main(input) { return (${fnSource})(input); }`, + multiArg + ? `export function main(...args) { return (${fnSource})(...args); }` + : `export function main(input) { return (${fnSource})(input); }`, ]; return lines.join("\n"); } async function bundleScriptTarget(args: { fn: ScriptFunction; - kind: "hooks" | "validate"; + kind: "hooks" | "validate" | "typeValidate"; sourceFilePath: string; sourceBindings: Map; tempDir: string; @@ -443,8 +447,10 @@ async function bundleScriptTarget(args: { const fnSource = stringifyFunction(fn); const argsObject = kind === "hooks" - ? `{ value: _value, data: _data, invoker: ${tailorPrincipalMap}, now: _now }` - : `{ value: _value, data: _data, invoker: ${tailorPrincipalMap} }`; + ? `{ value: _value, newRecord: _input, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap}, now: _now }` + : kind === "validate" + ? `{ newValue: _value, oldValue: _oldValue }` + : `{ newRecord: _newRecord, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap} }, __issues`; const inlineExpr = assertParsableExpression( `(${fnSource})(${argsObject})`, context, @@ -471,6 +477,7 @@ async function bundleScriptTarget(args: { declarations, fnSource, sourceFilePath, + kind === "typeValidate", ); const entryPath = join(tempDir, `tailordb-script-${targetIndex}.entry.ts`); diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index fbc51fc8a..381955101 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -2,7 +2,7 @@ import { describe, expectTypeOf, expect, test } from "vitest"; import { t } from "#/configure/types/index"; import { db, type TailorAnyDBField } from "./schema"; -import type { FieldValidateInput, ValidateConfig } from "#/configure/types/field.types"; +import type { FieldValidateInput } from "#/configure/types/field.types"; import type { DateString, DateTimeString, @@ -513,7 +513,7 @@ describe("TailorDBField type error message tests", () => { TypeLevelError<"hooks cannot be set on nested type fields"> >(); - const validated = db.string().validate(() => true); + const validated = db.string().validate(() => undefined); expectTypeOf(validated.validate).toEqualTypeOf< TypeLevelError<".validate() has already been set"> >(); @@ -620,7 +620,7 @@ describe("TailorDBField hooks modifier tests", () => { describe("TailorDBField validate modifier tests", () => { test("validate modifier does not affect type", () => { const _validateType = db.type("Test", { - email: db.string().validate(() => true), + email: db.string().validate(() => undefined), }); expectTypeOf>().toEqualTypeOf<{ id: UUIDString; @@ -628,45 +628,40 @@ describe("TailorDBField validate modifier tests", () => { }>(); }); - test("validate modifier can receive object with message", () => { + test("validate modifier can receive function returning error message", () => { const _validateType = db.type("Test", { - email: db.string().validate([({ value }) => value.includes("@"), "Email must contain @"]), + email: db + .string() + .validate(({ newValue }) => (!newValue.includes("@") ? "Email must contain @" : undefined)), }); expectTypeOf>().toEqualTypeOf<{ id: UUIDString; email: string; }>(); - // Validate that the validation is stored correctly in metadata const fieldMetadata = _validateType.fields.email.metadata; expect(fieldMetadata.validate).toBeDefined(); expect(fieldMetadata.validate).toHaveLength(1); - // Error message is part of the tuple [fn, message] - expect(fieldMetadata.validate?.[0]).toEqual([expect.any(Function), "Email must contain @"]); }); test("validate modifier can receive multiple validators", () => { const _validateType = db.type("Test", { - password: db - .string() - .validate( - ({ value }) => value.length >= 8, - [({ value }) => /[A-Z]/.test(value), "Password must contain uppercase letter"], - ), + password: db.string().validate( + ({ newValue }) => + newValue.length < 8 ? "Password must be at least 8 characters" : undefined, + ({ newValue }) => + !/[A-Z]/.test(newValue) ? "Password must contain uppercase letter" : undefined, + ), }); const fieldMetadata = _validateType.fields.password.metadata; expect(fieldMetadata.validate).toHaveLength(2); - // Second validator is a tuple [fn, errorMessage] - expect((fieldMetadata.validate?.[1] as [unknown, string])[1]).toBe( - "Password must contain uppercase letter", - ); }); test("calling validate modifier more than once causes type error", () => { - const validated = db.string().validate(() => true); + const validated = db.string().validate(() => undefined); // @ts-expect-error validate() cannot be called after validate() has already been called - validated.validate(() => true); + validated.validate(() => undefined); }); test("validate modifier on string field receives string", () => { @@ -783,7 +778,13 @@ describe("TailorDBType withTimestamps option tests", () => { expect(createHook).toBeDefined(); const specified = new Date("2025-02-10T09:00:00Z"); - const result = createHook!({ value: specified, data: {}, invoker: timestampHookInvoker }); + const result = createHook!({ + value: specified, + newRecord: {}, + oldRecord: null, + invoker: timestampHookInvoker, + now: new Date(), + }); expect(result).toBe(specified); }); @@ -793,7 +794,13 @@ describe("TailorDBType withTimestamps option tests", () => { expect(createHook).toBeDefined(); const before = Date.now(); - const result = createHook!({ value: null, data: {}, invoker: timestampHookInvoker }); + const result = createHook!({ + value: null, + newRecord: {}, + oldRecord: null, + invoker: timestampHookInvoker, + now: new Date(), + }); const after = Date.now(); expect(result).toBeInstanceOf(Date); expect((result as Date).getTime()).toBeGreaterThanOrEqual(before); @@ -806,7 +813,13 @@ describe("TailorDBType withTimestamps option tests", () => { expect(createHook).toBeDefined(); const specified = new Date("2025-02-10T09:00:00Z"); - const result = createHook!({ value: specified, data: {}, invoker: timestampHookInvoker }); + const result = createHook!({ + value: specified, + newRecord: {}, + oldRecord: null, + invoker: timestampHookInvoker, + now: new Date(), + }); expect(result).toBe(specified); }); @@ -816,7 +829,13 @@ describe("TailorDBType withTimestamps option tests", () => { expect(createHook).toBeDefined(); const before = Date.now(); - const result = createHook!({ value: null, data: {}, invoker: timestampHookInvoker }); + const result = createHook!({ + value: null, + newRecord: {}, + oldRecord: null, + invoker: timestampHookInvoker, + now: new Date(), + }); const after = Date.now(); expect(result).toBeInstanceOf(Date); expect((result as Date).getTime()).toBeGreaterThanOrEqual(before); @@ -829,7 +848,13 @@ describe("TailorDBType withTimestamps option tests", () => { expect(updateHook).toBeDefined(); const before = Date.now(); - const result = updateHook!({ value: null, data: {}, invoker: timestampHookInvoker }); + const result = updateHook!({ + value: null, + newRecord: {}, + oldRecord: null, + invoker: timestampHookInvoker, + now: new Date(), + }); const after = Date.now(); expect(result).toBeInstanceOf(Date); expect((result as Date).getTime()).toBeGreaterThanOrEqual(before); @@ -1054,14 +1079,13 @@ describe("TailorDBType plural form tests", () => { email: db.string(), }) .validate({ - name: [({ value }) => value.length > 0], - email: [({ value }) => value.includes("@"), "Invalid email format"], + name: ({ newValue }) => (newValue.length <= 0 ? "Name must not be empty" : undefined), + email: ({ newValue }) => (!newValue.includes("@") ? "Invalid email format" : undefined), }); expect(_userType.name).toBe("User"); expect(_userType.metadata.settings?.pluralForm).toBe("Users"); - // Validate that the validation function is stored correctly in metadata const emailMetadata = _userType.fields.email.metadata; expect(emailMetadata.validate).toBeDefined(); expect(emailMetadata.validate).toHaveLength(1); @@ -1172,7 +1196,7 @@ describe("TailorDBType validate modifier tests", () => { email: db.string(), }) .validate({ - email: () => true, + email: () => undefined, }); expectTypeOf>().toEqualTypeOf<{ @@ -1183,19 +1207,17 @@ describe("TailorDBType validate modifier tests", () => { expect(fieldMetadata.validate).toHaveLength(1); }); - test("validate modifier can receive object with message", () => { + test("validate modifier can receive function returning error message", () => { const _validateType = db .type("Test", { email: db.string(), }) .validate({ - email: [({ value }) => value.includes("@"), "Email must contain @"], + email: ({ newValue }) => (!newValue.includes("@") ? "Email must contain @" : undefined), }); const fieldMetadata = _validateType.fields.email.metadata; expect(fieldMetadata.validate).toHaveLength(1); - // Validator is a tuple [fn, errorMessage] - expect((fieldMetadata.validate?.[0] as [unknown, string])[1]).toBe("Email must contain @"); }); test("validate modifier can receive multiple validators", () => { @@ -1205,25 +1227,23 @@ describe("TailorDBType validate modifier tests", () => { }) .validate({ password: [ - ({ value }) => value.length >= 8, - [({ value }) => /[A-Z]/.test(value), "Password must contain uppercase letter"], + ({ newValue }) => + newValue.length < 8 ? "Password must be at least 8 characters" : undefined, + ({ newValue }) => + !/[A-Z]/.test(newValue) ? "Password must contain uppercase letter" : undefined, ], }); const fieldMetadata = _validateType.fields.password.metadata; expect(fieldMetadata.validate).toHaveLength(2); - // Second validator is a tuple [fn, errorMessage] - expect((fieldMetadata.validate?.[1] as [unknown, string])[1]).toBe( - "Password must contain uppercase letter", - ); }); test("type error occurs when validate is already set on TailorDBField", () => { db.type("Test", { - name: db.string().validate(() => true), + name: db.string().validate(() => undefined), // @ts-expect-error validate() cannot be called after validate() has already been called }).validate({ - name: () => true, + name: () => undefined, }); }); @@ -1232,24 +1252,100 @@ describe("TailorDBType validate modifier tests", () => { name: db.string(), }).validate({ // @ts-expect-error validate() cannot be called on the "id" field - id: () => true, + id: () => undefined, }); }); test("validate modifier on string field receives string", () => { - const _validate = db.type("Test", { name: db.string() }).validate; - expectTypeOf>().toExtend< - Parameters[0]["name"] - >(); + const testType = db.type("Test", { name: db.string() }); + testType.validate({ + name: ({ newValue }) => { + expectTypeOf(newValue).toEqualTypeOf(); + return undefined; + }, + }); }); test("validate modifier on optional field receives null", () => { - const _validate = db.type("Test", { + const testType = db.type("Test", { name: db.string({ optional: true }), - }).validate; - expectTypeOf< - ValidateConfig - >().toExtend[0]["name"]>(); + }); + testType.validate({ + name: ({ newValue }) => { + expectTypeOf(newValue).toEqualTypeOf(); + return undefined; + }, + }); + }); +}); + +describe("TailorDBType type-level validate (function form) tests", () => { + test("accepts type-level validate function", () => { + const _type = db + .type("Test", { + name: db.string(), + email: db.string(), + }) + .validate(({ newRecord }, issues) => { + if (!newRecord.name) issues("name", "Name is required"); + if (!newRecord.email) issues("email", "Email is required"); + }); + + expectTypeOf>().toEqualTypeOf<{ + id: UUIDString; + name: string; + email: string; + }>(); + }); + + test("issues function only accepts valid field paths", () => { + db.type("Test", { + name: db.string(), + email: db.string(), + }).validate(({ newRecord: _newRecord }, issues) => { + issues("name", "ok"); + issues("email", "ok"); + // @ts-expect-error "nonexistent" is not a valid field path + issues("nonexistent", "bad"); + }); + }); + + test("issues function accepts dotted paths for nested fields", () => { + db.type("Test", { + profile: db.object({ + displayName: db.string(), + email: db.string(), + }), + }).validate((_args, issues) => { + issues("profile", "ok"); + issues("profile.displayName", "ok"); + issues("profile.email", "ok"); + // @ts-expect-error "profile.nonexistent" is not a valid field path + issues("profile.nonexistent", "bad"); + }); + }); + + test("type-level validate function stores in metadata", () => { + const type = db + .type("Test", { + name: db.string(), + }) + .validate((_args, _issues) => {}); + + expect(type.metadata.typeValidate).toBeDefined(); + }); + + test("type-level validate receives newRecord and oldRecord", () => { + db.type("Test", { + name: db.string(), + age: db.int({ optional: true }), + }).validate(({ newRecord, oldRecord }) => { + expectTypeOf(newRecord.name).toEqualTypeOf(); + expectTypeOf(newRecord.age).toEqualTypeOf(); + if (oldRecord) { + expectTypeOf(oldRecord.name).toEqualTypeOf(); + } + }); }); }); @@ -1494,7 +1590,7 @@ describe("TailorDBField fluent API type preservation", () => { .string() .description("Email address") .index() - .validate(({ value }) => value.includes("@")); + .validate(({ newValue }) => (!newValue.includes("@") ? "Invalid email" : undefined)); expectTypeOf>().toEqualTypeOf(); }); @@ -1842,7 +1938,9 @@ describe("TailorDBField immutability", () => { test("field.validate() returns a new field without mutating the original", () => { const original = db.string(); - const withValidate = original.validate(({ value }) => value.length > 0); + const withValidate = original.validate(({ newValue }) => + newValue.length <= 0 ? "Must not be empty" : undefined, + ); expect(withValidate).not.toBe(original); expect(original.metadata.validate).toBeUndefined(); @@ -1932,9 +2030,9 @@ describe("TailorDBType does not mutate shared fields", () => { test("type.validate() does not mutate the shared field", () => { const sharedField = db.string(); - const typeA = db - .type("TypeA", { email: sharedField }) - .validate({ email: ({ value }) => value.includes("@") }); + const typeA = db.type("TypeA", { email: sharedField }).validate({ + email: ({ newValue }) => (!newValue.includes("@") ? "Invalid email" : undefined), + }); const typeB = db.type("TypeB", { email: sharedField }); expect(typeA.fields.email.metadata.validate).toBeDefined(); @@ -1956,7 +2054,9 @@ describe("TailorDBType does not mutate shared fields", () => { const emailField = db.string(); const fields = { email: emailField }; - db.type("TypeA", fields).validate({ email: ({ value }) => value.includes("@") }); + db.type("TypeA", fields).validate({ + email: ({ newValue }) => (!newValue.includes("@") ? "Invalid email" : undefined), + }); // The fields record should still reference the original field instance expect(fields.email).toBe(emailField); @@ -2051,7 +2151,8 @@ describe("TailorDBField clone tests", () => { }); test("clones validate correctly", () => { - const validator = ({ value }: { value: string }) => value.length > 0; + const validator = ({ newValue }: { newValue: string }) => + newValue.length <= 0 ? "Must not be empty" : undefined; const original = db.string().validate(validator); const cloned = original.clone(); @@ -2062,20 +2163,6 @@ describe("TailorDBField clone tests", () => { expect(cloned.metadata.validate).not.toBe(original.metadata.validate); }); - test("clones validate with tuple format correctly", () => { - const validator = ({ value }: { value: string }) => value.length > 0; - const original = db.string().validate([validator, "Value must not be empty"]); - const cloned = original.clone(); - - expect(cloned.metadata.validate).toBeDefined(); - expect(cloned.metadata.validate).toHaveLength(1); - expect(cloned.metadata.validate?.[0]).toEqual([validator, "Value must not be empty"]); - - // Verify deep copy (different reference for array and tuple) - expect(cloned.metadata.validate).not.toBe(original.metadata.validate); - expect(cloned.metadata.validate?.[0]).not.toBe(original.metadata.validate?.[0]); - }); - test("clones serial config correctly", () => { const original = db.int().serial({ start: 100 }); const cloned = original.clone(); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 00397a616..16864e29d 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -27,7 +27,7 @@ import type { TailorFieldType, TailorToTs, FieldValidateInput, - ValidateConfig, + ValidateFn, Validators, } from "#/configure/types/field.types"; import type { UUIDString } from "#/configure/types/scalar.types"; @@ -36,7 +36,7 @@ import type { InferredAttributes } from "#/runtime/types"; import type { output, InferFieldsOutput, TypeLevelError } from "#/types/helpers"; import type { RawPermissions } from "#/types/tailordb.generated"; import type { TailorTypeGqlPermission, TailorTypePermission } from "./permission"; -import type { Hook, Hooks, ExcludeNestedDBFields, TypeFeatures } from "./types"; +import type { Hook, Hooks, ExcludeNestedDBFields, TypeFeatures, TypeValidateFn } from "./types"; import type { StandardSchemaV1 } from "@standard-schema/spec"; // Erased DB fields stay assignable across builder method-state changes. @@ -366,6 +366,7 @@ export interface TailorDBType< _description?: string; hooks(hooks: Hooks): TailorDBType; + validate(fn: TypeValidateFn): TailorDBType; validate(validators: Validators): TailorDBType; features(features: Omit): TailorDBType; indexes(...indexes: IndexDef>[]): TailorDBType; @@ -846,6 +847,8 @@ function createTailorDBType< const _permissions: RawPermissions = {}; let _files: Record = {}; const _plugins: PluginAttachment[] = []; + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + let _typeValidate: Function | undefined; if (options.pluralForm) { if (name === options.pluralForm) { @@ -881,6 +884,7 @@ function createTailorDBType< permissions: _permissions, files: _files, ...(Object.keys(indexes).length > 0 && { indexes }), + ...(_typeValidate && { typeValidate: _typeValidate }), }; }, @@ -897,28 +901,15 @@ function createTailorDBType< return this; }, - validate(validators: Validators) { - Object.entries(validators).forEach(([fieldName, fieldValidators]) => { + validate(validatorsOrFn: Validators | TypeValidateFn) { + if (typeof validatorsOrFn === "function") { + _typeValidate = validatorsOrFn; + return this; + } + Object.entries(validatorsOrFn).forEach(([fieldName, fieldValidators]) => { const field = this.fields[fieldName] as TailorAnyDBField; - - const validators = fieldValidators as - | FieldValidateInput - | FieldValidateInput[]; - - const isValidateConfig = (v: unknown): v is ValidateConfig => { - return Array.isArray(v) && v.length === 2 && typeof v[1] === "string"; - }; - - let updatedField: TailorAnyDBField; - if (Array.isArray(validators)) { - if (isValidateConfig(validators)) { - updatedField = field.validate(validators); - } else { - updatedField = field.validate(...validators); - } - } else { - updatedField = field.validate(validators); - } + const fns = fieldValidators as ValidateFn | ValidateFn[]; + const updatedField = Array.isArray(fns) ? field.validate(...fns) : field.validate(fns); (this.fields as Record)[fieldName] = updatedField; }); return this; diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index eb500edb7..018f78d24 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -90,6 +90,8 @@ export interface TailorDBTypeMetadata { unique?: boolean; } >; + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + typeValidate?: Function; } /** @@ -145,11 +147,15 @@ export type TailorDBInstance< // --- Hook types (UX-focused, for configure layer) --- -type HookFn = (args: { - value: TValue; - data: TData extends Record +type HookArgs = + TData extends Record ? { readonly [K in keyof TData]?: TData[K] | null | undefined } : unknown; + +type HookFn = (args: { + value: TValue; + newRecord: HookArgs; + oldRecord: HookArgs | null; invoker: TailorPrincipal | null; now: Date; }) => TReturn; @@ -159,6 +165,25 @@ export type Hook = { update?: HookFn; }; +type DottedPaths = + T extends Record + ? { + [K in keyof T & string]: `${Prefix}${K}` | DottedPaths, `${Prefix}${K}.`>; + }[keyof T & string] + : never; + +export type TypeValidateFn< + F extends Record, + TData = { [K in keyof F]: output }, +> = ( + args: { + newRecord: HookArgs; + oldRecord: HookArgs | null; + invoker: TailorPrincipal | null; + }, + issues: (field: DottedPaths>, message: string) => void, +) => void; + export type Hooks< F extends Record, TData = { [K in keyof F]: output }, diff --git a/packages/sdk/src/configure/types/field.types.ts b/packages/sdk/src/configure/types/field.types.ts index 5a90597ba..41637959f 100644 --- a/packages/sdk/src/configure/types/field.types.ts +++ b/packages/sdk/src/configure/types/field.types.ts @@ -3,7 +3,6 @@ // This is a pure type module: type declarations only, no zod/schema // references, importable type-only from any layer. -import type { TailorPrincipal } from "#/runtime/types"; import type { output, InferFieldsOutput } from "#/types/helpers"; import type { DateString, @@ -95,33 +94,14 @@ export type ArrayFieldOutput = [O] extends [ : T; /** - * Validation function type + * Field validation function. Return an error message string to fail, or void/undefined to pass. */ -export type ValidateFn = (args: { - value: O; - data: D; - invoker: TailorPrincipal | null; -}) => boolean; +export type ValidateFn = (args: { newValue: O; oldValue: O | null }) => string | void; /** - * Validation configuration with custom error message + * Input type for field validation */ -export type ValidateConfig = [ValidateFn, string]; - -/** - * Field-level validation function - */ -type FieldValidateFn = ValidateFn; - -/** - * Field-level validation configuration - */ -type FieldValidateConfig = ValidateConfig; - -/** - * Input type for field validation - can be either a function or a tuple of [function, errorMessage] - */ -export type FieldValidateInput = FieldValidateFn | FieldValidateConfig; +export type FieldValidateInput = ValidateFn; /** * Base validators type for field collections @@ -138,13 +118,7 @@ type ValidatorsBase< validate: unknown; } ? never - : K]?: - | ValidateFn, InferFieldsOutput> - | ValidateConfig, InferFieldsOutput> - | ( - | ValidateFn, InferFieldsOutput> - | ValidateConfig, InferFieldsOutput> - )[]; + : K]?: ValidateFn> | ValidateFn>[]; }>; /** diff --git a/packages/sdk/src/configure/types/type-typename.test.ts b/packages/sdk/src/configure/types/type-typename.test.ts index 65a2dbd5d..c61da6699 100644 --- a/packages/sdk/src/configure/types/type-typename.test.ts +++ b/packages/sdk/src/configure/types/type-typename.test.ts @@ -24,7 +24,7 @@ describe("typeName method type safety", () => { TypeEquals >; - const validated = t.string().validate(() => true); + const validated = t.string().validate(() => undefined); type _ValidateDuplicate = Expect< TypeEquals >; diff --git a/packages/sdk/src/configure/types/type.test.ts b/packages/sdk/src/configure/types/type.test.ts index ef9ee64fd..c9b1bdadb 100644 --- a/packages/sdk/src/configure/types/type.test.ts +++ b/packages/sdk/src/configure/types/type.test.ts @@ -766,7 +766,9 @@ describe("TailorField clone-on-write / no aliasing", () => { test("validate() returns a clone and never mutates the original", () => { const original = t.string(); - const updated = original.validate((args) => args.value.length > 0); + const updated = original.validate((args) => + args.newValue.length <= 0 ? "Must not be empty" : undefined, + ); expect(original.metadata.validate).toBeUndefined(); expect(updated.metadata.validate).toHaveLength(1); @@ -828,7 +830,9 @@ describe("TailorField clone-on-write / no aliasing", () => { expect(enumClone.metadata.allowedValues).not.toBe(enumField.metadata.allowedValues); expect(enumClone.metadata.allowedValues?.[0]).not.toBe(enumField.metadata.allowedValues?.[0]); - const validated = t.string().validate((args) => args.value.length > 0); + const validated = t + .string() + .validate((args) => (args.newValue.length <= 0 ? "Must not be empty" : undefined)); const validatedClone = validated.description("name"); expect(validatedClone.metadata.validate).not.toBe(validated.metadata.validate); }); @@ -848,8 +852,8 @@ describe("TailorField clone-on-write / no aliasing", () => { test("validate() preserves function references through cloning", () => { const calls: unknown[] = []; const field = t.string().validate((args) => { - calls.push(args.value); - return args.value.length > 0; + calls.push(args.newValue); + return args.newValue.length <= 0 ? "Must not be empty" : undefined; }); const result = field.parse({ value: "x", data, invoker }); @@ -858,7 +862,9 @@ describe("TailorField clone-on-write / no aliasing", () => { }); test("validators survive a clone triggered by a later builder, leaving the original intact", () => { - const validated = t.string().validate((args) => args.value.length > 0); + const validated = t + .string() + .validate((args) => (args.newValue.length <= 0 ? "Must not be empty" : undefined)); // description() clones the field; the validators must carry over to the clone // and keep working, while the original stays unchanged. const described = validated.description("name"); diff --git a/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts b/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts index 87552f9be..5e7333979 100644 --- a/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts +++ b/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts @@ -20,7 +20,8 @@ describe("parseFieldConfig precompiled expressions", () => { }); test("uses precompiled validate expression when attached", () => { - const validator = ({ value }: { value: string }) => value.length > 0; + const validator = ({ newValue }: { newValue: string }) => + newValue.length <= 0 ? "Must not be empty" : undefined; setPrecompiledScriptExpr(validator, "PRECOMPILED_VALIDATE_EXPR"); const type = db.type("User", { diff --git a/packages/sdk/src/parser/service/tailordb/field.ts b/packages/sdk/src/parser/service/tailordb/field.ts index 3f4de1150..38b2d5503 100644 --- a/packages/sdk/src/parser/service/tailordb/field.ts +++ b/packages/sdk/src/parser/service/tailordb/field.ts @@ -199,11 +199,24 @@ const convertToScriptExpr = ( } const normalized = stringifyFunction(fn); return assertParsableExpression( - `(${normalized})({ value: _value, data: _data, invoker: ${tailorPrincipalMap}, now: _now })`, + `(${normalized})({ value: _value, newRecord: _input, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap}, now: _now })`, formatScriptContext(kind, context), ); }; +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +export const convertTypeValidateToExpr = (fn: Function): string => { + const precompiledExpr = getPrecompiledScriptExpr(fn as (...args: never[]) => unknown); + if (precompiledExpr) { + return precompiledExpr; + } + const normalized = stringifyFunction(fn); + return assertParsableExpression( + `(${normalized})({ newRecord: _newRecord, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap} }, __issues)`, + "type-validate", + ); +}; + /** * Parse TailorDBField into OperatorFieldConfig. * This transforms user-defined functions into script expressions. @@ -242,19 +255,12 @@ export function parseFieldConfig( ), } : {}), - validate: metadata.validate?.map((v) => { - const { fn, message } = - typeof v === "function" - ? { fn: v, message: `failed by \`${v.toString().trim()}\`` } - : { fn: v[0], message: v[1] }; - - return { - script: { - expr: convertToScriptExpr(fn, "validate", context), - }, - errorMessage: message, - }; - }), + validate: metadata.validate?.map((fn) => ({ + script: { + expr: convertToScriptExpr(fn, "validate", context), + }, + errorMessage: "", + })), hooks: metadata.hooks ? { create: metadata.hooks.create diff --git a/packages/sdk/src/parser/service/tailordb/schema.ts b/packages/sdk/src/parser/service/tailordb/schema.ts index 11f5f3fbb..6dc86107a 100644 --- a/packages/sdk/src/parser/service/tailordb/schema.ts +++ b/packages/sdk/src/parser/service/tailordb/schema.ts @@ -81,10 +81,7 @@ export const DBFieldMetadataSchema = z.strictObject({ }) .optional() .describe("Lifecycle hooks for the field"), - validate: z - .array(z.union([functionSchema, z.tuple([functionSchema, z.string()])])) - .optional() - .describe("Validation functions for the field"), + validate: z.array(functionSchema).optional().describe("Validation functions for the field"), serial: z .strictObject({ start: z.number().describe("Starting value for the serial sequence"), @@ -270,6 +267,7 @@ export const TailorDBTypeSchema = z.strictObject({ }), ) .optional(), + typeValidate: functionSchema.optional(), }) .strict(), }); diff --git a/packages/sdk/src/parser/service/tailordb/type-parser.ts b/packages/sdk/src/parser/service/tailordb/type-parser.ts index cd539376b..f6a8c21fd 100644 --- a/packages/sdk/src/parser/service/tailordb/type-parser.ts +++ b/packages/sdk/src/parser/service/tailordb/type-parser.ts @@ -1,6 +1,6 @@ import * as inflection from "inflection"; import { isPluginGeneratedType } from "#/parser/service/tailordb/type-source"; -import { parseFieldConfig } from "./field"; +import { convertTypeValidateToExpr, parseFieldConfig } from "./field"; import { parsePermissions } from "./permission"; import { validateRelationConfig, @@ -171,6 +171,9 @@ function parseTailorDBType( permissions: parsePermissions(metadata.permissions), indexes: metadata.indexes, files: metadata.files, + ...(typeof metadata.typeValidate === "function" && { + typeValidateExpr: convertTypeValidateToExpr(metadata.typeValidate), + }), }; } diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index 730a78ef5..30a450f03 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -103,13 +103,13 @@ describe("buildTypeScripts", () => { expect(createExpr).toContain('"label": _input["label"] ?? "now"'); }); - test("builds a validate script keyed by dotted path with negated boolean checks", () => { + test("builds a validate script with ?? chain and typeof string check", () => { const fields: Record = { age: { type: "integer", validate: [ - { script: { expr: "_value >= 0" }, errorMessage: "must be >= 0" }, - { script: { expr: "_value < 200" }, errorMessage: "must be < 200" }, + { script: { expr: "_value >= 0" }, errorMessage: "" }, + { script: { expr: "_value < 200" }, errorMessage: "" }, ], }, }; @@ -118,13 +118,47 @@ describe("buildTypeScripts", () => { expect(typeHook).toBeUndefined(); const createExpr = typeValidate?.create?.expr ?? ""; - // Same rules apply to create and update. expect(typeValidate?.update?.expr).toBe(createExpr); expect(createExpr).toContain("const __errs = {}"); - expect(createExpr).toContain('if (!(_value >= 0)) { __errs["age"] = "must be >= 0"; }'); - expect(createExpr).toContain('else if (!(_value < 200)) { __errs["age"] = "must be < 200"; }'); + expect(createExpr).toContain('const _value = _newRecord["age"]'); + expect(createExpr).toContain('const _oldValue = _oldRecord?.["age"] ?? null'); + expect(createExpr).toContain("(_value >= 0) ?? (_value < 200)"); + expect(createExpr).toContain('if (typeof __r === "string") { __errs["age"] = __r; }'); expect(createExpr).toContain("return __errs"); - // `now` is a hook-only concept; validators must not bind a timestamp. expect(createExpr).not.toContain("new Date()"); }); + + test("includes type-level validate with __issues function", () => { + const typeValidateExpr = + '(({ newRecord }) => { if (newRecord.start > newRecord.end) __issues("start", "bad"); })({ newRecord: _newRecord, oldRecord: _oldRecord }, __issues)'; + + const { typeValidate } = buildTypeScripts({}, typeValidateExpr); + const expr = typeValidate?.create?.expr ?? ""; + expect(typeValidate?.update?.expr).toBe(expr); + expect(expr).toContain("const __errs = {}"); + expect(expr).toContain("const __issues = (f, m) => { __errs[f] = m; }"); + expect(expr).toContain(typeValidateExpr); + expect(expr).toContain("return __errs"); + }); + + test("combines field validators and type-level validate in one script", () => { + const fields: Record = { + name: { + type: "string", + validate: [{ script: { expr: "checkName(_value)" }, errorMessage: "" }], + }, + }; + const typeValidateExpr = "typeValidateFn({ newRecord: _newRecord }, __issues)"; + + const { typeValidate } = buildTypeScripts(fields, typeValidateExpr); + const expr = typeValidate?.create?.expr ?? ""; + expect(expr).toContain('const _value = _newRecord["name"]'); + expect(expr).toContain("const __issues = (f, m) => { __errs[f] = m; }"); + expect(expr).toContain(typeValidateExpr); + }); + + test("no typeValidate output when no field validators and no type-level validate", () => { + expect(buildTypeScripts({})).toEqual({}); + expect(buildTypeScripts({}, undefined)).toEqual({}); + }); }); diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index 6d252e960..a26c8bd3d 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -1,5 +1,7 @@ // Platform-injected record map for type-level hook/validate scripts. const INPUT = "_input"; +const NEW_RECORD = "_newRecord"; +const OLD_RECORD = "_oldRecord"; // Shared operation timestamp bound once per script execution. const NOW = "_now"; @@ -94,34 +96,37 @@ function buildHookObject( * failing message keyed by its dotted field path. * @param {Record} fields - Field configurations * @param {string} accessExpr - JS expression to access the parent object + * @param {string} oldAccessExpr - JS expression to access the old record's parent object * @param {string} keyPrefix - Dotted path prefix for error keys * @returns {string[]} Array of validation statement strings */ function buildValidateStatements( fields: Record, accessExpr: string, + oldAccessExpr: string, keyPrefix: string, ): string[] { const statements: string[] = []; for (const [name, config] of Object.entries(fields)) { const access = `${accessExpr}[${key(name)}]`; + const oldAccess = `${oldAccessExpr}?.[${key(name)}]`; const fieldPath = keyPrefix ? `${keyPrefix}.${name}` : name; if (isNestedType(config) && config.fields) { - statements.push(...buildValidateStatements(config.fields, `(${access} || {})`, fieldPath)); + statements.push( + ...buildValidateStatements(config.fields, `(${access} || {})`, oldAccess, fieldPath), + ); continue; } const validators = (config.validate ?? []).filter((v) => v.script?.expr); if (validators.length > 0) { - const chain = validators - .map( - (v) => - `if (!(${v.script?.expr})) { __errs[${key(fieldPath)}] = ${key(v.errorMessage)}; }`, - ) - .join(" else "); - statements.push(`{ const _value = ${access}; ${chain} }`); + const chain = validators.map((v) => `(${v.script?.expr})`).join(" ?? "); + statements.push( + `{ const _value = ${access}; const _oldValue = ${oldAccess} ?? null;` + + ` const __r = ${chain}; if (typeof __r === "string") { __errs[${key(fieldPath)}] = __r; } }`, + ); } } @@ -129,14 +134,13 @@ function buildValidateStatements( } function wrapHook(objectExpr: string): string { - return `(() => { const ${NOW} = new Date(); const _data = ${INPUT}; return ${objectExpr}; })()`; + return `(() => { const ${NOW} = new Date(); return ${objectExpr}; })()`; } -function wrapValidate(statements: string[]): string { - return ( - `(() => { const _data = ${INPUT}; const __errs = {}; ` + - `${statements.join(" ")} return __errs; })()` - ); +function wrapValidate(statements: string[], typeValidateExpr?: string): string { + const issuesFn = typeValidateExpr ? " const __issues = (f, m) => { __errs[f] = m; };" : ""; + const typeValidateStmt = typeValidateExpr ? ` ${typeValidateExpr};` : ""; + return `(() => { const __errs = {};${issuesFn} ${statements.join(" ")}${typeValidateStmt} return __errs; })()`; } /** @@ -146,9 +150,13 @@ function wrapValidate(statements: string[]): string { * instant. Defaults are applied after hooks on create only. Validators * run with the same rules on create and update. * @param {Record} fields - Field configurations + * @param {string} [typeValidateExpr] - Precompiled type-level validate expression * @returns {TypeScripts} Aggregated type-level scripts */ -export function buildTypeScripts(fields: Record): TypeScripts { +export function buildTypeScripts( + fields: Record, + typeValidateExpr?: string, +): TypeScripts { const result: TypeScripts = {}; const hook: { create?: ScriptRef; update?: ScriptRef } = {}; @@ -162,9 +170,9 @@ export function buildTypeScripts(fields: Record): Typ result.typeHook = hook; } - const statements = buildValidateStatements(fields, INPUT, ""); - if (statements.length > 0) { - const expr = wrapValidate(statements); + const statements = buildValidateStatements(fields, NEW_RECORD, OLD_RECORD, ""); + if (statements.length > 0 || typeValidateExpr) { + const expr = wrapValidate(statements, typeValidateExpr); result.typeValidate = { create: { expr }, update: { expr } }; } diff --git a/packages/sdk/src/parser/service/tailordb/types.ts b/packages/sdk/src/parser/service/tailordb/types.ts index 347900578..999f2fa6a 100644 --- a/packages/sdk/src/parser/service/tailordb/types.ts +++ b/packages/sdk/src/parser/service/tailordb/types.ts @@ -172,4 +172,5 @@ export interface TailorDBType { permissions: Permissions; indexes?: TailorDBTypeMetadata["indexes"]; files?: TailorDBTypeMetadata["files"]; + typeValidateExpr?: string; } diff --git a/packages/sdk/src/types/tailordb.generated.ts b/packages/sdk/src/types/tailordb.generated.ts index e3fdf576b..446e1df91 100644 --- a/packages/sdk/src/types/tailordb.generated.ts +++ b/packages/sdk/src/types/tailordb.generated.ts @@ -57,7 +57,7 @@ export type DBFieldMetadata = { } | undefined; /** Validation functions for the field */ - validate?: (Function | [Function, string])[] | undefined; + validate?: Function[] | undefined; /** Serial (auto-increment) configuration */ serial?: | { @@ -1023,6 +1023,7 @@ export type TailorDBTypeRawInput = { }; } | undefined; + typeValidate?: Function | undefined; }; }; @@ -1055,7 +1056,7 @@ export type TailorDBTypeRaw = { update?: Function | undefined; } | undefined; - validate?: (Function | [Function, string])[] | undefined; + validate?: Function[] | undefined; serial?: | { start: number; @@ -1095,6 +1096,7 @@ export type TailorDBTypeRaw = { }; } | undefined; + typeValidate?: Function | undefined; }; }; diff --git a/packages/sdk/src/utils/test/index.test.ts b/packages/sdk/src/utils/test/index.test.ts index c8e87b867..b08df747a 100644 --- a/packages/sdk/src/utils/test/index.test.ts +++ b/packages/sdk/src/utils/test/index.test.ts @@ -170,12 +170,12 @@ describe("createTailorDBHook", () => { describe("create hook on a top-level field", () => { test("invokes the create hook with value, full data, and a null invoker", () => { - const seen: { value: unknown; data: unknown; invoker: unknown }[] = []; + const seen: { value: unknown; newRecord: unknown; invoker: unknown }[] = []; const type = db.type("Order", { total: db.float(), tax: db.float() }).hooks({ tax: { - create: ({ value, data, invoker }) => { - seen.push({ value, data, invoker }); - return (data as { total: number }).total * 0.1; + create: ({ value, newRecord, invoker }) => { + seen.push({ value, newRecord, invoker }); + return (newRecord as { total: number }).total * 0.1; }, }, }); @@ -184,7 +184,7 @@ describe("createTailorDBHook", () => { expect(seen).toEqual([ { value: undefined, - data: { total: 100, tax: undefined }, + newRecord: { total: 100, tax: undefined }, invoker: null, }, ]); diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index f0ec4ba9e..2f43dff2f 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -50,8 +50,10 @@ export function createTailorDBHook>(type: T) { } else if (field.metadata.hooks?.create) { hooked[key] = field.metadata.hooks.create({ value: (data as Record)[key], - data: data, + newRecord: data, + oldRecord: null, invoker: null, + now: new Date(), }); if (hooked[key] instanceof Date) { hooked[key] = hooked[key].toISOString(); From aa5f5042f1d8b48728ad389516b168f7275ddc99 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 6 Jul 2026 17:34:25 +0900 Subject: [PATCH 437/618] refactor(tailordb): redesign type-level hooks from per-field mapping to single function Replace Hooks (per-field mapping) with TypeHook ({ create?, update? }) where each function receives { input, oldRecord, invoker, now } and returns partial field overrides. Also rename HookFn parameter from newRecord to input since hooks operate on pre-record input. - Add TypeHookFn and TypeHook types to configure/services/tailordb/types.ts - Change .hooks() builder to store _typeHook instead of distributing to fields - Add typeHook to Zod schema and regenerate types - Update createTailorDBHook test helper to apply type-level hooks - Update buildTypeScripts to accept options object with typeHookExpr - Update example and template code to new hooks API --- example/generated/tailordb.ts | 2 +- example/seed/data/Customer.schema.ts | 4 +- example/tailordb/customer.ts | 10 +- .../inventory-management/src/db/orderItem.ts | 10 +- .../src/cli/commands/deploy/tailordb/index.ts | 10 +- .../tailordb/migrate/snapshot-manifest.ts | 8 +- .../tailordb/migrate/snapshot-types.ts | 1 + .../cli/commands/tailordb/migrate/snapshot.ts | 4 + .../tailordb/hooks-validate-bundler.ts | 24 ++-- .../services/tailordb/schema.test.ts | 136 +++++++----------- .../src/configure/services/tailordb/schema.ts | 25 ++-- .../src/configure/services/tailordb/types.ts | 29 ++-- .../sdk/src/configure/types/field-runtime.ts | 12 +- .../sdk/src/configure/types/field.types.ts | 2 +- .../src/parser/service/tailordb/field.test.ts | 22 ++- .../sdk/src/parser/service/tailordb/field.ts | 15 +- .../sdk/src/parser/service/tailordb/schema.ts | 6 + .../parser/service/tailordb/type-parser.ts | 12 +- .../service/tailordb/type-script.test.ts | 4 +- .../parser/service/tailordb/type-script.ts | 27 +++- .../sdk/src/parser/service/tailordb/types.ts | 1 + packages/sdk/src/types/auth.generated.ts | 17 ++- packages/sdk/src/types/tailordb.generated.ts | 12 ++ packages/sdk/src/utils/test/index.test.ts | 30 ++-- packages/sdk/src/utils/test/index.ts | 27 +++- 25 files changed, 256 insertions(+), 194 deletions(-) diff --git a/example/generated/tailordb.ts b/example/generated/tailordb.ts index ae1e3c5bb..a34d57b51 100644 --- a/example/generated/tailordb.ts +++ b/example/generated/tailordb.ts @@ -25,7 +25,7 @@ export interface Namespace { postalCode: string; address: string | null; city: string | null; - fullAddress: Generated; + fullAddress: string; state: string; createdAt: Generated; updatedAt: Generated; diff --git a/example/seed/data/Customer.schema.ts b/example/seed/data/Customer.schema.ts index 3d82e446d..99285ca3a 100644 --- a/example/seed/data/Customer.schema.ts +++ b/example/seed/data/Customer.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { customer } from "../../tailordb/customer"; const schemaType = t.object({ - ...customer.pickFields(["id","fullAddress","createdAt","updatedAt"], { optional: true }), - ...customer.omitFields(["id","fullAddress","createdAt","updatedAt"]), + ...customer.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...customer.omitFields(["id","createdAt","updatedAt"]), }); const hook = createTailorDBHook(customer); diff --git a/example/tailordb/customer.ts b/example/tailordb/customer.ts index ef6e8cfb3..0ab55385f 100644 --- a/example/tailordb/customer.ts +++ b/example/tailordb/customer.ts @@ -20,10 +20,12 @@ export const customer = db ...db.fields.timestamps(), }) .hooks({ - fullAddress: { - create: ({ newRecord }) => `${newRecord.postalCode} ${newRecord.address} ${newRecord.city}`, - update: ({ newRecord }) => `${newRecord.postalCode} ${newRecord.address} ${newRecord.city}`, - }, + create: ({ input }) => ({ + fullAddress: `${input.postalCode} ${input.address} ${input.city}`, + }), + update: ({ input }) => ({ + fullAddress: `${input.postalCode} ${input.address} ${input.city}`, + }), }) .validate({ name: ({ newValue }) => diff --git a/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts b/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts index b6b81686b..67495413b 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts @@ -25,10 +25,12 @@ export const orderItem = db ...db.fields.timestamps(), }) .hooks({ - totalPrice: { - create: ({ newRecord }) => (newRecord?.quantity ?? 0) * (newRecord.unitPrice ?? 0), - update: ({ newRecord }) => (newRecord?.quantity ?? 0) * (newRecord.unitPrice ?? 0), - }, + create: ({ input }) => ({ + totalPrice: (input?.quantity ?? 0) * (input.unitPrice ?? 0), + }), + update: ({ input }) => ({ + totalPrice: (input?.quantity ?? 0) * (input.unitPrice ?? 0), + }), }) .permission(permissionLoggedIn) .gqlPermission(gqlPermissionLoggedIn); diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/index.ts b/packages/sdk/src/cli/commands/deploy/tailordb/index.ts index 88280602b..2ed26dd60 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/index.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/index.ts @@ -1791,13 +1791,17 @@ function normalizeTailorDBCompareValue( return value; } + if (typeof value === "boolean") { + if (path.at(-1) === "optionalOnCreate" && value === false) { + return undefined; + } + return value; + } + if (typeof value === "number" || typeof value === "bigint" || typeof value === "string") { if (matchesNumericStringPath(path) && isNumericLikeValue(value)) { return String(value); } - // Platform returns an empty string for `expr` (validate scripts) and field/type - // `description` when the SDK omitted them, while local manifests omit the key - // entirely. Treat the empty string as unset so it matches an omitted value. if ( (path.at(-1) === "expr" || path.at(-1) === "description") && value === tailordbCompareKnownDefaults.emptyExpression diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts index efac3b739..3b9e297e7 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts @@ -165,10 +165,10 @@ export function generateTailorDBTypeManifestFromSnapshot( // Field hooks/validators are aggregated into type-level scripts so that a // single shared timestamp is observed across every field in one operation. - const { typeHook, typeValidate } = buildTypeScripts( - snapshotType.fields, - snapshotType.typeValidateExpr, - ); + const { typeHook, typeValidate } = buildTypeScripts(snapshotType.fields, { + typeHookExpr: snapshotType.typeHookExpr, + typeValidateExpr: snapshotType.typeValidateExpr, + }); return { name: snapshotType.name, diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-types.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-types.ts index 1e4c6501f..109be51d8 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-types.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-types.ts @@ -219,6 +219,7 @@ export interface TailorDBSnapshotType { record?: SnapshotRecordPermission; gql?: SnapshotGqlPermission; }; + typeHookExpr?: { create?: string; update?: string }; typeValidateExpr?: string; } diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts index 9ba5f1ee8..7b46c94c1 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts @@ -503,6 +503,10 @@ export function createSnapshotType(type: TailorDBType): TailorDBSnapshotType { snapshotType.files = { ...type.files }; } + if (type.typeHookExpr) { + snapshotType.typeHookExpr = type.typeHookExpr; + } + if (type.typeValidateExpr) { snapshotType.typeValidateExpr = type.typeValidateExpr; } diff --git a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts index d05c33e3e..9a1b4c7d0 100644 --- a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts +++ b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts @@ -24,7 +24,7 @@ type ScriptFunction = (...args: unknown[]) => unknown; type ScriptTarget = { fn: ScriptFunction; - kind: "hooks" | "validate" | "typeValidate"; + kind: "hooks" | "validate" | "typeHook" | "typeValidate"; }; /** Binding found in the source file: either an import or a top-level declaration */ @@ -120,6 +120,15 @@ function collectScriptTargets(type: TailorDBTypeSchemaOutput): ScriptTarget[] { collectFieldTargets(field); } + if (type.metadata.typeHook) { + for (const op of ["create", "update"] as const) { + const fn = toScriptFunction(type.metadata.typeHook[op]); + if (fn) { + targets.push({ fn, kind: "typeHook" }); + } + } + } + const typeValidateFn = toScriptFunction(type.metadata.typeValidate); if (typeValidateFn) { targets.push({ fn: typeValidateFn, kind: "typeValidate" }); @@ -435,7 +444,7 @@ export function buildMinimalEntryFromResolved( async function bundleScriptTarget(args: { fn: ScriptFunction; - kind: "hooks" | "validate" | "typeValidate"; + kind: "hooks" | "validate" | "typeHook" | "typeValidate"; sourceFilePath: string; sourceBindings: Map; tempDir: string; @@ -447,14 +456,13 @@ async function bundleScriptTarget(args: { const fnSource = stringifyFunction(fn); const argsObject = kind === "hooks" - ? `{ value: _value, newRecord: _input, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap}, now: _now }` + ? `{ value: _value, input: _input, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap}, now: _now }` : kind === "validate" ? `{ newValue: _value, oldValue: _oldValue }` - : `{ newRecord: _newRecord, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap} }, __issues`; - const inlineExpr = assertParsableExpression( - `(${fnSource})(${argsObject})`, - context, - ); + : kind === "typeHook" + ? `{ input: _input, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap}, now: _now }` + : `{ newRecord: _newRecord, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap} }, __issues`; + const inlineExpr = assertParsableExpression(`(${fnSource})(${argsObject})`, context); // Check if the function has free variables that need bundling const freeVars = findUndefinedReferences(`const __fn = ${fnSource};`); diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 381955101..4d33b86fa 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -778,12 +778,13 @@ describe("TailorDBType withTimestamps option tests", () => { expect(createHook).toBeDefined(); const specified = new Date("2025-02-10T09:00:00Z"); + const now = new Date("2025-06-01T00:00:00Z"); const result = createHook!({ value: specified, - newRecord: {}, + input: {}, oldRecord: null, invoker: timestampHookInvoker, - now: new Date(), + now, }); expect(result).toBe(specified); }); @@ -793,18 +794,15 @@ describe("TailorDBType withTimestamps option tests", () => { const createHook = createdAt.metadata.hooks?.create; expect(createHook).toBeDefined(); - const before = Date.now(); + const now = new Date("2025-06-01T12:00:00Z"); const result = createHook!({ value: null, - newRecord: {}, + input: {}, oldRecord: null, invoker: timestampHookInvoker, - now: new Date(), + now, }); - const after = Date.now(); - expect(result).toBeInstanceOf(Date); - expect((result as Date).getTime()).toBeGreaterThanOrEqual(before); - expect((result as Date).getTime()).toBeLessThanOrEqual(after); + expect(result).toBe(now); }); test("updatedAt create hook respects a user-specified value", () => { @@ -813,12 +811,13 @@ describe("TailorDBType withTimestamps option tests", () => { expect(createHook).toBeDefined(); const specified = new Date("2025-02-10T09:00:00Z"); + const now = new Date("2025-06-01T12:00:00Z"); const result = createHook!({ value: specified, - newRecord: {}, + input: {}, oldRecord: null, invoker: timestampHookInvoker, - now: new Date(), + now, }); expect(result).toBe(specified); }); @@ -828,37 +827,31 @@ describe("TailorDBType withTimestamps option tests", () => { const createHook = updatedAt.metadata.hooks?.create; expect(createHook).toBeDefined(); - const before = Date.now(); + const now = new Date("2025-06-01T12:00:00Z"); const result = createHook!({ value: null, - newRecord: {}, + input: {}, oldRecord: null, invoker: timestampHookInvoker, - now: new Date(), + now, }); - const after = Date.now(); - expect(result).toBeInstanceOf(Date); - expect((result as Date).getTime()).toBeGreaterThanOrEqual(before); - expect((result as Date).getTime()).toBeLessThanOrEqual(after); + expect(result).toBe(now); }); - test("updatedAt update hook uses current time", () => { + test("updatedAt update hook uses now", () => { const { updatedAt } = db.fields.timestamps(); const updateHook = updatedAt.metadata.hooks?.update; expect(updateHook).toBeDefined(); - const before = Date.now(); + const now = new Date("2025-06-01T12:00:00Z"); const result = updateHook!({ value: null, - newRecord: {}, + input: {}, oldRecord: null, invoker: timestampHookInvoker, - now: new Date(), + now, }); - const after = Date.now(); - expect(result).toBeInstanceOf(Date); - expect((result as Date).getTime()).toBeGreaterThanOrEqual(before); - expect((result as Date).getTime()).toBeLessThanOrEqual(after); + expect(result).toBe(now); }); }); @@ -1116,10 +1109,8 @@ describe("TailorDBType hooks modifier tests", () => { name: db.string(), }) .hooks({ - name: { - create: () => "created", - update: () => "updated", - }, + create: ({ input }) => ({ name: `${input.name}_created` }), + update: ({ input }) => ({ name: `${input.name}_updated` }), }); expectTypeOf>().toEqualTypeOf<{ id: UUIDString; @@ -1127,65 +1118,35 @@ describe("TailorDBType hooks modifier tests", () => { }>(); }); - test("setting hooks on id causes type error", () => { - db.type("Test", { - name: db.string(), - }).hooks({ - // @ts-expect-error hooks() cannot be called on the "id" field - id: { - create: () => "created", - }, + test("type hook stores create/update functions in metadata", () => { + const createFn = () => ({ name: "created" }); + const updateFn = () => ({ name: "updated" }); + const hookType = db.type("Test", { name: db.string() }).hooks({ + create: createFn, + update: updateFn, }); + expect(hookType.metadata.typeHook?.create).toBe(createFn); + expect(hookType.metadata.typeHook?.update).toBe(updateFn); }); - test("setting hooks on nested field causes type error", () => { - db.type("Test", { - name: db.object({ - first: db.string(), - last: db.string(), - }), - // @ts-expect-error hooks() cannot be called on nested fields - }).hooks({ - name: { - create: () => "created", - }, + test("type hook return type excludes id", () => { + db.type("Test", { name: db.string() }).hooks({ + // @ts-expect-error id cannot be returned from type hook + create: () => ({ id: "00000000-0000-0000-0000-000000000001" }), }); }); - test("hooks modifier on string field receives string", () => { - const testType = db.type("Test", { name: db.string() }); - const _hooks = testType.hooks; - type ExpectedHooksParam = Parameters[0]; - type ActualNameType = Exclude; - - expectTypeOf().toEqualTypeOf< - Hook< - { - id: UUIDString; - readonly name: string; - }, - string - > - >(); - }); - - test("hooks modifier on optional field receives null", () => { - const testType = db.type("Test", { - name: db.string({ optional: true }), + test("type hook input args receive correct types", () => { + db.type("Test", { name: db.string(), age: db.int({ optional: true }) }).hooks({ + create: ({ input, oldRecord, invoker, now }) => { + expectTypeOf(input.name).toEqualTypeOf(); + expectTypeOf(input.age).toEqualTypeOf(); + expectTypeOf(oldRecord).toBeNullable(); + expectTypeOf(invoker).toBeNullable(); + expectTypeOf(now).toEqualTypeOf(); + return {}; + }, }); - const _hooks = testType.hooks; - type ExpectedHooksParam = Parameters[0]; - type ActualNameType = Exclude; - - expectTypeOf().toEqualTypeOf< - Hook< - { - id: UUIDString; - name?: string | null; - }, - string | null - > - >(); }); }); @@ -2016,14 +1977,14 @@ describe("TailorDBField immutability", () => { }); describe("TailorDBType does not mutate shared fields", () => { - test("type.hooks() does not mutate the shared field", () => { + test("type.hooks() does not affect other types sharing the same field", () => { const sharedField = db.string(); - const typeA = db.type("TypeA", { name: sharedField }).hooks({ name: { create: () => "A" } }); + const typeA = db.type("TypeA", { name: sharedField }).hooks({ create: () => ({ name: "A" }) }); const typeB = db.type("TypeB", { name: sharedField }); - expect(typeA.fields.name.metadata.hooks).toBeDefined(); - expect(typeB.fields.name.metadata.hooks).toBeUndefined(); + expect(typeA.metadata.typeHook?.create).toBeDefined(); + expect(typeB.metadata.typeHook).toBeUndefined(); expect(sharedField.metadata.hooks).toBeUndefined(); }); @@ -2044,9 +2005,8 @@ describe("TailorDBType does not mutate shared fields", () => { const nameField = db.string(); const fields = { name: nameField }; - db.type("TypeA", fields).hooks({ name: { create: () => "hooked" } }); + db.type("TypeA", fields).hooks({ create: () => ({ name: "hooked" }) }); - // The fields record should still reference the original field instance expect(fields.name).toBe(nameField); }); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 16864e29d..f79ed46e4 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -36,7 +36,7 @@ import type { InferredAttributes } from "#/runtime/types"; import type { output, InferFieldsOutput, TypeLevelError } from "#/types/helpers"; import type { RawPermissions } from "#/types/tailordb.generated"; import type { TailorTypeGqlPermission, TailorTypePermission } from "./permission"; -import type { Hook, Hooks, ExcludeNestedDBFields, TypeFeatures, TypeValidateFn } from "./types"; +import type { Hook, TypeHook, ExcludeNestedDBFields, TypeFeatures, TypeValidateFn } from "./types"; import type { StandardSchemaV1 } from "@standard-schema/spec"; // Erased DB fields stay assignable across builder method-state changes. @@ -365,7 +365,7 @@ export interface TailorDBType< > extends TailorDBTypeBase { _description?: string; - hooks(hooks: Hooks): TailorDBType; + hooks(hook: TypeHook): TailorDBType; validate(fn: TypeValidateFn): TailorDBType; validate(validators: Validators): TailorDBType; features(features: Omit): TailorDBType; @@ -848,6 +848,8 @@ function createTailorDBType< let _files: Record = {}; const _plugins: PluginAttachment[] = []; // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + let _typeHook: { create?: Function; update?: Function } | undefined; + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type let _typeValidate: Function | undefined; if (options.pluralForm) { @@ -884,20 +886,13 @@ function createTailorDBType< permissions: _permissions, files: _files, ...(Object.keys(indexes).length > 0 && { indexes }), + ...(_typeHook && { typeHook: _typeHook }), ...(_typeValidate && { typeValidate: _typeValidate }), }; }, - hooks(hooks: Hooks) { - // `Hooks` is strongly typed, but `Object.entries()` loses that information. - // oxlint-disable-next-line no-explicit-any - Object.entries(hooks).forEach(([fieldName, fieldHooks]: [string, any]) => { - const field = this.fields[fieldName]; - if (field === undefined) throw new Error(`field not found: ${fieldName}`); - (this.fields as Record)[fieldName] = ( - field as TailorAnyDBField - ).hooks(fieldHooks); - }); + hooks(hook: TypeHook) { + _typeHook = hook; return this; }, @@ -1097,12 +1092,12 @@ export const db = { */ timestamps: () => ({ createdAt: datetime() - .hooks({ create: ({ value }) => value ?? new Date() }) + .hooks({ create: ({ value, now }) => value ?? now }) .description("Record creation timestamp"), updatedAt: datetime() .hooks({ - create: ({ value }) => value ?? new Date(), - update: () => new Date(), + create: ({ value, now }) => value ?? now, + update: ({ now }) => now, }) .description("Record update timestamp"), }), diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index 018f78d24..24e207af9 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -15,7 +15,6 @@ import type { RawPermissions, TailorDBServiceConfigInput, } from "#/types/tailordb.generated"; -import type { NonEmptyObject } from "type-fest"; export type SerialConfig = Prettify< { @@ -91,6 +90,8 @@ export interface TailorDBTypeMetadata { } >; // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + typeHook?: { create?: Function; update?: Function }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type typeValidate?: Function; } @@ -154,7 +155,7 @@ type HookArgs = type HookFn = (args: { value: TValue; - newRecord: HookArgs; + input: HookArgs; oldRecord: HookArgs | null; invoker: TailorPrincipal | null; now: Date; @@ -184,20 +185,20 @@ export type TypeValidateFn< issues: (field: DottedPaths>, message: string) => void, ) => void; -export type Hooks< +export type TypeHookFn< F extends Record, TData = { [K in keyof F]: output }, -> = NonEmptyObject<{ - [K in Exclude as F[K]["_defined"] extends { - hooks: unknown; - } - ? never - : F[K]["_defined"] extends { type: "nested" } - ? never - : K]?: F[K]["_defined"] extends { default: unknown } - ? Hook, output | null | undefined> - : Hook>; -}>; +> = (args: { + input: HookArgs; + oldRecord: HookArgs | null; + invoker: TailorPrincipal | null; + now: Date; +}) => { [K in Exclude]?: TData[K] | null | undefined }; + +export type TypeHook> = { + create?: TypeHookFn; + update?: TypeHookFn; +}; // --- Field helper types --- diff --git a/packages/sdk/src/configure/types/field-runtime.ts b/packages/sdk/src/configure/types/field-runtime.ts index 000cc50cf..bd12b4e7e 100644 --- a/packages/sdk/src/configure/types/field-runtime.ts +++ b/packages/sdk/src/configure/types/field-runtime.ts @@ -171,15 +171,11 @@ function validateValue( const validateFns = field._metadata.validate; if (validateFns && validateFns.length > 0) { - for (const validateInput of validateFns) { - const { fn, message } = - typeof validateInput === "function" - ? { fn: validateInput, message: "Validation failed" } - : { fn: validateInput[0], message: validateInput[1] }; - - if (!fn({ value, data, invoker })) { + for (const fn of validateFns) { + const result = fn({ newValue: value, oldValue: null }); + if (typeof result === "string") { issues.push({ - message, + message: result, path, }); } diff --git a/packages/sdk/src/configure/types/field.types.ts b/packages/sdk/src/configure/types/field.types.ts index 41637959f..f83b4d2ad 100644 --- a/packages/sdk/src/configure/types/field.types.ts +++ b/packages/sdk/src/configure/types/field.types.ts @@ -3,7 +3,7 @@ // This is a pure type module: type declarations only, no zod/schema // references, importable type-only from any layer. -import type { output, InferFieldsOutput } from "#/types/helpers"; +import type { output } from "#/types/helpers"; import type { DateString, DateTimeString, diff --git a/packages/sdk/src/parser/service/tailordb/field.test.ts b/packages/sdk/src/parser/service/tailordb/field.test.ts index e551aef14..e12dbf453 100644 --- a/packages/sdk/src/parser/service/tailordb/field.test.ts +++ b/packages/sdk/src/parser/service/tailordb/field.test.ts @@ -124,8 +124,8 @@ describe("parseFieldConfig validator expressions", () => { test("normalizes a method-shorthand validator whose body contains an arrow function", () => { // Method shorthand syntax, obtained the same way a user's helper object would produce it. const validators = { - isValid({ value }: { value: string }) { - return [value].map((v) => v.includes("@"))[0] ?? false; + isValid({ newValue }: { newValue: string; oldValue: string | null }): string | void { + if (!([newValue].map((v) => v.includes("@"))[0] ?? false)) return "invalid email"; }, }; const type = db.type("User", { @@ -162,8 +162,13 @@ describe("parseFieldConfig script expression validation", () => { }); test("throws a clear error when a validator cannot be converted to valid JavaScript", () => { - const check = function check({ value }: { value: string }) { - return value.length > 0; + const check = function check({ + newValue, + }: { + newValue: string; + oldValue: string | null; + }): string | void { + if (newValue.length === 0) return "must not be empty"; }.bind(null); const type = db.type("User", { email: db.string().validate(check), @@ -177,8 +182,13 @@ describe("parseFieldConfig script expression validation", () => { }); test("includes the type and field path in conversion errors from type parsing", () => { - const check = function check({ value }: { value: string }) { - return value.length > 0; + const check = function check({ + newValue, + }: { + newValue: string; + oldValue: string | null; + }): string | void { + if (newValue.length === 0) return "must not be empty"; }.bind(null); const type = db.type("User", { email: db.string().validate(check), diff --git a/packages/sdk/src/parser/service/tailordb/field.ts b/packages/sdk/src/parser/service/tailordb/field.ts index 38b2d5503..2d1a60dff 100644 --- a/packages/sdk/src/parser/service/tailordb/field.ts +++ b/packages/sdk/src/parser/service/tailordb/field.ts @@ -199,11 +199,24 @@ const convertToScriptExpr = ( } const normalized = stringifyFunction(fn); return assertParsableExpression( - `(${normalized})({ value: _value, newRecord: _input, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap}, now: _now })`, + `(${normalized})({ value: _value, input: _input, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap}, now: _now })`, formatScriptContext(kind, context), ); }; +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +export const convertTypeHookToExpr = (fn: Function): string => { + const precompiledExpr = getPrecompiledScriptExpr(fn as (...args: never[]) => unknown); + if (precompiledExpr) { + return precompiledExpr; + } + const normalized = stringifyFunction(fn); + return assertParsableExpression( + `(${normalized})({ input: _input, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap}, now: _now })`, + "type-hook", + ); +}; + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export const convertTypeValidateToExpr = (fn: Function): string => { const precompiledExpr = getPrecompiledScriptExpr(fn as (...args: never[]) => unknown); diff --git a/packages/sdk/src/parser/service/tailordb/schema.ts b/packages/sdk/src/parser/service/tailordb/schema.ts index 6dc86107a..e77d06ec7 100644 --- a/packages/sdk/src/parser/service/tailordb/schema.ts +++ b/packages/sdk/src/parser/service/tailordb/schema.ts @@ -267,6 +267,12 @@ export const TailorDBTypeSchema = z.strictObject({ }), ) .optional(), + typeHook: z + .strictObject({ + create: functionSchema.optional(), + update: functionSchema.optional(), + }) + .optional(), typeValidate: functionSchema.optional(), }) .strict(), diff --git a/packages/sdk/src/parser/service/tailordb/type-parser.ts b/packages/sdk/src/parser/service/tailordb/type-parser.ts index f6a8c21fd..43d76ec64 100644 --- a/packages/sdk/src/parser/service/tailordb/type-parser.ts +++ b/packages/sdk/src/parser/service/tailordb/type-parser.ts @@ -1,6 +1,6 @@ import * as inflection from "inflection"; import { isPluginGeneratedType } from "#/parser/service/tailordb/type-source"; -import { convertTypeValidateToExpr, parseFieldConfig } from "./field"; +import { convertTypeHookToExpr, convertTypeValidateToExpr, parseFieldConfig } from "./field"; import { parsePermissions } from "./permission"; import { validateRelationConfig, @@ -171,6 +171,16 @@ function parseTailorDBType( permissions: parsePermissions(metadata.permissions), indexes: metadata.indexes, files: metadata.files, + ...(metadata.typeHook && { + typeHookExpr: { + ...(typeof metadata.typeHook.create === "function" && { + create: convertTypeHookToExpr(metadata.typeHook.create), + }), + ...(typeof metadata.typeHook.update === "function" && { + update: convertTypeHookToExpr(metadata.typeHook.update), + }), + }, + }), ...(typeof metadata.typeValidate === "function" && { typeValidateExpr: convertTypeValidateToExpr(metadata.typeValidate), }), diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index 30a450f03..344c94dc9 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -132,7 +132,7 @@ describe("buildTypeScripts", () => { const typeValidateExpr = '(({ newRecord }) => { if (newRecord.start > newRecord.end) __issues("start", "bad"); })({ newRecord: _newRecord, oldRecord: _oldRecord }, __issues)'; - const { typeValidate } = buildTypeScripts({}, typeValidateExpr); + const { typeValidate } = buildTypeScripts({}, { typeValidateExpr }); const expr = typeValidate?.create?.expr ?? ""; expect(typeValidate?.update?.expr).toBe(expr); expect(expr).toContain("const __errs = {}"); @@ -150,7 +150,7 @@ describe("buildTypeScripts", () => { }; const typeValidateExpr = "typeValidateFn({ newRecord: _newRecord }, __issues)"; - const { typeValidate } = buildTypeScripts(fields, typeValidateExpr); + const { typeValidate } = buildTypeScripts(fields, { typeValidateExpr }); const expr = typeValidate?.create?.expr ?? ""; expect(expr).toContain('const _value = _newRecord["name"]'); expect(expr).toContain("const __issues = (f, m) => { __errs[f] = m; }"); diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index a26c8bd3d..6a865dc1f 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -149,21 +149,34 @@ function wrapValidate(statements: string[], typeValidateExpr?: string): string { * operation, so all fields touched in one create/update observe the same * instant. Defaults are applied after hooks on create only. Validators * run with the same rules on create and update. - * @param {Record} fields - Field configurations - * @param {string} [typeValidateExpr] - Precompiled type-level validate expression - * @returns {TypeScripts} Aggregated type-level scripts + * @param fields - Per-field script configuration + * @param options - Optional type-level hook/validate expressions + * @returns Aggregated type-level scripts */ export function buildTypeScripts( fields: Record, - typeValidateExpr?: string, + options?: { + typeHookExpr?: { create?: string; update?: string }; + typeValidateExpr?: string; + }, ): TypeScripts { const result: TypeScripts = {}; + const typeHookExpr = options?.typeHookExpr; + const typeValidateExpr = options?.typeValidateExpr; const hook: { create?: ScriptRef; update?: ScriptRef } = {}; for (const operation of ["create", "update"] as const) { - const objectExpr = buildHookObject(fields, INPUT, operation); - if (objectExpr !== null) { - hook[operation] = { expr: wrapHook(objectExpr) }; + const perFieldExpr = buildHookObject(fields, INPUT, operation); + const typeLevelExpr = typeHookExpr?.[operation]; + + if (perFieldExpr !== null && typeLevelExpr) { + hook[operation] = { + expr: wrapHook(`Object.assign({}, ${perFieldExpr}, ${typeLevelExpr})`), + }; + } else if (typeLevelExpr) { + hook[operation] = { expr: wrapHook(typeLevelExpr) }; + } else if (perFieldExpr !== null) { + hook[operation] = { expr: wrapHook(perFieldExpr) }; } } if (hook.create || hook.update) { diff --git a/packages/sdk/src/parser/service/tailordb/types.ts b/packages/sdk/src/parser/service/tailordb/types.ts index 999f2fa6a..a3dd0a75b 100644 --- a/packages/sdk/src/parser/service/tailordb/types.ts +++ b/packages/sdk/src/parser/service/tailordb/types.ts @@ -172,5 +172,6 @@ export interface TailorDBType { permissions: Permissions; indexes?: TailorDBTypeMetadata["indexes"]; files?: TailorDBTypeMetadata["files"]; + typeHookExpr?: { create?: string; update?: string }; typeValidateExpr?: string; } diff --git a/packages/sdk/src/types/auth.generated.ts b/packages/sdk/src/types/auth.generated.ts index efe5d57e8..0138877c7 100644 --- a/packages/sdk/src/types/auth.generated.ts +++ b/packages/sdk/src/types/auth.generated.ts @@ -1327,6 +1327,13 @@ export type AuthConfigInput = }; } | undefined; + typeHook?: + | { + create?: Function | undefined; + update?: Function | undefined; + } + | undefined; + typeValidate?: Function | undefined; }; readonly plugins: { pluginId: string; @@ -1698,7 +1705,7 @@ export type AuthConfig = update?: Function | undefined; } | undefined; - validate?: (Function | [Function, string])[] | undefined; + validate?: Function[] | undefined; serial?: | { start: number; @@ -1707,6 +1714,7 @@ export type AuthConfig = } | undefined; scale?: number | undefined | undefined; + default?: unknown; }; rawRelation?: | { @@ -2618,6 +2626,13 @@ export type AuthConfig = }; } | undefined; + typeHook?: + | { + create?: Function | undefined; + update?: Function | undefined; + } + | undefined; + typeValidate?: Function | undefined; }; }; usernameField: string; diff --git a/packages/sdk/src/types/tailordb.generated.ts b/packages/sdk/src/types/tailordb.generated.ts index 446e1df91..d5dcf0510 100644 --- a/packages/sdk/src/types/tailordb.generated.ts +++ b/packages/sdk/src/types/tailordb.generated.ts @@ -1023,6 +1023,12 @@ export type TailorDBTypeRawInput = { }; } | undefined; + typeHook?: + | { + create?: Function | undefined; + update?: Function | undefined; + } + | undefined; typeValidate?: Function | undefined; }; }; @@ -1096,6 +1102,12 @@ export type TailorDBTypeRaw = { }; } | undefined; + typeHook?: + | { + create?: Function | undefined; + update?: Function | undefined; + } + | undefined; typeValidate?: Function | undefined; }; }; diff --git a/packages/sdk/src/utils/test/index.test.ts b/packages/sdk/src/utils/test/index.test.ts index b08df747a..000bb6ff3 100644 --- a/packages/sdk/src/utils/test/index.test.ts +++ b/packages/sdk/src/utils/test/index.test.ts @@ -168,49 +168,43 @@ describe("createTailorDBHook", () => { }); }); - describe("create hook on a top-level field", () => { - test("invokes the create hook with value, full data, and a null invoker", () => { - const seen: { value: unknown; newRecord: unknown; invoker: unknown }[] = []; + describe("type-level create hook", () => { + test("invokes the create hook and applies field overrides", () => { + const seen: { input: unknown; invoker: unknown }[] = []; const type = db.type("Order", { total: db.float(), tax: db.float() }).hooks({ - tax: { - create: ({ value, newRecord, invoker }) => { - seen.push({ value, newRecord, invoker }); - return (newRecord as { total: number }).total * 0.1; - }, + create: ({ input, invoker }) => { + seen.push({ input, invoker }); + return { tax: (input as { total: number }).total * 0.1 }; }, }); const result = createTailorDBHook(type)({ total: 100, tax: undefined }); expect(result.tax).toBe(10); expect(seen).toEqual([ { - value: undefined, - newRecord: { total: 100, tax: undefined }, + input: { total: 100, tax: undefined }, invoker: null, }, ]); }); - test("normalizes a Date returned from the create hook to an ISO string", () => { + test("normalizes a Date returned from the type hook to an ISO string", () => { const fixed = new Date("2026-04-15T00:00:00.000Z"); const type = db .type("Test", { createdAt: db.datetime() }) - .hooks({ createdAt: { create: () => fixed } }); + .hooks({ create: () => ({ createdAt: fixed }) }); expect(createTailorDBHook(type)({}).createdAt).toBe("2026-04-15T00:00:00.000Z"); }); test("does not invoke a hook that only defines update (createTailorDBHook is create-only)", () => { let updateCalled = false; const type = db.type("Test", { updatedAt: db.datetime() }).hooks({ - updatedAt: { - update: () => { - updateCalled = true; - return new Date(); - }, + update: () => { + updateCalled = true; + return { updatedAt: new Date() }; }, }); const result = createTailorDBHook(type)({ updatedAt: "2026-01-01T00:00:00.000Z" }); expect(updateCalled).toBe(false); - // Falls through to plain passthrough expect(result.updatedAt).toBe("2026-01-01T00:00:00.000Z"); }); }); diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index 2f43dff2f..4965e0b66 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -24,12 +24,11 @@ export { // eslint-disable-next-line @typescript-eslint/no-explicit-any export function createTailorDBHook>(type: T) { return (data: unknown) => { - return Object.entries(type.fields).reduce( + const hooked = Object.entries(type.fields).reduce( (hooked, [key, value]) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const field = value as TailorField; if (key === "id") { - // Use existing id from data if provided, otherwise generate new UUID const existingId = data && typeof data === "object" ? (data as Record)[key] : undefined; hooked[key] = existingId ?? crypto.randomUUID(); @@ -39,8 +38,6 @@ export function createTailorDBHook>(type: T) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const nestedHook = createTailorDBHook({ fields: field.fields } as any); if (field.metadata.array) { - // For nested array fields, recurse per element and pass through non-array values - // (e.g. null/undefined for optional fields) so validation sees the original value. hooked[key] = Array.isArray(nestedValue) ? nestedValue.map((item) => nestedHook(item)) : nestedValue; @@ -50,7 +47,7 @@ export function createTailorDBHook>(type: T) { } else if (field.metadata.hooks?.create) { hooked[key] = field.metadata.hooks.create({ value: (data as Record)[key], - newRecord: data, + input: data, oldRecord: null, invoker: null, now: new Date(), @@ -64,7 +61,25 @@ export function createTailorDBHook>(type: T) { return hooked; }, {} as Record, - ) as Partial>; + ); + + // oxlint-disable-next-line typescript/no-unnecessary-condition -- metadata absent in recursive nested calls + if (type.metadata?.typeHook?.create) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + const overrides = (type.metadata.typeHook.create as Function)({ + input: data, + oldRecord: null, + invoker: null, + now: new Date(), + }); + if (overrides && typeof overrides === "object") { + for (const [key, value] of Object.entries(overrides as Record)) { + hooked[key] = value instanceof Date ? value.toISOString() : value; + } + } + } + + return hooked as Partial>; }; } From 3ebff9c514052e743c7c60cc548f4a8f61e7a2e7 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 6 Jul 2026 22:10:14 +0900 Subject: [PATCH 438/618] refactor(tailordb): replace input/oldRecord with typed oldValue in field-level hooks --- .../templates/tailordb/src/db/task.ts | 12 +++++------ .../migrate/snapshot-manifest.test.ts | 4 +++- .../tailordb/hooks-validate-bundler.ts | 2 +- .../services/tailordb/schema.test.ts | 19 +++++++----------- .../src/configure/services/tailordb/schema.ts | 6 +++--- .../src/configure/services/tailordb/types.ts | 11 +++++----- .../sdk/src/parser/service/tailordb/field.ts | 2 +- .../service/tailordb/type-script.test.ts | 20 ++++++++++++++----- .../parser/service/tailordb/type-script.ts | 13 ++++++++---- packages/sdk/src/utils/test/index.ts | 3 +-- 10 files changed, 51 insertions(+), 41 deletions(-) diff --git a/packages/create-sdk/templates/tailordb/src/db/task.ts b/packages/create-sdk/templates/tailordb/src/db/task.ts index 13dd98b66..879f5a0b7 100644 --- a/packages/create-sdk/templates/tailordb/src/db/task.ts +++ b/packages/create-sdk/templates/tailordb/src/db/task.ts @@ -30,14 +30,14 @@ export const task = db type: "n-1", toward: { type: category }, }), - isArchived: db.bool().description("Whether the task is archived"), + isArchived: db + .bool() + .description("Whether the task is archived") + .hooks({ + create: () => false, + }), ...db.fields.timestamps(), }) - .hooks({ - isArchived: { - create: () => false, - }, - }) .indexes( { fields: ["status", "priority"], unique: false }, { fields: ["assigneeId", "status"], unique: false, name: "task_assignee_status_idx" }, diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts index ee35bce05..79fb7ffdc 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts @@ -318,7 +318,9 @@ describe("snapshot-manifest", () => { // timestamp once and dispatches each field's hook. const createHook = manifest.schema?.typeHook?.create?.expr ?? ""; expect(createHook).toContain("const _now = new Date()"); - expect(createHook).toContain('"updatedAt": ((_value) => (now()))(_input["updatedAt"])'); + expect(createHook).toContain( + '"updatedAt": ((_value, _oldValue) => (now()))(_input["updatedAt"], _oldRecord?.["updatedAt"] ?? null)', + ); expect(manifest.schema?.typeHook?.update?.expr).toContain('_input["updatedAt"]'); expect(manifest.schema?.typeValidate).toBeUndefined(); }); diff --git a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts index 9a1b4c7d0..17f0b960c 100644 --- a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts +++ b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts @@ -456,7 +456,7 @@ async function bundleScriptTarget(args: { const fnSource = stringifyFunction(fn); const argsObject = kind === "hooks" - ? `{ value: _value, input: _input, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap}, now: _now }` + ? `{ value: _value, oldValue: _oldValue, invoker: ${tailorPrincipalMap}, now: _now }` : kind === "validate" ? `{ newValue: _value, oldValue: _oldValue }` : kind === "typeHook" diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 4d33b86fa..59d28bd40 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -608,12 +608,12 @@ describe("TailorDBField hooks modifier tests", () => { test("hooks modifier on string field receives string", () => { const _hooks = db.string().hooks; - expectTypeOf[0]>().toEqualTypeOf>(); + expectTypeOf[0]>().toEqualTypeOf>(); }); test("hooks modifier on optional field receives null", () => { const _hooks = db.string({ optional: true }).hooks; - expectTypeOf[0]>().toEqualTypeOf>(); + expectTypeOf[0]>().toEqualTypeOf>(); }); }); @@ -781,8 +781,7 @@ describe("TailorDBType withTimestamps option tests", () => { const now = new Date("2025-06-01T00:00:00Z"); const result = createHook!({ value: specified, - input: {}, - oldRecord: null, + oldValue: null, invoker: timestampHookInvoker, now, }); @@ -797,8 +796,7 @@ describe("TailorDBType withTimestamps option tests", () => { const now = new Date("2025-06-01T12:00:00Z"); const result = createHook!({ value: null, - input: {}, - oldRecord: null, + oldValue: null, invoker: timestampHookInvoker, now, }); @@ -814,8 +812,7 @@ describe("TailorDBType withTimestamps option tests", () => { const now = new Date("2025-06-01T12:00:00Z"); const result = createHook!({ value: specified, - input: {}, - oldRecord: null, + oldValue: null, invoker: timestampHookInvoker, now, }); @@ -830,8 +827,7 @@ describe("TailorDBType withTimestamps option tests", () => { const now = new Date("2025-06-01T12:00:00Z"); const result = createHook!({ value: null, - input: {}, - oldRecord: null, + oldValue: null, invoker: timestampHookInvoker, now, }); @@ -846,8 +842,7 @@ describe("TailorDBType withTimestamps option tests", () => { const now = new Date("2025-06-01T12:00:00Z"); const result = updateHook!({ value: null, - input: {}, - oldRecord: null, + oldValue: null, invoker: timestampHookInvoker, now, }); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index f79ed46e4..d2c9c54a3 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -166,7 +166,7 @@ type DBFieldVectorFn< OutputBase, > = () => TailorDBField, Output, OutputBase>; type DBFieldHooksFn = < - const H extends Hook, + const H extends Hook, >( hooks: H, ) => TailorDBField, Output, OutputBase>; @@ -470,7 +470,7 @@ type TailorDBFieldRuntime = Omit unique(): object; vector(): object; default(value: unknown): object; - hooks(hooks: Hook): object; + hooks(hooks: Hook): object; serial(config: SerialConfig): object; clone(options?: FieldOptions): TailorDBFieldRuntime; parse(args: FieldParseArgs): StandardSchemaV1.Result; @@ -618,7 +618,7 @@ function createTailorDBFieldRuntime< return cloneWith({ default: value }) as any; }, - hooks(hooks: Hook) { + hooks(hooks: Hook) { return cloneWith({ hooks }); }, diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index 24e207af9..f7a92a269 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -153,17 +153,16 @@ type HookArgs = ? { readonly [K in keyof TData]?: TData[K] | null | undefined } : unknown; -type HookFn = (args: { +type HookFn = (args: { value: TValue; - input: HookArgs; - oldRecord: HookArgs | null; + oldValue: TValue | null; invoker: TailorPrincipal | null; now: Date; }) => TReturn; -export type Hook = { - create?: HookFn; - update?: HookFn; +export type Hook = { + create?: HookFn; + update?: HookFn; }; type DottedPaths = diff --git a/packages/sdk/src/parser/service/tailordb/field.ts b/packages/sdk/src/parser/service/tailordb/field.ts index 2d1a60dff..2b2f36a24 100644 --- a/packages/sdk/src/parser/service/tailordb/field.ts +++ b/packages/sdk/src/parser/service/tailordb/field.ts @@ -199,7 +199,7 @@ const convertToScriptExpr = ( } const normalized = stringifyFunction(fn); return assertParsableExpression( - `(${normalized})({ value: _value, input: _input, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap}, now: _now })`, + `(${normalized})({ value: _value, oldValue: _oldValue, invoker: ${tailorPrincipalMap}, now: _now })`, formatScriptContext(kind, context), ); }; diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index 344c94dc9..94dc27f36 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -28,8 +28,12 @@ describe("buildTypeScripts", () => { // A single `new Date()` is bound once and dispatched to every field. const createExpr = typeHook?.create?.expr ?? ""; expect(createExpr.match(/new Date\(\)/g)).toHaveLength(1); - expect(createExpr).toContain('"createdAt": ((_value) => (_now))(_input["createdAt"])'); - expect(createExpr).toContain('"updatedAt": ((_value) => (_now))(_input["updatedAt"])'); + expect(createExpr).toContain( + '"createdAt": ((_value, _oldValue) => (_now))(_input["createdAt"], _oldRecord?.["createdAt"] ?? null)', + ); + expect(createExpr).toContain( + '"updatedAt": ((_value, _oldValue) => (_now))(_input["updatedAt"], _oldRecord?.["updatedAt"] ?? null)', + ); // createdAt has no update hook, so the update script only touches updatedAt. const updateExpr = typeHook?.update?.expr ?? ""; @@ -55,11 +59,15 @@ describe("buildTypeScripts", () => { const createExpr = buildTypeScripts(fields).typeHook?.create?.expr ?? ""; expect(createExpr).toContain('"profile": Object.assign({}, _input["profile"], {'); - expect(createExpr).toContain('(_input["profile"] || {})["displayName"]'); + expect(createExpr).toContain( + '(_input["profile"] || {})["displayName"], _oldRecord?.["profile"]?.["displayName"] ?? null)', + ); expect(createExpr).toContain( '"contact": Object.assign({}, (_input["profile"] || {})["contact"], {', ); - expect(createExpr).toContain('((_input["profile"] || {})["contact"] || {})["email"]'); + expect(createExpr).toContain( + '((_input["profile"] || {})["contact"] || {})["email"], _oldRecord?.["profile"]?.["contact"]?.["email"] ?? null)', + ); }); test("applies default as ?? fallback after hook on create only", () => { @@ -79,7 +87,9 @@ describe("buildTypeScripts", () => { const createExpr = typeHook?.create?.expr ?? ""; // hook + default: hookResult ?? defaultValue - expect(createExpr).toContain('"status": ((_value) => (_value))(_input["status"]) ?? "active"'); + expect(createExpr).toContain( + '"status": ((_value, _oldValue) => (_value))(_input["status"], _oldRecord?.["status"] ?? null) ?? "active"', + ); // default only: input ?? defaultValue expect(createExpr).toContain('"name": _input["name"] ?? "unnamed"'); diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index 6a865dc1f..042f0cc44 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -52,20 +52,23 @@ function serializeDefault(value: unknown, fieldType: string): string { * the operation. * @param {Record} fields - Field configurations * @param {string} accessExpr - JS expression to access the parent object + * @param {string} oldAccessExpr - JS expression to access the old record parent * @param {HookOperation} operation - Hook operation type * @returns {string | null} Object literal expression or null */ function buildHookObject( fields: Record, accessExpr: string, + oldAccessExpr: string, operation: HookOperation, ): string | null { const parts: string[] = []; for (const [name, config] of Object.entries(fields)) { const access = `${accessExpr}[${key(name)}]`; + const oldAccess = `${oldAccessExpr}?.[${key(name)}]`; if (isNestedType(config) && config.fields) { - const inner = buildHookObject(config.fields, `(${access} || {})`, operation); + const inner = buildHookObject(config.fields, `(${access} || {})`, oldAccess, operation); if (inner !== null) { parts.push(`${key(name)}: Object.assign({}, ${access}, ${inner})`); } @@ -77,10 +80,12 @@ function buildHookObject( if (hook && hasDefault) { parts.push( - `${key(name)}: ((_value) => (${hook.expr}))(${access}) ?? ${serializeDefault(config.default, config.type)}`, + `${key(name)}: ((_value, _oldValue) => (${hook.expr}))(${access}, ${oldAccess} ?? null) ?? ${serializeDefault(config.default, config.type)}`, ); } else if (hook) { - parts.push(`${key(name)}: ((_value) => (${hook.expr}))(${access})`); + parts.push( + `${key(name)}: ((_value, _oldValue) => (${hook.expr}))(${access}, ${oldAccess} ?? null)`, + ); } else if (hasDefault) { parts.push(`${key(name)}: ${access} ?? ${serializeDefault(config.default, config.type)}`); } @@ -166,7 +171,7 @@ export function buildTypeScripts( const hook: { create?: ScriptRef; update?: ScriptRef } = {}; for (const operation of ["create", "update"] as const) { - const perFieldExpr = buildHookObject(fields, INPUT, operation); + const perFieldExpr = buildHookObject(fields, INPUT, OLD_RECORD, operation); const typeLevelExpr = typeHookExpr?.[operation]; if (perFieldExpr !== null && typeLevelExpr) { diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index 4965e0b66..56a05c8c4 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -47,8 +47,7 @@ export function createTailorDBHook>(type: T) { } else if (field.metadata.hooks?.create) { hooked[key] = field.metadata.hooks.create({ value: (data as Record)[key], - input: data, - oldRecord: null, + oldValue: null, invoker: null, now: new Date(), }); From 0ee32bdd61a6cf48772786541155dbe719a8570e Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 6 Jul 2026 23:47:11 +0900 Subject: [PATCH 439/618] refactor(tailordb): simplify field validate API and remove record-syntax validators - Simplify ValidateFn from { newValue, oldValue } to { value } only - Remove Validators record-syntax overload from type-level validate - Block .default() on optional fields with TypeLevelError - Migrate all examples, templates, and tests to new API --- example/resolvers/add.ts | 6 +- example/resolvers/stepChain.ts | 8 +- example/tailordb/customer.ts | 18 +-- example/tailordb/file.ts | 4 +- .../inventory-management/src/db/inventory.ts | 2 +- .../inventory-management/src/db/orderItem.ts | 4 +- .../templates/tailordb/src/db/comment.ts | 2 +- .../templates/tailordb/src/db/task.ts | 9 +- .../perf/features/tailordb-validate.ts | 80 ++++------ .../deploy/__test_fixtures__/resolvers/add.ts | 6 +- .../tailordb/hooks-validate-bundler.ts | 2 +- .../services/tailordb/schema.test.ts | 143 ++++-------------- .../src/configure/services/tailordb/schema.ts | 20 +-- .../sdk/src/configure/types/field-runtime.ts | 2 +- .../sdk/src/configure/types/field.types.ts | 32 +--- packages/sdk/src/configure/types/type.test.ts | 10 +- .../tailordb/field.precompiled.test.ts | 4 +- .../src/parser/service/tailordb/field.test.ts | 22 +-- .../sdk/src/parser/service/tailordb/field.ts | 6 +- .../service/tailordb/type-script.test.ts | 2 +- .../parser/service/tailordb/type-script.ts | 11 +- 21 files changed, 115 insertions(+), 278 deletions(-) diff --git a/example/resolvers/add.ts b/example/resolvers/add.ts index b5eb462f9..f3a0460b7 100644 --- a/example/resolvers/add.ts +++ b/example/resolvers/add.ts @@ -1,10 +1,8 @@ import { createResolver, t } from "@tailor-platform/sdk"; const validators = [ - ({ newValue }: { newValue: number }) => - newValue >= 0 ? undefined : "Value must be non-negative", - ({ newValue }: { newValue: number }) => - newValue < 10 ? undefined : "Value must be less than 10", + ({ value }: { value: number }) => (value >= 0 ? undefined : "Value must be non-negative"), + ({ value }: { value: number }) => (value < 10 ? undefined : "Value must be less than 10"), ] as const; export default createResolver({ name: "add", diff --git a/example/resolvers/stepChain.ts b/example/resolvers/stepChain.ts index ac3f4de28..a5cd9dd04 100644 --- a/example/resolvers/stepChain.ts +++ b/example/resolvers/stepChain.ts @@ -14,14 +14,14 @@ export default createResolver({ first: t .string() .description("User's first name") - .validate(({ newValue }) => - newValue.length >= 2 ? undefined : "First name must be at least 2 characters", + .validate(({ value }) => + value.length >= 2 ? undefined : "First name must be at least 2 characters", ), last: t .string() .description("User's last name") - .validate(({ newValue }) => - newValue.length >= 2 ? undefined : "Last name must be at least 2 characters", + .validate(({ value }) => + value.length >= 2 ? undefined : "Last name must be at least 2 characters", ), }) .description("User's full name"), diff --git a/example/tailordb/customer.ts b/example/tailordb/customer.ts index 0ab55385f..66bb11532 100644 --- a/example/tailordb/customer.ts +++ b/example/tailordb/customer.ts @@ -3,17 +3,21 @@ import { defaultGqlPermission, defaultPermission } from "./permissions"; export const customer = db .type("Customer", "Customer information", { - name: db.string(), + name: db + .string() + .validate(({ value }) => + value.length <= 5 ? "Name must be longer than 5 characters" : undefined, + ), email: db.string(), phone: db.string({ optional: true }), country: db.string(), postalCode: db.string(), address: db.string({ optional: true }), city: db.string({ optional: true }).validate( - ({ newValue }) => - newValue && newValue.length <= 1 ? "City must be longer than 1 character" : undefined, - ({ newValue }) => - newValue && newValue.length >= 100 ? "City must be shorter than 100 characters" : undefined, + ({ value }) => + value && value.length <= 1 ? "City must be longer than 1 character" : undefined, + ({ value }) => + value && value.length >= 100 ? "City must be shorter than 100 characters" : undefined, ), fullAddress: db.string(), state: db.string(), @@ -27,10 +31,6 @@ export const customer = db fullAddress: `${input.postalCode} ${input.address} ${input.city}`, }), }) - .validate({ - name: ({ newValue }) => - newValue.length <= 5 ? "Name must be longer than 5 characters" : undefined, - }) .validate(({ newRecord }, issues) => { if (newRecord.country === "JP" && !newRecord.postalCode) { issues("postalCode", "Postal code is required for Japan"); diff --git a/example/tailordb/file.ts b/example/tailordb/file.ts index 25a8a1809..3a553bd55 100644 --- a/example/tailordb/file.ts +++ b/example/tailordb/file.ts @@ -4,9 +4,7 @@ export const attachedFiles = db.object( { id: db.uuid(), name: db.string(), - size: db - .int() - .validate(({ newValue }) => (newValue <= 0 ? "Size must be positive" : undefined)), + size: db.int().validate(({ value }) => (value <= 0 ? "Size must be positive" : undefined)), type: db.enum(["text", "image"]), }, { array: true }, diff --git a/packages/create-sdk/templates/inventory-management/src/db/inventory.ts b/packages/create-sdk/templates/inventory-management/src/db/inventory.ts index a708e73ff..77d951166 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/inventory.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/inventory.ts @@ -11,7 +11,7 @@ export const inventory = db quantity: db .int() .description("Quantity of the product in inventory") - .validate(({ newValue }) => (newValue < 0 ? "Quantity must be non-negative" : undefined)), + .validate(({ value }) => (value < 0 ? "Quantity must be non-negative" : undefined)), ...db.fields.timestamps(), }) .permission(permissionLoggedIn) diff --git a/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts b/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts index 67495413b..506484447 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts @@ -16,11 +16,11 @@ export const orderItem = db quantity: db .int() .description("Quantity of the product") - .validate(({ newValue }) => (newValue < 0 ? "Quantity must be non-negative" : undefined)), + .validate(({ value }) => (value < 0 ? "Quantity must be non-negative" : undefined)), unitPrice: db .float() .description("Unit price of the product") - .validate(({ newValue }) => (newValue < 0 ? "Unit price must be non-negative" : undefined)), + .validate(({ value }) => (value < 0 ? "Unit price must be non-negative" : undefined)), totalPrice: db.float({ optional: true }).description("Total price of the order item"), ...db.fields.timestamps(), }) diff --git a/packages/create-sdk/templates/tailordb/src/db/comment.ts b/packages/create-sdk/templates/tailordb/src/db/comment.ts index 028ba2b06..44e3297a1 100644 --- a/packages/create-sdk/templates/tailordb/src/db/comment.ts +++ b/packages/create-sdk/templates/tailordb/src/db/comment.ts @@ -7,7 +7,7 @@ export const comment = db .type("Comment", "A comment on a task", { body: db .string() - .validate(({ newValue }) => (newValue.length < 1 ? "Comment must not be empty" : undefined)), + .validate(({ value }) => (value.length < 1 ? "Comment must not be empty" : undefined)), taskId: db.uuid().relation({ type: "n-1", toward: { type: task }, diff --git a/packages/create-sdk/templates/tailordb/src/db/task.ts b/packages/create-sdk/templates/tailordb/src/db/task.ts index 879f5a0b7..2da97f74d 100644 --- a/packages/create-sdk/templates/tailordb/src/db/task.ts +++ b/packages/create-sdk/templates/tailordb/src/db/task.ts @@ -6,9 +6,8 @@ import { user } from "./user"; export const task = db .type("Task", "A task with comprehensive features", { title: db.string().validate( - ({ newValue }) => (newValue.length >= 3 ? undefined : "Title must be at least 3 characters"), - ({ newValue }) => - newValue.length <= 200 ? undefined : "Title must be at most 200 characters", + ({ value }) => (value.length >= 3 ? undefined : "Title must be at least 3 characters"), + ({ value }) => (value.length <= 200 ? undefined : "Title must be at most 200 characters"), ), description: db.string({ optional: true }), status: db.enum([ @@ -18,8 +17,8 @@ export const task = db { value: "CANCELLED", description: "No longer needed" }, ]), priority: db.int().validate( - ({ newValue }) => (newValue >= 0 ? undefined : "Priority must be non-negative"), - ({ newValue }) => (newValue <= 4 ? undefined : "Priority must be at most 4"), + ({ value }) => (value >= 0 ? undefined : "Priority must be non-negative"), + ({ value }) => (value <= 4 ? undefined : "Priority must be at most 4"), ), dueDate: db.datetime({ optional: true }), assigneeId: db.uuid({ optional: true }).relation({ diff --git a/packages/sdk/scripts/perf/features/tailordb-validate.ts b/packages/sdk/scripts/perf/features/tailordb-validate.ts index bf24b0c77..888b41f05 100644 --- a/packages/sdk/scripts/perf/features/tailordb-validate.ts +++ b/packages/sdk/scripts/perf/features/tailordb-validate.ts @@ -6,101 +6,81 @@ import { db } from "../../../src/configure"; export const type0 = db.type("Type0", { - name: db - .string() - .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + name: db.string().validate(({ value }) => (value.length <= 0 ? "Must not be empty" : undefined)), email: db .string() - .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), - age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), + .validate(({ value }) => (!value.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ value }) => (value < 0 ? "Must be non-negative" : undefined)), }); export const type1 = db.type("Type1", { - name: db - .string() - .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + name: db.string().validate(({ value }) => (value.length <= 0 ? "Must not be empty" : undefined)), email: db .string() - .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), - age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), + .validate(({ value }) => (!value.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ value }) => (value < 0 ? "Must be non-negative" : undefined)), }); export const type2 = db.type("Type2", { - name: db - .string() - .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + name: db.string().validate(({ value }) => (value.length <= 0 ? "Must not be empty" : undefined)), email: db .string() - .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), - age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), + .validate(({ value }) => (!value.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ value }) => (value < 0 ? "Must be non-negative" : undefined)), }); export const type3 = db.type("Type3", { - name: db - .string() - .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + name: db.string().validate(({ value }) => (value.length <= 0 ? "Must not be empty" : undefined)), email: db .string() - .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), - age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), + .validate(({ value }) => (!value.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ value }) => (value < 0 ? "Must be non-negative" : undefined)), }); export const type4 = db.type("Type4", { - name: db - .string() - .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + name: db.string().validate(({ value }) => (value.length <= 0 ? "Must not be empty" : undefined)), email: db .string() - .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), - age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), + .validate(({ value }) => (!value.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ value }) => (value < 0 ? "Must be non-negative" : undefined)), }); export const type5 = db.type("Type5", { - name: db - .string() - .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + name: db.string().validate(({ value }) => (value.length <= 0 ? "Must not be empty" : undefined)), email: db .string() - .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), - age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), + .validate(({ value }) => (!value.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ value }) => (value < 0 ? "Must be non-negative" : undefined)), }); export const type6 = db.type("Type6", { - name: db - .string() - .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + name: db.string().validate(({ value }) => (value.length <= 0 ? "Must not be empty" : undefined)), email: db .string() - .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), - age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), + .validate(({ value }) => (!value.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ value }) => (value < 0 ? "Must be non-negative" : undefined)), }); export const type7 = db.type("Type7", { - name: db - .string() - .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + name: db.string().validate(({ value }) => (value.length <= 0 ? "Must not be empty" : undefined)), email: db .string() - .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), - age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), + .validate(({ value }) => (!value.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ value }) => (value < 0 ? "Must be non-negative" : undefined)), }); export const type8 = db.type("Type8", { - name: db - .string() - .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + name: db.string().validate(({ value }) => (value.length <= 0 ? "Must not be empty" : undefined)), email: db .string() - .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), - age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), + .validate(({ value }) => (!value.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ value }) => (value < 0 ? "Must be non-negative" : undefined)), }); export const type9 = db.type("Type9", { - name: db - .string() - .validate(({ newValue }) => (newValue.length <= 0 ? "Must not be empty" : undefined)), + name: db.string().validate(({ value }) => (value.length <= 0 ? "Must not be empty" : undefined)), email: db .string() - .validate(({ newValue }) => (!newValue.includes("@") ? "Must be valid email" : undefined)), - age: db.int().validate(({ newValue }) => (newValue < 0 ? "Must be non-negative" : undefined)), + .validate(({ value }) => (!value.includes("@") ? "Must be valid email" : undefined)), + age: db.int().validate(({ value }) => (value < 0 ? "Must be non-negative" : undefined)), }); diff --git a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/add.ts b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/add.ts index b5eb462f9..f3a0460b7 100644 --- a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/add.ts +++ b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/resolvers/add.ts @@ -1,10 +1,8 @@ import { createResolver, t } from "@tailor-platform/sdk"; const validators = [ - ({ newValue }: { newValue: number }) => - newValue >= 0 ? undefined : "Value must be non-negative", - ({ newValue }: { newValue: number }) => - newValue < 10 ? undefined : "Value must be less than 10", + ({ value }: { value: number }) => (value >= 0 ? undefined : "Value must be non-negative"), + ({ value }: { value: number }) => (value < 10 ? undefined : "Value must be less than 10"), ] as const; export default createResolver({ name: "add", diff --git a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts index 17f0b960c..fd92484f1 100644 --- a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts +++ b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts @@ -458,7 +458,7 @@ async function bundleScriptTarget(args: { kind === "hooks" ? `{ value: _value, oldValue: _oldValue, invoker: ${tailorPrincipalMap}, now: _now }` : kind === "validate" - ? `{ newValue: _value, oldValue: _oldValue }` + ? `{ value: _value }` : kind === "typeHook" ? `{ input: _input, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap}, now: _now }` : `{ newRecord: _newRecord, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap} }, __issues`; diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 59d28bd40..1f7b5aec0 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -543,6 +543,10 @@ describe("TailorDBField type error message tests", () => { expectTypeOf(db.string().serial({ start: 0 }).default).toEqualTypeOf< TypeLevelError<"default cannot be set on serial fields"> >(); + + expectTypeOf(db.string({ optional: true }).default).toEqualTypeOf< + TypeLevelError<"default cannot be set on optional fields"> + >(); }); }); @@ -632,7 +636,7 @@ describe("TailorDBField validate modifier tests", () => { const _validateType = db.type("Test", { email: db .string() - .validate(({ newValue }) => (!newValue.includes("@") ? "Email must contain @" : undefined)), + .validate(({ value }) => (!value.includes("@") ? "Email must contain @" : undefined)), }); expectTypeOf>().toEqualTypeOf<{ id: UUIDString; @@ -647,10 +651,9 @@ describe("TailorDBField validate modifier tests", () => { test("validate modifier can receive multiple validators", () => { const _validateType = db.type("Test", { password: db.string().validate( - ({ newValue }) => - newValue.length < 8 ? "Password must be at least 8 characters" : undefined, - ({ newValue }) => - !/[A-Z]/.test(newValue) ? "Password must contain uppercase letter" : undefined, + ({ value }) => (value.length < 8 ? "Password must be at least 8 characters" : undefined), + ({ value }) => + !/[A-Z]/.test(value) ? "Password must contain uppercase letter" : undefined, ), }); @@ -1061,15 +1064,14 @@ describe("TailorDBType plural form tests", () => { }); test("validation and plural form coexist in tuple format", () => { - const _userType = db - .type(["User", "Users"], { - name: db.string(), - email: db.string(), - }) - .validate({ - name: ({ newValue }) => (newValue.length <= 0 ? "Name must not be empty" : undefined), - email: ({ newValue }) => (!newValue.includes("@") ? "Invalid email format" : undefined), - }); + const _userType = db.type(["User", "Users"], { + name: db + .string() + .validate(({ value }) => (value.length <= 0 ? "Name must not be empty" : undefined)), + email: db + .string() + .validate(({ value }) => (!value.includes("@") ? "Invalid email format" : undefined)), + }); expect(_userType.name).toBe("User"); expect(_userType.metadata.settings?.pluralForm).toBe("Users"); @@ -1145,96 +1147,6 @@ describe("TailorDBType hooks modifier tests", () => { }); }); -describe("TailorDBType validate modifier tests", () => { - test("validate modifier can receive function", () => { - const _validateType = db - .type("Test", { - email: db.string(), - }) - .validate({ - email: () => undefined, - }); - - expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; - email: string; - }>(); - const fieldMetadata = _validateType.fields.email.metadata; - expect(fieldMetadata.validate).toHaveLength(1); - }); - - test("validate modifier can receive function returning error message", () => { - const _validateType = db - .type("Test", { - email: db.string(), - }) - .validate({ - email: ({ newValue }) => (!newValue.includes("@") ? "Email must contain @" : undefined), - }); - - const fieldMetadata = _validateType.fields.email.metadata; - expect(fieldMetadata.validate).toHaveLength(1); - }); - - test("validate modifier can receive multiple validators", () => { - const _validateType = db - .type("Test", { - password: db.string(), - }) - .validate({ - password: [ - ({ newValue }) => - newValue.length < 8 ? "Password must be at least 8 characters" : undefined, - ({ newValue }) => - !/[A-Z]/.test(newValue) ? "Password must contain uppercase letter" : undefined, - ], - }); - - const fieldMetadata = _validateType.fields.password.metadata; - expect(fieldMetadata.validate).toHaveLength(2); - }); - - test("type error occurs when validate is already set on TailorDBField", () => { - db.type("Test", { - name: db.string().validate(() => undefined), - // @ts-expect-error validate() cannot be called after validate() has already been called - }).validate({ - name: () => undefined, - }); - }); - - test("setting validate on id causes type error", () => { - db.type("Test", { - name: db.string(), - }).validate({ - // @ts-expect-error validate() cannot be called on the "id" field - id: () => undefined, - }); - }); - - test("validate modifier on string field receives string", () => { - const testType = db.type("Test", { name: db.string() }); - testType.validate({ - name: ({ newValue }) => { - expectTypeOf(newValue).toEqualTypeOf(); - return undefined; - }, - }); - }); - - test("validate modifier on optional field receives null", () => { - const testType = db.type("Test", { - name: db.string({ optional: true }), - }); - testType.validate({ - name: ({ newValue }) => { - expectTypeOf(newValue).toEqualTypeOf(); - return undefined; - }, - }); - }); -}); - describe("TailorDBType type-level validate (function form) tests", () => { test("accepts type-level validate function", () => { const _type = db @@ -1546,7 +1458,7 @@ describe("TailorDBField fluent API type preservation", () => { .string() .description("Email address") .index() - .validate(({ newValue }) => (!newValue.includes("@") ? "Invalid email" : undefined)); + .validate(({ value }) => (!value.includes("@") ? "Invalid email" : undefined)); expectTypeOf>().toEqualTypeOf(); }); @@ -1894,8 +1806,8 @@ describe("TailorDBField immutability", () => { test("field.validate() returns a new field without mutating the original", () => { const original = db.string(); - const withValidate = original.validate(({ newValue }) => - newValue.length <= 0 ? "Must not be empty" : undefined, + const withValidate = original.validate(({ value }) => + value.length <= 0 ? "Must not be empty" : undefined, ); expect(withValidate).not.toBe(original); @@ -1986,8 +1898,10 @@ describe("TailorDBType does not mutate shared fields", () => { test("type.validate() does not mutate the shared field", () => { const sharedField = db.string(); - const typeA = db.type("TypeA", { email: sharedField }).validate({ - email: ({ newValue }) => (!newValue.includes("@") ? "Invalid email" : undefined), + const typeA = db.type("TypeA", { + email: sharedField.validate(({ value }) => + !value.includes("@") ? "Invalid email" : undefined, + ), }); const typeB = db.type("TypeB", { email: sharedField }); @@ -2009,11 +1923,12 @@ describe("TailorDBType does not mutate shared fields", () => { const emailField = db.string(); const fields = { email: emailField }; - db.type("TypeA", fields).validate({ - email: ({ newValue }) => (!newValue.includes("@") ? "Invalid email" : undefined), + db.type("TypeA", { + email: emailField.validate(({ value }) => + !value.includes("@") ? "Invalid email" : undefined, + ), }); - // The fields record should still reference the original field instance expect(fields.email).toBe(emailField); }); }); @@ -2106,8 +2021,8 @@ describe("TailorDBField clone tests", () => { }); test("clones validate correctly", () => { - const validator = ({ newValue }: { newValue: string }) => - newValue.length <= 0 ? "Must not be empty" : undefined; + const validator = ({ value }: { value: string }) => + value.length <= 0 ? "Must not be empty" : undefined; const original = db.string().validate(validator); const cloned = original.clone(); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index d2c9c54a3..1141ac59e 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -27,8 +27,6 @@ import type { TailorFieldType, TailorToTs, FieldValidateInput, - ValidateFn, - Validators, } from "#/configure/types/field.types"; import type { UUIDString } from "#/configure/types/scalar.types"; import type { PluginAttachment, PluginConfigs } from "#/plugin/types"; @@ -261,7 +259,9 @@ type DBFieldDefaultMethod = ? TypeLevelError<"default cannot be set on nested type fields"> : Defined extends { serial: true } ? TypeLevelError<"default cannot be set on serial fields"> - : DBFieldDefaultFn; + : null extends Output + ? TypeLevelError<"default cannot be set on optional fields"> + : DBFieldDefaultFn; /** * Full TailorDBField interface with builder methods. @@ -367,7 +367,6 @@ export interface TailorDBType< hooks(hook: TypeHook): TailorDBType; validate(fn: TypeValidateFn): TailorDBType; - validate(validators: Validators): TailorDBType; features(features: Omit): TailorDBType; indexes(...indexes: IndexDef>[]): TailorDBType; files( @@ -896,17 +895,8 @@ function createTailorDBType< return this; }, - validate(validatorsOrFn: Validators | TypeValidateFn) { - if (typeof validatorsOrFn === "function") { - _typeValidate = validatorsOrFn; - return this; - } - Object.entries(validatorsOrFn).forEach(([fieldName, fieldValidators]) => { - const field = this.fields[fieldName] as TailorAnyDBField; - const fns = fieldValidators as ValidateFn | ValidateFn[]; - const updatedField = Array.isArray(fns) ? field.validate(...fns) : field.validate(fns); - (this.fields as Record)[fieldName] = updatedField; - }); + validate(fn: TypeValidateFn) { + _typeValidate = fn; return this; }, diff --git a/packages/sdk/src/configure/types/field-runtime.ts b/packages/sdk/src/configure/types/field-runtime.ts index bd12b4e7e..c2de9c4a4 100644 --- a/packages/sdk/src/configure/types/field-runtime.ts +++ b/packages/sdk/src/configure/types/field-runtime.ts @@ -172,7 +172,7 @@ function validateValue( const validateFns = field._metadata.validate; if (validateFns && validateFns.length > 0) { for (const fn of validateFns) { - const result = fn({ newValue: value, oldValue: null }); + const result = fn({ value }); if (typeof result === "string") { issues.push({ message: result, diff --git a/packages/sdk/src/configure/types/field.types.ts b/packages/sdk/src/configure/types/field.types.ts index f83b4d2ad..5b0a62cdb 100644 --- a/packages/sdk/src/configure/types/field.types.ts +++ b/packages/sdk/src/configure/types/field.types.ts @@ -3,7 +3,6 @@ // This is a pure type module: type declarations only, no zod/schema // references, importable type-only from any layer. -import type { output } from "#/types/helpers"; import type { DateString, DateTimeString, @@ -11,7 +10,6 @@ import type { TimeString, UUIDString, } from "./scalar.types"; -import type { NonEmptyObject } from "type-fest"; export interface EnumValue { value: string; @@ -96,41 +94,13 @@ export type ArrayFieldOutput = [O] extends [ /** * Field validation function. Return an error message string to fail, or void/undefined to pass. */ -export type ValidateFn = (args: { newValue: O; oldValue: O | null }) => string | void; +export type ValidateFn = (args: { value: O }) => string | void; /** * Input type for field validation */ export type FieldValidateInput = ValidateFn; -/** - * Base validators type for field collections - * @template F - Record of fields - * @template ExcludeKeys - Keys to exclude from validation (default: "id" for TailorDB) - */ -type ValidatorsBase< - // Structural constraint only - // oxlint-disable-next-line no-explicit-any - F extends Record, - ExcludeKeys extends string = "id", -> = NonEmptyObject<{ - [K in Exclude as F[K]["_defined"] extends { - validate: unknown; - } - ? never - : K]?: ValidateFn> | ValidateFn>[]; -}>; - -/** - * Validators type (by default excludes "id" field for TailorDB compatibility) - * Can be used with both TailorField and TailorDBField - */ -export type Validators< - // Structural constraint only - // oxlint-disable-next-line no-explicit-any - F extends Record, -> = ValidatorsBase; - /** * Minimal structural interface for TailorField. * Defines only the properties needed by parser, plugin, cli, and types layers. diff --git a/packages/sdk/src/configure/types/type.test.ts b/packages/sdk/src/configure/types/type.test.ts index c9b1bdadb..e95b5ee9c 100644 --- a/packages/sdk/src/configure/types/type.test.ts +++ b/packages/sdk/src/configure/types/type.test.ts @@ -767,7 +767,7 @@ describe("TailorField clone-on-write / no aliasing", () => { test("validate() returns a clone and never mutates the original", () => { const original = t.string(); const updated = original.validate((args) => - args.newValue.length <= 0 ? "Must not be empty" : undefined, + args.value.length <= 0 ? "Must not be empty" : undefined, ); expect(original.metadata.validate).toBeUndefined(); @@ -832,7 +832,7 @@ describe("TailorField clone-on-write / no aliasing", () => { const validated = t .string() - .validate((args) => (args.newValue.length <= 0 ? "Must not be empty" : undefined)); + .validate((args) => (args.value.length <= 0 ? "Must not be empty" : undefined)); const validatedClone = validated.description("name"); expect(validatedClone.metadata.validate).not.toBe(validated.metadata.validate); }); @@ -852,8 +852,8 @@ describe("TailorField clone-on-write / no aliasing", () => { test("validate() preserves function references through cloning", () => { const calls: unknown[] = []; const field = t.string().validate((args) => { - calls.push(args.newValue); - return args.newValue.length <= 0 ? "Must not be empty" : undefined; + calls.push(args.value); + return args.value.length <= 0 ? "Must not be empty" : undefined; }); const result = field.parse({ value: "x", data, invoker }); @@ -864,7 +864,7 @@ describe("TailorField clone-on-write / no aliasing", () => { test("validators survive a clone triggered by a later builder, leaving the original intact", () => { const validated = t .string() - .validate((args) => (args.newValue.length <= 0 ? "Must not be empty" : undefined)); + .validate((args) => (args.value.length <= 0 ? "Must not be empty" : undefined)); // description() clones the field; the validators must carry over to the clone // and keep working, while the original stays unchanged. const described = validated.description("name"); diff --git a/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts b/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts index 5e7333979..56d69f653 100644 --- a/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts +++ b/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts @@ -20,8 +20,8 @@ describe("parseFieldConfig precompiled expressions", () => { }); test("uses precompiled validate expression when attached", () => { - const validator = ({ newValue }: { newValue: string }) => - newValue.length <= 0 ? "Must not be empty" : undefined; + const validator = ({ value }: { value: string }) => + value.length <= 0 ? "Must not be empty" : undefined; setPrecompiledScriptExpr(validator, "PRECOMPILED_VALIDATE_EXPR"); const type = db.type("User", { diff --git a/packages/sdk/src/parser/service/tailordb/field.test.ts b/packages/sdk/src/parser/service/tailordb/field.test.ts index e12dbf453..f8b6e9900 100644 --- a/packages/sdk/src/parser/service/tailordb/field.test.ts +++ b/packages/sdk/src/parser/service/tailordb/field.test.ts @@ -124,8 +124,8 @@ describe("parseFieldConfig validator expressions", () => { test("normalizes a method-shorthand validator whose body contains an arrow function", () => { // Method shorthand syntax, obtained the same way a user's helper object would produce it. const validators = { - isValid({ newValue }: { newValue: string; oldValue: string | null }): string | void { - if (!([newValue].map((v) => v.includes("@"))[0] ?? false)) return "invalid email"; + isValid({ value }: { value: string }): string | void { + if (!([value].map((v) => v.includes("@"))[0] ?? false)) return "invalid email"; }, }; const type = db.type("User", { @@ -162,13 +162,8 @@ describe("parseFieldConfig script expression validation", () => { }); test("throws a clear error when a validator cannot be converted to valid JavaScript", () => { - const check = function check({ - newValue, - }: { - newValue: string; - oldValue: string | null; - }): string | void { - if (newValue.length === 0) return "must not be empty"; + const check = function check({ value }: { value: string }): string | void { + if (value.length === 0) return "must not be empty"; }.bind(null); const type = db.type("User", { email: db.string().validate(check), @@ -182,13 +177,8 @@ describe("parseFieldConfig script expression validation", () => { }); test("includes the type and field path in conversion errors from type parsing", () => { - const check = function check({ - newValue, - }: { - newValue: string; - oldValue: string | null; - }): string | void { - if (newValue.length === 0) return "must not be empty"; + const check = function check({ value }: { value: string }): string | void { + if (value.length === 0) return "must not be empty"; }.bind(null); const type = db.type("User", { email: db.string().validate(check), diff --git a/packages/sdk/src/parser/service/tailordb/field.ts b/packages/sdk/src/parser/service/tailordb/field.ts index 2b2f36a24..f26b9aaec 100644 --- a/packages/sdk/src/parser/service/tailordb/field.ts +++ b/packages/sdk/src/parser/service/tailordb/field.ts @@ -198,8 +198,12 @@ const convertToScriptExpr = ( return precompiledExpr; } const normalized = stringifyFunction(fn); + const argsObject = + kind === "validate" + ? `{ value: _value }` + : `{ value: _value, oldValue: _oldValue, invoker: ${tailorPrincipalMap}, now: _now }`; return assertParsableExpression( - `(${normalized})({ value: _value, oldValue: _oldValue, invoker: ${tailorPrincipalMap}, now: _now })`, + `(${normalized})(${argsObject})`, formatScriptContext(kind, context), ); }; diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index 94dc27f36..ff5bc2fb1 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -131,7 +131,7 @@ describe("buildTypeScripts", () => { expect(typeValidate?.update?.expr).toBe(createExpr); expect(createExpr).toContain("const __errs = {}"); expect(createExpr).toContain('const _value = _newRecord["age"]'); - expect(createExpr).toContain('const _oldValue = _oldRecord?.["age"] ?? null'); + expect(createExpr).not.toContain("_oldValue"); expect(createExpr).toContain("(_value >= 0) ?? (_value < 200)"); expect(createExpr).toContain('if (typeof __r === "string") { __errs["age"] = __r; }'); expect(createExpr).toContain("return __errs"); diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index 042f0cc44..5fe134c06 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -101,27 +101,22 @@ function buildHookObject( * failing message keyed by its dotted field path. * @param {Record} fields - Field configurations * @param {string} accessExpr - JS expression to access the parent object - * @param {string} oldAccessExpr - JS expression to access the old record's parent object * @param {string} keyPrefix - Dotted path prefix for error keys * @returns {string[]} Array of validation statement strings */ function buildValidateStatements( fields: Record, accessExpr: string, - oldAccessExpr: string, keyPrefix: string, ): string[] { const statements: string[] = []; for (const [name, config] of Object.entries(fields)) { const access = `${accessExpr}[${key(name)}]`; - const oldAccess = `${oldAccessExpr}?.[${key(name)}]`; const fieldPath = keyPrefix ? `${keyPrefix}.${name}` : name; if (isNestedType(config) && config.fields) { - statements.push( - ...buildValidateStatements(config.fields, `(${access} || {})`, oldAccess, fieldPath), - ); + statements.push(...buildValidateStatements(config.fields, `(${access} || {})`, fieldPath)); continue; } @@ -129,7 +124,7 @@ function buildValidateStatements( if (validators.length > 0) { const chain = validators.map((v) => `(${v.script?.expr})`).join(" ?? "); statements.push( - `{ const _value = ${access}; const _oldValue = ${oldAccess} ?? null;` + + `{ const _value = ${access};` + ` const __r = ${chain}; if (typeof __r === "string") { __errs[${key(fieldPath)}] = __r; } }`, ); } @@ -188,7 +183,7 @@ export function buildTypeScripts( result.typeHook = hook; } - const statements = buildValidateStatements(fields, NEW_RECORD, OLD_RECORD, ""); + const statements = buildValidateStatements(fields, NEW_RECORD, ""); if (statements.length > 0 || typeValidateExpr) { const expr = wrapValidate(statements, typeValidateExpr); result.typeValidate = { create: { expr }, update: { expr } }; From 0274a7b2bb599309f30ea95baa88e816842b09ed Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 7 Jul 2026 00:10:23 +0900 Subject: [PATCH 440/618] fix(kysely): include default values in Generated<> wrapping Fields with .default() now produce Generated in Kysely types, matching the optionalOnCreate behavior sent to the platform. Update inventory-management template to reflect type-level hook migration (totalPrice no longer has a per-field hook). --- .../src/generated/kysely-tailordb.ts | 2 +- .../builtin/kysely-type/type-processor.test.ts | 12 ++++++++++++ .../src/plugin/builtin/kysely-type/type-processor.ts | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts b/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts index 11a6cd4fc..ae2988b26 100644 --- a/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts +++ b/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts @@ -64,7 +64,7 @@ export interface Namespace { productId: UUIDString; quantity: number; unitPrice: number; - totalPrice: Generated; + totalPrice: number | null; createdAt: Generated; updatedAt: Generated; } diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts index 789401b97..9f5ac813d 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts @@ -290,6 +290,18 @@ describe("Kysely TypeProcessor", () => { expect(result.typeDef).toContain("updatedAt: Generated;"); }); + test("should wrap fields with default in Generated<>", async () => { + const typeDef = await getTypeDef( + db.type("Order", { + status: db.string().default("pending"), + priority: db.int(), + }), + ); + + expect(typeDef).toContain("status: Generated;"); + expect(typeDef).toContain("priority: number;"); + }); + test("should always include Generated for id field", async () => { const typeDef = await getTypeDef( db.type("User", { diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts index 56690938e..af363a1d1 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts @@ -186,7 +186,7 @@ function generateFieldType(fieldConfig: OperatorFieldConfig): FieldTypeResult { usedUtilityTypes.Serial = true; finalType = `Serial<${finalType}>`; } - if (fieldConfig.hooks?.create) { + if (fieldConfig.hooks?.create || fieldConfig.default !== undefined) { finalType = `Generated<${finalType}>`; } From 50c47bd22f55cac739bdc1237dbc27b43f7e6888 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 7 Jul 2026 00:31:27 +0900 Subject: [PATCH 441/618] refactor(template): use .default(0) instead of optional for computed totalPrice --- .../templates/inventory-management/src/db/orderItem.ts | 2 +- .../inventory-management/src/generated/kysely-tailordb.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts b/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts index 506484447..609adcb2c 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts @@ -21,7 +21,7 @@ export const orderItem = db .float() .description("Unit price of the product") .validate(({ value }) => (value < 0 ? "Unit price must be non-negative" : undefined)), - totalPrice: db.float({ optional: true }).description("Total price of the order item"), + totalPrice: db.float().default(0).description("Total price of the order item"), ...db.fields.timestamps(), }) .hooks({ diff --git a/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts b/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts index ae2988b26..6777748fe 100644 --- a/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts +++ b/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts @@ -64,7 +64,7 @@ export interface Namespace { productId: UUIDString; quantity: number; unitPrice: number; - totalPrice: number | null; + totalPrice: Generated; createdAt: Generated; updatedAt: Generated; } From 47decd998ec1b40cebb3db8a7bb510b1b3d9ac31 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 7 Jul 2026 00:47:16 +0900 Subject: [PATCH 442/618] chore(codemod): add migration entries for TailorDB validate/hook API changes --- packages/sdk-codemod/src/registry.ts | 102 ++++++++++++++++++ packages/sdk/docs/migration/v2.md | 155 +++++++++++++++++++++++++++ 2 files changed, 257 insertions(+) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index c1a609f73..bdbcf9ab8 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -869,6 +869,108 @@ export const allCodemods: CodemodPackage[] = [ }, ], }, + { + id: "v2/tailordb-validate-simplify", + name: "ValidateFn simplification and type-level validate", + description: + "Field-level `ValidateFn` is simplified from `(args: { value, data, invoker }) => boolean` to `(args: { value }) => string | void` — the function now returns the error message directly instead of a separate `[fn, message]` tuple. The `ValidateConfig` tuple form and `Validators` record syntax on `db.type().validate()` are removed. Type-level validation uses `db.type().validate((args, issues) => void)` with `{ newRecord, oldRecord, invoker }` args and an `issues(field, message)` callback for cross-field rules.", + since: "1.0.0", + until: "2.0.0", + prereleaseUntil: V2_NEXT_3, + suspiciousPatterns: ["ValidateConfig", "Validators<", "ValidatorsBase"], + examples: [ + { + caption: + "Field-level validate: return an error message string instead of a boolean (tuple form removed):", + before: + '.validate(\n [({ value }) => value.length > 5, "Name must be longer than 5 characters"],\n)', + after: + '.validate(({ value }) =>\n value.length <= 5 ? "Name must be longer than 5 characters" : undefined,\n)', + }, + { + caption: + "Type-level validate: per-field record syntax replaced by a single function with `issues()` callback:", + before: + '.validate({\n name: [({ value }) => value.length > 5, "Name must be longer than 5"],\n})', + after: + '.validate(({ newRecord }, issues) => {\n if (newRecord.name && newRecord.name.length <= 5) {\n issues("name", "Name must be longer than 5");\n }\n})', + }, + ], + prompt: [ + "The v2 SDK simplifies field validation and introduces type-level validation.", + "", + "Field-level `.validate()` changes:", + "- Signature: `(args: { value, data, invoker }) => boolean` → `(args: { value }) => string | void`", + "- The function now returns the error message string directly (or undefined/void to pass)", + " instead of returning a boolean with the message in a separate tuple.", + "- The `[fn, errorMessage]` tuple form (`ValidateConfig`) is removed.", + "- `data` and `invoker` are no longer available in field-level validators.", + " Use type-level `.validate()` for cross-field or invoker-dependent rules.", + "", + "Type-level `.validate()` on `db.type()` changes:", + "- Old: `.validate({ fieldName: fn | [fn, msg] | fn[] })` (per-field record, `Validators` type)", + "- New: `.validate((args, issues) => void)` (single function, `TypeValidateFn` type)", + "- Args: `{ newRecord, oldRecord, invoker }` — `newRecord` is the record after hooks run", + "- Call `issues(field, message)` to report validation errors; `field` supports dotted paths", + "- Move per-field validators that need `data`/`invoker` to the type-level function", + "", + "For each remaining `ValidateConfig`, `Validators<`, or old-signature `.validate()` usage:", + "1. Rewrite field-level validators to return the error string directly", + "2. Move cross-field / invoker-dependent validators to the type-level function", + "3. Remove unused `ValidateConfig` / `Validators` type imports", + ].join("\n"), + }, + { + id: "v2/tailordb-hook-redesign", + name: "TailorDB hook redesign: field-level args and type-level hooks", + description: + "Field-level `HookFn` args change from `{ value, data, invoker }` to `{ value, oldValue, invoker, now }` — `data` (the full record) is replaced by `oldValue` (the previous field value) and `now` (operation timestamp). Type-level hooks on `db.type().hooks()` change from per-field mapping `{ fieldName: { create, update } }` (`Hooks`) to a single `{ create, update }` object (`TypeHook`) where each function takes `{ input, oldRecord, invoker, now }` and returns partial field overrides.", + since: "1.0.0", + until: "2.0.0", + prereleaseUntil: V2_NEXT_3, + suspiciousPatterns: ["Hooks<", "HookFn<"], + examples: [ + { + caption: + "Field-level hooks: `data` replaced by `oldValue` and `now`; use `now` instead of `new Date()`:", + before: + "db.datetime().hooks({\n create: ({ value }) => value ?? new Date(),\n update: () => new Date(),\n})", + after: + "db.datetime().hooks({\n create: ({ value, now }) => value ?? now,\n update: ({ now }) => now,\n})", + }, + { + caption: "Type-level hooks: per-field mapping replaced by single create/update functions:", + before: + ".hooks({\n fullAddress: {\n create: ({ data }) => `${data.postalCode} ${data.address}`,\n update: ({ data }) => `${data.postalCode} ${data.address}`,\n },\n})", + after: + ".hooks({\n create: ({ input }) => ({\n fullAddress: `${input.postalCode} ${input.address}`,\n }),\n update: ({ input }) => ({\n fullAddress: `${input.postalCode} ${input.address}`,\n }),\n})", + }, + ], + prompt: [ + "The v2 SDK redesigns TailorDB hooks at both field and type levels.", + "", + "Field-level `.hooks()` on individual fields:", + "- Args: `{ value, data, invoker }` → `{ value, oldValue, invoker, now }`", + "- `data` (full record) is removed; use `oldValue` (previous field value) instead", + "- `now` provides the operation timestamp — use `now` instead of `new Date()`", + "- If a field-level hook needs the full record (other fields), move it to a type-level hook", + "", + "Type-level `.hooks()` on `db.type()`:", + "- Old: `.hooks({ fieldName: { create: fn, update: fn } })` (per-field mapping, `Hooks` type)", + "- New: `.hooks({ create: fn, update: fn })` (single object, `TypeHook` type)", + "- Each function: `({ input, oldRecord, invoker, now }) => ({ fieldName: value, ... })`", + "- `input` is the pre-hook input (may have nullish values for optional/defaulted fields)", + "- `oldRecord` is null on create, the previous record on update", + "- Return an object with only the fields to override; unmentioned fields are unchanged", + "", + "Migration steps for each `.hooks()` call on a `db.type()`:", + "1. If the old per-field hooks only use `value`/`invoker` and don't reference `data`,", + " convert them to field-level hooks with the new args (`oldValue`, `now`)", + "2. If the old hooks reference `data` (cross-field access), convert to a type-level hook", + " using `input`/`oldRecord`", + "3. Remove unused `Hooks` / `HookFn<>` type imports", + ].join("\n"), + }, { id: "v2/node-minimum-22-15-0", name: "Node.js minimum version raised to 22.15.0", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index afeeed4b2..28f7d644c 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -811,6 +811,161 @@ After: .tailor/ ``` +## ValidateFn simplification and type-level validate + +**Migration:** Manual + +Field-level `ValidateFn` is simplified from `(args: { value, data, invoker }) => boolean` to `(args: { value }) => string | void` — the function now returns the error message directly instead of a separate `[fn, message]` tuple. The `ValidateConfig` tuple form and `Validators` record syntax on `db.type().validate()` are removed. Type-level validation uses `db.type().validate((args, issues) => void)` with `{ newRecord, oldRecord, invoker }` args and an `issues(field, message)` callback for cross-field rules. + +Field-level validate: return an error message string instead of a boolean (tuple form removed): + +Before: + +```ts +.validate( + [({ value }) => value.length > 5, "Name must be longer than 5 characters"], +) +``` + +After: + +```ts +.validate(({ value }) => + value.length <= 5 ? "Name must be longer than 5 characters" : undefined, +) +``` + +Type-level validate: per-field record syntax replaced by a single function with `issues()` callback: + +Before: + +```ts +.validate({ + name: [({ value }) => value.length > 5, "Name must be longer than 5"], +}) +``` + +After: + +```ts +.validate(({ newRecord }, issues) => { + if (newRecord.name && newRecord.name.length <= 5) { + issues("name", "Name must be longer than 5"); + } +}) +``` + +
+Prompt for an AI agent (to perform this migration) + +```text +The v2 SDK simplifies field validation and introduces type-level validation. + +Field-level `.validate()` changes: +- Signature: `(args: { value, data, invoker }) => boolean` → `(args: { value }) => string | void` +- The function now returns the error message string directly (or undefined/void to pass) + instead of returning a boolean with the message in a separate tuple. +- The `[fn, errorMessage]` tuple form (`ValidateConfig`) is removed. +- `data` and `invoker` are no longer available in field-level validators. + Use type-level `.validate()` for cross-field or invoker-dependent rules. + +Type-level `.validate()` on `db.type()` changes: +- Old: `.validate({ fieldName: fn | [fn, msg] | fn[] })` (per-field record, `Validators` type) +- New: `.validate((args, issues) => void)` (single function, `TypeValidateFn` type) +- Args: `{ newRecord, oldRecord, invoker }` — `newRecord` is the record after hooks run +- Call `issues(field, message)` to report validation errors; `field` supports dotted paths +- Move per-field validators that need `data`/`invoker` to the type-level function + +For each remaining `ValidateConfig`, `Validators<`, or old-signature `.validate()` usage: +1. Rewrite field-level validators to return the error string directly +2. Move cross-field / invoker-dependent validators to the type-level function +3. Remove unused `ValidateConfig` / `Validators` type imports +``` + +
+ +## TailorDB hook redesign: field-level args and type-level hooks + +**Migration:** Manual + +Field-level `HookFn` args change from `{ value, data, invoker }` to `{ value, oldValue, invoker, now }` — `data` (the full record) is replaced by `oldValue` (the previous field value) and `now` (operation timestamp). Type-level hooks on `db.type().hooks()` change from per-field mapping `{ fieldName: { create, update } }` (`Hooks`) to a single `{ create, update }` object (`TypeHook`) where each function takes `{ input, oldRecord, invoker, now }` and returns partial field overrides. + +Field-level hooks: `data` replaced by `oldValue` and `now`; use `now` instead of `new Date()`: + +Before: + +```ts +db.datetime().hooks({ + create: ({ value }) => value ?? new Date(), + update: () => new Date(), +}) +``` + +After: + +```ts +db.datetime().hooks({ + create: ({ value, now }) => value ?? now, + update: ({ now }) => now, +}) +``` + +Type-level hooks: per-field mapping replaced by single create/update functions: + +Before: + +```ts +.hooks({ + fullAddress: { + create: ({ data }) => `${data.postalCode} ${data.address}`, + update: ({ data }) => `${data.postalCode} ${data.address}`, + }, +}) +``` + +After: + +```ts +.hooks({ + create: ({ input }) => ({ + fullAddress: `${input.postalCode} ${input.address}`, + }), + update: ({ input }) => ({ + fullAddress: `${input.postalCode} ${input.address}`, + }), +}) +``` + +
+Prompt for an AI agent (to perform this migration) + +```text +The v2 SDK redesigns TailorDB hooks at both field and type levels. + +Field-level `.hooks()` on individual fields: +- Args: `{ value, data, invoker }` → `{ value, oldValue, invoker, now }` +- `data` (full record) is removed; use `oldValue` (previous field value) instead +- `now` provides the operation timestamp — use `now` instead of `new Date()` +- If a field-level hook needs the full record (other fields), move it to a type-level hook + +Type-level `.hooks()` on `db.type()`: +- Old: `.hooks({ fieldName: { create: fn, update: fn } })` (per-field mapping, `Hooks` type) +- New: `.hooks({ create: fn, update: fn })` (single object, `TypeHook` type) +- Each function: `({ input, oldRecord, invoker, now }) => ({ fieldName: value, ... })` +- `input` is the pre-hook input (may have nullish values for optional/defaulted fields) +- `oldRecord` is null on create, the previous record on update +- Return an object with only the fields to override; unmentioned fields are unchanged + +Migration steps for each `.hooks()` call on a `db.type()`: +1. If the old per-field hooks only use `value`/`invoker` and don't reference `data`, + convert them to field-level hooks with the new args (`oldValue`, `now`) +2. If the old hooks reference `data` (cross-field access), convert to a type-level hook + using `input`/`oldRecord` +3. Remove unused `Hooks` / `HookFn<>` type imports +``` + +
+ ## Behavioral changes (no migration required) These v2 changes alter runtime or CLI behavior; no source change is needed. From 42a528428a17b83b08d5153c8299a3342578fe0d Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 7 Jul 2026 00:58:00 +0900 Subject: [PATCH 443/618] docs(tailordb): update hooks and validation docs for new API and share now timestamp in test helper --- .changeset/tailordb-shared-now-hook.md | 2 +- packages/sdk/docs/services/tailordb.md | 116 ++++++++----------------- packages/sdk/src/utils/test/index.ts | 5 +- 3 files changed, 40 insertions(+), 83 deletions(-) diff --git a/.changeset/tailordb-shared-now-hook.md b/.changeset/tailordb-shared-now-hook.md index 58c467137..ef18c37f9 100644 --- a/.changeset/tailordb-shared-now-hook.md +++ b/.changeset/tailordb-shared-now-hook.md @@ -4,4 +4,4 @@ Add a `now` argument to TailorDB hooks. `now` is the operation timestamp and is shared across every field hooked in the same create/update, so multiple fields can be stamped with an identical `Date`. Hooks and validators are now applied per type rather than per field, which is what makes the shared timestamp possible. -As part of this, all of a type's hooks now run together and observe the same submitted input: a hook's `data` reflects what the client sent and does not include other fields' hook results. +As part of this, all of a type's hooks now run together and observe the same submitted input: a type-level hook's `input` reflects what the client sent and does not include other fields' hook results. diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index 16519eac2..0bae371e7 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -279,18 +279,16 @@ field, files entry, or relation on the same type. ### Hooks -Add hooks to execute functions during data creation or update. Hooks receive four arguments: - -- `value`: User input if provided, otherwise existing value on update or null on create -- `data`: The submitted record data (for accessing other field values) -- `invoker`: Principal performing the operation -- `now`: Operation timestamp (`Date`). The same instant is shared by every field's hook in the same create/update, so multiple fields can be stamped with an identical timestamp. - -All of a type's hooks run together as one operation and observe the same submitted input: `data` reflects what the client sent, so a hook does not see another field's hook result. Order between fields is not significant. +Add hooks to execute functions during data creation or update. #### Field-level Hooks -Set hooks directly on individual fields: +Set hooks directly on individual fields. Each hook receives: + +- `value`: The field value from the input (null on create when not provided) +- `oldValue`: The previous field value (null on create) +- `invoker`: Principal performing the operation +- `now`: Operation timestamp (`Date`), shared across all hooks in the same operation ```typescript db.string().hooks({ @@ -299,11 +297,18 @@ db.string().hooks({ }); ``` -**Note:** When setting hooks at the field level, the `data` argument type is `unknown` since the field doesn't know about other fields in the type. Use type-level hooks if you need to access other fields with type safety. +Field-level hooks operate on a single field and cannot access other fields. Use type-level hooks for cross-field logic. #### Type-level Hooks -Set hooks for multiple fields at once using `db.type().hooks()`: +Set hooks across multiple fields using `db.type().hooks()`. Each hook receives: + +- `input`: The submitted record data (pre-hook values) +- `oldRecord`: The existing record (null on create) +- `invoker`: Principal performing the operation +- `now`: Operation timestamp (`Date`), shared across all hooks in the same operation + +The hook returns an object with the fields to override: ```typescript export const customer = db @@ -313,10 +318,12 @@ export const customer = db fullName: db.string(), }) .hooks({ - fullName: { - create: ({ data }) => `${data.firstName} ${data.lastName}`, - update: ({ data }) => `${data.firstName} ${data.lastName}`, - }, + create: ({ input }) => ({ + fullName: `${input.firstName} ${input.lastName}`, + }), + update: ({ input }) => ({ + fullName: `${input.firstName} ${input.lastName}`, + }), }); ``` @@ -329,58 +336,29 @@ export const order = db updatedAt: db.datetime(), }) .hooks({ - createdAt: { create: ({ now }) => now }, - updatedAt: { create: ({ now }) => now, update: ({ now }) => now }, - }); -``` - -**Important:** Field-level and type-level hooks cannot coexist on the same field. TypeScript will prevent this at compile time: - -```typescript -// Compile error - cannot set hooks on the same field twice -export const user = db - .type("User", { - name: db.string().hooks({ create: ({ data }) => data.firstName }), // Field-level - }) - .hooks({ - name: { create: ({ data }) => data.lastName }, // Type-level - ERROR - }); - -// OK - set hooks on different fields -export const user = db - .type("User", { - firstName: db.string().hooks({ create: () => "John" }), // Field-level on firstName - lastName: db.string(), - }) - .hooks({ - lastName: { create: () => "Doe" }, // Type-level on lastName + create: ({ now }) => ({ createdAt: now, updatedAt: now }), + update: ({ now }) => ({ updatedAt: now }), }); ``` ### Validation -Add validation rules to fields. Validators receive three arguments (executed after hooks): - -- `value`: Field value after hook transformation -- `data`: Entire record data after hook transformations (for accessing other field values) -- `invoker`: Principal performing the operation - -Validators return `true` for success, `false` for failure. Use array form `[validator, errorMessage]` for custom error messages. +Add validation rules to fields. Validators run after hooks. #### Field-level Validation -Set validators directly on individual fields: +Set validators directly on individual fields. Each validator receives `{ value }` (the field value after hooks) and returns an error message string to fail, or void to pass: ```typescript db.string().validate( - ({ value }) => value.includes("@"), - [({ value }) => value.length >= 5, "Email must be at least 5 characters"], + ({ value }) => (value.includes("@") ? undefined : "Must contain @"), + ({ value }) => (value.length >= 5 ? undefined : "Must be at least 5 characters"), ); ``` #### Type-level Validation -Set validators for multiple fields at once using `db.type().validate()`: +Set a validator across all fields using `db.type().validate()`. The validator receives `{ newRecord, oldRecord, invoker }` and an `issues()` callback to report errors per field: ```typescript export const user = db @@ -388,35 +366,13 @@ export const user = db name: db.string(), email: db.string(), }) - .validate({ - name: [({ value }) => value.length > 5, "Name must be longer than 5 characters"], - email: [ - ({ value }) => value.includes("@"), - [({ value }) => value.length >= 5, "Email must be at least 5 characters"], - ], - }); -``` - -**Important:** Field-level and type-level validation cannot coexist on the same field. TypeScript will prevent this at compile time: - -```typescript -// Compile error - cannot set validation on the same field twice -export const user = db - .type("User", { - name: db.string().validate(({ value }) => value.length > 0), // Field-level - }) - .validate({ - name: [({ value }) => value.length < 100, "Too long"], // Type-level - ERROR - }); - -// OK - set validation on different fields -export const user = db - .type("User", { - name: db.string().validate(({ value }) => value.length > 0), // Field-level on name - email: db.string(), - }) - .validate({ - email: [({ value }) => value.includes("@"), "Invalid email"], // Type-level on email + .validate(({ newRecord }, issues) => { + if (newRecord.name.length <= 5) { + issues("name", "Name must be longer than 5 characters"); + } + if (!newRecord.email.includes("@")) { + issues("email", "Must contain @"); + } }); ``` diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index 56a05c8c4..5288005cc 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -24,6 +24,7 @@ export { // eslint-disable-next-line @typescript-eslint/no-explicit-any export function createTailorDBHook>(type: T) { return (data: unknown) => { + const now = new Date(); const hooked = Object.entries(type.fields).reduce( (hooked, [key, value]) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -49,7 +50,7 @@ export function createTailorDBHook>(type: T) { value: (data as Record)[key], oldValue: null, invoker: null, - now: new Date(), + now, }); if (hooked[key] instanceof Date) { hooked[key] = hooked[key].toISOString(); @@ -69,7 +70,7 @@ export function createTailorDBHook>(type: T) { input: data, oldRecord: null, invoker: null, - now: new Date(), + now, }); if (overrides && typeof overrides === "object") { for (const [key, value] of Object.entries(overrides as Record)) { From 7f2fce96438dcd75b389e86d887655e8aeb4412d Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 7 Jul 2026 07:29:56 +0900 Subject: [PATCH 444/618] test(tailordb): assert shared now timestamp between field-level and type-level hooks --- packages/sdk/src/utils/test/index.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/sdk/src/utils/test/index.test.ts b/packages/sdk/src/utils/test/index.test.ts index 000bb6ff3..eabc55e49 100644 --- a/packages/sdk/src/utils/test/index.test.ts +++ b/packages/sdk/src/utils/test/index.test.ts @@ -195,6 +195,20 @@ describe("createTailorDBHook", () => { expect(createTailorDBHook(type)({}).createdAt).toBe("2026-04-15T00:00:00.000Z"); }); + test("shares the same now timestamp between field-level and type-level hooks", () => { + let fieldNow: Date | undefined; + let typeNow: Date | undefined; + const type = db + .type("Test", { + createdAt: db.datetime().hooks({ create: ({ now }) => (fieldNow = now) }), + label: db.string(), + }) + .hooks({ create: ({ now }) => ((typeNow = now), { label: "x" }) }); + createTailorDBHook(type)({}); + expect(fieldNow).toBeInstanceOf(Date); + expect(typeNow).toBe(fieldNow); + }); + test("does not invoke a hook that only defines update (createTailorDBHook is create-only)", () => { let updateCalled = false; const type = db.type("Test", { updatedAt: db.datetime() }).hooks({ From dbe3b745bccbda661d890d4257afb8c3406f35e4 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 7 Jul 2026 10:19:30 +0900 Subject: [PATCH 445/618] fix(parser): add default and validate keys to resolver field schema Sync FieldMetadataSchema (used by resolver/executor/workflow fields) with DBFieldMetadataSchema: add default key and update validate to accept Function[] only. Regenerate example migrations for hooks drift and update zinfer-generated types. --- example/migrations/0005/diff.json | 690 ++++++++++++++++++ example/migrations/analyticsdb/0002/diff.json | 68 ++ packages/sdk/src/parser/schema-strict.test.ts | 2 +- .../sdk/src/parser/service/field/schema.ts | 6 +- packages/sdk/src/types/auth.generated.ts | 6 +- packages/sdk/src/types/field.generated.ts | 6 +- packages/sdk/src/types/resolver.generated.ts | 3 +- 7 files changed, 771 insertions(+), 10 deletions(-) create mode 100644 example/migrations/0005/diff.json create mode 100644 example/migrations/analyticsdb/0002/diff.json diff --git a/example/migrations/0005/diff.json b/example/migrations/0005/diff.json new file mode 100644 index 000000000..56bf931ca --- /dev/null +++ b/example/migrations/0005/diff.json @@ -0,0 +1,690 @@ +{ + "version": 1, + "namespace": "tailordb", + "createdAt": "2026-07-07T01:12:32.459Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "name", + "before": { + "type": "string", + "required": true, + "validate": [ + { + "script": { + "expr": "(({ value })=>value.length > 5)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "Name must be longer than 5 characters" + } + ] + }, + "after": { + "type": "string", + "required": true, + "validate": [ + { + "script": { + "expr": "(({ value })=>value.length <= 5 ? \"Name must be longer than 5 characters\" : undefined)({ value: _value })" + }, + "errorMessage": "" + } + ] + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "city", + "before": { + "type": "string", + "required": false, + "validate": [ + { + "script": { + "expr": "(({ value })=>value ? value.length > 1 : true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({ value })=>value ? value.length > 1 : true`" + }, + { + "script": { + "expr": "(({ value })=>value ? value.length < 100 : true)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({ value })=>value ? value.length < 100 : true`" + } + ] + }, + "after": { + "type": "string", + "required": false, + "validate": [ + { + "script": { + "expr": "(({ value })=>value && value.length <= 1 ? \"City must be longer than 1 character\" : undefined)({ value: _value })" + }, + "errorMessage": "" + }, + { + "script": { + "expr": "(({ value })=>value && value.length >= 100 ? \"City must be shorter than 100 characters\" : undefined)({ value: _value })" + }, + "errorMessage": "" + } + ] + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "fullAddress", + "before": { + "type": "string", + "required": true, + "hooks": { + "create": { + "expr": "(({ data })=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(({ data })=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "string", + "required": true + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "attachedFiles", + "before": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "size": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({ value })=>value > 0)({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "errorMessage": "failed by `({ value })=>value > 0`" + } + ] + }, + "type": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "text" + }, + { + "value": "image" + } + ] + } + } + }, + "after": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "size": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({ value })=>value <= 0 ? \"Size must be positive\" : undefined)({ value: _value })" + }, + "errorMessage": "" + } + ] + }, + "type": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "text" + }, + { + "value": "image" + } + ] + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": false +} diff --git a/example/migrations/analyticsdb/0002/diff.json b/example/migrations/analyticsdb/0002/diff.json new file mode 100644 index 000000000..cdd240473 --- /dev/null +++ b/example/migrations/analyticsdb/0002/diff.json @@ -0,0 +1,68 @@ +{ + "version": 1, + "namespace": "analyticsdb", + "createdAt": "2026-07-07T01:12:32.475Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Event", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Event", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value }) => value ?? /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + }, + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": false +} diff --git a/packages/sdk/src/parser/schema-strict.test.ts b/packages/sdk/src/parser/schema-strict.test.ts index 59ea742c3..c4693bb6d 100644 --- a/packages/sdk/src/parser/schema-strict.test.ts +++ b/packages/sdk/src/parser/schema-strict.test.ts @@ -161,7 +161,7 @@ describe("parser schemas", () => { const result = TailorFieldSchema.safeParse({ type: "string", metadata: { - validate: [() => true, [() => true, "Invalid value"]], + validate: [() => true, () => "Invalid value"], }, fields: {}, builderProperty: true, diff --git a/packages/sdk/src/parser/service/field/schema.ts b/packages/sdk/src/parser/service/field/schema.ts index ed2405ef5..2ee981969 100644 --- a/packages/sdk/src/parser/service/field/schema.ts +++ b/packages/sdk/src/parser/service/field/schema.ts @@ -32,11 +32,9 @@ const FieldMetadataSchema = z.strictObject({ }) .optional() .describe("Lifecycle hooks"), - validate: z - .array(z.union([functionSchema, z.tuple([functionSchema, z.string()])])) - .optional() - .describe("Validation functions for the field"), + validate: z.array(functionSchema).optional().describe("Validation functions for the field"), typeName: z.string().optional().describe("Type name for nested or enum fields"), + default: z.unknown().optional().describe("Default value for the field on create"), }); // strip unknown keys diff --git a/packages/sdk/src/types/auth.generated.ts b/packages/sdk/src/types/auth.generated.ts index 0138877c7..0dda3c0d7 100644 --- a/packages/sdk/src/types/auth.generated.ts +++ b/packages/sdk/src/types/auth.generated.ts @@ -1384,8 +1384,9 @@ export type AuthConfigInput = update?: Function | undefined; } | undefined; - validate?: (Function | [Function, string])[] | undefined; + validate?: Function[] | undefined; typeName?: string | undefined; + default?: unknown; }; fields: any; }; @@ -2679,8 +2680,9 @@ export type AuthConfig = update?: Function | undefined; } | undefined; - validate?: (Function | [Function, string])[] | undefined; + validate?: Function[] | undefined; typeName?: string | undefined; + default?: unknown; }; fields: any; }; diff --git a/packages/sdk/src/types/field.generated.ts b/packages/sdk/src/types/field.generated.ts index 65d0dd2d8..e51f08d4a 100644 --- a/packages/sdk/src/types/field.generated.ts +++ b/packages/sdk/src/types/field.generated.ts @@ -31,8 +31,9 @@ export type TailorFieldInput = { update?: Function | undefined; } | undefined; - validate?: (Function | [Function, string])[] | undefined; + validate?: Function[] | undefined; typeName?: string | undefined; + default?: unknown; }; fields: { [x: string]: TailorFieldInput; @@ -70,8 +71,9 @@ export type TailorField = { update?: Function | undefined; } | undefined; - validate?: (Function | [Function, string])[] | undefined; + validate?: Function[] | undefined; typeName?: string | undefined; + default?: unknown; }; fields: { [x: string]: TailorField; diff --git a/packages/sdk/src/types/resolver.generated.ts b/packages/sdk/src/types/resolver.generated.ts index f867c5d10..8d1cb17bc 100644 --- a/packages/sdk/src/types/resolver.generated.ts +++ b/packages/sdk/src/types/resolver.generated.ts @@ -39,8 +39,9 @@ export type Resolver = { update?: Function | undefined; } | undefined; - validate?: (Function | [Function, string])[] | undefined; + validate?: Function[] | undefined; typeName?: string | undefined; + default?: unknown; }; fields: { [x: string]: any; From 07f4ae151569c9835504de396220c4e123d9a710 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 7 Jul 2026 10:56:50 +0900 Subject: [PATCH 446/618] fix(tailordb): mark required fields optionalOnCreate for type-level hooks and strip hooks/validate from drift comparison --- .../migrate/snapshot-manifest.test.ts | 20 +++++++++++++++++++ .../tailordb/migrate/snapshot-manifest.ts | 7 ++++++- .../tailordb/migrate/snapshot.test.ts | 19 +----------------- .../cli/commands/tailordb/migrate/snapshot.ts | 3 ++- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts index 79fb7ffdc..6a42c3ea4 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts @@ -403,6 +403,26 @@ describe("snapshot-manifest", () => { expect(manifest.schema?.typeValidate?.update?.expr).toBe(validateExpr); }); + test("marks required fields optionalOnCreate when user type-level create hook exists", () => { + const snapshotType = createTestSnapshotType("Customer", { + fields: { + id: { type: "uuid", required: true }, + name: { type: "string", required: true }, + fullAddress: { type: "string", required: true }, + phone: { type: "string", required: false }, + }, + typeHookExpr: { + create: + '((_input, _oldRecord, _invoker, _now) => ({ fullAddress: "computed" }))(_input, null, _invoker, _now)', + }, + }); + + const manifest = generateTailorDBTypeManifestFromSnapshot(snapshotType); + expect(manifest.schema?.fields?.name?.optionalOnCreate).toBe(true); + expect(manifest.schema?.fields?.fullAddress?.optionalOnCreate).toBe(true); + expect(manifest.schema?.fields?.phone?.optionalOnCreate).toBeUndefined(); + }); + test("handles serial configuration", () => { const snapshotType = createTestSnapshotType("Order", { fields: { diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts index 3b9e297e7..30fdc3cc4 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts @@ -112,10 +112,15 @@ export function generateTailorDBTypeManifestFromSnapshot( } // Build fields + const hasUserTypeCreateHook = snapshotType.typeHookExpr?.create !== undefined; const fields: Record> = {}; for (const [fieldName, fieldConfig] of Object.entries(snapshotType.fields)) { if (fieldName === "id") continue; - fields[fieldName] = convertFieldConfigToProto(fieldConfig); + const fieldProto = convertFieldConfigToProto(fieldConfig); + if (hasUserTypeCreateHook && fieldConfig.required && !fieldProto.optionalOnCreate) { + fieldProto.optionalOnCreate = true; + } + fields[fieldName] = fieldProto; } // Build relationships diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts index 1f50f4c0c..e466300b8 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts @@ -3234,7 +3234,7 @@ describe("snapshot", () => { expect(drifts[0]!.details).toContain("allowedValues"); }); - test("reports detailed drift for hooks, validation, serial, and nested fields", () => { + test("reports detailed drift for serial and nested fields", () => { const snapshot: SchemaSnapshot = { version: SCHEMA_SNAPSHOT_VERSION, namespace, @@ -3248,13 +3248,6 @@ describe("snapshot", () => { metadata: { type: "nested", required: false, - hooks: { create: { expr: "snapshotCreate" } }, - validate: [ - { - script: { expr: "snapshotValid" }, - errorMessage: "Snapshot validation", - }, - ], serial: { start: 10, maxValue: 99, format: "S-%02d" }, fields: { child: { type: "string", required: false }, @@ -3271,14 +3264,6 @@ describe("snapshot", () => { metadata: { type: "nested", required: false, - hooks: { create: { expr: "remoteCreate" } }, - validate: [ - { - action: TailorDBType_PermitAction.ALLOW, - script: { expr: "remoteValid" }, - errorMessage: "Remote validation", - }, - ], serial: { start: 1, maxValue: 9, format: "R-%02d" }, fields: { child: { type: "number", required: false }, @@ -3290,8 +3275,6 @@ describe("snapshot", () => { const drifts = compareRemoteWithSnapshot(remoteTypes, snapshot); expect(drifts).toHaveLength(1); expect(drifts[0]!.kind).toBe("field_mismatch"); - expect(drifts[0]!.details).toContain("hooks.create"); - expect(drifts[0]!.details).toContain("validate[0].script"); expect(drifts[0]!.details).toContain("serial.start"); expect(drifts[0]!.details).toContain("fields.child.type"); }); diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts index 7b46c94c1..3db8ea1ce 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts @@ -2770,7 +2770,8 @@ function createRemoteComparableSnapshot(snapshot: SchemaSnapshot): NormalizedSch const fields = createSnapshotRecord(); for (const [fieldName, field] of Object.entries(type.fields)) { if (SYSTEM_FIELDS.has(fieldName)) continue; - fields[fieldName] = field; + const { hooks: _hooks, validate: _validate, default: _default, ...rest } = field; + fields[fieldName] = rest; } types[typeName] = { ...type, fields }; } From 1abb0a4b471f0f21bdaf15026cb611db78992529 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 7 Jul 2026 12:03:55 +0900 Subject: [PATCH 447/618] fix(tailordb): safely capture _invoker in type-level scripts Platform type hook context only injects _input and _oldRecord, not _invoker. Wrap generated hook/validate scripts with a safe typeof check so _invoker references do not throw ReferenceError at runtime. --- .../service/tailordb/type-script.test.ts | 26 +++++++++++++++++++ .../parser/service/tailordb/type-script.ts | 4 +-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index ff5bc2fb1..21c103f90 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -171,4 +171,30 @@ describe("buildTypeScripts", () => { expect(buildTypeScripts({})).toEqual({}); expect(buildTypeScripts({}, undefined)).toEqual({}); }); + + test("captures _invoker safely so scripts work when Platform does not inject it", () => { + const fields: Record = { + createdAt: { + type: "datetime", + hooks: { create: { expr: "_now" } }, + }, + }; + const typeHookExpr = { + create: "(({ input }) => ({ computed: input.a }))({ input: _input, invoker: _invoker })", + }; + + const { typeHook } = buildTypeScripts(fields, { typeHookExpr }); + + const hookExpr = typeHook?.create?.expr ?? ""; + expect(hookExpr).toMatch(/^\(\(_invoker\) =>/); + expect(hookExpr).toContain('typeof _invoker !== "undefined" ? _invoker : undefined'); + + const validateExpr = + buildTypeScripts( + { x: { type: "string", validate: [{ script: { expr: "true" }, errorMessage: "" }] } }, + { typeValidateExpr: "fn(_invoker)" }, + ).typeValidate?.create?.expr ?? ""; + expect(validateExpr).toMatch(/^\(\(_invoker\) =>/); + expect(validateExpr).toContain('typeof _invoker !== "undefined" ? _invoker : undefined'); + }); }); diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index 5fe134c06..cd8e6ba04 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -134,13 +134,13 @@ function buildValidateStatements( } function wrapHook(objectExpr: string): string { - return `(() => { const ${NOW} = new Date(); return ${objectExpr}; })()`; + return `((_invoker) => { const ${NOW} = new Date(); return ${objectExpr}; })(typeof _invoker !== "undefined" ? _invoker : undefined)`; } function wrapValidate(statements: string[], typeValidateExpr?: string): string { const issuesFn = typeValidateExpr ? " const __issues = (f, m) => { __errs[f] = m; };" : ""; const typeValidateStmt = typeValidateExpr ? ` ${typeValidateExpr};` : ""; - return `(() => { const __errs = {};${issuesFn} ${statements.join(" ")}${typeValidateStmt} return __errs; })()`; + return `((_invoker) => { const __errs = {};${issuesFn} ${statements.join(" ")}${typeValidateStmt} return __errs; })(typeof _invoker !== "undefined" ? _invoker : undefined)`; } /** From ee790790fdde9972fe68a8d96b0ba50c2d55d108 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 7 Jul 2026 12:14:59 +0900 Subject: [PATCH 448/618] fix(example): add fullAddress to Customer seed data for Kysely insert compatibility --- example/seed/data/Customer.jsonl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/example/seed/data/Customer.jsonl b/example/seed/data/Customer.jsonl index bbb6aacc5..393308674 100644 --- a/example/seed/data/Customer.jsonl +++ b/example/seed/data/Customer.jsonl @@ -1,5 +1,5 @@ -{"id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","name":"Acme Corporation","email":"contact@acme.com","phone":"03-1234-5678","country":"Japan","postalCode":"100-0001","address":"Chiyoda-ku","city":"Tokyo","state":"Tokyo"} -{"id":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","name":"Global Tech Inc","email":"info@globaltech.com","phone":"03-9876-5432","country":"Japan","postalCode":"150-0002","address":"Shibuya-ku","city":"Tokyo","state":"Tokyo"} -{"id":"cccccccc-cccc-cccc-cccc-cccccccccccc","name":"Enterprise Solutions Ltd","email":"sales@enterprise.com","phone":"06-1111-2222","country":"Japan","postalCode":"530-0001","address":"Kita-ku","city":"Osaka","state":"Osaka"} -{"id":"dddddddd-dddd-dddd-dddd-dddddddddddd","name":"Digital Services Co","email":"hello@digital.com","country":"Japan","postalCode":"810-0001","address":"Chuo-ku","city":"Fukuoka","state":"Fukuoka"} -{"id":"eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee","name":"Innovation Partners","email":"contact@innovation.com","phone":"011-3333-4444","country":"Japan","postalCode":"060-0001","address":"Chuo-ku","city":"Sapporo","state":"Hokkaido"} +{"id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","name":"Acme Corporation","email":"contact@acme.com","phone":"03-1234-5678","country":"Japan","postalCode":"100-0001","address":"Chiyoda-ku","city":"Tokyo","fullAddress":"100-0001 Chiyoda-ku Tokyo","state":"Tokyo"} +{"id":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","name":"Global Tech Inc","email":"info@globaltech.com","phone":"03-9876-5432","country":"Japan","postalCode":"150-0002","address":"Shibuya-ku","city":"Tokyo","fullAddress":"150-0002 Shibuya-ku Tokyo","state":"Tokyo"} +{"id":"cccccccc-cccc-cccc-cccc-cccccccccccc","name":"Enterprise Solutions Ltd","email":"sales@enterprise.com","phone":"06-1111-2222","country":"Japan","postalCode":"530-0001","address":"Kita-ku","city":"Osaka","fullAddress":"530-0001 Kita-ku Osaka","state":"Osaka"} +{"id":"dddddddd-dddd-dddd-dddd-dddddddddddd","name":"Digital Services Co","email":"hello@digital.com","country":"Japan","postalCode":"810-0001","address":"Chuo-ku","city":"Fukuoka","fullAddress":"810-0001 Chuo-ku Fukuoka","state":"Fukuoka"} +{"id":"eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee","name":"Innovation Partners","email":"contact@innovation.com","phone":"011-3333-4444","country":"Japan","postalCode":"060-0001","address":"Chuo-ku","city":"Sapporo","fullAddress":"060-0001 Chuo-ku Sapporo","state":"Hokkaido"} From b5a8fd7a748273adaab04e6eab8da05039c43550 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 7 Jul 2026 12:25:32 +0900 Subject: [PATCH 449/618] fix(e2e): provide fullAddress in Customer mutations until optionalOnCreate is supported --- example/e2e/executor.test.ts | 1 + example/e2e/tailordb.test.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/example/e2e/executor.test.ts b/example/e2e/executor.test.ts index 0eec3e402..658bd29bf 100644 --- a/example/e2e/executor.test.ts +++ b/example/e2e/executor.test.ts @@ -177,6 +177,7 @@ describe("dataplane", () => { email: "customer@example.com" country: "USA" postalCode: "12345" + fullAddress: "12345 USA" state: "California" } ) { diff --git a/example/e2e/tailordb.test.ts b/example/e2e/tailordb.test.ts index 0b2ba1d35..1bae57f7a 100644 --- a/example/e2e/tailordb.test.ts +++ b/example/e2e/tailordb.test.ts @@ -236,6 +236,7 @@ describe("dataplane", () => { email: "customer-${randomUUID()}@example.com" country: "USA" postalCode: "12345" + fullAddress: "12345 USA" state: "California" } ) { @@ -419,6 +420,7 @@ describe("dataplane", () => { email: "customer@example.com" country: "USA" postalCode: "12345" + fullAddress: "12345 USA" state: "California" } ) { @@ -546,6 +548,7 @@ describe("dataplane", () => { postalCode: "12345" address: "123 Main St" city: "Los Angeles" + fullAddress: "placeholder" state: "California" } ) { @@ -577,6 +580,7 @@ describe("dataplane", () => { email: "bob@example.com" country: "USA" postalCode: "12345" + fullAddress: "12345 USA" state: "California" } ) { From 197f942097ffef166a1ece627bda20ee8a29a610 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 7 Jul 2026 12:30:48 +0900 Subject: [PATCH 450/618] fix(e2e): remove per-field hooks assertion and provide fullAddress in hook test --- example/e2e/tailordb.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/example/e2e/tailordb.test.ts b/example/e2e/tailordb.test.ts index 1bae57f7a..c4f1c1d56 100644 --- a/example/e2e/tailordb.test.ts +++ b/example/e2e/tailordb.test.ts @@ -47,13 +47,11 @@ describe("controlplane", () => { type: "datetime", required: true, array: false, - hooks: expect.any(Object), }, updatedAt: { type: "datetime", required: true, array: false, - hooks: expect.any(Object), }, }, indexes: { @@ -548,7 +546,7 @@ describe("dataplane", () => { postalCode: "12345" address: "123 Main St" city: "Los Angeles" - fullAddress: "placeholder" + fullAddress: "12345 123 Main St Los Angeles" state: "California" } ) { From 6ea9ad24c8a51873a8c42d58914d55b7b9c4233e Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 7 Jul 2026 12:47:43 +0900 Subject: [PATCH 451/618] fix(tailordb): guard null data in createTailorDBHook, share now across nested calls, and strip nested hooks in remote snapshot comparison --- .../cli/commands/tailordb/migrate/snapshot.ts | 15 +++++++++++-- packages/sdk/src/utils/test/index.ts | 21 ++++++++----------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts index 3db8ea1ce..6020915c2 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts @@ -2763,6 +2763,18 @@ export function compareRemoteWithSnapshot( ); } +function stripFieldScriptProps(field: SnapshotFieldConfig): SnapshotFieldConfig { + const { hooks: _hooks, validate: _validate, default: _default, ...rest } = field; + if (rest.fields) { + const nested = createSnapshotRecord(); + for (const [name, f] of Object.entries(rest.fields)) { + nested[name] = stripFieldScriptProps(f); + } + return { ...rest, fields: nested }; + } + return rest; +} + function createRemoteComparableSnapshot(snapshot: SchemaSnapshot): NormalizedSchemaSnapshot { const types = createSnapshotRecord(); @@ -2770,8 +2782,7 @@ function createRemoteComparableSnapshot(snapshot: SchemaSnapshot): NormalizedSch const fields = createSnapshotRecord(); for (const [fieldName, field] of Object.entries(type.fields)) { if (SYSTEM_FIELDS.has(fieldName)) continue; - const { hooks: _hooks, validate: _validate, default: _default, ...rest } = field; - fields[fieldName] = rest; + fields[fieldName] = stripFieldScriptProps(field); } types[typeName] = { ...type, fields }; } diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index 5288005cc..0f146d38d 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -23,31 +23,28 @@ export { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function createTailorDBHook>(type: T) { - return (data: unknown) => { - const now = new Date(); + return (data: unknown, now: Date = new Date()) => { + const obj = data && typeof data === "object" ? (data as Record) : undefined; const hooked = Object.entries(type.fields).reduce( (hooked, [key, value]) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const field = value as TailorField; if (key === "id") { - const existingId = - data && typeof data === "object" ? (data as Record)[key] : undefined; - hooked[key] = existingId ?? crypto.randomUUID(); + hooked[key] = obj?.[key] ?? crypto.randomUUID(); } else if (field.type === "nested") { - const nestedValue = - data && typeof data === "object" ? (data as Record)[key] : undefined; // eslint-disable-next-line @typescript-eslint/no-explicit-any const nestedHook = createTailorDBHook({ fields: field.fields } as any); if (field.metadata.array) { + const nestedValue = obj?.[key]; hooked[key] = Array.isArray(nestedValue) - ? nestedValue.map((item) => nestedHook(item)) + ? nestedValue.map((item) => nestedHook(item, now)) : nestedValue; } else { - hooked[key] = nestedHook(nestedValue); + hooked[key] = nestedHook(obj?.[key], now); } } else if (field.metadata.hooks?.create) { hooked[key] = field.metadata.hooks.create({ - value: (data as Record)[key], + value: obj?.[key], oldValue: null, invoker: null, now, @@ -55,8 +52,8 @@ export function createTailorDBHook>(type: T) { if (hooked[key] instanceof Date) { hooked[key] = hooked[key].toISOString(); } - } else if (data && typeof data === "object") { - hooked[key] = (data as Record)[key]; + } else if (obj) { + hooked[key] = obj[key]; } return hooked; }, From de3ef5e7421a998624154df5e90da62e17664524 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 7 Jul 2026 17:54:45 +0900 Subject: [PATCH 452/618] revert(sdk): restore scalar string field outputs --- .changeset/revert-strict-scalar-strings.md | 7 + .changeset/strict-scalar-strings-codemod.md | 5 - .changeset/strict-tailor-field-strings.md | 5 - example/generated/tailordb.ts | 47 ++- example/migrations/0003/db.ts | 100 ++--- example/migrations/analyticsdb/0001/db.ts | 12 +- example/tests/bundled_execution.test.ts | 24 +- example/tests/fixtures/expected/db.ts | 47 ++- example/workflows/jobs/fetch-customer.ts | 11 +- example/workflows/order-processing.ts | 3 +- .../src/executor/onUserCreated.test.ts | 4 +- .../executor/src/executor/shared.test.ts | 2 +- .../templates/executor/src/executor/shared.ts | 3 +- .../templates/executor/src/generated/db.ts | 11 +- .../templates/generators/src/generated/db.ts | 17 +- .../src/resolver/getProduct.test.ts | 8 +- .../src/generated/kysely-tailordb.ts | 3 +- .../src/generated/kysely-tailordb.ts | 27 +- .../apps/admin/db/adminNote.ts | 4 +- .../templates/resolver/src/generated/db.ts | 3 +- .../src/resolver/showUserInfo.test.ts | 4 +- .../resolver/src/resolver/upsertUsers.test.ts | 4 +- .../templates/tailordb/src/generated/db.ts | 19 +- .../templates/workflow/src/generated/db.ts | 5 +- packages/sdk-codemod/src/registry.ts | 56 --- packages/sdk/docs/migration/v2.md | 97 ----- .../migrate/db-types-generator.test.ts | 42 ++- .../tailordb/migrate/db-types-generator.ts | 105 ++---- .../sdk/src/cli/shared/runtime-exprs.test.ts | 12 +- .../sdk/src/cli/shared/type-generator.test.ts | 45 +-- packages/sdk/src/cli/shared/type-generator.ts | 86 +---- packages/sdk/src/configure/index.ts | 25 -- .../src/configure/services/auth/index.test.ts | 11 +- .../services/executor/executor.test.ts | 39 +- .../services/executor/trigger/event.ts | 13 +- .../services/resolver/resolver.test.ts | 20 +- .../services/tailordb/schema.test.ts | 348 +++++------------- .../src/configure/services/tailordb/schema.ts | 190 +++++----- .../configure/services/workflow/job.test.ts | 20 +- .../services/workflow/test-env-key.ts | 2 +- .../sdk/src/configure/types/field-format.ts | 28 -- .../sdk/src/configure/types/field-runtime.ts | 26 +- .../sdk/src/configure/types/field.types.ts | 34 +- packages/sdk/src/configure/types/index.ts | 9 - .../sdk/src/configure/types/scalar.test.ts | 122 ------ packages/sdk/src/configure/types/scalar.ts | 207 ----------- .../sdk/src/configure/types/scalar.types.ts | 7 - packages/sdk/src/configure/types/type.test.ts | 66 ++-- packages/sdk/src/kysely/index.test-d.ts | 56 +-- packages/sdk/src/kysely/index.ts | 16 +- .../sdk/src/parser/service/auth/index.test.ts | 5 +- .../plugin/builtin/kysely-type/index.test.ts | 4 +- .../src/plugin/builtin/kysely-type/index.ts | 11 +- .../kysely-type/type-processor.test.ts | 20 +- .../builtin/kysely-type/type-processor.ts | 90 ++--- .../src/plugin/builtin/kysely-type/types.ts | 6 - packages/sdk/src/runtime/context.test.ts | 4 +- packages/sdk/src/runtime/context.ts | 3 +- packages/sdk/src/runtime/idp.test.ts | 44 +-- packages/sdk/src/runtime/idp.ts | 20 +- packages/sdk/src/runtime/types.ts | 4 +- packages/sdk/src/vitest/mock.test.ts | 25 +- packages/sdk/src/vitest/mock.ts | 33 +- 63 files changed, 625 insertions(+), 1701 deletions(-) create mode 100644 .changeset/revert-strict-scalar-strings.md delete mode 100644 .changeset/strict-scalar-strings-codemod.md delete mode 100644 .changeset/strict-tailor-field-strings.md delete mode 100644 packages/sdk/src/configure/types/field-format.ts delete mode 100644 packages/sdk/src/configure/types/scalar.test.ts delete mode 100644 packages/sdk/src/configure/types/scalar.ts delete mode 100644 packages/sdk/src/configure/types/scalar.types.ts diff --git a/.changeset/revert-strict-scalar-strings.md b/.changeset/revert-strict-scalar-strings.md new file mode 100644 index 000000000..13da3f913 --- /dev/null +++ b/.changeset/revert-strict-scalar-strings.md @@ -0,0 +1,7 @@ +--- +"@tailor-platform/create-sdk": major +"@tailor-platform/sdk": major +"@tailor-platform/sdk-codemod": patch +--- + +Restore Tailor field outputs for UUID, date, datetime, time, and decimal fields to plain string-compatible types and remove the strict scalar string migration guidance. diff --git a/.changeset/strict-scalar-strings-codemod.md b/.changeset/strict-scalar-strings-codemod.md deleted file mode 100644 index 8ce1ab50b..000000000 --- a/.changeset/strict-scalar-strings-codemod.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@tailor-platform/sdk-codemod": minor ---- - -Add the `v2/strict-scalar-strings` migration entry for the strict UUID/date/datetime/time/decimal field string types. The migration guide now documents the new scalar shapes and `is*String` / `parse*String` / `assert*String` helpers, and the runner surfaces the regenerate-and-typecheck migration steps as project-wide LLM guidance. diff --git a/.changeset/strict-tailor-field-strings.md b/.changeset/strict-tailor-field-strings.md deleted file mode 100644 index 2b12bbe2c..000000000 --- a/.changeset/strict-tailor-field-strings.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@tailor-platform/sdk": major ---- - -Tighten Tailor field output types for UUID, date, datetime, time, and decimal fields, including generated Kysely types and runtime user IDs. Add scalar string guard, parse, and assertion helpers. Datetime parsing now accepts ISO 8601 datetime offsets. diff --git a/example/generated/tailordb.ts b/example/generated/tailordb.ts index ae1e3c5bb..e769dbf3a 100644 --- a/example/generated/tailordb.ts +++ b/example/generated/tailordb.ts @@ -1,7 +1,6 @@ import { createGetDB, type Generated, - type UUIDString, type Timestamp, type ObjectColumnType, type Serial, @@ -17,7 +16,7 @@ import { export interface Namespace { "tailordb": { Customer: { - id: Generated; + id: Generated; name: string; email: string; phone: string | null; @@ -32,9 +31,9 @@ export interface Namespace { } Invoice: { - id: Generated; + id: Generated; invoiceNumber: Serial; - salesOrderID: UUIDString; + salesOrderID: string; amount: number | null; sequentialId: Serial; status: "draft" | "sent" | "paid" | "cancelled" | null; @@ -43,7 +42,7 @@ export interface Namespace { } NestedProfile: { - id: Generated; + id: Generated; userInfo: ObjectColumnType<{ name: string; age?: number | null; @@ -62,13 +61,13 @@ export interface Namespace { } PurchaseOrder: { - id: Generated; - supplierID: UUIDString; + id: Generated; + supplierID: string; totalPrice: number; discount: number | null; status: string; attachedFiles: { - id: UUIDString; + id: string; name: string; size: number; type: "text" | "image"; @@ -78,9 +77,9 @@ export interface Namespace { } SalesOrder: { - id: Generated; - customerID: UUIDString; - approvedByUserIDs: UUIDString[] | null; + id: Generated; + customerID: string; + approvedByUserIDs: string[] | null; totalPrice: number | null; discount: number | null; status: string | null; @@ -91,22 +90,22 @@ export interface Namespace { } SalesOrderCreated: { - id: Generated; - salesOrderID: UUIDString; - customerID: UUIDString; + id: Generated; + salesOrderID: string; + customerID: string; totalPrice: number | null; status: string | null; } Selfie: { - id: Generated; + id: Generated; name: string; - parentID: UUIDString | null; - dependId: UUIDString | null; + parentID: string | null; + dependId: string | null; } Supplier: { - id: Generated; + id: Generated; name: string; phone: string; fax: string | null; @@ -120,7 +119,7 @@ export interface Namespace { } User: { - id: Generated; + id: Generated; name: string; email: string; status: string | null; @@ -131,24 +130,24 @@ export interface Namespace { } UserLog: { - id: Generated; - userID: UUIDString; + id: Generated; + userID: string; message: string; createdAt: Generated; updatedAt: Generated; } UserSetting: { - id: Generated; + id: Generated; language: "jp" | "en"; - userID: UUIDString; + userID: string; createdAt: Generated; updatedAt: Generated; } }, "analyticsdb": { Event: { - id: Generated; + id: Generated; name: "CLICK" | "VIEW" | "PURCHASE"; createdAt: Generated; updatedAt: Generated; diff --git a/example/migrations/0003/db.ts b/example/migrations/0003/db.ts index 9e1b783ec..3dc32b41c 100644 --- a/example/migrations/0003/db.ts +++ b/example/migrations/0003/db.ts @@ -8,12 +8,10 @@ import { type ColumnType, type Transaction as KyselyTransaction, - type UUIDString, - type DateTimeString, } from "@tailor-platform/sdk/kysely"; import type { Env } from "@tailor-platform/sdk"; -type Timestamp = ColumnType; +type Timestamp = ColumnType; type Generated = T extends ColumnType ? ColumnType @@ -21,7 +19,7 @@ type Generated = interface Database { Customer: { - id: Generated; + id: Generated; name: string; email: string; phone: string | null; @@ -32,83 +30,63 @@ interface Database { fullAddress: string; state: string; createdAt: Timestamp; - updatedAt: ColumnType< - Date | DateTimeString | null, - Date | DateTimeString, - Date | DateTimeString - >; + updatedAt: ColumnType; }; Invoice: { - id: Generated; + id: Generated; invoiceNumber: string; - salesOrderID: UUIDString; + salesOrderID: string; amount: number | null; sequentialId: number; status: "draft" | "sent" | "paid" | "cancelled" | null; createdAt: Timestamp; - updatedAt: ColumnType< - Date | DateTimeString | null, - Date | DateTimeString, - Date | DateTimeString - >; + updatedAt: ColumnType; }; NestedProfile: { - id: Generated; + id: Generated; userInfo: string; metadata: string; archived: boolean | null; createdAt: Timestamp; - updatedAt: ColumnType< - Date | DateTimeString | null, - Date | DateTimeString, - Date | DateTimeString - >; + updatedAt: ColumnType; }; PurchaseOrder: { - id: Generated; - supplierID: UUIDString; + id: Generated; + supplierID: string; totalPrice: number; discount: number | null; status: string; attachedFiles: string[]; createdAt: Timestamp; - updatedAt: ColumnType< - Date | DateTimeString | null, - Date | DateTimeString, - Date | DateTimeString - >; + updatedAt: ColumnType; }; SalesOrder: { - id: Generated; - customerID: UUIDString; - approvedByUserIDs: UUIDString[] | null; + id: Generated; + customerID: string; + approvedByUserIDs: string[] | null; totalPrice: number | null; discount: number | null; status: string | null; cancelReason: string | null; canceledAt: Timestamp | null; createdAt: Timestamp; - updatedAt: ColumnType< - Date | DateTimeString | null, - Date | DateTimeString, - Date | DateTimeString - >; + updatedAt: ColumnType; }; SalesOrderCreated: { - id: Generated; - salesOrderID: UUIDString; - customerID: UUIDString; + id: Generated; + salesOrderID: string; + customerID: string; totalPrice: number | null; status: string | null; }; Selfie: { - id: Generated; + id: Generated; name: string; - parentID: UUIDString | null; - dependId: UUIDString | null; + parentID: string | null; + dependId: string | null; }; Supplier: { - id: Generated; + id: Generated; name: string; phone: string; fax: string | null; @@ -118,47 +96,31 @@ interface Database { state: "Alabama" | "Alaska"; city: string; createdAt: Timestamp; - updatedAt: ColumnType< - Date | DateTimeString | null, - Date | DateTimeString, - Date | DateTimeString - >; + updatedAt: ColumnType; }; User: { - id: Generated; + id: Generated; name: string; email: string; status: string | null; department: string | null; role: "MANAGER" | "STAFF"; createdAt: Timestamp; - updatedAt: ColumnType< - Date | DateTimeString | null, - Date | DateTimeString, - Date | DateTimeString - >; + updatedAt: ColumnType; }; UserLog: { - id: Generated; - userID: UUIDString; + id: Generated; + userID: string; message: string; createdAt: Timestamp; - updatedAt: ColumnType< - Date | DateTimeString | null, - Date | DateTimeString, - Date | DateTimeString - >; + updatedAt: ColumnType; }; UserSetting: { - id: Generated; + id: Generated; language: "jp" | "en"; - userID: UUIDString; + userID: string; createdAt: Timestamp; - updatedAt: ColumnType< - Date | DateTimeString | null, - Date | DateTimeString, - Date | DateTimeString - >; + updatedAt: ColumnType; }; } diff --git a/example/migrations/analyticsdb/0001/db.ts b/example/migrations/analyticsdb/0001/db.ts index 11cb40173..e4f20afd9 100644 --- a/example/migrations/analyticsdb/0001/db.ts +++ b/example/migrations/analyticsdb/0001/db.ts @@ -8,12 +8,10 @@ import { type ColumnType, type Transaction as KyselyTransaction, - type UUIDString, - type DateTimeString, } from "@tailor-platform/sdk/kysely"; import type { Env } from "@tailor-platform/sdk"; -type Timestamp = ColumnType; +type Timestamp = ColumnType; type Generated = T extends ColumnType ? ColumnType @@ -21,14 +19,10 @@ type Generated = interface Database { Event: { - id: Generated; + id: Generated; name: "CLICK" | "VIEW" | "PURCHASE"; createdAt: Timestamp; - updatedAt: ColumnType< - Date | DateTimeString | null, - Date | DateTimeString, - Date | DateTimeString - >; + updatedAt: ColumnType; }; } diff --git a/example/tests/bundled_execution.test.ts b/example/tests/bundled_execution.test.ts index 4ad303968..3d214bf25 100644 --- a/example/tests/bundled_execution.test.ts +++ b/example/tests/bundled_execution.test.ts @@ -134,7 +134,7 @@ describe("bundled execution tests", () => { }, }, user: { - id: "123e4567-e89b-12d3-a456-426614174000", + id: "test-user-id", type: "user", workspaceId: "test-workspace-id", }, @@ -167,18 +167,15 @@ describe("bundled execution tests", () => { }); const main = await importActualMain("executors/user-created.js"); - const payload = { newRecord: { id: "11111111-1111-4111-8111-111111111111" } }; + const payload = { newRecord: { id: "user-1" } }; const result = await main(payload); expect(result).toBeUndefined(); expect(db.executedQueries).toEqual([ - { - query: 'select * from "User" where "id" = $1', - params: ["11111111-1111-4111-8111-111111111111"], - }, + { query: 'select * from "User" where "id" = $1', params: ["user-1"] }, { query: 'insert into "UserLog" ("userID", "message") values ($1, $2)', - params: ["11111111-1111-4111-8111-111111111111", "User created: undefined (undefined)"], + params: ["user-1", "User created: undefined (undefined)"], }, ]); expect(db.createdClients).toMatchObject([{ namespace: "tailordb" }]); @@ -202,22 +199,19 @@ describe("bundled execution tests", () => { const main = await importActualMain("workflow-jobs/process-order.js"); const result = await main({ orderId: "order-123", - customerId: "123e4567-e89b-12d3-a456-426614174000", + customerId: "customer-456", }); expect(result).toEqual({ orderId: "order-123", - customerId: "123e4567-e89b-12d3-a456-426614174000", + customerId: "customer-456", customerEmail: "customer@example.com", notificationSent: true, processedAt: "2025-01-01 12:00:00", }); expect(wf.triggeredJobs).toEqual([ - { - jobName: "fetch-customer", - args: { customerId: "123e4567-e89b-12d3-a456-426614174000" }, - }, + { jobName: "fetch-customer", args: { customerId: "customer-456" } }, { jobName: "send-notification", args: { @@ -237,9 +231,9 @@ describe("bundled execution tests", () => { await expect( main({ orderId: "order-123", - customerId: "00000000-0000-0000-0000-000000000000", + customerId: "non-existent", }), - ).rejects.toThrow("Customer 00000000-0000-0000-0000-000000000000 not found"); + ).rejects.toThrow("Customer non-existent not found"); }); test("workflow-jobs/send-notification.js executes correctly", async () => { diff --git a/example/tests/fixtures/expected/db.ts b/example/tests/fixtures/expected/db.ts index ae1e3c5bb..e769dbf3a 100644 --- a/example/tests/fixtures/expected/db.ts +++ b/example/tests/fixtures/expected/db.ts @@ -1,7 +1,6 @@ import { createGetDB, type Generated, - type UUIDString, type Timestamp, type ObjectColumnType, type Serial, @@ -17,7 +16,7 @@ import { export interface Namespace { "tailordb": { Customer: { - id: Generated; + id: Generated; name: string; email: string; phone: string | null; @@ -32,9 +31,9 @@ export interface Namespace { } Invoice: { - id: Generated; + id: Generated; invoiceNumber: Serial; - salesOrderID: UUIDString; + salesOrderID: string; amount: number | null; sequentialId: Serial; status: "draft" | "sent" | "paid" | "cancelled" | null; @@ -43,7 +42,7 @@ export interface Namespace { } NestedProfile: { - id: Generated; + id: Generated; userInfo: ObjectColumnType<{ name: string; age?: number | null; @@ -62,13 +61,13 @@ export interface Namespace { } PurchaseOrder: { - id: Generated; - supplierID: UUIDString; + id: Generated; + supplierID: string; totalPrice: number; discount: number | null; status: string; attachedFiles: { - id: UUIDString; + id: string; name: string; size: number; type: "text" | "image"; @@ -78,9 +77,9 @@ export interface Namespace { } SalesOrder: { - id: Generated; - customerID: UUIDString; - approvedByUserIDs: UUIDString[] | null; + id: Generated; + customerID: string; + approvedByUserIDs: string[] | null; totalPrice: number | null; discount: number | null; status: string | null; @@ -91,22 +90,22 @@ export interface Namespace { } SalesOrderCreated: { - id: Generated; - salesOrderID: UUIDString; - customerID: UUIDString; + id: Generated; + salesOrderID: string; + customerID: string; totalPrice: number | null; status: string | null; } Selfie: { - id: Generated; + id: Generated; name: string; - parentID: UUIDString | null; - dependId: UUIDString | null; + parentID: string | null; + dependId: string | null; } Supplier: { - id: Generated; + id: Generated; name: string; phone: string; fax: string | null; @@ -120,7 +119,7 @@ export interface Namespace { } User: { - id: Generated; + id: Generated; name: string; email: string; status: string | null; @@ -131,24 +130,24 @@ export interface Namespace { } UserLog: { - id: Generated; - userID: UUIDString; + id: Generated; + userID: string; message: string; createdAt: Generated; updatedAt: Generated; } UserSetting: { - id: Generated; + id: Generated; language: "jp" | "en"; - userID: UUIDString; + userID: string; createdAt: Generated; updatedAt: Generated; } }, "analyticsdb": { Event: { - id: Generated; + id: Generated; name: "CLICK" | "VIEW" | "PURCHASE"; createdAt: Generated; updatedAt: Generated; diff --git a/example/workflows/jobs/fetch-customer.ts b/example/workflows/jobs/fetch-customer.ts index 844ce069a..d7891370e 100644 --- a/example/workflows/jobs/fetch-customer.ts +++ b/example/workflows/jobs/fetch-customer.ts @@ -1,14 +1,9 @@ import { createWorkflowJob } from "@tailor-platform/sdk"; import { getDB } from "../../generated/tailordb"; -import type { DateTimeString, UUIDString } from "@tailor-platform/sdk"; - -function serializeDateTime(value: Date | DateTimeString): string { - return value instanceof Date ? value.toISOString() : value; -} export const fetchCustomer = createWorkflowJob({ name: "fetch-customer", - body: async (input: { customerId: UUIDString }) => { + body: async (input: { customerId: string }) => { const db = getDB("tailordb"); const customer = await db .selectFrom("Customer") @@ -18,8 +13,8 @@ export const fetchCustomer = createWorkflowJob({ if (!customer) return undefined; return { ...customer, - createdAt: serializeDateTime(customer.createdAt), - updatedAt: serializeDateTime(customer.updatedAt), + createdAt: customer.createdAt.toISOString(), + updatedAt: customer.updatedAt.toISOString(), }; }, }); diff --git a/example/workflows/order-processing.ts b/example/workflows/order-processing.ts index 567caa6dd..375f16099 100644 --- a/example/workflows/order-processing.ts +++ b/example/workflows/order-processing.ts @@ -1,13 +1,12 @@ import { createWorkflow, createWorkflowJob } from "@tailor-platform/sdk"; import { fetchCustomer } from "./jobs/fetch-customer"; import { sendNotification } from "./jobs/send-notification"; -import type { UUIDString } from "@tailor-platform/sdk"; // Note: We're NOT importing generateReport and archiveData // Those jobs should be completely excluded from the bundle export const processOrder = createWorkflowJob({ name: "process-order", - body: (input: { orderId: string; customerId: UUIDString }, { env }) => { + body: (input: { orderId: string; customerId: string }, { env }) => { // Log env for demonstration console.log("Environment:", env); diff --git a/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts b/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts index 80d12b2bd..22d1113fc 100644 --- a/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts +++ b/packages/create-sdk/templates/executor/src/executor/onUserCreated.test.ts @@ -11,7 +11,7 @@ describe("onUserCreated executor", () => { } await onUserCreated.operation.body({ newRecord: { - id: "11111111-1111-4111-8111-111111111111", + id: "user-1", name: "Alice", email: "alice@example.com", role: "ADMIN", @@ -23,7 +23,7 @@ describe("onUserCreated executor", () => { expect(createAuditLog).toHaveBeenCalledExactlyOnceWith({ action: "USER_CREATED", entityType: "User", - entityId: "11111111-1111-4111-8111-111111111111", + entityId: "user-1", message: "Admin user created: Alice (alice@example.com)", }); }); diff --git a/packages/create-sdk/templates/executor/src/executor/shared.test.ts b/packages/create-sdk/templates/executor/src/executor/shared.test.ts index c7a70ab98..b56c0fce8 100644 --- a/packages/create-sdk/templates/executor/src/executor/shared.test.ts +++ b/packages/create-sdk/templates/executor/src/executor/shared.test.ts @@ -8,7 +8,7 @@ describe("createAuditLog", () => { await createAuditLog({ action: "USER_CREATED", entityType: "User", - entityId: "123e4567-e89b-12d3-a456-426614174000", + entityId: "test-id", message: "Test audit log", }); diff --git a/packages/create-sdk/templates/executor/src/executor/shared.ts b/packages/create-sdk/templates/executor/src/executor/shared.ts index d8e5abe1b..0483a944b 100644 --- a/packages/create-sdk/templates/executor/src/executor/shared.ts +++ b/packages/create-sdk/templates/executor/src/executor/shared.ts @@ -1,10 +1,9 @@ import { getDB } from "../generated/db"; -import type { UUIDString } from "@tailor-platform/sdk"; interface AuditLogInput { action: string; entityType: string; - entityId: UUIDString; + entityId: string; message: string; } diff --git a/packages/create-sdk/templates/executor/src/generated/db.ts b/packages/create-sdk/templates/executor/src/generated/db.ts index ecce78ef0..c50b7fcbe 100644 --- a/packages/create-sdk/templates/executor/src/generated/db.ts +++ b/packages/create-sdk/templates/executor/src/generated/db.ts @@ -1,7 +1,6 @@ import { createGetDB, type Generated, - type UUIDString, type Timestamp, type NamespaceDB, type NamespaceInsertable, @@ -15,18 +14,18 @@ import { export interface Namespace { "main-db": { AuditLog: { - id: Generated; + id: Generated; action: string; entityType: string; - entityId: UUIDString; + entityId: string; message: string; createdAt: Generated; updatedAt: Generated; } Notification: { - id: Generated; - userId: UUIDString; + id: Generated; + userId: string; title: string; body: string; isRead: boolean; @@ -35,7 +34,7 @@ export interface Namespace { } User: { - id: Generated; + id: Generated; name: string; email: string; role: "ADMIN" | "MEMBER"; diff --git a/packages/create-sdk/templates/generators/src/generated/db.ts b/packages/create-sdk/templates/generators/src/generated/db.ts index fb3814422..a9b7441d5 100644 --- a/packages/create-sdk/templates/generators/src/generated/db.ts +++ b/packages/create-sdk/templates/generators/src/generated/db.ts @@ -1,7 +1,6 @@ import { createGetDB, type Generated, - type UUIDString, type Timestamp, type NamespaceDB, type NamespaceInsertable, @@ -15,16 +14,16 @@ import { export interface Namespace { "main-db": { Category: { - id: Generated; + id: Generated; name: string; slug: string; - parentCategoryId: UUIDString | null; + parentCategoryId: string | null; } Order: { - id: Generated; - productId: UUIDString; - userId: UUIDString; + id: Generated; + productId: string; + userId: string; quantity: number; totalPrice: number; status: "PENDING" | "CONFIRMED" | "SHIPPED" | "DELIVERED" | "CANCELLED"; @@ -33,18 +32,18 @@ export interface Namespace { } Product: { - id: Generated; + id: Generated; name: string; description: string | null; price: number; status: "DRAFT" | "ACTIVE" | "DISCONTINUED"; - categoryId: UUIDString | null; + categoryId: string | null; createdAt: Generated; updatedAt: Generated; } User: { - id: Generated; + id: Generated; name: string; email: string; role: "ADMIN" | "MEMBER" | "VIEWER"; diff --git a/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts b/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts index 0239c0b4e..598ee996f 100644 --- a/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts +++ b/packages/create-sdk/templates/generators/src/resolver/getProduct.test.ts @@ -7,7 +7,7 @@ describe("getProduct resolver", () => { using db = mockTailordb(); // Select product db.enqueueResult({ - id: "00000000-0000-4000-8000-000000000001", + id: "product-1", name: "Widget", price: 9.99, status: "ACTIVE", @@ -20,7 +20,7 @@ describe("getProduct resolver", () => { db.enqueueResult({ name: "Gadgets" }); const result = await resolver.body({ - input: { productId: "00000000-0000-4000-8000-000000000001" }, + input: { productId: "product-1" }, caller: null, invoker: null, env: {}, @@ -39,7 +39,7 @@ describe("getProduct resolver", () => { using db = mockTailordb(); // Select product (no categoryId) db.enqueueResult({ - id: "00000000-0000-4000-8000-000000000002", + id: "product-2", name: "Standalone Item", price: 19.99, status: "DRAFT", @@ -50,7 +50,7 @@ describe("getProduct resolver", () => { }); const result = await resolver.body({ - input: { productId: "00000000-0000-4000-8000-000000000002" }, + input: { productId: "product-2" }, caller: null, invoker: null, env: {}, diff --git a/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts b/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts index 60cb1acae..67fefdd51 100644 --- a/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts +++ b/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts @@ -1,7 +1,6 @@ import { createGetDB, type Generated, - type UUIDString, type Timestamp, type NamespaceDB, type NamespaceInsertable, @@ -15,7 +14,7 @@ import { export interface Namespace { "main-db": { User: { - id: Generated; + id: Generated; name: string; email: string; role: "MANAGER" | "STAFF"; diff --git a/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts b/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts index 11a6cd4fc..6ab154858 100644 --- a/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts +++ b/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts @@ -1,7 +1,6 @@ import { createGetDB, type Generated, - type UUIDString, type Timestamp, type NamespaceDB, type NamespaceInsertable, @@ -15,7 +14,7 @@ import { export interface Namespace { "main-db": { Category: { - id: Generated; + id: Generated; name: string; description: string | null; createdAt: Generated; @@ -23,7 +22,7 @@ export interface Namespace { } Contact: { - id: Generated; + id: Generated; name: string; email: string; phone: string | null; @@ -33,35 +32,35 @@ export interface Namespace { } Inventory: { - id: Generated; - productId: UUIDString; + id: Generated; + productId: string; quantity: number; createdAt: Generated; updatedAt: Generated; } Notification: { - id: Generated; + id: Generated; message: string; createdAt: Generated; updatedAt: Generated; } Order: { - id: Generated; + id: Generated; name: string; description: string | null; orderDate: Timestamp; orderType: "PURCHASE" | "SALES"; - contactId: UUIDString; + contactId: string; createdAt: Generated; updatedAt: Generated; } OrderItem: { - id: Generated; - orderId: UUIDString; - productId: UUIDString; + id: Generated; + orderId: string; + productId: string; quantity: number; unitPrice: number; totalPrice: Generated; @@ -70,16 +69,16 @@ export interface Namespace { } Product: { - id: Generated; + id: Generated; name: string; description: string | null; - categoryId: UUIDString; + categoryId: string; createdAt: Generated; updatedAt: Generated; } User: { - id: Generated; + id: Generated; name: string; email: string; role: "MANAGER" | "STAFF"; diff --git a/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts b/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts index c9383e3bf..0a39ee828 100644 --- a/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts +++ b/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts @@ -8,9 +8,7 @@ export const adminNote = db .type("AdminNote", { title: db.string(), content: db.string(), - authorId: db.uuid().hooks({ - create: ({ invoker }) => invoker?.id ?? crypto.randomUUID(), - }), + authorId: db.uuid().hooks({ create: ({ invoker }) => invoker?.id ?? crypto.randomUUID() }), ...db.fields.timestamps(), }) // NOTE: This permits all operations for simplicity. diff --git a/packages/create-sdk/templates/resolver/src/generated/db.ts b/packages/create-sdk/templates/resolver/src/generated/db.ts index 870db86c1..b892f0e12 100644 --- a/packages/create-sdk/templates/resolver/src/generated/db.ts +++ b/packages/create-sdk/templates/resolver/src/generated/db.ts @@ -1,7 +1,6 @@ import { createGetDB, type Generated, - type UUIDString, type Timestamp, type NamespaceDB, type NamespaceInsertable, @@ -15,7 +14,7 @@ import { export interface Namespace { "main-db": { User: { - id: Generated; + id: Generated; name: string; email: string; age: number; diff --git a/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts b/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts index 2db8085d4..991ae9e4f 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/showUserInfo.test.ts @@ -19,7 +19,7 @@ describe("showUserInfo resolver", () => { test("returns custom user info", async () => { const customCaller = { - id: "123e4567-e89b-12d3-a456-426614174000", + id: "user-123", type: "machine_user" as const, workspaceId: "ws-456", attributes: { role: "admin" }, @@ -32,7 +32,7 @@ describe("showUserInfo resolver", () => { env: { appName: "Resolver Template", version: 1 }, }); expect(result).toEqual({ - userId: "123e4567-e89b-12d3-a456-426614174000", + userId: "user-123", userType: "machine_user", workspaceId: "ws-456", }); diff --git a/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts b/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts index 3ce7d25d2..a09b1fbd8 100644 --- a/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts +++ b/packages/create-sdk/templates/resolver/src/resolver/upsertUsers.test.ts @@ -13,9 +13,7 @@ describe("upsertUsers resolver", () => { mock.setQueryResolver((query) => { switch (query.kind) { case "SelectQueryNode": - return query.parameters.includes("exists@example.com") - ? [{ id: "11111111-1111-4111-8111-111111111111" }] - : []; + return query.parameters.includes("exists@example.com") ? [{ id: "user-1" }] : []; case "InsertQueryNode": case "UpdateQueryNode": return { numAffectedRows: 1 }; diff --git a/packages/create-sdk/templates/tailordb/src/generated/db.ts b/packages/create-sdk/templates/tailordb/src/generated/db.ts index a8cef984d..46840cc83 100644 --- a/packages/create-sdk/templates/tailordb/src/generated/db.ts +++ b/packages/create-sdk/templates/tailordb/src/generated/db.ts @@ -1,7 +1,6 @@ import { createGetDB, type Generated, - type UUIDString, type Timestamp, type ObjectColumnType, type NamespaceDB, @@ -16,17 +15,17 @@ import { export interface Namespace { "main-db": { Category: { - id: Generated; + id: Generated; name: string; description: string | null; - parentCategoryId: UUIDString | null; + parentCategoryId: string | null; } Comment: { - id: Generated; + id: Generated; body: string; - taskId: UUIDString; - authorId: UUIDString; + taskId: string; + authorId: string; metadata: ObjectColumnType<{ source: string; editedAt?: Timestamp | null; @@ -37,21 +36,21 @@ export interface Namespace { } Task: { - id: Generated; + id: Generated; title: string; description: string | null; status: "TODO" | "IN_PROGRESS" | "DONE" | "CANCELLED"; priority: number; dueDate: Timestamp | null; - assigneeId: UUIDString | null; - categoryId: UUIDString | null; + assigneeId: string | null; + categoryId: string | null; isArchived: Generated; createdAt: Generated; updatedAt: Generated; } User: { - id: Generated; + id: Generated; name: string; email: string; role: "ADMIN" | "MEMBER" | "VIEWER"; diff --git a/packages/create-sdk/templates/workflow/src/generated/db.ts b/packages/create-sdk/templates/workflow/src/generated/db.ts index e660a08da..bf14fa403 100644 --- a/packages/create-sdk/templates/workflow/src/generated/db.ts +++ b/packages/create-sdk/templates/workflow/src/generated/db.ts @@ -1,7 +1,6 @@ import { createGetDB, type Generated, - type UUIDString, type Timestamp, type NamespaceDB, type NamespaceInsertable, @@ -15,7 +14,7 @@ import { export interface Namespace { "main-db": { Order: { - id: Generated; + id: Generated; customerName: string; amount: number; status: "PENDING" | "PROCESSING" | "COMPLETED" | "FAILED"; @@ -24,7 +23,7 @@ export interface Namespace { } User: { - id: Generated; + id: Generated; name: string; email: string; age: number; diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index c1a609f73..81862ce1e 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -96,7 +96,6 @@ const RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN = new RegExp( ); const V2_NEXT_1 = "2.0.0-next.1"; const V2_NEXT_2 = "2.0.0-next.2"; -const V2_NEXT_3 = "2.0.0-next.3"; /** All registered codemods, in registration order. */ export const allCodemods: CodemodPackage[] = [ @@ -713,61 +712,6 @@ export const allCodemods: CodemodPackage[] = [ "trigger result as the job output directly (no Promise wrapper to unwrap).", ].join("\n"), }, - { - id: "v2/strict-scalar-strings", - name: "Strict scalar string types for UUID/date/datetime/time/decimal fields", - description: - "Tailor field outputs infer strict string shapes instead of plain `string`: UUID fields are `UUIDString`, date fields `DateString`, datetime fields `DateTimeString | Date`, time fields `TimeString`, and decimal fields `DecimalString` (all exported from `@tailor-platform/sdk`). Generated Kysely types, migration DB helper types, auth `tailor.d.ts` attributes, and runtime principal / IdP user ids use the same aliases, and the Kysely `Timestamp` columns now select as `Date | DateTimeString` instead of `Date`. String values that already match a shape keep typechecking unchanged; plain `string` values must be narrowed with the new `is*String` / `parse*String` / `assert*String` helpers or have their source types tightened.", - since: "1.0.0", - until: "2.0.0", - prereleaseUntil: V2_NEXT_3, - // No scriptPath and no scoping patterns: the migration is type-driven, so - // the prompt is surfaced as project-wide guidance instead of per-file. - examples: [ - { - caption: - "Tighten the source type — or narrow at an untyped boundary with the scalar helpers — instead of passing plain `string` to a strict scalar API:", - before: - 'async function loadCustomer(customerId: string) {\n return getDB("tailordb")\n .selectFrom("Customer")\n .where("id", "=", customerId)\n .selectAll()\n .executeTakeFirstOrThrow();\n}', - after: - 'import { type UUIDString, parseUUIDString } from "@tailor-platform/sdk";\n\nasync function loadCustomer(customerId: UUIDString) {\n return getDB("tailordb")\n .selectFrom("Customer")\n .where("id", "=", customerId)\n .selectAll()\n .executeTakeFirstOrThrow();\n}\n\n// At an untyped boundary:\nawait loadCustomer(parseUUIDString(value, "customerId"));', - }, - { - caption: - "Kysely `Timestamp` columns now select as `Date | DateTimeString`, so guard `Date` methods:", - before: "return { createdAt: customer.createdAt.toISOString() };", - after: - "return {\n createdAt:\n customer.createdAt instanceof Date\n ? customer.createdAt.toISOString()\n : customer.createdAt,\n};", - }, - { - caption: - "Record and principal ids are `UUIDString`, so test fixtures need UUID-shaped literals:", - before: 'const payload = { newRecord: { id: "user-1" } };', - after: 'const payload = { newRecord: { id: "11111111-1111-4111-8111-111111111111" } };', - }, - ], - prompt: [ - "The v2 SDK types UUID, date, datetime, time, and decimal Tailor field values as", - "strict string shapes (UUIDString, DateString, DateTimeString, TimeString,", - "DecimalString from @tailor-platform/sdk) instead of plain string. Generated", - "Kysely types, auth attributes, and runtime principal / IdP user ids use the same", - "aliases, and Kysely Timestamp columns now select as Date | DateTimeString.", - "Regenerate the generated types (tailor generate), then typecheck the project and", - "fix the remaining errors:", - "- String literals that already match a shape typecheck unchanged.", - "- For string or unknown values, tighten the source type (e.g. declare the", - " parameter as UUIDString, use t.uuid() for resolver inputs that carry record", - " ids), or narrow with the isUUIDString / parseUUIDString / assertUUIDString", - " helper families (same variants exist for date, datetime, time, and decimal).", - "- Guard Date methods on selected timestamp columns:", - " value instanceof Date ? value.toISOString() : value.", - '- Give test fixtures UUID-shaped ids (e.g. "11111111-1111-4111-8111-111111111111").', - ' The mockIdp default user id changed from "mock-id" to', - ' "123e4567-e89b-12d3-a456-426614174000".', - "- Existing migration db.ts snapshots keep compiling; leave them unchanged. New", - " migrations are generated with the strict shapes automatically.", - ].join("\n"), - }, { id: "v2/cli-token-keyring-storage", name: "CLI tokens stored in the OS keyring", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index afeeed4b2..c04f240fa 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -665,103 +665,6 @@ trigger result as the job output directly (no Promise wrapper to unwrap). -## Strict scalar string types for UUID/date/datetime/time/decimal fields - -**Migration:** Manual - -Tailor field outputs infer strict string shapes instead of plain `string`: UUID fields are `UUIDString`, date fields `DateString`, datetime fields `DateTimeString | Date`, time fields `TimeString`, and decimal fields `DecimalString` (all exported from `@tailor-platform/sdk`). Generated Kysely types, migration DB helper types, auth `tailor.d.ts` attributes, and runtime principal / IdP user ids use the same aliases, and the Kysely `Timestamp` columns now select as `Date | DateTimeString` instead of `Date`. String values that already match a shape keep typechecking unchanged; plain `string` values must be narrowed with the new `is*String` / `parse*String` / `assert*String` helpers or have their source types tightened. - -Tighten the source type — or narrow at an untyped boundary with the scalar helpers — instead of passing plain `string` to a strict scalar API: - -Before: - -```ts -async function loadCustomer(customerId: string) { - return getDB("tailordb") - .selectFrom("Customer") - .where("id", "=", customerId) - .selectAll() - .executeTakeFirstOrThrow(); -} -``` - -After: - -```ts -import { type UUIDString, parseUUIDString } from "@tailor-platform/sdk"; - -async function loadCustomer(customerId: UUIDString) { - return getDB("tailordb") - .selectFrom("Customer") - .where("id", "=", customerId) - .selectAll() - .executeTakeFirstOrThrow(); -} - -// At an untyped boundary: -await loadCustomer(parseUUIDString(value, "customerId")); -``` - -Kysely `Timestamp` columns now select as `Date | DateTimeString`, so guard `Date` methods: - -Before: - -```ts -return { createdAt: customer.createdAt.toISOString() }; -``` - -After: - -```ts -return { - createdAt: - customer.createdAt instanceof Date - ? customer.createdAt.toISOString() - : customer.createdAt, -}; -``` - -Record and principal ids are `UUIDString`, so test fixtures need UUID-shaped literals: - -Before: - -```ts -const payload = { newRecord: { id: "user-1" } }; -``` - -After: - -```ts -const payload = { newRecord: { id: "11111111-1111-4111-8111-111111111111" } }; -``` - -
-Prompt for an AI agent (to perform this migration) - -```text -The v2 SDK types UUID, date, datetime, time, and decimal Tailor field values as -strict string shapes (UUIDString, DateString, DateTimeString, TimeString, -DecimalString from @tailor-platform/sdk) instead of plain string. Generated -Kysely types, auth attributes, and runtime principal / IdP user ids use the same -aliases, and Kysely Timestamp columns now select as Date | DateTimeString. -Regenerate the generated types (tailor generate), then typecheck the project and -fix the remaining errors: -- String literals that already match a shape typecheck unchanged. -- For string or unknown values, tighten the source type (e.g. declare the - parameter as UUIDString, use t.uuid() for resolver inputs that carry record - ids), or narrow with the isUUIDString / parseUUIDString / assertUUIDString - helper families (same variants exist for date, datetime, time, and decimal). -- Guard Date methods on selected timestamp columns: - value instanceof Date ? value.toISOString() : value. -- Give test fixtures UUID-shaped ids (e.g. "11111111-1111-4111-8111-111111111111"). - The mockIdp default user id changed from "mock-id" to - "123e4567-e89b-12d3-a456-426614174000". -- Existing migration db.ts snapshots keep compiling; leave them unchanged. New - migrations are generated with the strict shapes automatically. -``` - -
- ## tailor-sdk binary → tailor **Migration:** Partially automatic diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.test.ts index 2b0471d5c..e470df5b9 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.test.ts @@ -144,10 +144,10 @@ describe("db-types-generator", () => { externalId: { type: "uuid", required: true }, referenceId: { type: "uuid", required: false }, }, - expectedContains: ["externalId: UUIDString;", "referenceId: UUIDString | null;"], + expectedContains: ["externalId: string;", "referenceId: string | null;"], }, { - testName: "generates types with date/datetime fields using Timestamp", + testName: "generates types with date strings and datetime Timestamps", typeName: "Event", fields: { eventDate: { type: "date", required: true }, @@ -155,8 +155,8 @@ describe("db-types-generator", () => { endTime: { type: "datetime", required: false }, }, expectedContains: [ - "type Timestamp = ColumnType;", - "eventDate: DateString;", + "type Timestamp = ColumnType;", + "eventDate: string;", "startTime: Timestamp;", "endTime: Timestamp | null;", ], @@ -248,7 +248,7 @@ describe("db-types-generator", () => { const { content } = await generateContent(snapshot); - expect(content).toContain("id: Generated;"); + expect(content).toContain("id: Generated;"); expect(content).toContain( "type Generated = T extends ColumnType", ); @@ -332,10 +332,40 @@ describe("db-types-generator", () => { const content = fs.readFileSync(filePath, "utf-8"); expect(content).toContain( - "updatedAt: ColumnType;", + "updatedAt: ColumnType;", ); }); + test("generates ColumnType for optional to required date change", async () => { + const snapshot = createMockSnapshot({ + User: { + fields: { + birthDate: { type: "date", required: true }, + }, + }, + }); + createMigrationDir(testDir, 1); + + const diff = createMockMigrationDiff({ + changes: [ + { + kind: "field_modified", + typeName: "User", + fieldName: "birthDate", + before: { type: "date", required: false }, + after: { type: "date", required: true }, + }, + ], + hasBreakingChanges: true, + requiresMigrationScript: true, + }); + + const filePath = await writeDbTypesFile(snapshot, testDir, 1, diff); + const content = fs.readFileSync(filePath, "utf-8"); + + expect(content).toContain("birthDate: ColumnType;"); + }); + test("generates ColumnType for added required fields", async () => { const snapshot = createMockSnapshot({ User: { diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.ts b/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.ts index 945478d88..baa254fad 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/db-types-generator.ts @@ -15,14 +15,6 @@ import { } from "./snapshot"; import type { MigrationDiff } from "./diff-calculator"; -type UsedUtilityType = - | "Timestamp" - | "Serial" - | "DateString" - | "DateTimeString" - | "DecimalString" - | "TimeString"; - /** * Information about enum value changes */ @@ -140,37 +132,26 @@ function generateDbTypesFromSnapshot(snapshot: SchemaSnapshot, diff?: MigrationD }; // Track which utility types are used - const usedUtilityTypes = new Set(); + const usedUtilityTypes = new Set<"Timestamp" | "Serial">(); // Generate type definitions const typeDefinitions: string[] = []; for (const type of types) { const result = generateTableType(type, breakingChangeFields); - for (const utilityType of result.usedUtilityTypes) { - usedUtilityTypes.add(utilityType); - } + if (result.usedTimestamp) usedUtilityTypes.add("Timestamp"); typeDefinitions.push(result.typeDef); } // Build imports // ColumnType is always needed for Generated and Timestamp utility types - const imports: string[] = [ - "type ColumnType", - "type Transaction as KyselyTransaction", - "type UUIDString", - ]; - if (usedUtilityTypes.has("DateString")) imports.push("type DateString"); - if (usedUtilityTypes.has("DateTimeString")) imports.push("type DateTimeString"); - if (usedUtilityTypes.has("DecimalString")) imports.push("type DecimalString"); - if (usedUtilityTypes.has("TimeString")) imports.push("type TimeString"); + const imports: string[] = ["type ColumnType", "type Transaction as KyselyTransaction"]; // Build utility type declarations const utilityTypeDeclarations: string[] = []; if (usedUtilityTypes.has("Timestamp")) { utilityTypeDeclarations.push( - "type Timestamp = ColumnType;", + "type Timestamp = ColumnType;", ); - if (!usedUtilityTypes.has("DateTimeString")) imports.push("type DateTimeString"); } utilityTypeDeclarations.push( "type Generated = T extends ColumnType\n ? ColumnType\n : ColumnType;", @@ -243,22 +224,22 @@ function generateEmptyDbTypes(namespace: string): string { * Generate table type definition from a snapshot type * @param {TailorDBSnapshotType} type - Snapshot type * @param {BreakingChangeFieldInfo} breakingChangeFields - Breaking change field info - * @returns Generated type and utility type usage + * @returns {{ typeDef: string; usedTimestamp: boolean; usedColumnType: boolean }} Generated type and utility type usage */ function generateTableType( type: TailorDBSnapshotType, breakingChangeFields: BreakingChangeFieldInfo, ): { typeDef: string; - usedUtilityTypes: Set; + usedTimestamp: boolean; usedColumnType: boolean; } { const fieldLines: string[] = []; - const usedUtilityTypes = new Set(); + let usedTimestamp = false; let usedColumnType = false; // Add id field first - fieldLines.push(" id: Generated;"); + fieldLines.push(" id: Generated;"); // Get fields that are changing from optional to required for this type const optionalToRequiredFields = @@ -277,9 +258,7 @@ function generateTableType( const enumValueChange = enumValueChangesForType.get(fieldName); const result = generateFieldType(fieldConfig, isOptionalToRequired, enumValueChange); fieldLines.push(` ${fieldName}: ${result.type};`); - for (const utilityType of result.usedUtilityTypes) { - usedUtilityTypes.add(utilityType); - } + usedTimestamp = usedTimestamp || result.usedTimestamp; usedColumnType = usedColumnType || result.usedColumnType; } @@ -289,43 +268,37 @@ function generateTableType( // Treat as optional→required change (isOptionalToRequired: true) const result = generateFieldType(fieldConfig, true, undefined); fieldLines.push(` ${fieldName}: ${result.type};`); - for (const utilityType of result.usedUtilityTypes) { - usedUtilityTypes.add(utilityType); - } + usedTimestamp = usedTimestamp || result.usedTimestamp; usedColumnType = usedColumnType || result.usedColumnType; } const typeDef = ` ${type.name}: {\n${fieldLines.join("\n")}\n }`; - return { typeDef, usedUtilityTypes, usedColumnType }; + return { typeDef, usedTimestamp, usedColumnType }; } function mapToTsType(fieldType: string): { type: string; - usedUtilityTypes: Set; + usedTimestamp: boolean; } { switch (fieldType) { case "uuid": - return { type: "UUIDString", usedUtilityTypes: new Set() }; case "string": - return { type: "string", usedUtilityTypes: new Set() }; case "decimal": - return { type: "DecimalString", usedUtilityTypes: new Set(["DecimalString"]) }; + return { type: "string", usedTimestamp: false }; case "integer": case "float": case "number": - return { type: "number", usedUtilityTypes: new Set() }; + return { type: "number", usedTimestamp: false }; case "date": - return { type: "DateString", usedUtilityTypes: new Set(["DateString"]) }; + return { type: "string", usedTimestamp: false }; case "datetime": - return { type: "Timestamp", usedUtilityTypes: new Set(["Timestamp"]) }; - case "time": - return { type: "TimeString", usedUtilityTypes: new Set(["TimeString"]) }; + return { type: "Timestamp", usedTimestamp: true }; case "bool": case "boolean": - return { type: "boolean", usedUtilityTypes: new Set() }; + return { type: "boolean", usedTimestamp: false }; default: - return { type: "string", usedUtilityTypes: new Set() }; + return { type: "string", usedTimestamp: false }; } } @@ -353,36 +326,22 @@ function generateEnumChangeColumnType( return `ColumnType<${selectType}, ${afterType}, ${afterType}>`; } -function generateOptionalToRequiredDateColumnType( - config: SnapshotFieldConfig, -): { type: string; usedUtilityTypes: Set } | null { +function generateOptionalToRequiredDateColumnType(config: SnapshotFieldConfig): string | null { if (config.type !== "date" && config.type !== "datetime") return null; if (config.type === "date") { if (config.array) { - return { - type: "ColumnType", - usedUtilityTypes: new Set(["DateString"]), - }; + return "ColumnType"; } - return { - type: "ColumnType", - usedUtilityTypes: new Set(["DateString"]), - }; + return "ColumnType"; } if (config.array) { - return { - type: "ColumnType<(Date | DateTimeString)[] | null, (Date | DateTimeString)[], (Date | DateTimeString)[]>", - usedUtilityTypes: new Set(["DateTimeString"]), - }; + return "ColumnType"; } - return { - type: "ColumnType", - usedUtilityTypes: new Set(["DateTimeString"]), - }; + return "ColumnType"; } /** @@ -390,7 +349,7 @@ function generateOptionalToRequiredDateColumnType( * @param {SnapshotFieldConfig} config - Field configuration * @param {boolean} isOptionalToRequired - Whether this field is changing from optional to required * @param {EnumValueChange} [enumValueChange] - Enum value change info if applicable - * @returns Generated type string and utility type usage + * @returns {{ type: string; usedTimestamp: boolean; usedColumnType: boolean }} Generated type string and utility type usage */ function generateFieldType( config: SnapshotFieldConfig, @@ -398,21 +357,21 @@ function generateFieldType( enumValueChange?: EnumValueChange, ): { type: string; - usedUtilityTypes: Set; + usedTimestamp: boolean; usedColumnType: boolean; } { // Handle enum value changes specially if (enumValueChange) { return { type: generateEnumChangeColumnType(enumValueChange, config), - usedUtilityTypes: new Set(), + usedTimestamp: false, usedColumnType: true, }; } // Get base type let baseType: string; - let usedUtilityTypes = new Set(); + let usedTimestamp = false; if (config.type === "enum") { const enumValues = config.allowedValues?.map((v) => v.value) ?? []; @@ -420,7 +379,7 @@ function generateFieldType( } else { const mapped = mapToTsType(config.type); baseType = mapped.type; - usedUtilityTypes = mapped.usedUtilityTypes; + usedTimestamp = mapped.usedTimestamp; } // Apply array modifier @@ -436,8 +395,8 @@ function generateFieldType( const dateColumnType = generateOptionalToRequiredDateColumnType(config); if (dateColumnType) { return { - type: dateColumnType.type, - usedUtilityTypes: dateColumnType.usedUtilityTypes, + type: dateColumnType, + usedTimestamp: false, usedColumnType: true, }; } @@ -447,7 +406,7 @@ function generateFieldType( // INSERT/UPDATE requires T (must provide a value) return { type: `ColumnType<${type} | null, ${type}, ${type}>`, - usedUtilityTypes, + usedTimestamp, usedColumnType: true, }; } @@ -456,7 +415,7 @@ function generateFieldType( type = `${type} | null`; } - return { type, usedUtilityTypes, usedColumnType: false }; + return { type, usedTimestamp, usedColumnType: false }; } /** diff --git a/packages/sdk/src/cli/shared/runtime-exprs.test.ts b/packages/sdk/src/cli/shared/runtime-exprs.test.ts index 97a39eee6..c127955d7 100644 --- a/packages/sdk/src/cli/shared/runtime-exprs.test.ts +++ b/packages/sdk/src/cli/shared/runtime-exprs.test.ts @@ -69,14 +69,14 @@ describe("buildExecutorArgsExpr", () => { namespaceName: "app", actor: { userType: "USER_TYPE_USER", - userId: "11111111-1111-4111-8111-111111111111", + userId: "user-1", workspaceId: "workspace-1", attributeMap: { role: "admin" }, attributes: ["role"], }, }).actor, ).toEqual({ - id: "11111111-1111-4111-8111-111111111111", + id: "user-1", type: "user", workspaceId: "workspace-1", attributes: { role: "admin" }, @@ -89,7 +89,7 @@ describe("buildExecutorArgsExpr", () => { const actors = [ null, undefined, - { userType: "USER_TYPE_UNSPECIFIED", userId: "11111111-1111-4111-8111-111111111111" }, + { userType: "USER_TYPE_UNSPECIFIED", userId: "user-1" }, { userType: "USER_TYPE_USER", userId: nilUuid }, { userType: "USER_TYPE_USER" }, ]; @@ -210,7 +210,7 @@ describe("buildResolverOperationHookExpr", () => { const users = [ null, undefined, - { type: "USER_TYPE_UNSPECIFIED", id: "11111111-1111-4111-8111-111111111111" }, + { type: "USER_TYPE_UNSPECIFIED", id: "user-1" }, { type: "USER_TYPE_USER", id: nilUuid }, ]; for (const user of users) { @@ -223,14 +223,14 @@ describe("INVOKER_EXPR", () => { test("maps invoker payloads to TailorPrincipal shape", () => { expect( runInvokerExpr({ - id: "11111111-1111-4111-8111-111111111111", + id: "user-1", type: "user", workspaceId: "workspace-1", attributeMap: { role: "member" }, attributes: ["role"], }), ).toEqual({ - id: "11111111-1111-4111-8111-111111111111", + id: "user-1", type: "user", workspaceId: "workspace-1", attributes: { role: "member" }, diff --git a/packages/sdk/src/cli/shared/type-generator.test.ts b/packages/sdk/src/cli/shared/type-generator.test.ts index 0f12761b7..3c3ef6203 100644 --- a/packages/sdk/src/cli/shared/type-generator.test.ts +++ b/packages/sdk/src/cli/shared/type-generator.test.ts @@ -1,7 +1,6 @@ import * as path from "pathe"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { defineAuth } from "#/configure/services/auth/index"; -import { db } from "#/configure/services/tailordb/schema"; import { t } from "#/configure/types/type"; import { extractAttributesFromConfig, @@ -237,8 +236,7 @@ describe("extractAttributesFromConfig + generateTypeDefinition", () => { auth: defineAuth("auth", { machineUserAttributes: { role: t.enum(["ADMIN", "WORKER"]), - externalId: t.uuid(), - balance: t.decimal(), + roles: t.enum(["ADMIN", "WORKER"], { array: true }), isActive: t.bool(), tags: t.string({ array: true }), }, @@ -246,8 +244,7 @@ describe("extractAttributesFromConfig + generateTypeDefinition", () => { admin: { attributes: { role: "ADMIN", - externalId: "123e4567-e89b-12d3-a456-426614174000", - balance: "123.45", + roles: ["ADMIN"], isActive: true, tags: ["root"], }, @@ -260,47 +257,11 @@ describe("extractAttributesFromConfig + generateTypeDefinition", () => { const content = generateTypeDefinition(attributes, undefined); expect(content).toContain('role: "ADMIN" | "WORKER";'); - expect(content).toContain( - 'import type { DecimalString, UUIDString } from "@tailor-platform/sdk";', - ); - expect(content).toContain("externalId: UUIDString;"); - expect(content).toContain("balance: DecimalString;"); + expect(content).toContain('roles: ("ADMIN" | "WORKER")[];'); expect(content).toContain("isActive: boolean;"); expect(content).toContain("tags: string[];"); }); - test("preserves scalar types from userProfile attributes and attributeList", () => { - const userType = db.type("User", { - email: db.string().unique(), - externalId: db.uuid(), - balance: db.decimal(), - }); - const config = { - name: "test-app", - auth: defineAuth("auth", { - userProfile: { - type: userType, - usernameField: "email", - attributes: { - externalId: true, - balance: true, - }, - attributeList: ["externalId"] as ["externalId"], - }, - }), - }; - - const { attributes, attributeList } = extractAttributesFromConfig(config); - const content = generateTypeDefinition(attributes, attributeList); - - expect(content).toContain( - 'import type { DecimalString, UUIDString } from "@tailor-platform/sdk";', - ); - expect(content).toContain("externalId: UUIDString;"); - expect(content).toContain("balance: DecimalString;"); - expect(content).toContain("__tuple?: [UUIDString];"); - }); - test("extracts machine user names into MachineUserNameRegistry", () => { const config = { name: "test-app", diff --git a/packages/sdk/src/cli/shared/type-generator.ts b/packages/sdk/src/cli/shared/type-generator.ts index 907346bba..d675e6f87 100644 --- a/packages/sdk/src/cli/shared/type-generator.ts +++ b/packages/sdk/src/cli/shared/type-generator.ts @@ -10,41 +10,6 @@ export interface AttributesConfig { export type AttributeListConfig = readonly string[]; -type ScalarTypeName = - | "DateString" - | "DateTimeString" - | "DecimalString" - | "TimeString" - | "UUIDString"; - -const scalarTypeNames: ScalarTypeName[] = [ - "DateString", - "DateTimeString", - "DecimalString", - "TimeString", - "UUIDString", -]; - -const knownAttributeListTypes = new Set([ - "boolean", - "number", - "string", - ...scalarTypeNames, -]); - -const fieldTypeScriptTypes: Record = { - boolean: "boolean", - bool: "boolean", - uuid: "UUIDString", - date: "DateString", - datetime: "DateTimeString | Date", - time: "TimeString", - decimal: "DecimalString", - integer: "number", - float: "number", - number: "number", -}; - interface ExtractedAttributes { attributes?: AttributesConfig; attributeList?: AttributeListConfig; @@ -93,16 +58,6 @@ export function generateTypeDefinition( connectionNames?: readonly string[], aiGatewayNames?: readonly string[], ): string { - const attributeListTypes = attributeList?.map(normalizeAttributeListType); - const scalarTypeImports = collectScalarTypeImports([ - ...Object.values(attributes ?? {}), - ...(attributeListTypes ?? []), - ]); - const scalarTypeImportSource = - scalarTypeImports.length > 0 - ? `import type { ${scalarTypeImports.join(", ")} } from "@tailor-platform/sdk";\n\n` - : ""; - // Generate Attributes interface // attributes values are type string representations (e.g., "string", "boolean", "string[]") const attributeFields = attributes @@ -119,7 +74,7 @@ ${attributeFields} }`; // Generate AttributeList type as a tuple of strings based on the length - const listType = attributeListTypes ? `[${attributeListTypes.join(", ")}]` : "[]"; + const listType = attributeList ? `[${attributeList.map(() => "string").join(", ")}]` : "[]"; // Use interface with __tuple marker for declaration merging and tuple type support const listBody = `{ @@ -202,7 +157,7 @@ ${connectionNameFields} ${aiGatewayNameFields} }`; - return `${scalarTypeImportSource}${ml /* ts */ ` + return ml /* ts */ ` // This file is auto-generated by @tailor-platform/sdk // Do not edit this file manually // Regenerated automatically when running 'tailor deploy' or 'tailor generate' @@ -219,17 +174,7 @@ declare module "@tailor-platform/sdk" { export {}; -`}`; -} - -function collectScalarTypeImports(types: readonly string[]): ScalarTypeName[] { - return scalarTypeNames.filter((typeName) => - types.some((type) => new RegExp(`\\b${typeName}\\b`).test(type)), - ); -} - -function normalizeAttributeListType(typeOrKey: string): string { - return knownAttributeListTypes.has(typeOrKey) ? typeOrKey : "string"; +`; } function collectAttributesFromConfig(config: AppConfig): ExtractedAttributes { @@ -263,20 +208,23 @@ function collectAttributesFromConfig(config: AppConfig): ExtractedAttributes { const type = field?.type; const metadata = field?.metadata; - if (!type && !metadata) { + // Default to string if no metadata + if (!metadata) { return "string"; } - const typeStr = - type === "enum" && metadata?.allowedValues - ? metadata.allowedValues.map((v) => `"${v.value}"`).join(" | ") - : type - ? (fieldTypeScriptTypes[type] ?? "string") - : "string"; + let typeStr = "string"; + + if (type === "boolean") { + typeStr = "boolean"; + } else if (type === "enum" && metadata.allowedValues) { + // Generate union type from enum values + typeStr = metadata.allowedValues.map((v) => `"${v.value}"`).join(" | "); + } // Add array suffix if needed - if (metadata?.array) { - return typeStr.includes("|") ? `(${typeStr})[]` : `${typeStr}[]`; + if (metadata.array) { + typeStr = typeStr.includes(" | ") ? `(${typeStr})[]` : `${typeStr}[]`; } return typeStr; @@ -298,9 +246,7 @@ function collectAttributesFromConfig(config: AppConfig): ExtractedAttributes { const selectedAttributes = userProfile?.attributes; const fields = userProfile?.type?.fields; - const attributeList = userProfile?.attributeList?.map((key) => - inferAttributeType(fields?.[key]), - ); + const attributeList = userProfile?.attributeList; // Convert attributes to AttributesConfig by inferring types from field metadata const attributes: AttributesConfig | undefined = selectedAttributes diff --git a/packages/sdk/src/configure/index.ts b/packages/sdk/src/configure/index.ts index 8f1da15eb..1ed2e3525 100644 --- a/packages/sdk/src/configure/index.ts +++ b/packages/sdk/src/configure/index.ts @@ -16,31 +16,6 @@ export namespace t { } export { type TailorField } from "#/configure/types/type"; -export type { - DateString, - DateTimeString, - DecimalString, - TimeString, - TimeZoneOffsetString, - UUIDString, -} from "#/configure/types/scalar.types"; -export { - assertDateString, - assertDateTimeString, - assertDecimalString, - assertTimeString, - assertUUIDString, - isDateString, - isDateTimeString, - isDecimalString, - isTimeString, - isUUIDString, - parseDateString, - parseDateTimeString, - parseDecimalString, - parseTimeString, - parseUUIDString, -} from "#/configure/types/scalar"; export { type TailorPrincipal, type Attributes, diff --git a/packages/sdk/src/configure/services/auth/index.test.ts b/packages/sdk/src/configure/services/auth/index.test.ts index 9b7f10f6c..d04262be7 100644 --- a/packages/sdk/src/configure/services/auth/index.test.ts +++ b/packages/sdk/src/configure/services/auth/index.test.ts @@ -1,4 +1,5 @@ // oxlint-disable vitest/expect-expect -- Type-only assertions are checked by TypeScript. +import { randomUUID } from "node:crypto"; import { describe, expect, test, expectTypeOf } from "vitest"; import { t } from "#/configure/types/type"; import { db } from "../tailordb/schema"; @@ -35,9 +36,7 @@ const attributeMapConfig: Attributes = { }; const attributeListConfig: AttributeList = ["externalId"]; -const adminExternalId = "00000000-0000-4000-8000-000000000001"; -const workerExternalId = "00000000-0000-4000-8000-000000000002"; -const machineUserAttributeList: [typeof adminExternalId] = [adminExternalId]; +const machineUserAttributeList: [string] = [randomUUID()]; const basicUserProfile = { type: userType, usernameField: "email" } as const; describe("defineAuth", () => { @@ -55,7 +54,7 @@ describe("defineAuth", () => { role: "ADMIN", isActive: true, tags: ["root"], - externalId: adminExternalId, + externalId: "admin-external-id", }, attributeList: machineUserAttributeList, }, @@ -94,7 +93,7 @@ describe("defineAuth", () => { role: "ADMIN", isActive: true, tags: ["root"], - externalId: adminExternalId, + externalId: "admin-external-id", }, attributeList: machineUserAttributeList, }, @@ -103,7 +102,7 @@ describe("defineAuth", () => { role: "WORKER", isActive: false, tags: [], - externalId: workerExternalId, + externalId: "worker-external-id", }, }, }, diff --git a/packages/sdk/src/configure/services/executor/executor.test.ts b/packages/sdk/src/configure/services/executor/executor.test.ts index 25710ffd4..1dcc063f4 100644 --- a/packages/sdk/src/configure/services/executor/executor.test.ts +++ b/packages/sdk/src/configure/services/executor/executor.test.ts @@ -16,7 +16,6 @@ import { } from "./trigger/event"; import { scheduleTrigger } from "./trigger/schedule"; import { incomingWebhookTrigger } from "./trigger/webhook"; -import type { UUIDString } from "#/configure/types/scalar.types"; import type { TailorPrincipal } from "#/runtime/types"; import type { Operation } from "./operation"; @@ -282,7 +281,7 @@ describe("recordCreatedTrigger", () => { appNamespace: string; typeName: string; newRecord: { - id: UUIDString; + id: string; name: string; age: number; }; @@ -298,7 +297,7 @@ describe("recordCreatedTrigger", () => { appNamespace: string; typeName: string; newRecord: { - id: UUIDString; + id: string; name: string; age: number; }; @@ -344,12 +343,12 @@ describe("recordUpdatedTrigger", () => { appNamespace: string; typeName: string; newRecord: { - id: UUIDString; + id: string; name: string; age: number; }; oldRecord: { - id: UUIDString; + id: string; name: string; age: number; }; @@ -365,12 +364,12 @@ describe("recordUpdatedTrigger", () => { appNamespace: string; typeName: string; newRecord: { - id: UUIDString; + id: string; name: string; age: number; }; oldRecord: { - id: UUIDString; + id: string; name: string; age: number; }; @@ -416,7 +415,7 @@ describe("recordDeletedTrigger", () => { appNamespace: string; typeName: string; oldRecord: { - id: UUIDString; + id: string; name: string; age: number; }; @@ -432,7 +431,7 @@ describe("recordDeletedTrigger", () => { appNamespace: string; typeName: string; oldRecord: { - id: UUIDString; + id: string; name: string; age: number; }; @@ -642,7 +641,7 @@ describe("resolverExecutedTrigger", () => { const resolver = createResolver({ name: "test", operation: "query", - body: () => ({ userId: "123e4567-e89b-12d3-a456-426614174000" }), + body: () => ({ userId: "user-123" }), output: t.object({ userId: t.string(), }), @@ -780,19 +779,19 @@ describe("recordTrigger (multi-event)", () => { // Can narrow by kind if (args.event === "created") { expectTypeOf(args.newRecord).toExtend<{ - id: UUIDString; + id: string; name: string; age: number; }>(); } if (args.event === "updated") { expectTypeOf(args.newRecord).toExtend<{ - id: UUIDString; + id: string; name: string; age: number; }>(); expectTypeOf(args.oldRecord).toExtend<{ - id: UUIDString; + id: string; name: string; age: number; }>(); @@ -827,7 +826,7 @@ describe("recordTrigger (multi-event)", () => { body: (args) => { if (args.event === "deleted") { expectTypeOf(args.oldRecord).toExtend<{ - id: UUIDString; + id: string; name: string; age: number; }>(); @@ -862,7 +861,7 @@ describe("idpUserTrigger (multi-event)", () => { workspaceId: string; appNamespace: string; namespaceName: string; - userId: UUIDString; + userId: string; }>(); if (args.event === "created") { expectTypeOf(args.event).toEqualTypeOf<"created">(); @@ -891,7 +890,7 @@ describe("authAccessTokenTrigger (multi-event)", () => { workspaceId: string; appNamespace: string; namespaceName: string; - userId: UUIDString; + userId: string; }>(); if (args.event === "issued") { expectTypeOf(args.event).toEqualTypeOf<"issued">(); @@ -1010,7 +1009,7 @@ describe("gqlTarget", () => { } `, variables: () => ({ - id: "123e4567-e89b-12d3-a456-426614174000", + id: "test-id", }), }, }); @@ -1191,7 +1190,7 @@ describe("workflowTarget", () => { operation: { kind: "workflow", workflow: testWorkflow, - args: { orderId: "123e4567-e89b-12d3-a456-426614174000" }, + args: { orderId: "test-id" }, }, }); expect(executor.operation.kind).toBe("workflow"); @@ -1272,7 +1271,7 @@ describe("workflowTarget", () => { operation: { kind: "workflow", workflow: testWorkflow, - args: { orderId: "123e4567-e89b-12d3-a456-426614174000" }, + args: { orderId: "test-id" }, invoker: "admin", }, }); @@ -1308,7 +1307,7 @@ describe("workflowTarget", () => { workflow: testWorkflow, args: (args) => { expectTypeOf(args).not.toHaveProperty("invoker"); - return { orderId: "123e4567-e89b-12d3-a456-426614174000" }; + return { orderId: "test-id" }; }, }, }); diff --git a/packages/sdk/src/configure/services/executor/trigger/event.ts b/packages/sdk/src/configure/services/executor/trigger/event.ts index d3fabe194..8e5469281 100644 --- a/packages/sdk/src/configure/services/executor/trigger/event.ts +++ b/packages/sdk/src/configure/services/executor/trigger/event.ts @@ -1,7 +1,6 @@ import type { ResolverConfig } from "#/configure/services/resolver/resolver"; import type { TailorDBType } from "#/configure/services/tailordb/schema"; import type { IdpName } from "#/configure/types/idp-name"; -import type { UUIDString } from "#/configure/types/scalar.types"; import type { TailorEnv, TailorPrincipal } from "#/runtime/types"; import type { TailorDBTrigger as ParserTailorDBTrigger, @@ -77,21 +76,21 @@ export interface IdpUserCreatedArgs extends EventArgs { event: "created"; rawEvent: "idp.user.created"; namespaceName: string; - userId: UUIDString; + userId: string; } export interface IdpUserUpdatedArgs extends EventArgs { event: "updated"; rawEvent: "idp.user.updated"; namespaceName: string; - userId: UUIDString; + userId: string; } export interface IdpUserDeletedArgs extends EventArgs { event: "deleted"; rawEvent: "idp.user.deleted"; namespaceName: string; - userId: UUIDString; + userId: string; } export type IdpUserArgs = IdpUserCreatedArgs | IdpUserUpdatedArgs | IdpUserDeletedArgs; @@ -101,21 +100,21 @@ export interface AuthAccessTokenIssuedArgs extends EventArgs { event: "issued"; rawEvent: "auth.access_token.issued"; namespaceName: string; - userId: UUIDString; + userId: string; } export interface AuthAccessTokenRefreshedArgs extends EventArgs { event: "refreshed"; rawEvent: "auth.access_token.refreshed"; namespaceName: string; - userId: UUIDString; + userId: string; } export interface AuthAccessTokenRevokedArgs extends EventArgs { event: "revoked"; rawEvent: "auth.access_token.revoked"; namespaceName: string; - userId: UUIDString; + userId: string; } export type AuthAccessTokenArgs = diff --git a/packages/sdk/src/configure/services/resolver/resolver.test.ts b/packages/sdk/src/configure/services/resolver/resolver.test.ts index b33e6d51f..8d857e09d 100644 --- a/packages/sdk/src/configure/services/resolver/resolver.test.ts +++ b/packages/sdk/src/configure/services/resolver/resolver.test.ts @@ -2,12 +2,6 @@ import { describe, expectTypeOf, test, expect } from "vitest"; import { db } from "#/configure/services/tailordb/index"; import { t } from "#/configure/types/index"; import { createResolver } from "./resolver"; -import type { - DateString, - DateTimeString, - TimeString, - UUIDString, -} from "#/configure/types/scalar.types"; import type { TailorPrincipal } from "#/runtime/types"; import type { output } from "#/types/helpers"; import type { ResolverInput } from "#/types/resolver.generated"; @@ -176,12 +170,12 @@ describe("createResolver", () => { input: inputType, output: t.object({ success: t.bool() }), body: (context) => { - expectTypeOf(context.input.id).toEqualTypeOf(); + expectTypeOf(context.input.id).toBeString(); expectTypeOf(context.input.name).toBeString(); expectTypeOf(context.input.active).toBeBoolean(); expectTypeOf(context.input.count).toBeNumber(); expectTypeOf(context.input.score).toBeNumber(); - expectTypeOf(context.input.createdAt).toEqualTypeOf(); + expectTypeOf(context.input.createdAt).toExtend(); expectTypeOf(context.input.tags).toBeArray(); expectTypeOf(context.input.metadata.key).toBeString(); return { success: true }; @@ -256,7 +250,7 @@ describe("createResolver", () => { body: (context) => { expectTypeOf(context.caller).toEqualTypeOf(); if (!context.caller) return { userId: "anonymous" }; - expectTypeOf(context.caller.id).toEqualTypeOf(); + expectTypeOf(context.caller.id).toBeString(); expectTypeOf(context.caller.type).toEqualTypeOf<"user" | "machine_user">(); expectTypeOf(context.caller.workspaceId).toBeString(); return { userId: context.caller.id }; @@ -317,14 +311,14 @@ describe("createResolver", () => { input: inputType, output: t.object({ summary: t.string() }), body: (context) => { - expectTypeOf(context.input.uuid).toEqualTypeOf(); + expectTypeOf(context.input.uuid).toBeString(); expectTypeOf(context.input.string).toBeString(); expectTypeOf(context.input.bool).toBeBoolean(); expectTypeOf(context.input.int).toBeNumber(); expectTypeOf(context.input.float).toBeNumber(); - expectTypeOf(context.input.date).toEqualTypeOf(); - expectTypeOf(context.input.datetime).toEqualTypeOf(); - expectTypeOf(context.input.time).toEqualTypeOf(); + expectTypeOf(context.input.date).toExtend(); + expectTypeOf(context.input.datetime).toExtend(); + expectTypeOf(context.input.time).toBeString(); return { summary: "ok" }; }, }); diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 84a86e33b..5bf0b77c8 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -3,13 +3,6 @@ import { describe, expectTypeOf, expect, test } from "vitest"; import { t } from "#/configure/types/index"; import { db, type TailorAnyDBField } from "./schema"; import type { FieldValidateInput, ValidateConfig } from "#/configure/types/field.types"; -import type { - DateString, - DateTimeString, - DecimalString, - TimeString, - UUIDString, -} from "#/configure/types/scalar.types"; import type { TailorPrincipal } from "#/runtime/types"; import type { output, TypeLevelError } from "#/types/helpers"; import type { Hook } from "./types"; @@ -21,7 +14,7 @@ describe("TailorDBField basic field type tests", () => { name: db.string(), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; name: string; }>(); }); @@ -31,7 +24,7 @@ describe("TailorDBField basic field type tests", () => { age: db.int(), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; age: number; }>(); }); @@ -41,7 +34,7 @@ describe("TailorDBField basic field type tests", () => { active: db.bool(), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; active: boolean; }>(); }); @@ -51,111 +44,48 @@ describe("TailorDBField basic field type tests", () => { price: db.float(), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; price: number; }>(); }); - test("uuid field outputs UUID string type correctly", () => { + test("uuid field outputs string type correctly", () => { const _uuidType = db.type("Test", { uuid: db.uuid(), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; - uuid: UUIDString; + id: string; + uuid: string; }>(); }); - test("date field outputs date string type correctly", () => { + test("date field outputs string type correctly", () => { const _dateType = db.type("Test", { birthDate: db.date(), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; - birthDate: DateString; + id: string; + birthDate: string; }>(); }); - test("datetime field outputs datetime string | Date type correctly", () => { + test("datetime field outputs string | Date type correctly", () => { const _datetimeType = db.type("Test", { timestamp: db.datetime(), }); expectTypeOf>().toMatchObjectType<{ - id: UUIDString; - timestamp: DateTimeString | Date; + id: string; + timestamp: string | Date; }>(); }); - test("time field outputs time string type correctly", () => { + test("time field outputs string type correctly", () => { const _timeType = db.type("Test", { openingTime: db.time(), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; - openingTime: TimeString; - }>(); - }); - - test("pickFields preserves the generated id UUID type", () => { - const _schemaType = t.object({ - ...db - .type("Test", { - name: db.string(), - }) - .pickFields(["id"], { optional: true }), - }); - - expectTypeOf>().toEqualTypeOf<{ - id?: UUIDString | null; - }>(); - }); - - test("pickFields recomputes output from the base field type", () => { - const _schemaType = t.object({ - ...db - .type("Test", { - names: db.string({ array: true }), - nickname: db.string({ optional: true }), - }) - .pickFields(["id", "names", "nickname"], { array: false, optional: false }), - }); - - expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; - names: string; - nickname: string; - }>(); - }); - - test("pickFields recomputes enum and nested object output from the base field type", () => { - const _schemaType = t.object({ - ...db - .type("Test", { - status: db.enum(["active", "inactive"], { array: true }), - profile: db.object({ name: db.string() }, { array: true }), - }) - .pickFields(["status", "profile"], { array: false }), - }); - - expectTypeOf>().toEqualTypeOf<{ - status: "active" | "inactive"; - profile: { name: string }; - }>(); - }); - - test("pickFields preserves existing options that are not overridden", () => { - const _schemaType = t.object({ - ...db - .type("Test", { - names: db.string({ array: true }), - nickname: db.string({ optional: true }), - }) - .pickFields(["names", "nickname"], { array: true }), - }); - - expectTypeOf>().toEqualTypeOf<{ - names: string[]; - nickname?: string[] | null; + id: string; + openingTime: string; }>(); }); }); @@ -166,7 +96,7 @@ describe("TailorDBField optional option tests", () => { description: db.string({ optional: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; description?: string | null; }>(); }); @@ -178,7 +108,7 @@ describe("TailorDBField optional option tests", () => { count: db.int({ optional: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; title: string; description?: string | null; count?: number | null; @@ -192,7 +122,7 @@ describe("TailorDBField array option tests", () => { tags: db.string({ array: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; tags: string[]; }>(); }); @@ -202,7 +132,7 @@ describe("TailorDBField array option tests", () => { items: db.string({ optional: true, array: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; items?: string[] | null; }>(); }); @@ -214,7 +144,7 @@ describe("TailorDBField array option tests", () => { flags: db.bool({ array: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; tags: string[]; numbers: number[]; flags: boolean[]; @@ -269,7 +199,7 @@ describe("TailorDBField enum field tests", () => { priority: db.enum(["high", "medium", "low"], { optional: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; priority?: "high" | "medium" | "low" | null; }>(); }); @@ -290,7 +220,7 @@ describe("TailorDBField enum field tests", () => { categories: db.enum(["a", "b", "c"], { array: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; categories: ("a" | "b" | "c")[]; }>(); }); @@ -432,7 +362,7 @@ describe("TailorDBField modifier chain tests", () => { email: db.string().index(), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; email: string; }>(); }); @@ -442,7 +372,7 @@ describe("TailorDBField modifier chain tests", () => { username: db.string().unique(), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; username: string; }>(); }); @@ -547,9 +477,9 @@ describe("TailorDBField relation modifier tests", () => { }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; title: string; - authorId: UUIDString; + authorId: string; }>(); }); @@ -579,7 +509,7 @@ describe("TailorDBField hooks modifier tests", () => { }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; name: string; }>(); }); @@ -610,7 +540,7 @@ describe("TailorDBField validate modifier tests", () => { email: db.string().validate(() => true), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; email: string; }>(); }); @@ -620,7 +550,7 @@ describe("TailorDBField validate modifier tests", () => { email: db.string().validate([({ value }) => value.includes("@"), "Email must contain @"]), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; email: string; }>(); @@ -755,10 +685,10 @@ describe("TailorDBType withTimestamps option tests", () => { ...db.fields.timestamps(), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; name: string; - createdAt: DateTimeString | Date; - updatedAt: DateTimeString | Date; + createdAt: string | Date; + updatedAt: string | Date; }>(); }); @@ -839,7 +769,7 @@ describe("TailorDBType composite type tests", () => { closingTime: db.time(), }); expectTypeOf>().toMatchObjectType<{ - id: UUIDString; + id: string; name: string; email: string; age?: number | null; @@ -847,9 +777,9 @@ describe("TailorDBType composite type tests", () => { tags: string[]; role: "admin" | "user" | "guest"; score: number; - birthDate: DateString; - lastLogin?: DateTimeString | Date | null; - closingTime: TimeString; + birthDate: string; + lastLogin?: string | Date | null; + closingTime: string; }>(); }); }); @@ -860,7 +790,7 @@ describe("TailorDBType edge case tests", () => { value: db.string(), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; value: string; }>(); }); @@ -872,7 +802,7 @@ describe("TailorDBType edge case tests", () => { c: db.bool({ optional: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; a?: string | null; b?: number | null; c?: boolean | null; @@ -886,7 +816,7 @@ describe("TailorDBType edge case tests", () => { booleans: db.bool({ array: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; strings: string[]; numbers: number[]; booleans: boolean[]; @@ -912,7 +842,7 @@ describe("TailorDBType type consistency tests", () => { name: db.string(), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; name: string; }>(); }); @@ -1023,11 +953,11 @@ describe("TailorDBType plural form tests", () => { }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; title: string; content?: string | null; - createdAt: DateTimeString | Date; - updatedAt: DateTimeString | Date; + createdAt: string | Date; + updatedAt: string | Date; }>(); expect(_postType.name).toBe("Post"); @@ -1085,7 +1015,7 @@ describe("TailorDBType hooks modifier tests", () => { }, }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; name: string; }>(); }); @@ -1124,7 +1054,7 @@ describe("TailorDBType hooks modifier tests", () => { expectTypeOf().toEqualTypeOf< Hook< { - id: UUIDString; + id: string; readonly name: string; }, string @@ -1143,7 +1073,7 @@ describe("TailorDBType hooks modifier tests", () => { expectTypeOf().toEqualTypeOf< Hook< { - id: UUIDString; + id: string; name?: string | null; }, string | null @@ -1163,7 +1093,7 @@ describe("TailorDBType validate modifier tests", () => { }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; email: string; }>(); const fieldMetadata = _validateType.fields.email.metadata; @@ -1225,7 +1155,7 @@ describe("TailorDBType validate modifier tests", () => { test("validate modifier on string field receives string", () => { const _validate = db.type("Test", { name: db.string() }).validate; - expectTypeOf>().toExtend< + expectTypeOf>().toExtend< Parameters[0]["name"] >(); }); @@ -1234,9 +1164,9 @@ describe("TailorDBType validate modifier tests", () => { const _validate = db.type("Test", { name: db.string({ optional: true }), }).validate; - expectTypeOf< - ValidateConfig - >().toExtend[0]["name"]>(); + expectTypeOf>().toExtend< + Parameters[0]["name"] + >(); }); }); @@ -1249,7 +1179,7 @@ describe("db.object tests", () => { }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; user: { name: string; age: number; @@ -1276,7 +1206,7 @@ describe("db.object tests", () => { }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; user: { name: string; age?: number | null; @@ -1296,7 +1226,7 @@ describe("db.object tests", () => { ), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; user?: { name: string; avatar?: string | null; @@ -1315,7 +1245,7 @@ describe("db.object tests", () => { ), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; users: { name: string; age: number; @@ -1332,7 +1262,7 @@ describe("db.object tests", () => { }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; user: { name: string; tags: string[]; @@ -1353,7 +1283,7 @@ describe("db.object tests", () => { ), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; optionalUsers?: | { name: string; @@ -1372,7 +1302,7 @@ describe("db.object tests", () => { }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; settings: { enabled: boolean; push?: boolean | null; @@ -1390,7 +1320,7 @@ describe("db.object tests", () => { }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; product: { name: string; price: number; @@ -1496,7 +1426,7 @@ describe("TailorDBField fluent API type preservation", () => { .uuid() .description("User reference") .relation({ type: "n-1", toward: { type: User } }); - expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); }); }); @@ -1549,7 +1479,7 @@ describe("TailorDBType files method tests", () => { describe("TailorDBField runtime validation tests", () => { const invoker: TailorPrincipal = { - id: "123e4567-e89b-12d3-a456-426614174000", + id: "test", type: "user", workspaceId: "workspace-test", attributes: {}, @@ -1644,58 +1574,6 @@ describe("TailorDBField runtime validation tests", () => { expect(bad.issues?.[0]?.message).toBe( 'Expected to match "yyyy-MM-dd" format: received 2025/01/01', ); - - const calendarDateShape = field.parse({ value: "2025-02-30", data, invoker }); - expect(calendarDateShape.issues).toBeUndefined(); - if (calendarDateShape.issues) { - throw new Error("Unexpected issues"); - } - expect(calendarDateShape.value).toBe("2025-02-30"); - }); - - test("validates datetime format", () => { - const field = db.datetime(); - for (const value of [ - "2025-01-01T10:11:12Z", - "2025-01-01T10:11:12.123456Z", - "2025-01-01T10:11:12+09:00", - "2025-01-01t10:11:12-08:00", - "2025-02-30T10:11:12Z", - ]) { - const ok = field.parse({ value, data, invoker }); - expect(ok.issues).toBeUndefined(); - if (ok.issues) { - throw new Error("Unexpected issues"); - } - expect(ok.value).toBe(value); - } - - const bad = field.parse({ - value: "2025-01-01T10:11:12+0900", - data, - invoker, - }); - expect(bad.issues?.[0]?.message).toBe( - "Expected to match ISO format: received 2025-01-01T10:11:12+0900", - ); - - const invalidTime = field.parse({ - value: "2025-01-01T25:11:12Z", - data, - invoker, - }); - expect(invalidTime.issues?.[0]?.message).toBe( - "Expected to match ISO format: received 2025-01-01T25:11:12Z", - ); - - const invalidOffset = field.parse({ - value: "2025-01-01T10:11:12+24:00", - data, - invoker, - }); - expect(invalidOffset.issues?.[0]?.message).toBe( - "Expected to match ISO format: received 2025-01-01T10:11:12+24:00", - ); }); test("validates time format", () => { @@ -1988,6 +1866,23 @@ describe("TailorDBField clone tests", () => { expectTypeOf>().toEqualTypeOf(); }); + test("clone preserves enum value output type", () => { + const original = db.enum(["active", "inactive"], { array: true }); + const cloned = original.clone(); + + expect(cloned.metadata.array).toBe(true); + expectTypeOf>().toEqualTypeOf<("active" | "inactive")[]>(); + }); + + test("clone preserves existing optional output when overriding array", () => { + const original = db.object({ name: db.string() }, { optional: true }); + const cloned = original.clone({ array: true }); + + expect(cloned.metadata.required).toBe(false); + expect(cloned.metadata.array).toBe(true); + expectTypeOf>().toEqualTypeOf<{ name: string }[] | null>(); + }); + test("clone with both optional and array overrides", () => { const original = db.string(); const cloned = original.clone({ optional: true, array: true }); @@ -1997,6 +1892,18 @@ describe("TailorDBField clone tests", () => { expectTypeOf>().toEqualTypeOf(); }); + test("pickFields with options preserves field output base type", () => { + const User = db.type("User", { + role: db.enum(["admin", "member"]), + profile: db.object({ name: db.string() }, { optional: true }), + }); + + const picked = User.pickFields(["role", "profile"], { array: true }); + + expectTypeOf>().toEqualTypeOf<("admin" | "member")[]>(); + expectTypeOf>().toEqualTypeOf<{ name: string }[] | null>(); + }); + test("clones unique modifier correctly", () => { const original = db.string().unique(); const cloned = original.clone(); @@ -2110,83 +2017,26 @@ describe("TailorDBField clone tests", () => { expect(cloned.fields.name).not.toBe(original.fields.name); expect(cloned.fields.age).not.toBe(original.fields.age); }); - - test("clone recomputes output from the base field type", () => { - const clonedArray = db.string({ array: true }).clone({ array: true }); - const clonedScalar = db.string({ array: true }).clone({ array: false }); - const clonedRequired = db.string({ optional: true }).clone({ optional: false }); - const clonedUnchanged = db.string({ optional: true, array: true }).clone(); - const clonedOptionalArray = db.string({ optional: true }).clone({ array: true }); - const clonedRequiredArray = db - .string({ optional: true, array: true }) - .clone({ optional: false }); - const clonedEnumArray = db.enum(["active", "inactive"], { array: true }).clone(); - const clonedEnumScalar = db.enum(["active", "inactive"], { array: true }).clone({ - array: false, - }); - const clonedObjectArray = db.object({ name: db.string() }, { array: true }).clone(); - const clonedObjectScalar = db.object({ name: db.string() }, { array: true }).clone({ - array: false, - }); - - expectTypeOf>().not.toBeAny(); - expectTypeOf>().not.toBeAny(); - expectTypeOf>().not.toBeAny(); - expectTypeOf>().not.toBeAny(); - expectTypeOf>().not.toBeAny(); - expectTypeOf>().not.toBeAny(); - expectTypeOf>().not.toBeAny(); - expectTypeOf>().not.toBeAny(); - expectTypeOf>().not.toBeAny(); - expectTypeOf>().not.toBeAny(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf<("active" | "inactive")[]>(); - expectTypeOf>().toEqualTypeOf<"active" | "inactive">(); - expectTypeOf>().toEqualTypeOf<{ name: string }[]>(); - expectTypeOf>().toEqualTypeOf<{ name: string }>(); - - const _indexed = clonedScalar.index(); - }); - - test("clone preserves array guards for dynamic array overrides", () => { - const maybeArray = true as boolean; - const clonedExistingArray = db.string({ array: true }).clone({ array: maybeArray }); - const clonedExistingScalar = db.string().clone({ array: maybeArray }); - - expectTypeOf>().toEqualTypeOf(); - expectTypeOf(clonedExistingArray.index).toEqualTypeOf< - TypeLevelError<"index cannot be set on array fields"> - >(); - expectTypeOf(clonedExistingArray.unique).toEqualTypeOf< - TypeLevelError<"unique cannot be set on array fields"> - >(); - expectTypeOf>().toEqualTypeOf(); - }); }); describe("TailorDBField decimal type tests", () => { - test("decimal field outputs decimal string type correctly", () => { + test("decimal field outputs string type correctly", () => { const _decimalType = db.type("Test", { price: db.decimal(), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; - price: DecimalString; + id: string; + price: string; }>(); }); - test("optional decimal field outputs decimal string | null type correctly", () => { + test("optional decimal field outputs string | null type correctly", () => { const _decimalType = db.type("Test", { discount: db.decimal({ optional: true }), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; - discount?: DecimalString | null; + id: string; + discount?: string | null; }>(); }); @@ -2225,7 +2075,7 @@ describe("TailorDBField decimal type tests", () => { ])("decimal parse validates valid decimal string %s", (value) => { const field = db.decimal(); const invoker: TailorPrincipal = { - id: "123e4567-e89b-12d3-a456-426614174000", + id: "test", type: "user", workspaceId: "workspace-test", attributes: {}, @@ -2239,7 +2089,7 @@ describe("TailorDBField decimal type tests", () => { (value) => { const field = db.decimal(); const invoker: TailorPrincipal = { - id: "123e4567-e89b-12d3-a456-426614174000", + id: "test", type: "user", workspaceId: "workspace-test", attributes: {}, diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 246256ca5..a73f19239 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -30,7 +30,6 @@ import type { ValidateConfig, Validators, } from "#/configure/types/field.types"; -import type { UUIDString } from "#/configure/types/scalar.types"; import type { PluginAttachment, PluginConfigs } from "#/plugin/types"; import type { InferredAttributes } from "#/runtime/types"; import type { output, InferFieldsOutput, TypeLevelError } from "#/types/helpers"; @@ -100,34 +99,29 @@ type WithDBFieldCloneOptions< Defined extends DefinedDBFieldMetadata, NewOpt extends FieldOptions, > = Omit & { - array: DBFieldCloneArrayOption; -}; -type DBFieldCloneArrayOption< - Defined extends DefinedDBFieldMetadata, - NewOpt extends FieldOptions, -> = NewOpt extends { array: false } - ? false - : NewOpt extends { array: true } + array: NewOpt extends { array: true } ? true - : Defined["array"]; -type DBFieldCloneFieldOptions< - Defined extends DefinedDBFieldMetadata, - Output, - NewOpt extends FieldOptions, -> = { - optional: NewOpt extends { optional: infer NewOptional extends boolean } - ? NewOptional - : null extends Output - ? true - : false; - array: DBFieldCloneArrayOption; + : NewOpt extends { array: false } + ? false + : Defined["array"]; }; -type DBFieldCloneOutput< - Defined extends DefinedDBFieldMetadata, - Output, - OutputBase, - NewOpt extends FieldOptions, -> = FieldOutput>; +type NonNullableDBFieldOutput = Exclude; +type DBFieldScalarOutput = + NonNullableDBFieldOutput extends (infer Item)[] ? Item : NonNullableDBFieldOutput; +type DBFieldCloneArrayOutput = NewOpt extends { + array: true; +} + ? DBFieldScalarOutput[] + : NewOpt extends { array: false } + ? DBFieldScalarOutput + : NonNullableDBFieldOutput; +type DBFieldCloneOutput = NewOpt extends { optional: true } + ? DBFieldCloneArrayOutput | null + : NewOpt extends { optional: false } + ? DBFieldCloneArrayOutput + : null extends Output + ? DBFieldCloneArrayOutput | null + : DBFieldCloneArrayOutput; type FileKeyConflictError< Fields extends Record, User extends object, @@ -137,82 +131,77 @@ type FileKeyConflictError< TypeLevelError<"file keys cannot use existing field names"> > >; -type DBFieldDescriptionFn = ( +type DBFieldDescriptionFn = ( description: string, -) => TailorDBField, Output, OutputBase>; -type DBFieldRelationFn = { +) => TailorDBField, Output>; +type DBFieldRelationFn = { ( config: RelationConfig, - ): TailorDBField, Output, OutputBase>; - ( - config: S, - ): TailorDBField, Output, OutputBase>; + ): TailorDBField, Output>; + (config: S): TailorDBField, Output>; }; -type DBFieldIndexFn< - Defined extends DefinedDBFieldMetadata, - Output, - OutputBase, -> = () => TailorDBField, Output, OutputBase>; -type DBFieldUniqueFn< - Defined extends DefinedDBFieldMetadata, - Output, - OutputBase, -> = () => TailorDBField, Output, OutputBase>; -type DBFieldVectorFn< - Defined extends DefinedDBFieldMetadata, - Output, - OutputBase, -> = () => TailorDBField, Output, OutputBase>; -type DBFieldHooksFn = < +type DBFieldIndexFn = () => TailorDBField< + WithDBFieldIndex, + Output +>; +type DBFieldUniqueFn = () => TailorDBField< + WithDBFieldUnique, + Output +>; +type DBFieldVectorFn = () => TailorDBField< + WithDBFieldVector, + Output +>; +type DBFieldHooksFn = < const H extends Hook, >( hooks: H, -) => TailorDBField, Output, OutputBase>; -type DBFieldValidateFn = ( +) => TailorDBField, Output>; +type DBFieldValidateFn = ( ...validate: FieldValidateInput[] -) => TailorDBField, Output, OutputBase>; -type DBFieldSerialFn = ( +) => TailorDBField, Output>; +type DBFieldSerialFn = ( config: SerialConfig, -) => TailorDBField, Output, OutputBase>; -type DBFieldDescriptionMethod = +) => TailorDBField, Output>; +type DBFieldDescriptionMethod = IsAny extends true - ? DBFieldDescriptionFn + ? DBFieldDescriptionFn : Defined extends { description: unknown } ? TypeLevelError<".description() has already been set"> - : DBFieldDescriptionFn; -type DBFieldRelationMethod = + : DBFieldDescriptionFn; +type DBFieldRelationMethod = IsAny extends true - ? DBFieldRelationFn + ? DBFieldRelationFn : Defined extends { relation: unknown } ? TypeLevelError<".relation() has already been set"> - : DBFieldRelationFn; -type DBFieldIndexMethod = + : DBFieldRelationFn; +type DBFieldIndexMethod = IsAny extends true - ? DBFieldIndexFn + ? DBFieldIndexFn : Defined extends { index: unknown } ? TypeLevelError<".index() has already been set"> : Defined extends { array: true } ? TypeLevelError<"index cannot be set on array fields"> - : DBFieldIndexFn; -type DBFieldUniqueMethod = + : DBFieldIndexFn; +type DBFieldUniqueMethod = IsAny extends true - ? DBFieldUniqueFn + ? DBFieldUniqueFn : Defined extends { unique: unknown } ? TypeLevelError<".unique() has already been set"> : Defined extends { array: true } ? TypeLevelError<"unique cannot be set on array fields"> - : DBFieldUniqueFn; -type DBFieldVectorMethod = + : DBFieldUniqueFn; +type DBFieldVectorMethod = IsAny extends true - ? DBFieldVectorFn + ? DBFieldVectorFn : Defined extends { vector: unknown } ? TypeLevelError<".vector() has already been set"> : Defined extends { type: "string"; array: false } - ? DBFieldVectorFn + ? DBFieldVectorFn : TypeLevelError<"vector can only be set on non-array string fields">; -type DBFieldHooksMethod = +type DBFieldHooksMethod = IsAny extends true - ? DBFieldHooksFn + ? DBFieldHooksFn : Defined extends { serial: true; hooks: { create: false; update: false }; @@ -224,28 +213,28 @@ type DBFieldHooksMethod : Defined extends { type: "nested" } ? TypeLevelError<"hooks cannot be set on nested type fields"> - : DBFieldHooksFn; -type DBFieldValidateMethod = + : DBFieldHooksFn; +type DBFieldValidateMethod = IsAny extends true - ? DBFieldValidateFn + ? DBFieldValidateFn : Defined extends { validate: unknown } ? TypeLevelError<".validate() has already been set"> - : DBFieldValidateFn; -type DBFieldSerialMethod = + : DBFieldValidateFn; +type DBFieldSerialMethod = IsAny extends true - ? DBFieldSerialFn + ? DBFieldSerialFn : Defined extends { serial: true } ? TypeLevelError<".serial() has already been set"> : Defined extends { serial: false } ? TypeLevelError<"serial cannot be set after hooks"> : IsAny extends true ? Defined extends { type: "integer" | "string"; array: false } - ? DBFieldSerialFn + ? DBFieldSerialFn : TypeLevelError<"serial can only be set on non-array integer or string fields"> : null extends Output ? TypeLevelError<"serial can only be set on non-array integer or string fields"> : Defined extends { type: "integer" | "string"; array: false } - ? DBFieldSerialFn + ? DBFieldSerialFn : TypeLevelError<"serial can only be set on non-array integer or string fields">; /** @@ -256,7 +245,6 @@ export interface TailorDBField< Defined extends DefinedDBFieldMetadata = DefinedDBFieldMetadata, // oxlint-disable-next-line no-explicit-any Output = any, - OutputBase = Output, > extends Omit, "fields"> { readonly fields: Record; _metadata: DBFieldMetadata; @@ -281,53 +269,49 @@ export interface TailorDBField< /** * Set a description for the field */ - description: DBFieldDescriptionMethod; + description: DBFieldDescriptionMethod; /** * Define a relation to another type. */ - relation: DBFieldRelationMethod; + relation: DBFieldRelationMethod; /** * Add an index to the field */ - index: DBFieldIndexMethod; + index: DBFieldIndexMethod; /** * Make the field unique (also adds an index) */ - unique: DBFieldUniqueMethod; + unique: DBFieldUniqueMethod; /** * Enable vector search on the field (string type only) */ - vector: DBFieldVectorMethod; + vector: DBFieldVectorMethod; /** * Add hooks for create/update operations on this field. */ - hooks: DBFieldHooksMethod; + hooks: DBFieldHooksMethod; /** * Add validation functions to the field. */ - validate: DBFieldValidateMethod; + validate: DBFieldValidateMethod; /** * Configure serial/auto-increment behavior */ - serial: DBFieldSerialMethod; + serial: DBFieldSerialMethod; /** * Clone the field with optional overrides for field options */ - clone>( + clone( options?: NewOpt, - ): TailorDBField< - WithDBFieldCloneOptions, - DBFieldCloneOutput, - OutputBase - >; + ): TailorDBField, DBFieldCloneOutput>; } /** @@ -369,8 +353,8 @@ export interface TailorDBType< keys: K[], options: Opt, ): { - [P in K]: Fields[P] extends TailorDBField - ? TailorDBField, DBFieldCloneOutput, OBase> + [P in K]: Fields[P] extends TailorDBField + ? TailorDBField, DBFieldCloneOutput> : never; }; omitFields(keys: K[]): Omit; @@ -425,7 +409,7 @@ type TailorDBFieldInstance< T extends TailorFieldType, Opt extends FieldOptions, OutputBase = TailorToTs[T], -> = TailorDBField, DBFieldOutput, OutputBase>; +> = TailorDBField, DBFieldOutput>; type TailorDBFieldRuntimeInstance< T extends TailorFieldType, Opt extends FieldOptions, @@ -733,7 +717,7 @@ function date(options?: Opt) { /** * Create a datetime field (date and time). - * Format: ISO 8601 "yyyy-MM-ddTHH:mm:ssZ" or "yyyy-MM-ddTHH:mm:ss+09:00" + * Format: ISO 8601, such as "yyyy-MM-ddTHH:mm:ssZ" or "yyyy-MM-ddTHH:mm:ss+09:00" * @param options - Field configuration options * @returns A datetime field * @example db.datetime() @@ -766,8 +750,7 @@ function _enum( options?: Opt, ): TailorDBField< { type: "enum"; array: Opt extends { array: true } ? true : false }, - FieldOutput, Opt>, - AllowedValuesOutput + FieldOutput, Opt> > { return createField<"enum", Opt, AllowedValuesOutput>("enum", options, undefined, values); } @@ -786,8 +769,7 @@ function object< >(fields: F, options?: Opt) { return createField("nested", options, fields) as unknown as TailorDBField< { type: "nested"; array: Opt extends { array: true } ? true : false }, - FieldOutput, Opt>, - InferFieldsOutput + FieldOutput, Opt> >; } @@ -980,7 +962,7 @@ function createTailorDBType< return brandValue(dbType, "tailordb-type"); } -const idField = createField<"uuid", Record, UUIDString>("uuid"); +const idField = uuid(); type idField = typeof idField; type DBType> = TailorDBInstance< { id: idField } & F diff --git a/packages/sdk/src/configure/services/workflow/job.test.ts b/packages/sdk/src/configure/services/workflow/job.test.ts index 7f1aebccf..2ca82f851 100644 --- a/packages/sdk/src/configure/services/workflow/job.test.ts +++ b/packages/sdk/src/configure/services/workflow/job.test.ts @@ -111,7 +111,7 @@ describe("WorkflowJob type inference", () => { }); try { const invoker: TailorPrincipal = { - id: "11111111-1111-4111-8111-111111111111", + id: "principal-1", type: "user", workspaceId: "workspace-1", attributes: {}, @@ -127,9 +127,7 @@ describe("WorkflowJob type inference", () => { }); await withRegisteredJobRuntime(async () => { - await expect(parent.body(undefined, { env: {}, invoker })).resolves.toBe( - "11111111-1111-4111-8111-111111111111", - ); + await expect(parent.body(undefined, { env: {}, invoker })).resolves.toBe("principal-1"); }); } finally { if (descriptor) { @@ -142,7 +140,7 @@ describe("WorkflowJob type inference", () => { test("direct body calls propagate invoker to triggered child jobs", async () => { const invoker: TailorPrincipal = { - id: "11111111-1111-4111-8111-111111111111", + id: "principal-1", type: "user", workspaceId: "workspace-1", attributes: { role: "ADMIN" }, @@ -158,22 +156,20 @@ describe("WorkflowJob type inference", () => { }); await withRegisteredJobRuntime(async () => { - await expect(parent.body(undefined, { env: {}, invoker })).resolves.toBe( - "11111111-1111-4111-8111-111111111111", - ); + await expect(parent.body(undefined, { env: {}, invoker })).resolves.toBe("principal-1"); }); }); test("concurrent direct body calls isolate invokers for child triggers", async () => { const firstInvoker: TailorPrincipal = { - id: "11111111-1111-4111-8111-111111111111", + id: "principal-1", type: "user", workspaceId: "workspace-1", attributes: {}, attributeList: [], }; const secondInvoker: TailorPrincipal = { - id: "22222222-2222-4222-8222-222222222222", + id: "principal-2", type: "machine_user", workspaceId: "workspace-1", attributes: {}, @@ -208,9 +204,9 @@ describe("WorkflowJob type inference", () => { const second = parent.body({ gate: "second" }, { env: {}, invoker: secondInvoker }); releaseFirst(); - await expect(first).resolves.toBe("11111111-1111-4111-8111-111111111111"); + await expect(first).resolves.toBe("principal-1"); releaseSecond(); - await expect(second).resolves.toBe("22222222-2222-4222-8222-222222222222"); + await expect(second).resolves.toBe("principal-2"); }); }); diff --git a/packages/sdk/src/configure/services/workflow/test-env-key.ts b/packages/sdk/src/configure/services/workflow/test-env-key.ts index 5ce588702..4dbaa0982 100644 --- a/packages/sdk/src/configure/services/workflow/test-env-key.ts +++ b/packages/sdk/src/configure/services/workflow/test-env-key.ts @@ -59,7 +59,7 @@ export function withWorkflowTestInvoker(invoker: TailorPrincipal | null, run: } type RuntimeInvoker = { - id: TailorPrincipal["id"]; + id: string; type: "user" | "machine_user"; workspaceId: string; attributes?: string[] | TailorPrincipal["attributes"]; diff --git a/packages/sdk/src/configure/types/field-format.ts b/packages/sdk/src/configure/types/field-format.ts deleted file mode 100644 index 05b571cfc..000000000 --- a/packages/sdk/src/configure/types/field-format.ts +++ /dev/null @@ -1,28 +0,0 @@ -const regex = { - uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, - date: /^\d{4}-\d{2}-\d{2}$/, - datetime: - /^\d{4}-\d{2}-\d{2}[Tt](?[01]\d|2[0-3]):(?[0-5]\d):(?[0-5]\d|60)(\.(?\d+))?(?[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/, - time: /^(?[01]\d|2[0-3]):(?[0-5]\d)$/, - decimal: /^-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/, -} as const; - -export function isValidUUIDString(value: string): boolean { - return regex.uuid.test(value); -} - -export function isValidDateString(value: string): boolean { - return regex.date.test(value); -} - -export function isValidDateTimeString(value: string): boolean { - return regex.datetime.test(value); -} - -export function isValidTimeString(value: string): boolean { - return regex.time.test(value); -} - -export function isValidDecimalString(value: string): boolean { - return regex.decimal.test(value); -} diff --git a/packages/sdk/src/configure/types/field-runtime.ts b/packages/sdk/src/configure/types/field-runtime.ts index 000cc50cf..38055deb5 100644 --- a/packages/sdk/src/configure/types/field-runtime.ts +++ b/packages/sdk/src/configure/types/field-runtime.ts @@ -1,10 +1,3 @@ -import { - isValidDateString, - isValidDateTimeString, - isValidDecimalString, - isValidTimeString, - isValidUUIDString, -} from "#/configure/types/field-format"; import type { FieldMetadata, TailorFieldType } from "#/configure/types/field.types"; import type { TailorPrincipal } from "#/runtime/types"; import type { StandardSchemaV1 } from "@standard-schema/spec"; @@ -41,6 +34,15 @@ type FieldParseRuntimeArgs = FieldParseInternalArgs & field: FieldRuntime; }; +const regex = { + uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, + date: /^(?\d{4})-(?\d{2})-(?\d{2})$/, + time: /^(?[01]\d|2[0-3]):(?[0-5]\d)$/, + datetime: + /^(?\d{4})-(?\d{2})-(?\d{2})[Tt](?[01]\d|2[0-3]):(?[0-5]\d):(?[0-5]\d|60)(\.(?\d+))?(?[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/, + decimal: /^-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/, +} as const; + function validateValue( args: FieldValidateValueArgs, ): StandardSchemaV1.Issue[] { @@ -86,7 +88,7 @@ function validateValue( break; case "uuid": - if (typeof value !== "string" || !isValidUUIDString(value)) { + if (typeof value !== "string" || !regex.uuid.test(value)) { issues.push({ message: `Expected a valid UUID: received ${String(value)}`, path, @@ -94,7 +96,7 @@ function validateValue( } break; case "date": - if (typeof value !== "string" || !isValidDateString(value)) { + if (typeof value !== "string" || !regex.date.test(value)) { issues.push({ message: `Expected to match "yyyy-MM-dd" format: received ${String(value)}`, path, @@ -102,7 +104,7 @@ function validateValue( } break; case "datetime": - if (typeof value !== "string" || !isValidDateTimeString(value)) { + if (typeof value !== "string" || !regex.datetime.test(value)) { issues.push({ message: `Expected to match ISO format: received ${String(value)}`, path, @@ -110,7 +112,7 @@ function validateValue( } break; case "time": - if (typeof value !== "string" || !isValidTimeString(value)) { + if (typeof value !== "string" || !regex.time.test(value)) { issues.push({ message: `Expected to match "HH:mm" format: received ${String(value)}`, path, @@ -118,7 +120,7 @@ function validateValue( } break; case "decimal": - if (typeof value !== "string" || !isValidDecimalString(value)) { + if (typeof value !== "string" || !regex.decimal.test(value)) { issues.push({ message: `Expected a decimal string: received ${String(value)}`, path, diff --git a/packages/sdk/src/configure/types/field.types.ts b/packages/sdk/src/configure/types/field.types.ts index 5a90597ba..f7dae4a00 100644 --- a/packages/sdk/src/configure/types/field.types.ts +++ b/packages/sdk/src/configure/types/field.types.ts @@ -3,17 +3,6 @@ // This is a pure type module: type declarations only, no zod/schema // references, importable type-only from any layer. -import type { TailorPrincipal } from "#/runtime/types"; -import type { output, InferFieldsOutput } from "#/types/helpers"; -import type { - DateString, - DateTimeString, - DecimalString, - TimeString, - UUIDString, -} from "./scalar.types"; -import type { NonEmptyObject } from "type-fest"; - export interface EnumValue { value: string; description?: string; @@ -32,25 +21,16 @@ export type TailorFieldType = | "time" | "nested"; -export type { - DateString, - DateTimeString, - DecimalString, - TimeString, - TimeZoneOffsetString, - UUIDString, -} from "./scalar.types"; - export type TailorToTs = { string: string; integer: number; float: number; - decimal: DecimalString; + decimal: string; boolean: boolean; - uuid: UUIDString; - date: DateString; - datetime: DateTimeString | Date; - time: TimeString; + uuid: string; + date: string; + datetime: string | Date; + time: string; enum: string; object: Record; nested: Record; @@ -94,6 +74,10 @@ export type ArrayFieldOutput = [O] extends [ ? T[] : T; +import type { TailorPrincipal } from "#/runtime/types"; +import type { output, InferFieldsOutput } from "#/types/helpers"; +import type { NonEmptyObject } from "type-fest"; + /** * Validation function type */ diff --git a/packages/sdk/src/configure/types/index.ts b/packages/sdk/src/configure/types/index.ts index 398ec2c08..7521bddc3 100644 --- a/packages/sdk/src/configure/types/index.ts +++ b/packages/sdk/src/configure/types/index.ts @@ -1,12 +1,3 @@ export * from "./machine-user"; export * from "./type"; export * from "./field"; -export * from "./scalar"; -export type { - DateString, - DateTimeString, - DecimalString, - TimeString, - TimeZoneOffsetString, - UUIDString, -} from "./scalar.types"; diff --git a/packages/sdk/src/configure/types/scalar.test.ts b/packages/sdk/src/configure/types/scalar.test.ts deleted file mode 100644 index 46e61ae73..000000000 --- a/packages/sdk/src/configure/types/scalar.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -// oxlint-disable vitest/expect-expect -- Type-only assertions are checked by TypeScript. -import { describe, expect, expectTypeOf, test } from "vitest"; -import { - assertDateString, - assertDateTimeString, - assertDecimalString, - assertTimeString, - assertUUIDString, - isDateString, - isDateTimeString, - isDecimalString, - isTimeString, - isUUIDString, - parseDateString, - parseDateTimeString, - parseDecimalString, - parseTimeString, - parseUUIDString, -} from "../index"; -import type { DateString, DateTimeString, DecimalString, TimeString, UUIDString } from "../index"; - -describe("scalar string helpers", () => { - test("type guard helpers narrow unknown values", () => { - const uuid: unknown = "550e8400-e29b-41d4-a716-446655440000"; - const date: unknown = "2026-07-03"; - const datetime: unknown = "2026-07-03T12:34:56+09:00"; - const time: unknown = "12:34"; - const decimal: unknown = "123.45"; - - if (isUUIDString(uuid)) { - expectTypeOf(uuid).toEqualTypeOf(); - } - if (isDateString(date)) { - expectTypeOf(date).toEqualTypeOf(); - } - if (isDateTimeString(datetime)) { - expectTypeOf(datetime).toEqualTypeOf(); - } - if (isTimeString(time)) { - expectTypeOf(time).toEqualTypeOf(); - } - if (isDecimalString(decimal)) { - expectTypeOf(decimal).toEqualTypeOf(); - } - - expect(isUUIDString(uuid)).toBe(true); - expect(isDateString(date)).toBe(true); - expect(isDateTimeString(datetime)).toBe(true); - expect(isTimeString(time)).toBe(true); - expect(isDecimalString(decimal)).toBe(true); - }); - - test("type guard helpers reject invalid values", () => { - expect(isUUIDString("not-a-uuid")).toBe(false); - expect(isDateString("2026/07/03")).toBe(false); - expect(isDateTimeString("2026-07-03T12:34:56+0900")).toBe(false); - expect(isTimeString("12:34:56")).toBe(false); - expect(isTimeString("24:00")).toBe(false); - expect(isTimeString("23:60")).toBe(false); - expect(isTimeString("99:99")).toBe(false); - expect(isDecimalString("1_000")).toBe(false); - expect(isUUIDString(123)).toBe(false); - }); - - test("parse helpers return typed scalar strings", () => { - const uuid = parseUUIDString("550e8400-e29b-41d4-a716-446655440000"); - const date = parseDateString("2026-07-03"); - const datetime = parseDateTimeString("2026-07-03T12:34:56Z"); - const time = parseTimeString("12:34"); - const decimal = parseDecimalString("123.45"); - - expectTypeOf(uuid).toEqualTypeOf(); - expectTypeOf(date).toEqualTypeOf(); - expectTypeOf(datetime).toEqualTypeOf(); - expectTypeOf(time).toEqualTypeOf(); - expectTypeOf(decimal).toEqualTypeOf(); - - expect(uuid).toBe("550e8400-e29b-41d4-a716-446655440000"); - expect(date).toBe("2026-07-03"); - expect(datetime).toBe("2026-07-03T12:34:56Z"); - expect(time).toBe("12:34"); - expect(decimal).toBe("123.45"); - }); - - test("parse helpers throw TypeError with the given label", () => { - expect(() => parseUUIDString("not-a-uuid", "customerId")).toThrow( - new TypeError("customerId must be a UUID string"), - ); - expect(() => parseDateString("2026/07/03", "businessDate")).toThrow( - new TypeError("businessDate must be a date string"), - ); - expect(() => parseDateTimeString("2026-07-03T12:34:56+0900", "scheduledAt")).toThrow( - new TypeError("scheduledAt must be a datetime string"), - ); - expect(() => parseTimeString("12:34:56", "openingTime")).toThrow( - new TypeError("openingTime must be a time string"), - ); - expect(() => parseDecimalString("1_000", "amount")).toThrow( - new TypeError("amount must be a decimal string"), - ); - }); - - test("assert helpers narrow unknown values", () => { - const uuid: unknown = "550e8400-e29b-41d4-a716-446655440000"; - const date: unknown = "2026-07-03"; - const datetime: unknown = "2026-07-03T12:34:56Z"; - const time: unknown = "12:34"; - const decimal: unknown = "123.45"; - - assertUUIDString(uuid); - assertDateString(date); - assertDateTimeString(datetime); - assertTimeString(time); - assertDecimalString(decimal); - - expectTypeOf(uuid).toEqualTypeOf(); - expectTypeOf(date).toEqualTypeOf(); - expectTypeOf(datetime).toEqualTypeOf(); - expectTypeOf(time).toEqualTypeOf(); - expectTypeOf(decimal).toEqualTypeOf(); - }); -}); diff --git a/packages/sdk/src/configure/types/scalar.ts b/packages/sdk/src/configure/types/scalar.ts deleted file mode 100644 index 3eccb9ffd..000000000 --- a/packages/sdk/src/configure/types/scalar.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { - isValidDateString, - isValidDateTimeString, - isValidDecimalString, - isValidTimeString, - isValidUUIDString, -} from "./field-format"; -import type { - DateString, - DateTimeString, - DecimalString, - TimeString, - UUIDString, -} from "./scalar.types"; - -function scalarTypeError(label: string, expected: string): TypeError { - return new TypeError(`${label} must be a ${expected}`); -} - -type ScalarHelpers = { - isString: (value: unknown) => value is T; - parseString: (value: unknown, label?: string) => T; - assertString: (value: unknown, label?: string) => asserts value is T; -}; - -function makeScalarHelpers( - isValid: (value: string) => boolean, - expected: string, -): ScalarHelpers { - const isString = (value: unknown): value is T => typeof value === "string" && isValid(value); - const parseString = (value: unknown, label = "value"): T => { - if (isString(value)) return value; - throw scalarTypeError(label, expected); - }; - const assertString = (value: unknown, label?: string): asserts value is T => { - parseString(value, label); - }; - - return { isString, parseString, assertString }; -} - -const uuidString: ScalarHelpers = makeScalarHelpers( - isValidUUIDString, - "UUID string", -); -const dateString: ScalarHelpers = makeScalarHelpers( - isValidDateString, - "date string", -); -const dateTimeString: ScalarHelpers = makeScalarHelpers( - isValidDateTimeString, - "datetime string", -); -const timeString: ScalarHelpers = makeScalarHelpers( - isValidTimeString, - "time string", -); -const decimalString: ScalarHelpers = makeScalarHelpers( - isValidDecimalString, - "decimal string", -); - -/** - * Check whether a value is a UUID string accepted by Tailor fields. - * @param value - Value to check - * @returns True when the value is a UUID string - */ -export function isUUIDString(value: unknown): value is UUIDString { - return uuidString.isString(value); -} - -/** - * Check whether a value is a date string accepted by Tailor fields. - * @param value - Value to check - * @returns True when the value matches `yyyy-MM-dd` - */ -export function isDateString(value: unknown): value is DateString { - return dateString.isString(value); -} - -/** - * Check whether a value is a datetime string accepted by Tailor fields. - * @param value - Value to check - * @returns True when the value matches the supported ISO datetime format - */ -export function isDateTimeString(value: unknown): value is DateTimeString { - return dateTimeString.isString(value); -} - -/** - * Check whether a value is a time string accepted by Tailor fields. - * @param value - Value to check - * @returns True when the value matches `HH:mm` - */ -export function isTimeString(value: unknown): value is TimeString { - return timeString.isString(value); -} - -/** - * Check whether a value is a decimal string accepted by Tailor fields. - * @param value - Value to check - * @returns True when the value is a decimal string - */ -export function isDecimalString(value: unknown): value is DecimalString { - return decimalString.isString(value); -} - -/** - * Parse a value as a UUID string. - * @param value - Value to parse - * @param label - Name used in the error message - * @returns The original value typed as `UUIDString` - */ -export function parseUUIDString(value: unknown, label = "value"): UUIDString { - return uuidString.parseString(value, label); -} - -/** - * Parse a value as a date string. - * @param value - Value to parse - * @param label - Name used in the error message - * @returns The original value typed as `DateString` - */ -export function parseDateString(value: unknown, label = "value"): DateString { - return dateString.parseString(value, label); -} - -/** - * Parse a value as a datetime string. - * @param value - Value to parse - * @param label - Name used in the error message - * @returns The original value typed as `DateTimeString` - */ -export function parseDateTimeString(value: unknown, label = "value"): DateTimeString { - return dateTimeString.parseString(value, label); -} - -/** - * Parse a value as a time string. - * @param value - Value to parse - * @param label - Name used in the error message - * @returns The original value typed as `TimeString` - */ -export function parseTimeString(value: unknown, label = "value"): TimeString { - return timeString.parseString(value, label); -} - -/** - * Parse a value as a decimal string. - * @param value - Value to parse - * @param label - Name used in the error message - * @returns The original value typed as `DecimalString` - */ -export function parseDecimalString(value: unknown, label = "value"): DecimalString { - return decimalString.parseString(value, label); -} - -/** - * Assert that a value is a UUID string. - * @param value - Value to check - * @param label - Name used in the error message - */ -export function assertUUIDString(value: unknown, label?: string): asserts value is UUIDString { - uuidString.assertString(value, label); -} - -/** - * Assert that a value is a date string. - * @param value - Value to check - * @param label - Name used in the error message - */ -export function assertDateString(value: unknown, label?: string): asserts value is DateString { - dateString.assertString(value, label); -} - -/** - * Assert that a value is a datetime string. - * @param value - Value to check - * @param label - Name used in the error message - */ -export function assertDateTimeString( - value: unknown, - label?: string, -): asserts value is DateTimeString { - dateTimeString.assertString(value, label); -} - -/** - * Assert that a value is a time string. - * @param value - Value to check - * @param label - Name used in the error message - */ -export function assertTimeString(value: unknown, label?: string): asserts value is TimeString { - timeString.assertString(value, label); -} - -/** - * Assert that a value is a decimal string. - * @param value - Value to check - * @param label - Name used in the error message - */ -export function assertDecimalString( - value: unknown, - label?: string, -): asserts value is DecimalString { - decimalString.assertString(value, label); -} diff --git a/packages/sdk/src/configure/types/scalar.types.ts b/packages/sdk/src/configure/types/scalar.types.ts deleted file mode 100644 index 5b2653349..000000000 --- a/packages/sdk/src/configure/types/scalar.types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type DateString = `${number}-${number}-${number}`; -export type TimeString = `${number}:${number}`; -export type TimeZoneOffsetString = "Z" | "z" | `${"+" | "-"}${TimeString}`; -export type DateTimeString = - `${DateString}${"T" | "t"}${TimeString}:${number}${"" | `.${number}`}${TimeZoneOffsetString}`; -export type UUIDString = `${string}-${string}-${string}-${string}-${string}`; -export type DecimalString = `${number}`; diff --git a/packages/sdk/src/configure/types/type.test.ts b/packages/sdk/src/configure/types/type.test.ts index ef9ee64fd..95d0ddad6 100644 --- a/packages/sdk/src/configure/types/type.test.ts +++ b/packages/sdk/src/configure/types/type.test.ts @@ -4,10 +4,9 @@ import { t } from "./type"; import type { TailorPrincipal } from "#/runtime/types"; import type { output } from "#/types/helpers"; import type { AllowedValues } from "./field"; -import type { DateString, DateTimeString, TimeString, UUIDString } from "./scalar.types"; const invoker: TailorPrincipal = { - id: "123e4567-e89b-12d3-a456-426614174000", + id: "test", type: "user", workspaceId: "workspace-test", attributes: {}, @@ -60,39 +59,39 @@ describe("TailorType basic field type tests", () => { }>(); }); - test("uuid field outputs UUID string type correctly", () => { + test("uuid field outputs string type correctly", () => { const _uuidType = t.object({ id: t.uuid(), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; }>(); }); - test("date field outputs date string type correctly", () => { + test("date field outputs string type correctly", () => { const _dateType = t.object({ birthDate: t.date(), }); expectTypeOf>().toEqualTypeOf<{ - birthDate: DateString; + birthDate: string; }>(); }); - test("datetime field outputs datetime string | Date type correctly", () => { + test("datetime field outputs string | Date type correctly", () => { const _datetimeType = t.object({ createdAt: t.datetime(), }); expectTypeOf>().toEqualTypeOf<{ - createdAt: DateTimeString | Date; + createdAt: string | Date; }>(); }); - test("time field outputs time string type correctly", () => { + test("time field outputs string type correctly", () => { const _timeType = t.object({ openingTime: t.time(), }); expectTypeOf>().toEqualTypeOf<{ - openingTime: TimeString; + openingTime: string; }>(); }); }); @@ -270,7 +269,7 @@ describe("TailorType composite type tests", () => { role: t.enum(["admin", "user", "guest"]), }); expectTypeOf>().toEqualTypeOf<{ - id: UUIDString; + id: string; name: string; email: string; age?: number | null; @@ -446,7 +445,7 @@ describe("t.object tests", () => { }); expectTypeOf>().toEqualTypeOf<{ items: { - id: UUIDString; + id: string; name: string; }[]; }>(); @@ -465,7 +464,7 @@ describe("t.object tests", () => { expectTypeOf>().toEqualTypeOf<{ optionalItems?: | { - id: UUIDString; + id: string; value?: string | null; }[] | null; @@ -595,47 +594,28 @@ describe("TailorField runtime validation tests", () => { }, ])("validates $name format", ({ field, validValue, invalidValue, invalidMessage }) => { const ok = field.parse({ value: validValue, data, invoker }); - if (ok.issues) { - throw new Error("Unexpected issues"); - } - expect(ok.value).toBe(validValue); + expect(expectParsed(ok)).toBe(validValue); const bad = field.parse({ value: invalidValue, data, invoker }); expect(bad.issues).toBeDefined(); expect(bad.issues?.[0]?.message).toEqual(invalidMessage); }); - test("accepts a date with an out-of-range day (e.g. February 30)", () => { - const result = t.date().parse({ value: "2025-02-30", data, invoker }); - expect(expectParsed(result)).toBe("2025-02-30"); - }); - - test.each([ - "2025-12-21T10:11:12Z", - "2025-12-21T10:11:12.123456Z", - "2025-12-21T10:11:12+09:00", - "2025-12-21t10:11:12-08:00", - "2025-02-30T10:11:12Z", - ])("validates datetime format - accepts %s", (value) => { - const result = t.datetime().parse({ value, data, invoker }); - expect(expectParsed(result)).toBe(value); - }); - test.each([ { - value: "2025-12-21T10:11:12+0900", - message: "Expected to match ISO format: received 2025-12-21T10:11:12+0900", - }, - { - value: "2025-12-21T25:11:12Z", - message: "Expected to match ISO format: received 2025-12-21T25:11:12Z", + name: "out-of-range time", + field: t.time(), + value: "99:99", + message: 'Expected to match "HH:mm" format: received 99:99', }, { - value: "2025-12-21T10:11:12+24:00", - message: "Expected to match ISO format: received 2025-12-21T10:11:12+24:00", + name: "datetime with a non-dot fractional separator", + field: t.datetime(), + value: "2025-12-21T10:11:12x123Z", + message: "Expected to match ISO format: received 2025-12-21T10:11:12x123Z", }, - ])("validates datetime format - rejects $value", ({ value, message }) => { - const result = t.datetime().parse({ value, data, invoker }); + ])("rejects $name", ({ field, value, message }) => { + const result = field.parse({ value, data, invoker }); expect(result.issues).toBeDefined(); expect(result.issues?.[0]?.message).toEqual(message); }); diff --git a/packages/sdk/src/kysely/index.test-d.ts b/packages/sdk/src/kysely/index.test-d.ts index 4210155f1..4b6d0d89f 100644 --- a/packages/sdk/src/kysely/index.test-d.ts +++ b/packages/sdk/src/kysely/index.test-d.ts @@ -4,11 +4,8 @@ import type { ObjectColumnType, ArrayColumnType, Timestamp, - DateString, - DateTimeString, NamespaceInsertable, NamespaceSelectable, - UUIDString, } from "./index"; // Sanity check: verify typecheck catches errors @@ -23,17 +20,15 @@ describe("typecheck sanity", () => { type TestNamespace = { testNs: { Receipt: { - id: Generated; - // 1. plain date - receiptDate: DateString; - // 2. plain timestamp - createdAt: Timestamp; - // 3. timestamp inside object + id: Generated; + // 1. plain timestamp + receiptDate: Timestamp; + // 2. timestamp inside object dueSchedule: ObjectColumnType<{ dueDate: Timestamp; reminderAt?: Timestamp | null; }>; - // 4. timestamp inside object x array + // 3. timestamp inside object x array metadata: ArrayColumnType< ObjectColumnType<{ created: Timestamp; @@ -41,19 +36,18 @@ type TestNamespace = { version: number; }> >; - // 5. timestamp array + // 4. timestamp array eventDates: ArrayColumnType; }; }; }; describe("NamespaceInsertable", () => { - test("should accept strict date strings and datetimes on insert", () => { + test("should accept Date and string for nested datetime on insert", () => { type ReceiptInsertable = NamespaceInsertable; assertType({ - receiptDate: "2024-01-01", - createdAt: new Date(), + receiptDate: new Date(), dueSchedule: { dueDate: new Date(), }, @@ -63,49 +57,37 @@ describe("NamespaceInsertable", () => { assertType({ receiptDate: "2024-01-01", - createdAt: "2024-01-01T00:00:00Z", dueSchedule: { - dueDate: "2024-01-01T00:00:00Z", + dueDate: "2024-01-01", }, - metadata: [{ created: "2024-01-01T00:00:00Z", version: 1 }], - eventDates: ["2024-01-01T00:00:00Z"], + metadata: [{ created: "2024-01-01", version: 1 }], + eventDates: ["2024-01-01"], }); }); }); describe("NamespaceSelectable", () => { - test("should return strict date strings and datetimes", () => { + test("should return Date for both top-level and nested datetime", () => { type ReceiptSelectable = NamespaceSelectable; - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf< - Date | DateTimeString - >(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); // Nullable nested fields should be required in select - expectTypeOf().toEqualTypeOf< - Date | DateTimeString | null - >(); + expectTypeOf().toEqualTypeOf(); }); test("should return array of resolved objects for ObjectArrayColumnType", () => { type ReceiptSelectable = NamespaceSelectable; expectTypeOf().toEqualTypeOf< - { - created: Date | DateTimeString; - lastUpdated: Date | DateTimeString | null; - version: number; - }[] - >(); - expectTypeOf().toEqualTypeOf< - Date | DateTimeString + { created: Date; lastUpdated: Date | null; version: number }[] >(); + expectTypeOf().toEqualTypeOf(); }); - test("should return datetime arrays for timestamp array", () => { + test("should return Date[] for timestamp array", () => { type ReceiptSelectable = NamespaceSelectable; - expectTypeOf().toEqualTypeOf<(Date | DateTimeString)[]>(); + expectTypeOf().toEqualTypeOf(); }); }); diff --git a/packages/sdk/src/kysely/index.ts b/packages/sdk/src/kysely/index.ts index 10288b3d5..e063a33de 100644 --- a/packages/sdk/src/kysely/index.ts +++ b/packages/sdk/src/kysely/index.ts @@ -16,7 +16,6 @@ import { type Transaction as KyselyTransaction, type Updateable, } from "kysely"; -import type { DateTimeString } from "#/configure/types/scalar.types"; export { type ColumnType, @@ -31,20 +30,7 @@ export { export { TailordbDialect } from "@tailor-platform/function-kysely-tailordb"; -export type { - DateString, - DateTimeString, - DecimalString, - TimeString, - TimeZoneOffsetString, - UUIDString, -} from "#/configure/types/scalar.types"; - -export type Timestamp = ColumnType< - Date | DateTimeString, - Date | DateTimeString, - Date | DateTimeString ->; +export type Timestamp = ColumnType; type ResolveSelect = T extends ColumnType ? S : T; type ResolveInsert = T extends ColumnType ? I : T; type ResolveUpdate = T extends ColumnType ? U : T; diff --git a/packages/sdk/src/parser/service/auth/index.test.ts b/packages/sdk/src/parser/service/auth/index.test.ts index 907ebae9d..a882352ab 100644 --- a/packages/sdk/src/parser/service/auth/index.test.ts +++ b/packages/sdk/src/parser/service/auth/index.test.ts @@ -5,7 +5,6 @@ import { brandValue } from "#/utils/brand"; import { AuthConfigSchema, OAuth2ClientSchema } from "./schema"; import type { AuthServiceInput } from "#/configure/services/auth/types"; import type { TailorDBInstance } from "#/configure/services/tailordb/types"; -import type { UUIDString } from "#/configure/types/scalar.types"; import type { OptionalKeysOf } from "type-fest"; import type { z } from "zod"; @@ -96,11 +95,11 @@ describe("AuthServiceInput and AuthConfigSchema type alignment", () => { role: string; isActive: boolean; tags: string[]; - externalId: UUIDString; + externalId: string; }>(); expectTypeOf().toMatchObjectType<{ - attributeList: [UUIDString]; + attributeList: [string]; }>(); }); diff --git a/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts index 9210fe9c4..f9643c89b 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts @@ -78,13 +78,13 @@ describe("KyselyTypePlugin integration tests", () => { expect(result.name).toBe("User"); expect(result.typeDef).toContain("User: {"); - expect(result.typeDef).toContain("id: Generated;"); + expect(result.typeDef).toContain("id: Generated;"); expect(result.typeDef).toContain("name: string;"); expect(result.typeDef).toContain("email: string;"); expect(result.typeDef).toContain("age: number | null;"); expect(result.typeDef).toContain("isActive: boolean;"); expect(result.typeDef).toContain("score: number | null;"); - expect(result.typeDef).toContain("birthDate: DateString | null;"); + expect(result.typeDef).toContain("birthDate: string | null;"); expect(result.typeDef).toContain("lastLogin: Timestamp | null;"); expect(result.typeDef).toContain("tags: string[];"); expect(result.typeDef).toContain("createdAt: Generated;"); diff --git a/packages/sdk/src/plugin/builtin/kysely-type/index.ts b/packages/sdk/src/plugin/builtin/kysely-type/index.ts index c5da7764e..9d47c9a88 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/index.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/index.ts @@ -42,17 +42,8 @@ export function kyselyTypePlugin( (acc, type) => ({ Timestamp: acc.Timestamp || type.usedUtilityTypes.Timestamp, Serial: acc.Serial || type.usedUtilityTypes.Serial, - DateString: acc.DateString || type.usedUtilityTypes.DateString, - DecimalString: acc.DecimalString || type.usedUtilityTypes.DecimalString, - TimeString: acc.TimeString || type.usedUtilityTypes.TimeString, }), - { - Timestamp: false, - Serial: false, - DateString: false, - DecimalString: false, - TimeString: false, - }, + { Timestamp: false, Serial: false }, ); allNamespaceData.push({ diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts index 789401b97..a7286e774 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts @@ -62,11 +62,7 @@ describe("Kysely TypeProcessor", () => { endDate: db.datetime(), cancelledAt: db.datetime({ optional: true }), }), - expected: [ - "startDate: DateString;", - "endDate: Timestamp;", - "cancelledAt: Timestamp | null;", - ], + expected: ["startDate: string;", "endDate: Timestamp;", "cancelledAt: Timestamp | null;"], }, { name: "uuid types", @@ -74,7 +70,7 @@ describe("Kysely TypeProcessor", () => { userId: db.uuid(), deviceId: db.uuid({ optional: true }), }), - expected: ["userId: UUIDString;", "deviceId: UUIDString | null;"], + expected: ["userId: string;", "deviceId: string | null;"], }, ])("should handle $name", async ({ type, expected }) => { const typeDef = await getTypeDef(type); @@ -104,7 +100,7 @@ describe("Kysely TypeProcessor", () => { ); expect(typeDef).toContain("eventDates: ArrayColumnType;"); - expect(typeDef).toContain("optionalDates: DateString[] | null;"); + expect(typeDef).toContain("optionalDates: string[] | null;"); }); }); @@ -184,7 +180,7 @@ describe("Kysely TypeProcessor", () => { expect(typeDef).toContain("phone?: string | null"); }); - test("should use DateString for date fields inside nested objects", async () => { + test("should use Date | string instead of Timestamp for date fields inside nested objects", async () => { const type = db.type("Receipt", { receiptDate: db.date(), dueSchedule: db.object({ @@ -195,10 +191,10 @@ describe("Kysely TypeProcessor", () => { const result = await processKyselyType(parseTailorDBType(toSchemaOutput(type))); - expect(result.typeDef).toContain("receiptDate: DateString;"); + expect(result.typeDef).toContain("receiptDate: string;"); // Nested object with datetime is wrapped in ObjectColumnType expect(result.typeDef).toContain("ObjectColumnType<"); - expect(result.typeDef).toContain("dueDate: DateString"); + expect(result.typeDef).toContain("dueDate: string"); expect(result.typeDef).toContain("reminderAt?: Timestamp | null"); expect(result.usedUtilityTypes.Timestamp).toBe(true); }); @@ -290,14 +286,14 @@ describe("Kysely TypeProcessor", () => { expect(result.typeDef).toContain("updatedAt: Generated;"); }); - test("should always include Generated for id field", async () => { + test("should always include Generated for id field", async () => { const typeDef = await getTypeDef( db.type("User", { name: db.string(), }), ); - expect(typeDef).toContain("id: Generated;"); + expect(typeDef).toContain("id: Generated;"); }); test.each([ diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts index 56690938e..da3ec72ca 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts @@ -2,38 +2,7 @@ import multiline from "#/utils/multiline"; import { type KyselyNamespaceMetadata, type KyselyTypeMetadata } from "./types"; import type { OperatorFieldConfig, TailorDBType } from "#/parser/service/tailordb/types"; -type UsedUtilityTypes = { - Timestamp: boolean; - Serial: boolean; - DateString: boolean; - DecimalString: boolean; - TimeString: boolean; -}; - -function createUsedUtilityTypes(): UsedUtilityTypes { - return { - Timestamp: false, - Serial: false, - DateString: false, - DecimalString: false, - TimeString: false, - }; -} - -function mergeUsedUtilityTypes( - results: { usedUtilityTypes: UsedUtilityTypes }[], -): UsedUtilityTypes { - return results.reduce( - (acc, result) => ({ - Timestamp: acc.Timestamp || result.usedUtilityTypes.Timestamp, - Serial: acc.Serial || result.usedUtilityTypes.Serial, - DateString: acc.DateString || result.usedUtilityTypes.DateString, - DecimalString: acc.DecimalString || result.usedUtilityTypes.DecimalString, - TimeString: acc.TimeString || result.usedUtilityTypes.TimeString, - }), - createUsedUtilityTypes(), - ); -} +type UsedUtilityTypes = { Timestamp: boolean; Serial: boolean }; type FieldTypeResult = { type: string; @@ -69,7 +38,7 @@ function getNestedType(fieldConfig: OperatorFieldConfig): FieldTypeResult { if (!fields || typeof fields !== "object") { return { type: "string", - usedUtilityTypes: createUsedUtilityTypes(), + usedUtilityTypes: { Timestamp: false, Serial: false }, }; } @@ -82,7 +51,13 @@ function getNestedType(fieldConfig: OperatorFieldConfig): FieldTypeResult { }; }); - const aggregatedUtilityTypes = mergeUsedUtilityTypes(fieldResults); + const aggregatedUtilityTypes = fieldResults.reduce( + (acc, result) => ({ + Timestamp: acc.Timestamp || result.usedUtilityTypes.Timestamp, + Serial: acc.Serial || result.usedUtilityTypes.Serial, + }), + { Timestamp: false, Serial: false }, + ); const fieldTypes = fieldResults.map((r) => r.fieldType); const obj = `{\n ${fieldTypes.join(";\n ")}${fieldTypes.length > 0 ? ";" : ""}\n}`; @@ -101,36 +76,26 @@ function getNestedType(fieldConfig: OperatorFieldConfig): FieldTypeResult { */ function getBaseType(fieldConfig: OperatorFieldConfig): FieldTypeResult { const fieldType = fieldConfig.type; - const usedUtilityTypes = createUsedUtilityTypes(); + const usedUtilityTypes = { Timestamp: false, Serial: false }; let type: string; switch (fieldType) { case "uuid": - type = "UUIDString"; - break; case "string": - type = "string"; - break; case "decimal": - usedUtilityTypes.DecimalString = true; - type = "DecimalString"; + type = "string"; break; case "integer": case "float": type = "number"; break; + case "date": + type = "string"; + break; case "datetime": usedUtilityTypes.Timestamp = true; type = "Timestamp"; break; - case "date": - usedUtilityTypes.DateString = true; - type = "DateString"; - break; - case "time": - usedUtilityTypes.TimeString = true; - type = "TimeString"; - break; case "bool": case "boolean": type = "boolean"; @@ -210,11 +175,18 @@ function generateTableInterface(type: TailorDBType): { })); const fields = [ - "id: Generated;", + "id: Generated;", ...fieldResults.map((result) => `${result.fieldName}: ${result.type};`), ]; - const aggregatedUtilityTypes = mergeUsedUtilityTypes(fieldResults); + const aggregatedUtilityTypes = fieldResults.reduce( + (acc, result) => ({ + Timestamp: acc.Timestamp || result.usedUtilityTypes.Timestamp, + + Serial: acc.Serial || result.usedUtilityTypes.Serial, + }), + { Timestamp: false, Serial: false }, + ); const typeDef = multiline /* ts */ ` ${type.name}: { @@ -255,26 +227,14 @@ export function generateUnifiedKyselyTypes(namespaceData: KyselyNamespaceMetadat (acc, ns) => ({ Timestamp: acc.Timestamp || ns.usedUtilityTypes.Timestamp, Serial: acc.Serial || ns.usedUtilityTypes.Serial, - DateString: acc.DateString || ns.usedUtilityTypes.DateString, - DecimalString: acc.DecimalString || ns.usedUtilityTypes.DecimalString, - TimeString: acc.TimeString || ns.usedUtilityTypes.TimeString, }), - createUsedUtilityTypes(), + { Timestamp: false, Serial: false }, ); - const utilityTypeImports: string[] = ["type Generated", "type UUIDString"]; + const utilityTypeImports: string[] = ["type Generated"]; if (globalUsedUtilityTypes.Timestamp) { utilityTypeImports.push("type Timestamp"); } - if (globalUsedUtilityTypes.DateString) { - utilityTypeImports.push("type DateString"); - } - if (globalUsedUtilityTypes.DecimalString) { - utilityTypeImports.push("type DecimalString"); - } - if (globalUsedUtilityTypes.TimeString) { - utilityTypeImports.push("type TimeString"); - } const hasObjectColumnType = namespaceData.some((ns) => ns.types.some((t) => t.typeDef.includes("ObjectColumnType<")), ); diff --git a/packages/sdk/src/plugin/builtin/kysely-type/types.ts b/packages/sdk/src/plugin/builtin/kysely-type/types.ts index d238a2114..fd1cd3c0e 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/types.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/types.ts @@ -8,9 +8,6 @@ export interface KyselyTypeMetadata { usedUtilityTypes: { Timestamp: boolean; Serial: boolean; - DateString: boolean; - DecimalString: boolean; - TimeString: boolean; }; } @@ -20,8 +17,5 @@ export interface KyselyNamespaceMetadata { usedUtilityTypes: { Timestamp: boolean; Serial: boolean; - DateString: boolean; - DecimalString: boolean; - TimeString: boolean; }; } diff --git a/packages/sdk/src/runtime/context.test.ts b/packages/sdk/src/runtime/context.test.ts index 7153f2b46..3de72d57e 100644 --- a/packages/sdk/src/runtime/context.test.ts +++ b/packages/sdk/src/runtime/context.test.ts @@ -23,7 +23,7 @@ describe("@tailor-platform/sdk/runtime/context", () => { test("getInvoker exposes SDK shape (attributes map + attributeList array)", () => { using _invokerSpy = vi.spyOn(globalThis.tailor.context, "getInvoker").mockReturnValue({ - id: "11111111-1111-4111-8111-111111111111", + id: "u-1", type: "machine_user", workspaceId: "ws-1", attributes: ["role"], @@ -33,7 +33,7 @@ describe("@tailor-platform/sdk/runtime/context", () => { const invoker = context.getInvoker(); expect(invoker).toEqual({ - id: "11111111-1111-4111-8111-111111111111", + id: "u-1", type: "machine_user", workspaceId: "ws-1", attributes: { role: "MANAGER" }, diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index 805a851d5..203ae6863 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -12,7 +12,6 @@ * } */ -import type { UUIDString } from "#/configure/types/scalar.types"; import type { TailorPrincipal } from "#/runtime/types"; /** @@ -30,7 +29,7 @@ export type Invoker = TailorPrincipal; */ export interface ContextInvoker { /** The invoker's ID */ - id: UUIDString; + id: string; /** The invoker's type */ type: "user" | "machine_user"; /** The workspace ID */ diff --git a/packages/sdk/src/runtime/idp.test.ts b/packages/sdk/src/runtime/idp.test.ts index 3f68941eb..0c32ccdaa 100644 --- a/packages/sdk/src/runtime/idp.test.ts +++ b/packages/sdk/src/runtime/idp.test.ts @@ -19,32 +19,18 @@ describe("@tailor-platform/sdk/runtime/idp", () => { test("Client.user forwards args and namespace", async () => { using idpM = mockIdp(); - idpM.enqueueResult({ - id: "11111111-1111-4111-8111-111111111111", - name: "alice", - disabled: false, - }); + idpM.enqueueResult({ id: "u-1", name: "alice", disabled: false }); const client = new idp.Client({ namespace: "ns" }); - const result = await client.user("11111111-1111-4111-8111-111111111111"); + const result = await client.user("u-1"); - expect(result).toEqual({ - id: "11111111-1111-4111-8111-111111111111", - name: "alice", - disabled: false, - }); - expect(idpM.calls).toEqual([ - { method: "user", args: ["11111111-1111-4111-8111-111111111111"], namespace: "ns" }, - ]); + expect(result).toEqual({ id: "u-1", name: "alice", disabled: false }); + expect(idpM.calls).toEqual([{ method: "user", args: ["u-1"], namespace: "ns" }]); }); test("Client.userByName forwards", async () => { using idpM = mockIdp(); - idpM.enqueueResult({ - id: "11111111-1111-4111-8111-111111111111", - name: "alice", - disabled: false, - }); + idpM.enqueueResult({ id: "u-1", name: "alice", disabled: false }); const client = new idp.Client({ namespace: "ns" }); await client.userByName("alice"); @@ -55,7 +41,7 @@ describe("@tailor-platform/sdk/runtime/idp", () => { test("Client.users forwards options", async () => { using idpM = mockIdp(); idpM.enqueueResult({ - users: [{ id: "11111111-1111-4111-8111-111111111111", name: "alice", disabled: false }], + users: [{ id: "u-1", name: "alice", disabled: false }], nextPageToken: null, totalCount: 1, }); @@ -70,15 +56,15 @@ describe("@tailor-platform/sdk/runtime/idp", () => { test("Client.createUser / updateUser / deleteUser forward", async () => { using idpM = mockIdp(); idpM.enqueueResults( - { id: "22222222-2222-4222-8222-222222222222", name: "bob", disabled: false }, - { id: "22222222-2222-4222-8222-222222222222", name: "bob2", disabled: false }, + { 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: "22222222-2222-4222-8222-222222222222", name: "bob2" }); - const removed = await client.deleteUser("22222222-2222-4222-8222-222222222222"); + await client.updateUser({ id: "u-2", name: "bob2" }); + const removed = await client.deleteUser("u-2"); expect(removed).toBe(true); expect(idpM.calls.map((c) => c.method)).toEqual(["createUser", "updateUser", "deleteUser"]); @@ -87,10 +73,7 @@ describe("@tailor-platform/sdk/runtime/idp", () => { test("Client.sendPasswordResetEmail forwards", async () => { using idpM = mockIdp(); const client = new idp.Client({ namespace: "ns" }); - const args = { - userId: "11111111-1111-4111-8111-111111111111", - redirectUri: "https://example.com/reset", - } as const; + const args = { userId: "u-1", redirectUri: "https://example.com/reset" }; const ok = await client.sendPasswordResetEmail(args); expect(ok).toBe(true); @@ -102,10 +85,7 @@ describe("@tailor-platform/sdk/runtime/idp", () => { test("Client.unenrollMfa forwards", async () => { using idpM = mockIdp(); const client = new idp.Client({ namespace: "ns" }); - const args = { - userId: "11111111-1111-4111-8111-111111111111", - mfaFactorId: "f-1", - } as const; + const args = { userId: "u-1", mfaFactorId: "f-1" }; const ok = await client.unenrollMfa(args); expect(ok).toBe(true); diff --git a/packages/sdk/src/runtime/idp.ts b/packages/sdk/src/runtime/idp.ts index c6e64c41e..c707de793 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 type { UUIDString } from "#/configure/types/scalar.types"; - /** Configuration object for {@link Client}. */ export interface ClientConfig { namespace: string; @@ -20,7 +18,7 @@ export interface ClientConfig { /** User record returned by IDP operations. */ export interface User { - id: UUIDString; + id: string; name: string; disabled: boolean; createdAt?: string; @@ -39,7 +37,7 @@ export interface User { /** Filter options for {@link Client.users}. */ export interface UserQuery { /** Filter by user IDs */ - ids?: UUIDString[]; + ids?: string[]; /** Filter by user names */ names?: string[]; } @@ -74,7 +72,7 @@ export interface CreateUserInput { /** Input for {@link Client.updateUser}. */ export interface UpdateUserInput { /** The user's ID */ - id: UUIDString; + id: string; /** New name for the user */ name?: string; /** New password for the user. Cannot be used with clearPassword. */ @@ -88,7 +86,7 @@ export interface UpdateUserInput { /** Input for {@link Client.sendPasswordResetEmail}. */ export interface SendPasswordResetEmailInput { /** The ID of the user */ - userId: UUIDString; + userId: string; /** The URI to redirect to after password reset */ redirectUri: string; /** The sender display name. Defaults to 'Tailor Platform IdP'. */ @@ -100,7 +98,7 @@ export interface SendPasswordResetEmailInput { /** Input for {@link Client.unenrollMfa}. */ export interface UnenrollMfaInput { /** The ID of the user whose factor will be unenrolled. */ - userId: UUIDString; + userId: string; /** * The ID of the factor to unenroll. Factor IDs are exposed on the user * record (see {@link User.mfaFactorIds}). @@ -111,11 +109,11 @@ export interface UnenrollMfaInput { /** Instance methods exposed by `tailor.idp.Client`. */ export interface IdpClientInstance { users(options?: ListUsersOptions): Promise; - user(userId: UUIDString): Promise; + user(userId: string): Promise; userByName(name: string): Promise; createUser(input: CreateUserInput): Promise; updateUser(input: UpdateUserInput): Promise; - deleteUser(userId: UUIDString): Promise; + deleteUser(userId: string): Promise; sendPasswordResetEmail(input: SendPasswordResetEmailInput): Promise; unenrollMfa(input: UnenrollMfaInput): Promise; } @@ -163,7 +161,7 @@ export class Client { * @param userId - IDP user ID * @returns The matching user */ - user(userId: UUIDString): Promise { + user(userId: string): Promise { return this.#impl.user(userId); } @@ -199,7 +197,7 @@ export class Client { * @param userId - IDP user ID * @returns `true` when the user was deleted */ - deleteUser(userId: UUIDString): Promise { + deleteUser(userId: string): Promise { return this.#impl.deleteUser(userId); } diff --git a/packages/sdk/src/runtime/types.ts b/packages/sdk/src/runtime/types.ts index 9e54d1798..8aa961e2e 100644 --- a/packages/sdk/src/runtime/types.ts +++ b/packages/sdk/src/runtime/types.ts @@ -4,8 +4,6 @@ // not reference zod or schema modules, so every layer can import it type-only // without pulling any runtime dependency. -import type { UUIDString } from "#/configure/types/scalar.types"; - // Interfaces for module augmentation // Users can extend these via: declare module "@tailor-platform/sdk" { interface Attributes { ... } } // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -25,7 +23,7 @@ export type InferredAttributeList = AttributeList["__tuple"] extends [] /** Represents a user or machine user principal in the Tailor Platform. */ export type TailorPrincipal = { /** The ID of the principal. */ - id: UUIDString; + id: string; /** The type of the principal. */ type: "user" | "machine_user"; /** The ID of the workspace the principal belongs to. */ diff --git a/packages/sdk/src/vitest/mock.test.ts b/packages/sdk/src/vitest/mock.test.ts index 837581d69..ccb13c8c8 100644 --- a/packages/sdk/src/vitest/mock.test.ts +++ b/packages/sdk/src/vitest/mock.test.ts @@ -583,28 +583,19 @@ describe("mock", () => { test("records calls with method, args, namespace", async () => { using idp = mockIdp(); const client = new (globalThis as any).tailor.idp.Client({ namespace: "ns" }); - await client.user("11111111-1111-4111-8111-111111111111"); - expect(idp.calls).toEqual([ - { method: "user", args: ["11111111-1111-4111-8111-111111111111"], namespace: "ns" }, - ]); + await client.user("u-1"); + expect(idp.calls).toEqual([{ method: "user", args: ["u-1"], namespace: "ns" }]); }); test("enqueueResults provides ordered responses", async () => { using idp = mockIdp(); - idp.enqueueResults( - { id: "11111111-1111-4111-8111-111111111111", name: "alice", disabled: false }, - true, - ); + idp.enqueueResults({ id: "u-1", name: "alice", disabled: false }, true); const client = new (globalThis as any).tailor.idp.Client({ namespace: "ns" }); - const user = await client.user("11111111-1111-4111-8111-111111111111"); - expect(user).toEqual({ - id: "11111111-1111-4111-8111-111111111111", - name: "alice", - disabled: false, - }); + const user = await client.user("u-1"); + expect(user).toEqual({ id: "u-1", name: "alice", disabled: false }); - const deleted = await client.deleteUser("11111111-1111-4111-8111-111111111111"); + const deleted = await client.deleteUser("u-1"); expect(deleted).toBe(true); }); @@ -613,7 +604,7 @@ describe("mock", () => { idp.setResolver((method) => { if (method === "users") return { - users: [{ id: "11111111-1111-4111-8111-111111111111", name: "bob", disabled: false }], + users: [{ id: "u-1", name: "bob", disabled: false }], nextPageToken: null, totalCount: 1, }; @@ -628,7 +619,7 @@ describe("mock", () => { test("reset clears state", async () => { using idp = mockIdp(); const client = new (globalThis as any).tailor.idp.Client({ namespace: "ns" }); - await client.user("11111111-1111-4111-8111-111111111111"); + await client.user("u-1"); idp.reset(); expect(idp.calls).toHaveLength(0); }); diff --git a/packages/sdk/src/vitest/mock.ts b/packages/sdk/src/vitest/mock.ts index cbd369bae..c73f24467 100644 --- a/packages/sdk/src/vitest/mock.ts +++ b/packages/sdk/src/vitest/mock.ts @@ -40,7 +40,6 @@ import type { AIGatewayName, AuthConnectionTokenResult, ConnectionName, - UUIDString, } from "@tailor-platform/sdk"; export { RUNTIME_FLAG_KEY } from "./globals"; @@ -710,29 +709,23 @@ export function mockAuthconnection() { const IDP_DEFAULTS: Record = { users: { users: [], nextPageToken: null, totalCount: 0 }, - user: { - id: "123e4567-e89b-12d3-a456-426614174000", - name: "mock-user", - disabled: false, - mfaEnrolled: false, - mfaFactorIds: [], - }, + user: { id: "mock-id", name: "mock-user", disabled: false, mfaEnrolled: false, mfaFactorIds: [] }, userByName: { - id: "123e4567-e89b-12d3-a456-426614174000", + id: "mock-id", name: "mock-user", disabled: false, mfaEnrolled: false, mfaFactorIds: [], }, createUser: { - id: "123e4567-e89b-12d3-a456-426614174000", + id: "mock-id", name: "mock-user", disabled: false, mfaEnrolled: false, mfaFactorIds: [], }, updateUser: { - id: "123e4567-e89b-12d3-a456-426614174000", + id: "mock-id", name: "mock-user", disabled: false, mfaEnrolled: false, @@ -753,7 +746,7 @@ const IDP_DEFAULTS: Record = { * test("resolver-based", async () => { * using idp = mockIdp(); * idp.setResolver((method) => - * method === "user" ? { id: "11111111-1111-4111-8111-111111111111", name: "alice", disabled: false } : null, + * method === "user" ? { id: "u-1", name: "alice", disabled: false } : null, * ); * // … * }); @@ -786,11 +779,11 @@ export function mockIdp() { ) { const namespace = config.namespace; this.users = async (options?: unknown) => handle("users", [options], namespace); - this.user = async (userId: UUIDString) => handle("user", [userId], namespace); + this.user = async (userId: string) => handle("user", [userId], namespace); this.userByName = async (name: string) => handle("userByName", [name], namespace); this.createUser = async (input: unknown) => handle("createUser", [input], namespace); this.updateUser = async (input: unknown) => handle("updateUser", [input], namespace); - this.deleteUser = async (userId: UUIDString) => handle("deleteUser", [userId], namespace); + this.deleteUser = async (userId: string) => handle("deleteUser", [userId], namespace); this.sendPasswordResetEmail = async (input: unknown) => handle("sendPasswordResetEmail", [input], namespace); this.unenrollMfa = async (input: unknown) => handle("unenrollMfa", [input], namespace); @@ -798,21 +791,21 @@ export function mockIdp() { users(options?: { first?: number; after?: string; - query?: { ids?: UUIDString[]; names?: string[] }; + query?: { ids?: string[]; names?: string[] }; }): Promise<{ users: IdpUser[]; nextPageToken: string | null; totalCount: number }>; - user(userId: UUIDString): Promise; + user(userId: string): Promise; userByName(name: string): Promise; createUser(input: { name: string; password?: string; disabled?: boolean }): Promise; updateUser(input: { - id: UUIDString; + id: string; name?: string; password?: string; clearPassword?: boolean; disabled?: boolean; }): Promise; - deleteUser(userId: UUIDString): Promise; - sendPasswordResetEmail(input: { userId: UUIDString; redirectUri: string }): Promise; - unenrollMfa(input: { userId: UUIDString; mfaFactorId: string }): Promise; + deleteUser(userId: string): Promise; + sendPasswordResetEmail(input: { userId: string; redirectUri: string }): Promise; + unenrollMfa(input: { userId: string; mfaFactorId: string }): Promise; }; root.idp = { Client }; From aecaf8c1bb7813a32e998ea7d034684541cb1c85 Mon Sep 17 00:00:00 2001 From: dqn Date: Tue, 7 Jul 2026 20:37:16 +0900 Subject: [PATCH 453/618] feat: adopt politty skill management --- .changeset/politty-skill-management.md | 6 + .changeset/remove-tailor-sdk-skills-shim.md | 2 +- .../codemods/v2/sdk-skills-shim/codemod.yaml | 2 +- .../v2/sdk-skills-shim/scripts/transform.ts | 6 +- .../tests/basic-package-json/expected.json | 4 +- .../tests/basic-shell/expected.sh | 4 +- .../tests/basic-yaml/expected.yml | 2 +- .../tests/version-qualified/expected.sh | 4 +- packages/sdk-codemod/src/registry.ts | 13 +- packages/sdk/README.md | 14 +- packages/sdk/agent-skills/tailor/SKILL.md | 2 + packages/sdk/docs/cli-reference.md | 15 +- packages/sdk/docs/cli/skills.md | 70 ++++++- packages/sdk/docs/cli/skills.template.md | 2 +- packages/sdk/docs/migration/v2.md | 12 +- packages/sdk/src/cli/commands/skills/index.ts | 13 -- .../src/cli/commands/skills/install.test.ts | 21 -- .../sdk/src/cli/commands/skills/install.ts | 43 ---- packages/sdk/src/cli/index.ts | 89 ++++++++- packages/sdk/src/cli/options.test.ts | 4 +- .../sdk/src/cli/shared/readonly-guard.test.ts | 3 - .../src/cli/shared/skills-installer.test.ts | 104 ---------- .../sdk/src/cli/shared/skills-installer.ts | 83 -------- .../src/cli/shared/skills-packaged.test.ts | 3 + packages/sdk/src/cli/skills-command.test.ts | 186 ++++++++++++++++++ skills/tailor/SKILL.md | 2 + 26 files changed, 387 insertions(+), 322 deletions(-) create mode 100644 .changeset/politty-skill-management.md delete mode 100644 packages/sdk/src/cli/commands/skills/index.ts delete mode 100644 packages/sdk/src/cli/commands/skills/install.test.ts delete mode 100644 packages/sdk/src/cli/commands/skills/install.ts delete mode 100644 packages/sdk/src/cli/shared/skills-installer.test.ts delete mode 100644 packages/sdk/src/cli/shared/skills-installer.ts create mode 100644 packages/sdk/src/cli/skills-command.test.ts diff --git a/.changeset/politty-skill-management.md b/.changeset/politty-skill-management.md new file mode 100644 index 000000000..0332a12c2 --- /dev/null +++ b/.changeset/politty-skill-management.md @@ -0,0 +1,6 @@ +--- +"@tailor-platform/sdk": major +"@tailor-platform/sdk-codemod": patch +--- + +Replace `tailor skills install` with project-local `tailor skills add`, `list`, `remove`, and `sync` commands for bundled Tailor SDK agent skills. diff --git a/.changeset/remove-tailor-sdk-skills-shim.md b/.changeset/remove-tailor-sdk-skills-shim.md index 1038a3dfc..982a4f64e 100644 --- a/.changeset/remove-tailor-sdk-skills-shim.md +++ b/.changeset/remove-tailor-sdk-skills-shim.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk": major --- -Remove the deprecated `tailor-sdk-skills` binary shim. Use `tailor-sdk skills install` to install the bundled Tailor SDK agent skill. +Remove the deprecated `tailor-sdk-skills` binary shim. Use `tailor skills add` to install the bundled Tailor SDK agent skill. diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/codemod.yaml b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/codemod.yaml index 07ab7bbff..45ebf71b5 100644 --- a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/codemod.yaml +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/codemod.yaml @@ -1,6 +1,6 @@ name: "@tailor-platform/sdk-skills-shim" version: "1.0.0" -description: "Replace deprecated `tailor-sdk-skills` binary with `tailor-sdk skills install`" +description: "Replace deprecated `tailor-sdk-skills` binary with `tailor skills add`" engine: jssg language: text since: "1.0.0" diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/scripts/transform.ts index bdac5661a..6c40f1742 100644 --- a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/scripts/transform.ts @@ -7,7 +7,7 @@ import * as path from "pathe"; // to the new subcommand. `[ \t]+` (not `\s+`) prevents the optional-install // alternative from greedily reaching across newlines into the next command. const SHIM_PATTERN = /\btailor-sdk-skills(?:@[^\s'"`]+)?(?:[ \t]+install)?\b(?!-)/g; -const REPLACEMENT = "tailor-sdk skills install"; +const REPLACEMENT = "tailor skills add"; function replaceShim(value: string): string { return value.replace(SHIM_PATTERN, REPLACEMENT); @@ -48,10 +48,10 @@ function transformPackageJson(source: string): string | null { } /** - * Replace `tailor-sdk-skills` invocations with `tailor-sdk skills install`. + * Replace `tailor-sdk-skills` invocations with `tailor skills add`. * * The standalone `tailor-sdk-skills` binary is removed in v2; users must call - * the subcommand on the main `tailor-sdk` CLI instead. + * the subcommand on the main `tailor` CLI instead. * @param source - File contents * @param filePath - Absolute path to the file (used to dispatch package.json vs text) * @returns Transformed source or null when nothing matched. diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-package-json/expected.json b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-package-json/expected.json index fddd67a36..d37fb88d9 100644 --- a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-package-json/expected.json +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-package-json/expected.json @@ -1,7 +1,7 @@ { "name": "demo", "scripts": { - "postinstall": "tailor-sdk skills install", - "skills": "pnpm exec tailor-sdk skills install" + "postinstall": "tailor skills add", + "skills": "pnpm exec tailor skills add" } } diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-shell/expected.sh index 9d6ee6bda..478dafee5 100644 --- a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-shell/expected.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash set -euo pipefail -pnpm exec tailor-sdk skills install -npx tailor-sdk skills install --help +pnpm exec tailor skills add +npx tailor skills add --help diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-yaml/expected.yml b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-yaml/expected.yml index d410ac2fd..4737b1169 100644 --- a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-yaml/expected.yml +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-yaml/expected.yml @@ -2,4 +2,4 @@ jobs: install-skills: runs-on: ubuntu-latest steps: - - run: tailor-sdk skills install + - run: tailor skills add diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/version-qualified/expected.sh b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/version-qualified/expected.sh index 8022201c7..bd85aa7e2 100644 --- a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/version-qualified/expected.sh +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/version-qualified/expected.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -npx tailor-sdk skills install -pnpm dlx tailor-sdk skills install --help +npx tailor skills add +pnpm dlx tailor skills add --help tailor-sdk-skills-helper run diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 81862ce1e..97403f01a 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -170,9 +170,8 @@ export const allCodemods: CodemodPackage[] = [ }, { id: "v2/sdk-skills-shim", - name: "tailor-sdk-skills → tailor-sdk skills install", - description: - "Replace deprecated `tailor-sdk-skills` invocations with `tailor-sdk skills install`", + name: "tailor-sdk-skills → tailor skills add", + description: "Replace deprecated `tailor-sdk-skills` invocations with `tailor skills add`", since: "1.0.0", until: "2.0.0", prereleaseUntil: V2_NEXT_1, @@ -183,14 +182,14 @@ export const allCodemods: CodemodPackage[] = [ { lang: "sh", before: "npx tailor-sdk-skills", - after: "tailor-sdk skills install", + after: "tailor skills add", }, ], prompt: [ - "The standalone tailor-sdk-skills binary is removed in v2; call the skills install", - "subcommand on the main tailor-sdk CLI instead. Replace any remaining", + "The standalone tailor-sdk-skills binary is removed in v2; call the skills add", + "subcommand on the main tailor CLI instead. Replace any remaining", "tailor-sdk-skills invocations the codemod did not rewrite with", - "`tailor-sdk skills install`.", + "`tailor skills add`.", ].join("\n"), }, { diff --git a/packages/sdk/README.md b/packages/sdk/README.md index e604aa191..7bdacd5ca 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -53,16 +53,16 @@ For more details, see the [Quickstart Guide](./docs/quickstart.md). Install the `tailor` skill from the locally installed SDK package: ```bash -npx tailor skills install +npx tailor skills add -# Example: install to Codex in non-interactive mode -npx tailor skills install -a codex -y +# Or refresh all skills owned by this CLI +npx tailor skills sync ``` -This uses the `skills` CLI under the hood, sourcing the skill from -`node_modules/@tailor-platform/sdk/agent-skills` so the skill version always matches -the installed SDK version. Files are copied (not symlinked) so they survive -`pnpm install` wiping `node_modules`. +This sources the skill from `node_modules/@tailor-platform/sdk/agent-skills` +so the skill version always matches the installed SDK version. Files are copied +into the current project under `.agents/skills` and `.claude/skills`, so rerun +`skills sync` after upgrading the SDK. ## Learn More diff --git a/packages/sdk/agent-skills/tailor/SKILL.md b/packages/sdk/agent-skills/tailor/SKILL.md index 68c54ad5b..e1ca701de 100644 --- a/packages/sdk/agent-skills/tailor/SKILL.md +++ b/packages/sdk/agent-skills/tailor/SKILL.md @@ -1,6 +1,8 @@ --- name: tailor description: Use this skill when working with @tailor-platform/sdk projects, including service configuration, CLI usage, and docs navigation. +metadata: + politty-cli: "@tailor-platform/sdk:tailor" --- # Tailor SDK Knowledge diff --git a/packages/sdk/docs/cli-reference.md b/packages/sdk/docs/cli-reference.md index 38948c1f9..b16bc6b17 100644 --- a/packages/sdk/docs/cli-reference.md +++ b/packages/sdk/docs/cli-reference.md @@ -396,12 +396,15 @@ Commands for upgrading SDK versions with automated code migration. ### [Skills Commands](./cli/skills.md) -Commands for installing Tailor SDK agent skills. - -| Command | Description | -| ------------------------------------------------ | -------------------------------------------------------------- | -| [skills](./cli/skills.md#skills) | Manage Tailor SDK agent skills. | -| [skills install](./cli/skills.md#skills-install) | Install the tailor agent skill from the installed SDK package. | +Commands for managing Tailor SDK agent skills. + +| Command | Description | +| ---------------------------------------------- | --------------------------------------------- | +| [skills](./cli/skills.md#skills) | Manage Tailor SDK agent skills. | +| [skills add](./cli/skills.md#skills-add) | Install Tailor SDK agent skills. | +| [skills list](./cli/skills.md#skills-list) | List Tailor SDK agent skills. | +| [skills remove](./cli/skills.md#skills-remove) | Remove installed Tailor SDK agent skills. | +| [skills sync](./cli/skills.md#skills-sync) | Remove and reinstall Tailor SDK agent skills. | ### [Plugin Commands](./cli/plugin.md) diff --git a/packages/sdk/docs/cli/skills.md b/packages/sdk/docs/cli/skills.md index 84514c903..4f5a946a8 100644 --- a/packages/sdk/docs/cli/skills.md +++ b/packages/sdk/docs/cli/skills.md @@ -12,25 +12,75 @@ See [Global Options](../cli-reference.md#global-options) for options available t **Commands** -| Command | Description | -| ----------------------------------- | -------------------------------------------------------------- | -| [`skills install`](#skills-install) | Install the tailor agent skill from the installed SDK package. | +| Command | Description | +| --------------------------------- | --------------------------------------------- | +| [`skills sync`](#skills-sync) | Remove and reinstall Tailor SDK agent skills. | +| [`skills add`](#skills-add) | Install Tailor SDK agent skills. | +| [`skills remove`](#skills-remove) | Remove installed Tailor SDK agent skills. | +| [`skills list`](#skills-list) | List Tailor SDK agent skills. | -### skills install +### skills add -Install the tailor agent skill from the installed SDK package. +Install Tailor SDK agent skills. **Usage** ``` -tailor skills install [options] +tailor skills add [name] +``` + +**Arguments** + +| Argument | Description | Required | +| -------- | --------------------------------------- | -------- | +| `name` | Skill name(s) to install (default: all) | No | + +See [Global Options](../cli-reference.md#global-options) for options available to all commands. + +### skills list + +List Tailor SDK agent skills. + +**Usage** + +``` +tailor skills list +``` + +See [Global Options](../cli-reference.md#global-options) for options available to all commands. + +### skills remove + +Remove installed Tailor SDK agent skills. + +**Usage** + +``` +tailor skills remove [name] +``` + +**Arguments** + +| Argument | Description | Required | +| -------- | ----------------------------------- | -------- | +| `name` | Skill name to remove (default: all) | No | + +See [Global Options](../cli-reference.md#global-options) for options available to all commands. + +### skills sync + +Remove and reinstall Tailor SDK agent skills. + +**Usage** + +``` +tailor skills sync [options] ``` **Options** -| Option | Alias | Description | Required | Default | -| ----------------- | ----- | ---------------------------------------------------------------------------- | -------- | --------------- | -| `--agent ` | `-a` | vercel/skills agent name (e.g. claude-code, codex). Defaults to claude-code. | No | `"claude-code"` | -| `--yes` | `-y` | Auto-approve prompts. | No | `false` | +| Option | Alias | Description | Required | Default | +| --------------------- | ----- | -------------------------------- | -------- | ------- | +| `--exclude ` | `-x` | Skill names to exclude from sync | No | `[]` | See [Global Options](../cli-reference.md#global-options) for options available to all commands. diff --git a/packages/sdk/docs/cli/skills.template.md b/packages/sdk/docs/cli/skills.template.md index 78f3dd82c..05b32feb4 100644 --- a/packages/sdk/docs/cli/skills.template.md +++ b/packages/sdk/docs/cli/skills.template.md @@ -2,7 +2,7 @@ politty: index: title: "Skills Commands" - description: "Commands for installing Tailor SDK agent skills." + description: "Commands for managing Tailor SDK agent skills." --- {{politty:command:skills}} diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index c04f240fa..434e648d0 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -82,11 +82,11 @@ After: tailor function test-run resolvers/add.ts --arg '{"a":1}' ``` -## tailor-sdk-skills → tailor-sdk skills install +## tailor-sdk-skills → tailor skills add **Migration:** Partially automatic -Replace deprecated `tailor-sdk-skills` invocations with `tailor-sdk skills install` +Replace deprecated `tailor-sdk-skills` invocations with `tailor skills add` Before: @@ -97,17 +97,17 @@ npx tailor-sdk-skills After: ```sh -tailor-sdk skills install +tailor skills add ```
Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text -The standalone tailor-sdk-skills binary is removed in v2; call the skills install -subcommand on the main tailor-sdk CLI instead. Replace any remaining +The standalone tailor-sdk-skills binary is removed in v2; call the skills add +subcommand on the main tailor CLI instead. Replace any remaining tailor-sdk-skills invocations the codemod did not rewrite with -`tailor-sdk skills install`. +`tailor skills add`. ```
diff --git a/packages/sdk/src/cli/commands/skills/index.ts b/packages/sdk/src/cli/commands/skills/index.ts deleted file mode 100644 index 9d4821c80..000000000 --- a/packages/sdk/src/cli/commands/skills/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineCommand, runCommand } from "politty"; -import { installCommand } from "./install"; - -export const skillsCommand = defineCommand({ - name: "skills", - description: "Manage Tailor SDK agent skills.", - subCommands: { - install: installCommand, - }, - async run() { - await runCommand(installCommand, []); - }, -}); diff --git a/packages/sdk/src/cli/commands/skills/install.test.ts b/packages/sdk/src/cli/commands/skills/install.test.ts deleted file mode 100644 index 981f533eb..000000000 --- a/packages/sdk/src/cli/commands/skills/install.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { existsSync, statSync } from "node:fs"; -import { resolve } from "pathe"; -import { describe, expect, test } from "vitest"; -import { installCommand, resolveBundledSkillsDir } from "./install"; - -describe("resolveBundledSkillsDir", () => { - test("resolves to the SDK package's agent-skills/ directory", async () => { - const dir = await resolveBundledSkillsDir(); - expect(dir.endsWith("/agent-skills")).toBe(true); - expect(existsSync(dir)).toBe(true); - expect(statSync(dir).isDirectory()).toBe(true); - expect(existsSync(resolve(dir, "tailor", "SKILL.md"))).toBe(true); - }); -}); - -describe("installCommand args", () => { - test("defaults agent to 'claude-code' (vercel/skills' canonical name)", () => { - const parsed = installCommand.args.parse({}); - expect(parsed.agent).toBe("claude-code"); - }); -}); diff --git a/packages/sdk/src/cli/commands/skills/install.ts b/packages/sdk/src/cli/commands/skills/install.ts deleted file mode 100644 index 1d546c80c..000000000 --- a/packages/sdk/src/cli/commands/skills/install.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { dirname, resolve } from "pathe"; -import { resolvePackageJSON } from "pkg-types"; -import { arg } from "politty"; -import { z } from "zod"; -import { defineAppCommand } from "#/cli/shared/command"; -import { runSkillsInstaller } from "#/cli/shared/skills-installer"; - -// Resolve the SDK package root at runtime so the skills directory is found -// regardless of how the file is bundled (tsdown inlines non-entry modules). -// The directory is named `agent-skills` (not `skills`) to avoid `gh skill`'s -// `**/skills/*/SKILL.md` recursive match that would otherwise pick up both -// this bundled copy and the repo-root `skills/` and report a conflict. -export async function resolveBundledSkillsDir(): Promise { - const pkgJsonPath = await resolvePackageJSON(import.meta.url); - return resolve(dirname(pkgJsonPath), "agent-skills"); -} - -const DEFAULT_AGENT = "claude-code"; - -export const installCommand = defineAppCommand({ - name: "install", - description: "Install the tailor agent skill from the installed SDK package.", - args: z.strictObject({ - agent: arg(z.string().default(DEFAULT_AGENT), { - alias: "a", - description: `vercel/skills agent name (e.g. ${DEFAULT_AGENT}, codex). Defaults to ${DEFAULT_AGENT}.`, - }), - yes: arg(z.boolean().default(false), { - alias: "y", - description: "Auto-approve prompts.", - }), - }), - run: async (args) => { - const exitCode = await runSkillsInstaller({ - source: await resolveBundledSkillsDir(), - agent: args.agent, - yes: args.yes, - }); - if (exitCode !== 0) { - process.exit(exitCode); - } - }, -}); diff --git a/packages/sdk/src/cli/index.ts b/packages/sdk/src/cli/index.ts index 36c226167..1e95a994f 100644 --- a/packages/sdk/src/cli/index.ts +++ b/packages/sdk/src/cli/index.ts @@ -1,7 +1,10 @@ #!/usr/bin/env node -import { defineCommand, runMain } from "politty"; +import { dirname, resolve } from "pathe"; +import { resolvePackageJSON } from "pkg-types"; +import { defineCommand, runCommand, runMain, type AnyCommand } from "politty"; import { withCompletionCommand } from "politty/completion"; +import { withSkillCommand } from "politty/skill"; import { z } from "zod"; import { apiCommand } from "./commands/api"; import { authCommand } from "./commands/auth"; @@ -24,7 +27,6 @@ import { removeCommand } from "./commands/remove"; import { secretCommand } from "./commands/secret"; import { setupCommand } from "./commands/setup"; import { showCommand } from "./commands/show"; -import { skillsCommand } from "./commands/skills"; import { staticwebsiteCommand } from "./commands/staticwebsite"; import { tailordbCommand } from "./commands/tailordb"; import { upgradeCommand } from "./commands/upgrade"; @@ -50,8 +52,73 @@ initCrashReporting(); const packageJson = await readPackageJson(); const cliName = Object.keys(packageJson.bin ?? {})[0] || "tailor"; +const packageName = packageJson.name ?? "@tailor-platform/sdk"; +const packageJsonPath = await resolvePackageJSON(import.meta.url); +const bundledSkillsDir = resolve(dirname(packageJsonPath), "agent-skills"); -export const mainCommand = withCompletionCommand( +type CommandArgShape = Record; + +function replaceCommandArgs(command: AnyCommand, removals: string[] = []): AnyCommand { + const args = command.args as { shape?: CommandArgShape } | undefined; + if (!args?.shape) { + return command; + } + const shape = { ...args.shape }; + for (const name of removals) { + delete shape[name]; + } + return { + ...command, + args: z.strictObject({ + ...shape, + }), + }; +} + +function removeCommandAliases(command: AnyCommand): AnyCommand { + const next = { ...command }; + delete next.aliases; + return next; +} + +function alignSkillCommand(command: AnyCommand): AnyCommand { + const subCommands = command.subCommands ?? {}; + const add = { + ...removeCommandAliases(replaceCommandArgs(subCommands.add as AnyCommand, ["verbose"])), + description: "Install Tailor SDK agent skills.", + }; + const list = { + ...replaceCommandArgs(subCommands.list as AnyCommand, ["json"]), + description: "List Tailor SDK agent skills.", + }; + const remove = { + ...removeCommandAliases(replaceCommandArgs(subCommands.remove as AnyCommand)), + description: "Remove installed Tailor SDK agent skills.", + }; + const sync = { + ...replaceCommandArgs(subCommands.sync as AnyCommand, ["verbose"]), + description: "Remove and reinstall Tailor SDK agent skills.", + }; + return { + ...command, + description: "Manage Tailor SDK agent skills.", + async run() { + const result = await runCommand(add, []); + if (!result.success) { + throw result.error; + } + }, + subCommands: { + ...subCommands, + add, + list, + remove, + sync, + }, + }; +} + +const commandWithSkills = withSkillCommand( defineCommand({ name: cliName, description: @@ -81,7 +148,6 @@ Run \`${cliName} plugin list\` to see which plugins are installed and where they secret: secretCommand, setup: setupCommand, show: showCommand, - skills: skillsCommand, staticwebsite: staticwebsiteCommand, tailordb: tailordbCommand, upgrade: upgradeCommand, @@ -90,7 +156,22 @@ Run \`${cliName} plugin list\` to see which plugins are installed and where they workspace: workspaceCommand, }, }), + { + sourceDir: bundledSkillsDir, + package: packageName, + mode: "copy", + descriptionAppend: false, + }, ); +const commandWithSkillsSubCommands = commandWithSkills.subCommands ?? {}; + +export const mainCommand = withCompletionCommand({ + ...commandWithSkills, + subCommands: { + ...commandWithSkillsSubCommands, + skills: alignSkillCommand(commandWithSkillsSubCommands.skills as AnyCommand), + }, +}); runMain(mainCommand, { version: packageJson.version, diff --git a/packages/sdk/src/cli/options.test.ts b/packages/sdk/src/cli/options.test.ts index dce1736fe..5c61f4ac4 100644 --- a/packages/sdk/src/cli/options.test.ts +++ b/packages/sdk/src/cli/options.test.ts @@ -86,7 +86,7 @@ async function walkMainCommands(visit: (command: AnyCommand, path: string[]) => const subCommands = mainCommand.subCommands; expect(subCommands).toBeDefined(); - for (const [name, cmd] of Object.entries(subCommands ?? {})) { + for (const [name, cmd] of Object.entries(subCommands)) { await walkCommand(cmd, visit, [name]); } } @@ -120,7 +120,7 @@ describe("CLI options", () => { // `plugin list` uses BUILTIN_COMMAND_NAMES (a leaf module, to avoid an // import cycle) to flag shadowed plugins. Exclude the wrapper-added // `completion` command and any internal `__`-prefixed commands. - const registered = Object.keys(mainCommand.subCommands ?? {}).filter( + const registered = Object.keys(mainCommand.subCommands).filter( (name) => !name.startsWith("__") && name !== "completion", ); expect(new Set(registered)).toEqual(new Set(BUILTIN_COMMAND_NAMES)); diff --git a/packages/sdk/src/cli/shared/readonly-guard.test.ts b/packages/sdk/src/cli/shared/readonly-guard.test.ts index 8204f8d24..d5b008a90 100644 --- a/packages/sdk/src/cli/shared/readonly-guard.test.ts +++ b/packages/sdk/src/cli/shared/readonly-guard.test.ts @@ -99,9 +99,6 @@ const READ_OR_LOCAL_COMMAND_PATHS = new Set([ "secret/vault/list.ts", // Setup (local file generation) "setup/index.ts", - // Skills (local file install) - "skills/index.ts", - "skills/install.ts", // Static website (read-only) "staticwebsite/index.ts", "staticwebsite/get.ts", diff --git a/packages/sdk/src/cli/shared/skills-installer.test.ts b/packages/sdk/src/cli/shared/skills-installer.test.ts deleted file mode 100644 index cf9a9c1e1..000000000 --- a/packages/sdk/src/cli/shared/skills-installer.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { SKILL_NAME, buildSkillsAddArgs, runSkillsInstaller } from "./skills-installer"; - -const TEST_SOURCE = "/fake/sdk/skills"; - -interface MockChildProcess { - on(event: "close", listener: (code: number | null) => void): MockChildProcess; - on(event: "error", listener: (error: Error) => void): MockChildProcess; -} - -const createMockChildProcess = () => { - const listeners = { - close: [] as Array<(code: number | null) => void>, - error: [] as Array<(error: Error) => void>, - }; - - function on(event: "close", listener: (code: number | null) => void): MockChildProcess; - function on(event: "error", listener: (error: Error) => void): MockChildProcess; - function on( - event: "close" | "error", - listener: ((code: number | null) => void) | ((error: Error) => void), - ): MockChildProcess { - if (event === "close") { - listeners.close.push(listener as (code: number | null) => void); - } else { - listeners.error.push(listener as (error: Error) => void); - } - return process; - } - - const process: MockChildProcess = { on }; - - return { - process, - emitClose: (code: number | null) => listeners.close.forEach((listener) => listener(code)), - emitError: (error: Error) => listeners.error.forEach((listener) => listener(error)), - }; -}; - -describe("skills-installer", () => { - test("builds skills add arguments with the provided source and --copy", () => { - expect(buildSkillsAddArgs({ source: TEST_SOURCE })).toEqual([ - "skills", - "add", - TEST_SOURCE, - "--skill", - SKILL_NAME, - "--copy", - ]); - }); - - test("prefers TAILOR_SKILLS_SOURCE env var over the passed source", () => { - vi.stubEnv("TAILOR_SKILLS_SOURCE", "/override/skills"); - try { - expect(buildSkillsAddArgs({ source: TEST_SOURCE })[2]).toBe("/override/skills"); - } finally { - vi.unstubAllEnvs(); - } - }); - - test("appends --agent and --yes when provided", () => { - expect( - buildSkillsAddArgs({ source: TEST_SOURCE, agent: "codex", yes: true }).slice(-3), - ).toEqual(["--agent", "codex", "--yes"]); - }); - - test("runs npx with generated arguments and returns exit code", async () => { - const mock = createMockChildProcess(); - const spawnFn = vi.fn(() => mock.process); - - const promise = runSkillsInstaller({ - source: TEST_SOURCE, - agent: "codex", - spawnFn, - }); - - expect(spawnFn).toHaveBeenCalledWith( - expect.stringMatching(/^npx(\\.cmd)?$/), - ["skills", "add", TEST_SOURCE, "--skill", SKILL_NAME, "--copy", "--agent", "codex"], - { stdio: "inherit" }, - ); - - mock.emitClose(0); - await expect(promise).resolves.toBe(0); - }); - - test("returns 1 when child process exits without status code", async () => { - const mock = createMockChildProcess(); - const spawnFn = vi.fn(() => mock.process); - const promise = runSkillsInstaller({ source: TEST_SOURCE, spawnFn }); - - mock.emitClose(null); - await expect(promise).resolves.toBe(1); - }); - - test("rejects when npx execution fails", async () => { - const mock = createMockChildProcess(); - const spawnFn = vi.fn(() => mock.process); - const promise = runSkillsInstaller({ source: TEST_SOURCE, spawnFn }); - - mock.emitError(new Error("spawn failed")); - await expect(promise).rejects.toThrow("spawn failed"); - }); -}); diff --git a/packages/sdk/src/cli/shared/skills-installer.ts b/packages/sdk/src/cli/shared/skills-installer.ts deleted file mode 100644 index aaa262b9a..000000000 --- a/packages/sdk/src/cli/shared/skills-installer.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { spawn } from "node:child_process"; - -export const SKILL_NAME = "tailor"; -const SKILLS_SOURCE_ENV_KEY = "TAILOR_SKILLS_SOURCE"; - -interface ChildProcessLike { - on(event: "close", listener: (code: number | null) => void): ChildProcessLike; - on(event: "error", listener: (error: Error) => void): ChildProcessLike; -} - -type SpawnLike = ( - command: string, - args: string[], - options: { stdio: "inherit" }, -) => ChildProcessLike; - -export interface RunSkillsInstallerOptions { - source: string; - agent?: string; - yes?: boolean; - spawnFn?: SpawnLike; -} - -interface BuildSkillsAddArgsOptions { - source: string; - agent?: string; - yes?: boolean; -} - -function resolveNpxCommand(platform: NodeJS.Platform = process.platform): string { - return platform === "win32" ? "npx.cmd" : "npx"; -} - -function resolveSkillsSource(source: string): string { - return process.env[SKILLS_SOURCE_ENV_KEY] ?? source; -} - -/** - * Build CLI arguments for `skills add` with the fixed tailor skill target. - * `--copy` is included so the installed skill survives `pnpm install` wiping `node_modules`. - * @param options - Options controlling the generated `skills add` arguments - * @param options.source - Skill source package or path passed to `skills add` - * @param options.agent - Target agent name passed through with `--agent` - * @param options.yes - Whether to add `--yes` for non-interactive installation - * @returns CLI arguments for `npx skills add` - */ -export function buildSkillsAddArgs(options: BuildSkillsAddArgsOptions): string[] { - const args = [ - "skills", - "add", - resolveSkillsSource(options.source), - "--skill", - SKILL_NAME, - "--copy", - ]; - if (options.agent) args.push("--agent", options.agent); - if (options.yes) args.push("--yes"); - return args; -} - -/** - * Run `npx skills add` to install the tailor skill. - * @param options - Runtime options for skill installation - * @returns Process exit code from the spawned `npx` command - */ -export async function runSkillsInstaller(options: RunSkillsInstallerOptions): Promise { - const args = buildSkillsAddArgs({ - source: options.source, - agent: options.agent, - yes: options.yes, - }); - const spawnFn = - options.spawnFn ?? - ((command: string, spawnArgs: string[], spawnOptions: { stdio: "inherit" }) => - spawn(command, spawnArgs, spawnOptions)); - - const childProcess = spawnFn(resolveNpxCommand(), args, { stdio: "inherit" }); - - return await new Promise((resolve, reject) => { - childProcess.on("error", (error) => reject(error)); - childProcess.on("close", (code) => resolve(code ?? 1)); - }); -} diff --git a/packages/sdk/src/cli/shared/skills-packaged.test.ts b/packages/sdk/src/cli/shared/skills-packaged.test.ts index 6321dd979..5547db08a 100644 --- a/packages/sdk/src/cli/shared/skills-packaged.test.ts +++ b/packages/sdk/src/cli/shared/skills-packaged.test.ts @@ -42,6 +42,9 @@ describe("packaged skills", () => { expect(frontmatter?.name).toBe(name); expect(typeof frontmatter?.description).toBe("string"); expect((frontmatter?.description as string).trim().length).toBeGreaterThan(0); + expect(frontmatter?.metadata).toMatchObject({ + "politty-cli": "@tailor-platform/sdk:tailor", + }); }); test("references existing SDK files for every node_modules/@tailor-platform/sdk path", () => { diff --git a/packages/sdk/src/cli/skills-command.test.ts b/packages/sdk/src/cli/skills-command.test.ts new file mode 100644 index 000000000..fa538f166 --- /dev/null +++ b/packages/sdk/src/cli/skills-command.test.ts @@ -0,0 +1,186 @@ +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { join } from "pathe"; +import { extractFields, isLazyCommand, runCommand } from "politty"; +import { describe, expect, test, vi } from "vitest"; +import { z } from "zod"; +import { commonArgs } from "./shared/args"; +import { logger } from "./shared/logger"; +import { tempCwd } from "./shared/test-helpers/temp-cwd"; +import { mainCommand } from "./index"; +import type { AnyCommand, RunResult, SubCommandValue } from "politty"; + +vi.mock("node:module", async () => { + const actual = await vi.importActual("node:module"); + return { ...actual, register: vi.fn() }; +}); + +vi.mock("politty", async () => { + const actual = await vi.importActual("politty"); + return { ...actual, runMain: vi.fn() }; +}); + +// strip unknown global args to match the CLI runner. +const testGlobalArgs = z.object(commonArgs); + +async function resolveCommand(cmd: SubCommandValue): Promise { + if (isLazyCommand(cmd)) { + return await cmd.load(); + } + if (typeof cmd === "function") { + return await cmd(); + } + return cmd; +} + +function expectDefined(value: T | undefined): T { + expect(value).toBeDefined(); + return value as T; +} + +function expectCommandFailure(result: RunResult, message: string): void { + expect(result.success).toBe(false); + if (result.success) { + throw new Error("Expected command to fail"); + } + expect(result.error.message).toContain(message); +} + +async function importMainCommandForCurrentCwd(): Promise { + vi.resetModules(); + const module = await import("./index"); + return module.mainCommand; +} + +describe("skills command", () => { + test("uses politty skill management subcommands without legacy aliases", async () => { + const skillsCommand = await resolveCommand(expectDefined(mainCommand.subCommands.skills)); + const skillSubCommands = expectDefined(skillsCommand.subCommands); + const addCommand = await resolveCommand(expectDefined(skillSubCommands.add)); + const removeCommand = await resolveCommand(expectDefined(skillSubCommands.remove)); + + expect(Object.keys(skillSubCommands).toSorted()).toEqual(["add", "list", "remove", "sync"]); + expect(skillsCommand.run).toBeTypeOf("function"); + expect(addCommand.aliases).toBeUndefined(); + expect(removeCommand.aliases).toBeUndefined(); + expect( + extractFields(expectDefined(addCommand.args)).fields.map((field) => field.name), + ).not.toEqual(expect.arrayContaining(["agent", "yes"])); + }); + + test("does not shadow inherited global flags with local defaults", async () => { + const skillsCommand = await resolveCommand(expectDefined(mainCommand.subCommands.skills)); + const skillSubCommands = expectDefined(skillsCommand.subCommands); + const addCommand = await resolveCommand(expectDefined(skillSubCommands.add)); + const listCommand = await resolveCommand(expectDefined(skillSubCommands.list)); + + expect(expectDefined(addCommand.args).parse({})).not.toHaveProperty("verbose"); + expect(expectDefined(listCommand.args).parse({})).not.toHaveProperty("json"); + + const consoleLog = vi.spyOn(console, "log").mockImplementation(() => {}); + try { + const result = await runCommand(mainCommand, ["--json", "skills", "list"], { + globalArgs: testGlobalArgs, + captureLogs: true, + }); + + expect(result.success).toBe(true); + expect(consoleLog).toHaveBeenCalledWith(expect.stringContaining('"name":"tailor"')); + } finally { + consoleLog.mockRestore(); + logger.jsonMode = false; + } + }); + + test("rejects removed legacy forms before side effects", async () => { + using tmp = tempCwd("tailor-skills-command-"); + writeFileSync(join(tmp.dir, "package.json"), "{}\n"); + const cwdMainCommand = await importMainCommandForCurrentCwd(); + + const installResult = await runCommand(cwdMainCommand, ["skills", "install"], { + globalArgs: testGlobalArgs, + captureLogs: true, + }); + expectCommandFailure(installResult, "Unknown subcommand"); + expect(existsSync(join(tmp.dir, ".agents/skills/tailor"))).toBe(false); + + const agentResult = await runCommand(cwdMainCommand, ["skills", "add", "--agent", "codex"], { + globalArgs: testGlobalArgs, + captureLogs: true, + }); + expectCommandFailure(agentResult, "Unknown flags: agent"); + expect(existsSync(join(tmp.dir, ".agents/skills/tailor"))).toBe(false); + }); + + test("rejects unknown skill flags before side effects", async () => { + using tmp = tempCwd("tailor-skills-command-"); + writeFileSync(join(tmp.dir, "package.json"), "{}\n"); + const cwdMainCommand = await importMainCommandForCurrentCwd(); + + const addResult = await runCommand(cwdMainCommand, ["skills", "add", "--agnt", "codex"], { + globalArgs: testGlobalArgs, + captureLogs: true, + }); + + expectCommandFailure(addResult, "Unknown flags: agnt"); + expect(existsSync(join(tmp.dir, ".agents/skills/tailor"))).toBe(false); + + const installResult = await runCommand(cwdMainCommand, ["skills", "add"], { + globalArgs: testGlobalArgs, + captureLogs: true, + }); + expect(installResult.success).toBe(true); + + const removeResult = await runCommand(cwdMainCommand, ["skills", "remove", "--nam", "nope"], { + globalArgs: testGlobalArgs, + captureLogs: true, + }); + expectCommandFailure(removeResult, "Unknown flags: nam"); + expect(existsSync(join(tmp.dir, ".agents/skills/tailor"))).toBe(true); + + const syncResult = await runCommand(cwdMainCommand, ["skills", "sync", "--exlude", "tailor"], { + globalArgs: testGlobalArgs, + captureLogs: true, + }); + expectCommandFailure(syncResult, "Unknown flags: exlude"); + expect(existsSync(join(tmp.dir, ".agents/skills/tailor"))).toBe(true); + }); + + test("propagates default install failures from the skills command", async () => { + using tmp = tempCwd("tailor-skills-command-"); + writeFileSync(join(tmp.dir, "package.json"), "{}\n"); + const cwdMainCommand = await importMainCommandForCurrentCwd(); + mkdirSync(join(tmp.dir, ".agents/skills/tailor"), { recursive: true }); + writeFileSync( + join(tmp.dir, ".agents/skills/tailor/SKILL.md"), + "---\nname: tailor\ndescription: manual\n---\n# Manual\n", + ); + + const result = await runCommand(cwdMainCommand, ["skills"], { + globalArgs: testGlobalArgs, + captureLogs: true, + }); + + expect(result.success).toBe(false); + expectCommandFailure(result, 'Refusing to install "tailor"'); + }); + + test("removes installed Tailor skills", async () => { + using tmp = tempCwd("tailor-skills-command-"); + writeFileSync(join(tmp.dir, "package.json"), "{}\n"); + const cwdMainCommand = await importMainCommandForCurrentCwd(); + + const addResult = await runCommand(cwdMainCommand, ["skills", "add"], { + globalArgs: testGlobalArgs, + captureLogs: true, + }); + expect(addResult.success).toBe(true); + expect(existsSync(join(tmp.dir, ".agents/skills/tailor/SKILL.md"))).toBe(true); + + const removeResult = await runCommand(cwdMainCommand, ["skills", "remove"], { + globalArgs: testGlobalArgs, + captureLogs: true, + }); + expect(removeResult.success).toBe(true); + expect(existsSync(join(tmp.dir, ".agents/skills/tailor"))).toBe(false); + }); +}); diff --git a/skills/tailor/SKILL.md b/skills/tailor/SKILL.md index 68c54ad5b..e1ca701de 100644 --- a/skills/tailor/SKILL.md +++ b/skills/tailor/SKILL.md @@ -1,6 +1,8 @@ --- name: tailor description: Use this skill when working with @tailor-platform/sdk projects, including service configuration, CLI usage, and docs navigation. +metadata: + politty-cli: "@tailor-platform/sdk:tailor" --- # Tailor SDK Knowledge From 4751214c0923e094a844f9ce322279a47e871075 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 12:58:45 +0900 Subject: [PATCH 454/618] feat: rename TailorDB builder to db.table --- .changeset/db-table-builder.md | 18 ++ AGENTS.md | 2 +- docs/architecture.md | 2 +- docs/changeset.md | 2 +- example/analyticsdb/event.ts | 2 +- example/tailordb/customer.ts | 2 +- example/tailordb/invoice.ts | 2 +- example/tailordb/nested.ts | 2 +- example/tailordb/purchaseOrder.ts | 2 +- example/tailordb/salesOrder.ts | 4 +- example/tailordb/selfie.ts | 2 +- example/tailordb/supplier.ts | 2 +- example/tailordb/user.ts | 2 +- example/tailordb/userLog.ts | 2 +- example/tailordb/userSetting.ts | 2 +- .../app/tailordb/salesOrder.ts | 2 +- .../app/tailordb/supplier.ts | 2 +- .../migration-fixtures/app/tailordb/user.ts | 2 +- .../steps/0000/tailordb/salesOrder.ts | 2 +- .../steps/0000/tailordb/supplier.ts | 2 +- .../steps/0000/tailordb/user.ts | 2 +- .../steps/0001/tailordb/salesOrder.ts | 2 +- .../steps/0001/tailordb/supplier.ts | 2 +- .../steps/0001/tailordb/user.ts | 2 +- .../steps/0002/tailordb/salesOrder.ts | 2 +- .../steps/0002/tailordb/supplier.ts | 2 +- .../steps/0002/tailordb/user.ts | 2 +- .../steps/0003/tailordb/salesOrder.ts | 2 +- .../steps/0003/tailordb/supplier.ts | 2 +- .../steps/0003/tailordb/user.ts | 2 +- .../steps/0004/tailordb/salesOrder.ts | 2 +- .../steps/0004/tailordb/supplier.ts | 2 +- .../steps/0004/tailordb/user.ts | 2 +- .../steps/0005/tailordb/salesOrder.ts | 2 +- .../steps/0005/tailordb/supplier.ts | 2 +- .../steps/0005/tailordb/user.ts | 2 +- .../steps/0006/tailordb/salesOrder.ts | 2 +- .../steps/0006/tailordb/supplier.ts | 2 +- .../steps/0006/tailordb/user.ts | 2 +- .../steps/0007/tailordb/salesOrder.ts | 2 +- .../steps/0007/tailordb/supplier.ts | 2 +- .../steps/0007/tailordb/user.ts | 2 +- .../scaffold/tailordb/creator-profile.ts | 2 +- .../templates/executor/src/db/auditLog.ts | 2 +- .../templates/executor/src/db/notification.ts | 2 +- .../templates/executor/src/db/user.ts | 2 +- .../templates/generators/src/db/category.ts | 2 +- .../templates/generators/src/db/order.ts | 2 +- .../templates/generators/src/db/product.ts | 2 +- .../templates/generators/src/db/user.ts | 2 +- .../templates/hello-world/src/db/user.ts | 2 +- .../inventory-management/src/db/category.ts | 2 +- .../inventory-management/src/db/contact.ts | 2 +- .../inventory-management/src/db/inventory.ts | 2 +- .../src/db/notification.ts | 2 +- .../inventory-management/src/db/order.ts | 2 +- .../inventory-management/src/db/orderItem.ts | 2 +- .../inventory-management/src/db/product.ts | 2 +- .../inventory-management/src/db/user.ts | 2 +- .../apps/admin/db/adminNote.ts | 2 +- .../multi-application/apps/user/db/user.ts | 2 +- .../templates/resolver/src/db/user.ts | 2 +- .../templates/static-web-site/src/db/user.ts | 2 +- .../create-sdk/templates/tailordb/README.md | 2 +- .../templates/tailordb/src/db/category.ts | 2 +- .../templates/tailordb/src/db/comment.ts | 2 +- .../templates/tailordb/src/db/task.ts | 2 +- .../templates/tailordb/src/db/user.ts | 2 +- .../templates/workflow/src/db/order.ts | 2 +- .../templates/workflow/src/db/user.ts | 2 +- .../tests/helper-call/expected.ts | 2 +- .../tests/helper-call/input.ts | 2 +- .../multi-import-drops-only-auth/expected.ts | 2 +- .../multi-import-drops-only-auth/input.ts | 2 +- .../tests/multi-import-keeps-auth/expected.ts | 2 +- .../tests/multi-import-keeps-auth/input.ts | 2 +- .../codemods/v2/db-type-to-table/codemod.yaml | 7 + .../v2/db-type-to-table/scripts/transform.ts | 169 +++++++++++++++ .../tests/sdk-import/expected.ts | 34 +++ .../tests/sdk-import/input.ts | 34 +++ .../tests/tailordb-callbacks/expected.ts | 4 +- .../tests/tailordb-callbacks/input.ts | 4 +- packages/sdk-codemod/src/registry.ts | 27 +++ packages/sdk-codemod/src/transform.test.ts | 4 + packages/sdk-codemod/tsdown.config.ts | 1 + packages/sdk/docs/migration/v2.md | 39 ++++ packages/sdk/docs/plugin/custom.md | 8 +- packages/sdk/docs/plugin/index.md | 4 +- packages/sdk/docs/services/auth.md | 4 +- packages/sdk/docs/services/resolver.md | 2 +- .../sdk/docs/services/tailordb-migration.md | 2 +- packages/sdk/docs/services/tailordb.md | 62 +++--- packages/sdk/e2e/deploy.test.ts | 8 +- .../e2e/fixtures/migration/tailordb/post.ts | 2 +- .../e2e/fixtures/migration/tailordb/user.ts | 2 +- packages/sdk/e2e/migration.test.ts | 8 +- .../scripts/perf/features/executor-record.ts | 2 +- .../scripts/perf/features/tailordb-basic.ts | 20 +- .../scripts/perf/features/tailordb-enum.ts | 20 +- .../scripts/perf/features/tailordb-hooks.ts | 20 +- .../scripts/perf/features/tailordb-object.ts | 20 +- .../perf/features/tailordb-optional.ts | 20 +- .../perf/features/tailordb-relation.ts | 22 +- .../perf/features/tailordb-validate.ts | 20 +- .../__test_fixtures__/tailordb/order.ts | 2 +- .../deploy/__test_fixtures__/tailordb/user.ts | 2 +- .../generate/plugin-type-generator.ts | 2 +- .../src/cli/query/type-field-order.test.ts | 4 +- .../sdk/src/cli/services/application.test.ts | 2 +- .../tailordb/hooks-validate-bundler.test.ts | 2 +- .../src/cli/services/tailordb/service.test.ts | 12 +- .../src/configure/services/auth/index.test.ts | 2 +- .../services/executor/executor.test.ts | 2 +- .../services/tailordb/schema.test.ts | 197 ++++++++++-------- .../src/configure/services/tailordb/schema.ts | 46 ++-- .../src/configure/services/tailordb/types.ts | 2 +- .../sdk/src/parser/service/auth/index.test.ts | 2 +- .../tailordb/field.precompiled.test.ts | 4 +- .../src/parser/service/tailordb/field.test.ts | 8 +- .../service/tailordb/type-parser.test.ts | 120 +++++------ .../parser/service/tailordb/type-parser.ts | 2 +- .../builtin/enum-constants/index.test.ts | 8 +- .../plugin/builtin/file-utils/index.test.ts | 12 +- .../plugin/builtin/kysely-type/index.test.ts | 18 +- .../kysely-type/type-processor.test.ts | 46 ++-- packages/sdk/src/plugin/manager.test.ts | 10 +- packages/sdk/src/plugin/manager.ts | 4 +- packages/sdk/src/utils/test/index.test.ts | 38 ++-- packages/sdk/src/utils/test/internal.ts | 12 +- 129 files changed, 823 insertions(+), 475 deletions(-) create mode 100644 .changeset/db-table-builder.md create mode 100644 packages/sdk-codemod/codemods/v2/db-type-to-table/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts diff --git a/.changeset/db-table-builder.md b/.changeset/db-table-builder.md new file mode 100644 index 000000000..5774b3349 --- /dev/null +++ b/.changeset/db-table-builder.md @@ -0,0 +1,18 @@ +--- +"@tailor-platform/sdk": major +"@tailor-platform/create-sdk": major +"@tailor-platform/sdk-codemod": patch +--- + +Rename the TailorDB schema builder from `db.type()` to `db.table()`. + +Update TailorDB definitions: + +```diff + import { db } from "@tailor-platform/sdk"; + +-export const user = db.type("User", { ++export const user = db.table("User", { + name: db.string(), + }); +``` diff --git a/AGENTS.md b/AGENTS.md index 67c36fc0e..75eca3b51 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,7 +73,7 @@ Refer to `example/` for working implementations of all patterns (config, models, Key files: - `example/tailor.config.ts` - Configuration with defineConfig, defineAuth, defineIdp, defineStaticWebSite, defineAIGateway, definePlugins -- `example/tailordb/*.ts` - Model definitions with `db.type()` +- `example/tailordb/*.ts` - Model definitions with `db.table()` - `example/resolvers/*.ts` - Resolver implementations with `createResolver` - `example/executors/*.ts` - Executor implementations with `createExecutor` - `example/workflows/*.ts` - Workflow implementations with `createWorkflow` / `createWorkflowJob` diff --git a/docs/architecture.md b/docs/architecture.md index f5f729a98..2f52f7bc7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -16,7 +16,7 @@ The main `packages/sdk` has four core modules: ``` src/ -├── configure/ User-facing API (defineConfig, db.type, createResolver, ...) +├── configure/ User-facing API (defineConfig, db.table, createResolver, ...) ├── parser/ Validation & transformation layer (Zod schemas → internal types) ├── cli/ CLI commands, bundling, deployment └── plugin/ Plugin system (manager + built-in plugins) diff --git a/docs/changeset.md b/docs/changeset.md index d5995c03c..d0facd955 100644 --- a/docs/changeset.md +++ b/docs/changeset.md @@ -24,7 +24,7 @@ The key question for choosing a level: **How does this change affect SDK users?* SDK users must modify their code or configuration to upgrade. -- Removing or renaming a public API (`defineConfig`, `db.type`, `createResolver`, etc.) +- Removing or renaming a public API (`defineConfig`, `db.table`, `createResolver`, etc.) - Changing the signature or behavior of an existing API in an incompatible way - Changing `tailor.config.ts` format in a way that requires migration - Raising the minimum Node.js version diff --git a/example/analyticsdb/event.ts b/example/analyticsdb/event.ts index c95248f3b..76369e80c 100644 --- a/example/analyticsdb/event.ts +++ b/example/analyticsdb/event.ts @@ -1,7 +1,7 @@ import { db } from "@tailor-platform/sdk"; export const event = db - .type("Event", { + .table("Event", { name: db.enum(["CLICK", "VIEW", "PURCHASE"]), ...db.fields.timestamps(), }) diff --git a/example/tailordb/customer.ts b/example/tailordb/customer.ts index efde41cb1..f12a7fb4d 100644 --- a/example/tailordb/customer.ts +++ b/example/tailordb/customer.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "./permissions"; export const customer = db - .type("Customer", "Customer information", { + .table("Customer", "Customer information", { name: db.string(), email: db.string(), phone: db.string({ optional: true }), diff --git a/example/tailordb/invoice.ts b/example/tailordb/invoice.ts index 04963b2cc..d019b0d7e 100644 --- a/example/tailordb/invoice.ts +++ b/example/tailordb/invoice.ts @@ -3,7 +3,7 @@ import { defaultGqlPermission, defaultPermission } from "./permissions"; import { salesOrder } from "./salesOrder"; export const invoice = db - .type("Invoice", { + .table("Invoice", { invoiceNumber: db.string().serial({ start: 1000, format: "INV-%05d", diff --git a/example/tailordb/nested.ts b/example/tailordb/nested.ts index b3fa800ba..c39106055 100644 --- a/example/tailordb/nested.ts +++ b/example/tailordb/nested.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "./permissions"; export const nestedProfile = db - .type("NestedProfile", "Nested Profile Type", { + .table("NestedProfile", "Nested Profile Type", { userInfo: db .object({ name: db.string().description("User's full name"), diff --git a/example/tailordb/purchaseOrder.ts b/example/tailordb/purchaseOrder.ts index 72d636943..faae74ebf 100644 --- a/example/tailordb/purchaseOrder.ts +++ b/example/tailordb/purchaseOrder.ts @@ -4,7 +4,7 @@ import { defaultGqlPermission, defaultPermission } from "./permissions"; import { supplier } from "./supplier"; export const purchaseOrder = db - .type(["PurchaseOrder", "PurchaseOrderList"], { + .table(["PurchaseOrder", "PurchaseOrderList"], { supplierID: db.uuid().relation({ type: "n-1", toward: { type: supplier }, diff --git a/example/tailordb/salesOrder.ts b/example/tailordb/salesOrder.ts index 2f79c8c87..d5ab96284 100644 --- a/example/tailordb/salesOrder.ts +++ b/example/tailordb/salesOrder.ts @@ -4,7 +4,7 @@ import { defaultPermission, defaultGqlPermission } from "./permissions"; import { user } from "./user"; export const salesOrder = db - .type(["SalesOrder", "SalesOrderList"], { + .table(["SalesOrder", "SalesOrderList"], { customerID: db.uuid().relation({ type: "n-1", toward: { type: customer }, @@ -32,7 +32,7 @@ export const salesOrder = db .gqlPermission(defaultGqlPermission); export const salesOrderCreated = db - .type(["SalesOrderCreated", "SalesOrderCreatedList"], { + .table(["SalesOrderCreated", "SalesOrderCreatedList"], { salesOrderID: db.uuid(), customerID: db.uuid(), totalPrice: db.int({ optional: true }), diff --git a/example/tailordb/selfie.ts b/example/tailordb/selfie.ts index 088007b73..bf4247c05 100644 --- a/example/tailordb/selfie.ts +++ b/example/tailordb/selfie.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "./permissions"; export const selfie = db - .type("Selfie", { + .table("Selfie", { name: db.string(), parentID: db.uuid({ optional: true }).relation({ type: "n-1", diff --git a/example/tailordb/supplier.ts b/example/tailordb/supplier.ts index 6706c23c5..85730c370 100644 --- a/example/tailordb/supplier.ts +++ b/example/tailordb/supplier.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "./permissions"; export const supplier = db - .type("Supplier", { + .table("Supplier", { name: db.string(), phone: db.string(), fax: db.string({ optional: true }), diff --git a/example/tailordb/user.ts b/example/tailordb/user.ts index 1bc50a0f9..fcb053bbd 100644 --- a/example/tailordb/user.ts +++ b/example/tailordb/user.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "./permissions"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string().unique(), status: db.string({ optional: true }), diff --git a/example/tailordb/userLog.ts b/example/tailordb/userLog.ts index 6da54b6b6..bd1f7560b 100644 --- a/example/tailordb/userLog.ts +++ b/example/tailordb/userLog.ts @@ -3,7 +3,7 @@ import { defaultGqlPermission, defaultPermission } from "./permissions"; import { user } from "./user"; export const userLog = db - .type("UserLog", { + .table("UserLog", { userID: db.uuid().relation({ type: "n-1", toward: { type: user } }), message: db.string(), ...db.fields.timestamps(), diff --git a/example/tailordb/userSetting.ts b/example/tailordb/userSetting.ts index 0f354e607..7d75c86a4 100644 --- a/example/tailordb/userSetting.ts +++ b/example/tailordb/userSetting.ts @@ -3,7 +3,7 @@ import { defaultGqlPermission, defaultPermission } from "./permissions"; import { user } from "./user"; export const userSetting = db - .type("UserSetting", { + .table("UserSetting", { language: db.enum(["jp", "en"]), userID: db.uuid().relation({ type: "1-1", diff --git a/example/tests/migration-fixtures/app/tailordb/salesOrder.ts b/example/tests/migration-fixtures/app/tailordb/salesOrder.ts index 3f6891755..6bf2082d4 100644 --- a/example/tests/migration-fixtures/app/tailordb/salesOrder.ts +++ b/example/tests/migration-fixtures/app/tailordb/salesOrder.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const salesOrder = db - .type("SalesOrder", { + .table("SalesOrder", { customerID: db.uuid(), status: db.string({ optional: true }), totalPrice: db.int({ optional: true }), diff --git a/example/tests/migration-fixtures/app/tailordb/supplier.ts b/example/tests/migration-fixtures/app/tailordb/supplier.ts index ba1019ca9..d629a21ef 100644 --- a/example/tests/migration-fixtures/app/tailordb/supplier.ts +++ b/example/tests/migration-fixtures/app/tailordb/supplier.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const supplier = db - .type("Supplier", { + .table("Supplier", { name: db.string({ optional: true }), country: db.string({ optional: true }), phone: db.string(), diff --git a/example/tests/migration-fixtures/app/tailordb/user.ts b/example/tests/migration-fixtures/app/tailordb/user.ts index d70b9ade4..ddd101a91 100644 --- a/example/tests/migration-fixtures/app/tailordb/user.ts +++ b/example/tests/migration-fixtures/app/tailordb/user.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string(), ...db.fields.timestamps(), diff --git a/example/tests/migration-fixtures/steps/0000/tailordb/salesOrder.ts b/example/tests/migration-fixtures/steps/0000/tailordb/salesOrder.ts index 3f6891755..6bf2082d4 100644 --- a/example/tests/migration-fixtures/steps/0000/tailordb/salesOrder.ts +++ b/example/tests/migration-fixtures/steps/0000/tailordb/salesOrder.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const salesOrder = db - .type("SalesOrder", { + .table("SalesOrder", { customerID: db.uuid(), status: db.string({ optional: true }), totalPrice: db.int({ optional: true }), diff --git a/example/tests/migration-fixtures/steps/0000/tailordb/supplier.ts b/example/tests/migration-fixtures/steps/0000/tailordb/supplier.ts index ba1019ca9..d629a21ef 100644 --- a/example/tests/migration-fixtures/steps/0000/tailordb/supplier.ts +++ b/example/tests/migration-fixtures/steps/0000/tailordb/supplier.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const supplier = db - .type("Supplier", { + .table("Supplier", { name: db.string({ optional: true }), country: db.string({ optional: true }), phone: db.string(), diff --git a/example/tests/migration-fixtures/steps/0000/tailordb/user.ts b/example/tests/migration-fixtures/steps/0000/tailordb/user.ts index d70b9ade4..ddd101a91 100644 --- a/example/tests/migration-fixtures/steps/0000/tailordb/user.ts +++ b/example/tests/migration-fixtures/steps/0000/tailordb/user.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string(), ...db.fields.timestamps(), diff --git a/example/tests/migration-fixtures/steps/0001/tailordb/salesOrder.ts b/example/tests/migration-fixtures/steps/0001/tailordb/salesOrder.ts index 3f6891755..6bf2082d4 100644 --- a/example/tests/migration-fixtures/steps/0001/tailordb/salesOrder.ts +++ b/example/tests/migration-fixtures/steps/0001/tailordb/salesOrder.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const salesOrder = db - .type("SalesOrder", { + .table("SalesOrder", { customerID: db.uuid(), status: db.string({ optional: true }), totalPrice: db.int({ optional: true }), diff --git a/example/tests/migration-fixtures/steps/0001/tailordb/supplier.ts b/example/tests/migration-fixtures/steps/0001/tailordb/supplier.ts index ba1019ca9..d629a21ef 100644 --- a/example/tests/migration-fixtures/steps/0001/tailordb/supplier.ts +++ b/example/tests/migration-fixtures/steps/0001/tailordb/supplier.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const supplier = db - .type("Supplier", { + .table("Supplier", { name: db.string({ optional: true }), country: db.string({ optional: true }), phone: db.string(), diff --git a/example/tests/migration-fixtures/steps/0001/tailordb/user.ts b/example/tests/migration-fixtures/steps/0001/tailordb/user.ts index fd40638d8..cf63affb9 100644 --- a/example/tests/migration-fixtures/steps/0001/tailordb/user.ts +++ b/example/tests/migration-fixtures/steps/0001/tailordb/user.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string(), status: db.string({ optional: true }), diff --git a/example/tests/migration-fixtures/steps/0002/tailordb/salesOrder.ts b/example/tests/migration-fixtures/steps/0002/tailordb/salesOrder.ts index 3f6891755..6bf2082d4 100644 --- a/example/tests/migration-fixtures/steps/0002/tailordb/salesOrder.ts +++ b/example/tests/migration-fixtures/steps/0002/tailordb/salesOrder.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const salesOrder = db - .type("SalesOrder", { + .table("SalesOrder", { customerID: db.uuid(), status: db.string({ optional: true }), totalPrice: db.int({ optional: true }), diff --git a/example/tests/migration-fixtures/steps/0002/tailordb/supplier.ts b/example/tests/migration-fixtures/steps/0002/tailordb/supplier.ts index ba1019ca9..d629a21ef 100644 --- a/example/tests/migration-fixtures/steps/0002/tailordb/supplier.ts +++ b/example/tests/migration-fixtures/steps/0002/tailordb/supplier.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const supplier = db - .type("Supplier", { + .table("Supplier", { name: db.string({ optional: true }), country: db.string({ optional: true }), phone: db.string(), diff --git a/example/tests/migration-fixtures/steps/0002/tailordb/user.ts b/example/tests/migration-fixtures/steps/0002/tailordb/user.ts index f3872104b..992f421f7 100644 --- a/example/tests/migration-fixtures/steps/0002/tailordb/user.ts +++ b/example/tests/migration-fixtures/steps/0002/tailordb/user.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string(), status: db.string({ optional: true }), diff --git a/example/tests/migration-fixtures/steps/0003/tailordb/salesOrder.ts b/example/tests/migration-fixtures/steps/0003/tailordb/salesOrder.ts index 3f6891755..6bf2082d4 100644 --- a/example/tests/migration-fixtures/steps/0003/tailordb/salesOrder.ts +++ b/example/tests/migration-fixtures/steps/0003/tailordb/salesOrder.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const salesOrder = db - .type("SalesOrder", { + .table("SalesOrder", { customerID: db.uuid(), status: db.string({ optional: true }), totalPrice: db.int({ optional: true }), diff --git a/example/tests/migration-fixtures/steps/0003/tailordb/supplier.ts b/example/tests/migration-fixtures/steps/0003/tailordb/supplier.ts index ba1019ca9..d629a21ef 100644 --- a/example/tests/migration-fixtures/steps/0003/tailordb/supplier.ts +++ b/example/tests/migration-fixtures/steps/0003/tailordb/supplier.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const supplier = db - .type("Supplier", { + .table("Supplier", { name: db.string({ optional: true }), country: db.string({ optional: true }), phone: db.string(), diff --git a/example/tests/migration-fixtures/steps/0003/tailordb/user.ts b/example/tests/migration-fixtures/steps/0003/tailordb/user.ts index 288e6bd53..f403469b4 100644 --- a/example/tests/migration-fixtures/steps/0003/tailordb/user.ts +++ b/example/tests/migration-fixtures/steps/0003/tailordb/user.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string(), status: db.string({ optional: true }), diff --git a/example/tests/migration-fixtures/steps/0004/tailordb/salesOrder.ts b/example/tests/migration-fixtures/steps/0004/tailordb/salesOrder.ts index 3f6891755..6bf2082d4 100644 --- a/example/tests/migration-fixtures/steps/0004/tailordb/salesOrder.ts +++ b/example/tests/migration-fixtures/steps/0004/tailordb/salesOrder.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const salesOrder = db - .type("SalesOrder", { + .table("SalesOrder", { customerID: db.uuid(), status: db.string({ optional: true }), totalPrice: db.int({ optional: true }), diff --git a/example/tests/migration-fixtures/steps/0004/tailordb/supplier.ts b/example/tests/migration-fixtures/steps/0004/tailordb/supplier.ts index ba1019ca9..d629a21ef 100644 --- a/example/tests/migration-fixtures/steps/0004/tailordb/supplier.ts +++ b/example/tests/migration-fixtures/steps/0004/tailordb/supplier.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const supplier = db - .type("Supplier", { + .table("Supplier", { name: db.string({ optional: true }), country: db.string({ optional: true }), phone: db.string(), diff --git a/example/tests/migration-fixtures/steps/0004/tailordb/user.ts b/example/tests/migration-fixtures/steps/0004/tailordb/user.ts index a98e08378..069ca98ce 100644 --- a/example/tests/migration-fixtures/steps/0004/tailordb/user.ts +++ b/example/tests/migration-fixtures/steps/0004/tailordb/user.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string(), status: db.string({ optional: true }), diff --git a/example/tests/migration-fixtures/steps/0005/tailordb/salesOrder.ts b/example/tests/migration-fixtures/steps/0005/tailordb/salesOrder.ts index 1a88e6655..da886b93e 100644 --- a/example/tests/migration-fixtures/steps/0005/tailordb/salesOrder.ts +++ b/example/tests/migration-fixtures/steps/0005/tailordb/salesOrder.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const salesOrder = db - .type("SalesOrder", { + .table("SalesOrder", { customerID: db.uuid(), status: db.string({ optional: true }), totalPrice: db.int({ optional: true }), diff --git a/example/tests/migration-fixtures/steps/0005/tailordb/supplier.ts b/example/tests/migration-fixtures/steps/0005/tailordb/supplier.ts index d04c84bd5..29363494a 100644 --- a/example/tests/migration-fixtures/steps/0005/tailordb/supplier.ts +++ b/example/tests/migration-fixtures/steps/0005/tailordb/supplier.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const supplier = db - .type("Supplier", { + .table("Supplier", { name: db.string(), country: db.string(), phone: db.string(), diff --git a/example/tests/migration-fixtures/steps/0005/tailordb/user.ts b/example/tests/migration-fixtures/steps/0005/tailordb/user.ts index 5da16e6e4..f0c68fa5d 100644 --- a/example/tests/migration-fixtures/steps/0005/tailordb/user.ts +++ b/example/tests/migration-fixtures/steps/0005/tailordb/user.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string(), status: db.string({ optional: true }), diff --git a/example/tests/migration-fixtures/steps/0006/tailordb/salesOrder.ts b/example/tests/migration-fixtures/steps/0006/tailordb/salesOrder.ts index 1a88e6655..da886b93e 100644 --- a/example/tests/migration-fixtures/steps/0006/tailordb/salesOrder.ts +++ b/example/tests/migration-fixtures/steps/0006/tailordb/salesOrder.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const salesOrder = db - .type("SalesOrder", { + .table("SalesOrder", { customerID: db.uuid(), status: db.string({ optional: true }), totalPrice: db.int({ optional: true }), diff --git a/example/tests/migration-fixtures/steps/0006/tailordb/supplier.ts b/example/tests/migration-fixtures/steps/0006/tailordb/supplier.ts index d04c84bd5..29363494a 100644 --- a/example/tests/migration-fixtures/steps/0006/tailordb/supplier.ts +++ b/example/tests/migration-fixtures/steps/0006/tailordb/supplier.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const supplier = db - .type("Supplier", { + .table("Supplier", { name: db.string(), country: db.string(), phone: db.string(), diff --git a/example/tests/migration-fixtures/steps/0006/tailordb/user.ts b/example/tests/migration-fixtures/steps/0006/tailordb/user.ts index e4e6fd5a0..e8a614be8 100644 --- a/example/tests/migration-fixtures/steps/0006/tailordb/user.ts +++ b/example/tests/migration-fixtures/steps/0006/tailordb/user.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const user = db - .type("User", { + .table("User", { name: db.string().index().unique(), email: db.string(), status: db.string({ optional: true }), diff --git a/example/tests/migration-fixtures/steps/0007/tailordb/salesOrder.ts b/example/tests/migration-fixtures/steps/0007/tailordb/salesOrder.ts index 1a88e6655..da886b93e 100644 --- a/example/tests/migration-fixtures/steps/0007/tailordb/salesOrder.ts +++ b/example/tests/migration-fixtures/steps/0007/tailordb/salesOrder.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const salesOrder = db - .type("SalesOrder", { + .table("SalesOrder", { customerID: db.uuid(), status: db.string({ optional: true }), totalPrice: db.int({ optional: true }), diff --git a/example/tests/migration-fixtures/steps/0007/tailordb/supplier.ts b/example/tests/migration-fixtures/steps/0007/tailordb/supplier.ts index d04c84bd5..29363494a 100644 --- a/example/tests/migration-fixtures/steps/0007/tailordb/supplier.ts +++ b/example/tests/migration-fixtures/steps/0007/tailordb/supplier.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const supplier = db - .type("Supplier", { + .table("Supplier", { name: db.string(), country: db.string(), phone: db.string(), diff --git a/example/tests/migration-fixtures/steps/0007/tailordb/user.ts b/example/tests/migration-fixtures/steps/0007/tailordb/user.ts index 4f95072b1..c5496b60c 100644 --- a/example/tests/migration-fixtures/steps/0007/tailordb/user.ts +++ b/example/tests/migration-fixtures/steps/0007/tailordb/user.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { defaultGqlPermission, defaultPermission } from "../permissions"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string(), status: db.string({ optional: true }), diff --git a/llm-challenge/problems/sdk-api/tailordb-array-unique-recovery/scaffold/tailordb/creator-profile.ts b/llm-challenge/problems/sdk-api/tailordb-array-unique-recovery/scaffold/tailordb/creator-profile.ts index b05beec54..9b31b25f3 100644 --- a/llm-challenge/problems/sdk-api/tailordb-array-unique-recovery/scaffold/tailordb/creator-profile.ts +++ b/llm-challenge/problems/sdk-api/tailordb-array-unique-recovery/scaffold/tailordb/creator-profile.ts @@ -1,7 +1,7 @@ import { db } from "@tailor-platform/sdk"; export const CreatorProfile = db - .type("CreatorProfile", { + .table("CreatorProfile", { handle: db.string().unique(), displayName: db.string(), tags: db.string({ array: true }).unique(), diff --git a/packages/create-sdk/templates/executor/src/db/auditLog.ts b/packages/create-sdk/templates/executor/src/db/auditLog.ts index 474175e33..0dca43a79 100644 --- a/packages/create-sdk/templates/executor/src/db/auditLog.ts +++ b/packages/create-sdk/templates/executor/src/db/auditLog.ts @@ -1,7 +1,7 @@ import { db } from "@tailor-platform/sdk"; export const auditLog = db - .type("AuditLog", "Records system events for auditing", { + .table("AuditLog", "Records system events for auditing", { action: db.string(), entityType: db.string(), entityId: db.uuid(), diff --git a/packages/create-sdk/templates/executor/src/db/notification.ts b/packages/create-sdk/templates/executor/src/db/notification.ts index 5df5b2398..17ca1474a 100644 --- a/packages/create-sdk/templates/executor/src/db/notification.ts +++ b/packages/create-sdk/templates/executor/src/db/notification.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { user } from "./user"; export const notification = db - .type("Notification", { + .table("Notification", { userId: db.uuid().relation({ type: "n-1", toward: { type: user }, diff --git a/packages/create-sdk/templates/executor/src/db/user.ts b/packages/create-sdk/templates/executor/src/db/user.ts index bed22445a..2b03277c8 100644 --- a/packages/create-sdk/templates/executor/src/db/user.ts +++ b/packages/create-sdk/templates/executor/src/db/user.ts @@ -1,7 +1,7 @@ import { db } from "@tailor-platform/sdk"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string().unique(), role: db.enum(["ADMIN", "MEMBER"]), diff --git a/packages/create-sdk/templates/generators/src/db/category.ts b/packages/create-sdk/templates/generators/src/db/category.ts index e37d91bad..5c7fbef5d 100644 --- a/packages/create-sdk/templates/generators/src/db/category.ts +++ b/packages/create-sdk/templates/generators/src/db/category.ts @@ -1,7 +1,7 @@ import { db } from "@tailor-platform/sdk"; export const category = db - .type("Category", { + .table("Category", { name: db.string(), slug: db.string().unique(), parentCategoryId: db.uuid({ optional: true }).relation({ diff --git a/packages/create-sdk/templates/generators/src/db/order.ts b/packages/create-sdk/templates/generators/src/db/order.ts index c01b0fd7f..36f0a8b05 100644 --- a/packages/create-sdk/templates/generators/src/db/order.ts +++ b/packages/create-sdk/templates/generators/src/db/order.ts @@ -3,7 +3,7 @@ import { product } from "./product"; import { user } from "./user"; export const order = db - .type("Order", { + .table("Order", { productId: db.uuid().relation({ type: "n-1", toward: { type: product }, diff --git a/packages/create-sdk/templates/generators/src/db/product.ts b/packages/create-sdk/templates/generators/src/db/product.ts index bfb0a44c2..81a50a080 100644 --- a/packages/create-sdk/templates/generators/src/db/product.ts +++ b/packages/create-sdk/templates/generators/src/db/product.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { category } from "./category"; export const product = db - .type("Product", { + .table("Product", { name: db.string(), description: db.string({ optional: true }), price: db.float(), diff --git a/packages/create-sdk/templates/generators/src/db/user.ts b/packages/create-sdk/templates/generators/src/db/user.ts index d4d54e9b5..82e6ead69 100644 --- a/packages/create-sdk/templates/generators/src/db/user.ts +++ b/packages/create-sdk/templates/generators/src/db/user.ts @@ -1,7 +1,7 @@ import { db } from "@tailor-platform/sdk"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string().unique(), role: db.enum([ diff --git a/packages/create-sdk/templates/hello-world/src/db/user.ts b/packages/create-sdk/templates/hello-world/src/db/user.ts index fc4c0ffa1..0916904d5 100644 --- a/packages/create-sdk/templates/hello-world/src/db/user.ts +++ b/packages/create-sdk/templates/hello-world/src/db/user.ts @@ -5,7 +5,7 @@ import { } from "@tailor-platform/sdk"; export const user = db - .type("User", { + .table("User", { name: db.string().description("Name of the user"), email: db.string().unique().description("Email address of the user"), role: db.enum(["MANAGER", "STAFF"]), diff --git a/packages/create-sdk/templates/inventory-management/src/db/category.ts b/packages/create-sdk/templates/inventory-management/src/db/category.ts index 66286d51c..cd641bd95 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/category.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/category.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { gqlPermissionManager, permissionManager } from "./common/permission"; export const category = db - .type("Category", { + .table("Category", { name: db.string().description("Name of the category").unique(), description: db.string({ optional: true }).description("Description of the category"), ...db.fields.timestamps(), diff --git a/packages/create-sdk/templates/inventory-management/src/db/contact.ts b/packages/create-sdk/templates/inventory-management/src/db/contact.ts index f6df8e5b0..5c04231b3 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/contact.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/contact.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { gqlPermissionManager, permissionManager } from "./common/permission"; export const contact = db - .type("Contact", { + .table("Contact", { name: db.string().description("Name of the contact"), email: db.string().unique().description("Email address of the contact"), phone: db.string({ optional: true }).description("Phone number of the contact"), diff --git a/packages/create-sdk/templates/inventory-management/src/db/inventory.ts b/packages/create-sdk/templates/inventory-management/src/db/inventory.ts index 6deb5d780..7c04a1191 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/inventory.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/inventory.ts @@ -3,7 +3,7 @@ import { gqlPermissionLoggedIn, permissionLoggedIn } from "./common/permission"; import { product } from "./product"; export const inventory = db - .type("Inventory", { + .table("Inventory", { productId: db .uuid() .description("ID of the product") diff --git a/packages/create-sdk/templates/inventory-management/src/db/notification.ts b/packages/create-sdk/templates/inventory-management/src/db/notification.ts index 12ead23d6..4383cd93c 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/notification.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/notification.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { loggedIn, managerRole, permissionManager } from "./common/permission"; export const notification = db - .type("Notification", { + .table("Notification", { message: db.string().description("Notification message"), ...db.fields.timestamps(), }) diff --git a/packages/create-sdk/templates/inventory-management/src/db/order.ts b/packages/create-sdk/templates/inventory-management/src/db/order.ts index 4131e281d..cd518df5b 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/order.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/order.ts @@ -3,7 +3,7 @@ import { contact } from "./contact"; import { gqlPermissionLoggedIn, permissionLoggedIn } from "./common/permission"; export const order = db - .type("Order", { + .table("Order", { name: db.string().description("Name of the order"), description: db.string({ optional: true }).description("Description of the order"), orderDate: db.datetime().description("Date of the order"), diff --git a/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts b/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts index 2fc8c572e..ffdd2ebc5 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts @@ -4,7 +4,7 @@ import { product } from "./product"; import { gqlPermissionLoggedIn, permissionLoggedIn } from "./common/permission"; export const orderItem = db - .type("OrderItem", { + .table("OrderItem", { orderId: db .uuid() .description("ID of the order") diff --git a/packages/create-sdk/templates/inventory-management/src/db/product.ts b/packages/create-sdk/templates/inventory-management/src/db/product.ts index d7c980797..bb60e1e4d 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/product.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/product.ts @@ -3,7 +3,7 @@ import { category } from "./category"; import { gqlPermissionManager, permissionManager } from "./common/permission"; export const product = db - .type("Product", { + .table("Product", { name: db.string().description("Name of the product"), description: db.string({ optional: true }).description("Description of the product"), categoryId: db diff --git a/packages/create-sdk/templates/inventory-management/src/db/user.ts b/packages/create-sdk/templates/inventory-management/src/db/user.ts index 0ca4c5bbe..5a83c5b77 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/user.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/user.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { gqlPermissionManager, permissionManager } from "./common/permission"; export const user = db - .type("User", { + .table("User", { name: db.string().description("Name of the user"), email: db.string().unique().description("Email address of the user"), role: db.enum(["MANAGER", "STAFF"]), diff --git a/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts b/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts index 0a39ee828..047f39a86 100644 --- a/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts +++ b/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts @@ -5,7 +5,7 @@ import { } from "@tailor-platform/sdk"; export const adminNote = db - .type("AdminNote", { + .table("AdminNote", { title: db.string(), content: db.string(), authorId: db.uuid().hooks({ create: ({ invoker }) => invoker?.id ?? crypto.randomUUID() }), diff --git a/packages/create-sdk/templates/multi-application/apps/user/db/user.ts b/packages/create-sdk/templates/multi-application/apps/user/db/user.ts index a070dc24a..2e4acc659 100644 --- a/packages/create-sdk/templates/multi-application/apps/user/db/user.ts +++ b/packages/create-sdk/templates/multi-application/apps/user/db/user.ts @@ -5,7 +5,7 @@ import { } from "@tailor-platform/sdk"; export const user = db - .type("User", { + .table("User", { name: db.string().unique(), role: db.enum(["USER", "ADMIN"]), ...db.fields.timestamps(), diff --git a/packages/create-sdk/templates/resolver/src/db/user.ts b/packages/create-sdk/templates/resolver/src/db/user.ts index 87ee4bd63..436b16d2f 100644 --- a/packages/create-sdk/templates/resolver/src/db/user.ts +++ b/packages/create-sdk/templates/resolver/src/db/user.ts @@ -1,7 +1,7 @@ import { db } from "@tailor-platform/sdk"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string().unique(), age: db.int(), diff --git a/packages/create-sdk/templates/static-web-site/src/db/user.ts b/packages/create-sdk/templates/static-web-site/src/db/user.ts index bed22445a..2b03277c8 100644 --- a/packages/create-sdk/templates/static-web-site/src/db/user.ts +++ b/packages/create-sdk/templates/static-web-site/src/db/user.ts @@ -1,7 +1,7 @@ import { db } from "@tailor-platform/sdk"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string().unique(), role: db.enum(["ADMIN", "MEMBER"]), diff --git a/packages/create-sdk/templates/tailordb/README.md b/packages/create-sdk/templates/tailordb/README.md index 35649defc..b936f3fd4 100644 --- a/packages/create-sdk/templates/tailordb/README.md +++ b/packages/create-sdk/templates/tailordb/README.md @@ -1,6 +1,6 @@ # TailorDB Template -Comprehensive TailorDB type definitions demonstrating all features of `db.type()`. +Comprehensive TailorDB type definitions demonstrating all features of `db.table()`. ## Features diff --git a/packages/create-sdk/templates/tailordb/src/db/category.ts b/packages/create-sdk/templates/tailordb/src/db/category.ts index 37e97c585..280288a69 100644 --- a/packages/create-sdk/templates/tailordb/src/db/category.ts +++ b/packages/create-sdk/templates/tailordb/src/db/category.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { allPermission, allGqlPermission } from "./permission"; export const category = db - .type("Category", "Task category with hierarchical structure", { + .table("Category", "Task category with hierarchical structure", { name: db.string(), description: db.string({ optional: true }), parentCategoryId: db.uuid({ optional: true }).relation({ diff --git a/packages/create-sdk/templates/tailordb/src/db/comment.ts b/packages/create-sdk/templates/tailordb/src/db/comment.ts index 5f8f18067..382e62e6c 100644 --- a/packages/create-sdk/templates/tailordb/src/db/comment.ts +++ b/packages/create-sdk/templates/tailordb/src/db/comment.ts @@ -4,7 +4,7 @@ import { task } from "./task"; import { user } from "./user"; export const comment = db - .type("Comment", "A comment on a task", { + .table("Comment", "A comment on a task", { body: db.string().validate([({ value }) => value.length >= 1, "Comment must not be empty"]), taskId: db.uuid().relation({ type: "n-1", diff --git a/packages/create-sdk/templates/tailordb/src/db/task.ts b/packages/create-sdk/templates/tailordb/src/db/task.ts index 4e4a56143..ce98b737e 100644 --- a/packages/create-sdk/templates/tailordb/src/db/task.ts +++ b/packages/create-sdk/templates/tailordb/src/db/task.ts @@ -4,7 +4,7 @@ import { rolePermission, roleGqlPermission } from "./permission"; import { user } from "./user"; export const task = db - .type("Task", "A task with comprehensive features", { + .table("Task", "A task with comprehensive features", { title: db .string() .validate( diff --git a/packages/create-sdk/templates/tailordb/src/db/user.ts b/packages/create-sdk/templates/tailordb/src/db/user.ts index 0f459b6c3..741e8b7d5 100644 --- a/packages/create-sdk/templates/tailordb/src/db/user.ts +++ b/packages/create-sdk/templates/tailordb/src/db/user.ts @@ -2,7 +2,7 @@ import { db } from "@tailor-platform/sdk"; import { allPermission, allGqlPermission } from "./permission"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string().unique(), role: db.enum([ diff --git a/packages/create-sdk/templates/workflow/src/db/order.ts b/packages/create-sdk/templates/workflow/src/db/order.ts index d163f811f..909bbf423 100644 --- a/packages/create-sdk/templates/workflow/src/db/order.ts +++ b/packages/create-sdk/templates/workflow/src/db/order.ts @@ -1,7 +1,7 @@ import { db } from "@tailor-platform/sdk"; export const order = db - .type("Order", { + .table("Order", { customerName: db.string(), amount: db.int(), status: db.enum(["PENDING", "PROCESSING", "COMPLETED", "FAILED"]), diff --git a/packages/create-sdk/templates/workflow/src/db/user.ts b/packages/create-sdk/templates/workflow/src/db/user.ts index 87ee4bd63..436b16d2f 100644 --- a/packages/create-sdk/templates/workflow/src/db/user.ts +++ b/packages/create-sdk/templates/workflow/src/db/user.ts @@ -1,7 +1,7 @@ import { db } from "@tailor-platform/sdk"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string().unique(), age: db.int(), diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/expected.ts index 7d62ccf47..f4a2e2294 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/expected.ts @@ -4,5 +4,5 @@ createResolver({ name: "orders", operation: "query", authInvoker: "kiosk", - body: () => db.type("Order"), + body: () => db.table("Order"), }); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/input.ts index 6a340cdae..785057c3c 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-call-unwrap/tests/helper-call/input.ts @@ -4,5 +4,5 @@ createResolver({ name: "orders", operation: "query", authInvoker: auth.invoker("kiosk"), - body: () => db.type("Order"), + body: () => db.table("Order"), }); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts index 8d7d81e0f..717436393 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts @@ -4,5 +4,5 @@ createResolver({ name: "orders", operation: "query", invoker: "kiosk", - body: () => db.type("Order"), + body: () => db.table("Order"), }); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/input.ts index 6a340cdae..785057c3c 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/input.ts @@ -4,5 +4,5 @@ createResolver({ name: "orders", operation: "query", authInvoker: auth.invoker("kiosk"), - body: () => db.type("Order"), + body: () => db.table("Order"), }); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts index be541bbe7..864acf8ce 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts @@ -6,5 +6,5 @@ createResolver({ invoker: "kiosk", // `auth` is still referenced below, so the import must be preserved. ownerType: auth.machineUser, - body: () => db.type("Order"), + body: () => db.table("Order"), }); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/input.ts index 5be04d7f4..7fdee6353 100644 --- a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/input.ts +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/input.ts @@ -6,5 +6,5 @@ createResolver({ authInvoker: auth.invoker("kiosk"), // `auth` is still referenced below, so the import must be preserved. ownerType: auth.machineUser, - body: () => db.type("Order"), + body: () => db.table("Order"), }); diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/codemod.yaml b/packages/sdk-codemod/codemods/v2/db-type-to-table/codemod.yaml new file mode 100644 index 000000000..8635334f1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/db-type-to-table" +version: "1.0.0" +description: "Rename TailorDB schema builder calls from db.type() to db.table()" +engine: jssg +language: typescript +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts new file mode 100644 index 000000000..6f4097483 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts @@ -0,0 +1,169 @@ +import { parse, Lang } from "@ast-grep/napi"; +import { + findImportStatements, + importBindings, + importSource, +} from "../../../../src/ast-grep-helpers"; +import type { Edit, SgNode } from "@ast-grep/napi"; + +const SDK_MODULE = "@tailor-platform/sdk"; + +function sourceLang(filePath: string, source: string): Lang { + return filePath.endsWith(".tsx") || filePath.endsWith(".jsx") || source.includes(" node.children().filter((child) => child.kind() === "identifier")) + .map((node) => node.text()); +} + +function isInsideImportStatement(node: SgNode): boolean { + let current = node.parent(); + while (current) { + if (current.kind() === "import_statement") return true; + current = current.parent(); + } + return false; +} + +function addShadowedRange( + shadowedRanges: Map>, + name: string, + scopeNode: SgNode, +) { + const range = scopeNode.range(); + if (!shadowedRanges.has(name)) shadowedRanges.set(name, []); + shadowedRanges.get(name)!.push({ start: range.start.index, end: range.end.index }); +} + +function nearestScope(node: SgNode): SgNode { + let current: SgNode | null = node.parent(); + while (current) { + const kind = current.kind(); + if (kind === "statement_block" || kind === "program" || kind === "for_statement") { + return current; + } + current = current.parent(); + } + return node; +} + +function parameterScope(node: SgNode): SgNode { + let current: SgNode | null = node.parent(); + while (current) { + const kind = current.kind(); + if (kind === "formal_parameters") { + current = current.parent(); + continue; + } + if ( + kind === "function_declaration" || + kind === "function_expression" || + kind === "arrow_function" || + kind === "method_definition" + ) { + return current; + } + break; + } + return nearestScope(node); +} + +function buildShadowedRanges(root: SgNode, names: Set) { + const shadowedRanges = new Map>(); + + for (const decl of root.findAll({ + rule: { any: [{ kind: "function_declaration" }, { kind: "variable_declarator" }] }, + })) { + if (isInsideImportStatement(decl)) continue; + const name = decl + .children() + .find((child) => child.kind() === "identifier" && names.has(child.text())); + if (name) addShadowedRange(shadowedRanges, name.text(), nearestScope(decl)); + } + + for (const param of root.findAll({ + rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] }, + })) { + const name = param + .children() + .find((child) => child.kind() === "identifier" && names.has(child.text())); + if (name) addShadowedRange(shadowedRanges, name.text(), parameterScope(param)); + } + + return shadowedRanges; +} + +function isShadowed( + node: SgNode, + shadowedRanges: Map>, +): boolean { + const ranges = shadowedRanges.get(node.text()); + if (!ranges) return false; + const position = node.range().start.index; + return ranges.some((range) => position >= range.start && position < range.end); +} + +function isSdkDbMember( + object: SgNode | null, + dbNames: Set, + namespaceNames: Set, + shadowedRanges: Map>, +) { + if (!object) return false; + if (object.kind() === "identifier") + return dbNames.has(object.text()) && !isShadowed(object, shadowedRanges); + if (object.kind() !== "member_expression") return false; + + const base = object.field("object"); + const property = object.field("property"); + return ( + base?.kind() === "identifier" && + namespaceNames.has(base.text()) && + !isShadowed(base, shadowedRanges) && + property?.text() === "db" + ); +} + +export default function transform(source: string, filePath: string): string | null { + if (!source.includes(".type") || !source.includes(SDK_MODULE)) return null; + + let root: SgNode; + try { + root = parse(sourceLang(filePath, source), source).root(); + } catch { + return null; + } + + const imports = findImportStatements(root).filter( + (importStmt) => importSource(importStmt) === SDK_MODULE, + ); + if (imports.length === 0) return null; + + const dbNames = new Set(); + const namespaceNames = new Set(); + for (const importStmt of imports) { + for (const binding of importBindings(importStmt)) { + if (binding.importedName === "db" && !binding.typeOnly) dbNames.add(binding.localName); + } + for (const name of namespaceImportNames(importStmt)) { + namespaceNames.add(name); + } + } + if (dbNames.size === 0 && namespaceNames.size === 0) return null; + + const shadowedRanges = buildShadowedRanges(root, new Set([...dbNames, ...namespaceNames])); + const edits: Edit[] = []; + for (const member of root.findAll({ rule: { kind: "member_expression" } })) { + const property = member.field("property"); + if (property?.text() !== "type") continue; + if (!isSdkDbMember(member.field("object"), dbNames, namespaceNames, shadowedRanges)) continue; + edits.push(property.replace("table")); + } + + return edits.length > 0 ? root.commitEdits(edits) : null; +} diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts new file mode 100644 index 000000000..7380b3221 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts @@ -0,0 +1,34 @@ +import { db, defineConfig } from "@tailor-platform/sdk"; +import { db as schema } from "@tailor-platform/sdk"; +import * as sdk from "@tailor-platform/sdk"; + +export const user = db.table("User", { + name: db.string(), +}); + +export const project = schema.table("Project", { + title: schema.string(), +}); + +export const team = sdk.db.table("Team", { + label: sdk.db.string(), +}); + +const local = { + type: (name: string) => name, +}; + +local.type("NoChange"); + +function useLocalDb(db: { type: (name: string) => string }) { + return db.type("NoChange"); +} + +{ + const schema = { + type: (name: string) => name, + }; + schema.type("NoChange"); +} + +defineConfig({}); diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts new file mode 100644 index 000000000..32aa1ecfd --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts @@ -0,0 +1,34 @@ +import { db, defineConfig } from "@tailor-platform/sdk"; +import { db as schema } from "@tailor-platform/sdk"; +import * as sdk from "@tailor-platform/sdk"; + +export const user = db.type("User", { + name: db.string(), +}); + +export const project = schema.type("Project", { + title: schema.string(), +}); + +export const team = sdk.db.type("Team", { + label: sdk.db.string(), +}); + +const local = { + type: (name: string) => name, +}; + +local.type("NoChange"); + +function useLocalDb(db: { type: (name: string) => string }) { + return db.type("NoChange"); +} + +{ + const schema = { + type: (name: string) => name, + }; + schema.type("NoChange"); +} + +defineConfig({}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts index e8feb308e..1d23e07aa 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/expected.ts @@ -92,7 +92,7 @@ const reviewer = t.string(); const zodLike = { parse: (arg: unknown) => arg }; export const user = db - .type("User", { + .table("User", { role, note: db.string(), }) @@ -118,7 +118,7 @@ export const user = db }); export const audit = db - .type("Audit", { + .table("Audit", { create: db.string(), update: db.string(), }) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts index 8411583a1..5609bd0b4 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/tailordb-callbacks/input.ts @@ -93,7 +93,7 @@ const reviewer = t.string(); const zodLike = { parse: (arg: unknown) => arg }; export const user = db - .type("User", { + .table("User", { role, note: db.string(), }) @@ -119,7 +119,7 @@ export const user = db }); export const audit = db - .type("Audit", { + .table("Audit", { create: db.string(), update: db.string(), }) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 81862ce1e..a5b2cb4c8 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -529,6 +529,33 @@ export const allCodemods: CodemodPackage[] = [ "declarations automatically on SDK import.", ].join("\n"), }, + { + id: "v2/db-type-to-table", + name: "db.type() → db.table()", + description: + "Rename TailorDB schema builder calls from `db.type()` to `db.table()`. TailorDB schema definitions now use table terminology in SDK projects.", + since: "1.0.0", + until: "2.0.0", + prereleaseUntil: V2_NEXT_2, + scriptPath: "v2/db-type-to-table/scripts/transform.js", + filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], + legacyPatterns: ["db.type"], + examples: [ + { + before: + 'import { db } from "@tailor-platform/sdk";\n\nexport const user = db.type("User", {\n name: db.string(),\n});', + after: + 'import { db } from "@tailor-platform/sdk";\n\nexport const user = db.table("User", {\n name: db.string(),\n});', + }, + ], + prompt: [ + "In Tailor SDK v2, TailorDB schema definitions use db.table(...) instead of", + "db.type(...). The codemod rewrites member accesses on db imported from", + "@tailor-platform/sdk, including aliases such as `import { db as schema }`.", + "Review any remaining db.type references and rename SDK TailorDB schema builder", + "calls to db.table. Leave unrelated local objects with a .type() method unchanged.", + ].join("\n"), + }, { id: "v2/execute-script-arg", name: "executeScript arg JSON.stringify → value", diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index 7d9c859e5..c69f26ce6 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -130,4 +130,8 @@ describe("codemod transforms", () => { test("v2/wait-point-rename transforms correctly", async () => { await expect(runFixtureCases("v2/wait-point-rename")).resolves.toBeUndefined(); }); + + test("v2/db-type-to-table transforms correctly", async () => { + await expect(runFixtureCases("v2/db-type-to-table")).resolves.toBeUndefined(); + }); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index 6548b5bf7..8f8d5d132 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -36,6 +36,7 @@ export default defineConfig([ "codemods/v2/auth-connection-token-helper/scripts/transform.ts", "v2/tailordb-namespace/scripts/transform": "codemods/v2/tailordb-namespace/scripts/transform.ts", + "v2/db-type-to-table/scripts/transform": "codemods/v2/db-type-to-table/scripts/transform.ts", "v2/runtime-globals-opt-in/scripts/transform": "codemods/v2/runtime-globals-opt-in/scripts/transform.ts", "v2/execute-script-arg/scripts/transform": diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index c04f240fa..fa8c923cc 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -471,6 +471,45 @@ declarations automatically on SDK import. +## db.type() → db.table() + +**Migration:** Partially automatic + +Rename TailorDB schema builder calls from `db.type()` to `db.table()`. TailorDB schema definitions now use table terminology in SDK projects. + +Before: + +```ts +import { db } from "@tailor-platform/sdk"; + +export const user = db.type("User", { + name: db.string(), +}); +``` + +After: + +```ts +import { db } from "@tailor-platform/sdk"; + +export const user = db.table("User", { + name: db.string(), +}); +``` + +
+Prompt for an AI agent (to finish the cases the codemod could not migrate) + +```text +In Tailor SDK v2, TailorDB schema definitions use db.table(...) instead of +db.type(...). The codemod rewrites member accesses on db imported from +@tailor-platform/sdk, including aliases such as `import { db as schema }`. +Review any remaining db.type references and rename SDK TailorDB schema builder +calls to db.table. Leave unrelated local objects with a .type() method unchanged. +``` + +
+ ## executeScript arg JSON.stringify → value **Migration:** Partially automatic diff --git a/packages/sdk/docs/plugin/custom.md b/packages/sdk/docs/plugin/custom.md index 33e07c7cd..950c077ef 100644 --- a/packages/sdk/docs/plugin/custom.md +++ b/packages/sdk/docs/plugin/custom.md @@ -93,7 +93,7 @@ interface Plugin { onTypeLoaded(context) { const { type, typeConfig, namespace } = context; return { - types: { archive: db.type(`Deleted_${type.name}`, { ... }) }, + types: { archive: db.table(`Deleted_${type.name}`, { ... }) }, extends: { fields: { deletedAt: db.datetime({ optional: true }) } }, executors: [{ name: `${type.name}-on-delete`, resolve: async () => await import("./on-delete"), context: { sourceType: type, namespace } }], }; @@ -122,7 +122,7 @@ Same as `TypePluginOutput` but without `extends` (namespace plugins cannot exten ```typescript onNamespaceLoaded(context) { return { - types: { auditLog: db.type("AuditLog", { action: db.string(), ... }) }, + types: { auditLog: db.table("AuditLog", { action: db.string(), ... }) }, }; }, ``` @@ -343,7 +343,7 @@ function processSoftDelete( // Generate archive type const archiveType = db - .type(`${prefix}${type.name}`, { + .table(`${prefix}${type.name}`, { originalId: db.uuid().description("ID of the deleted record"), originalData: db.string().description("JSON snapshot of deleted record"), deletedAt: db.datetime().description("When the record was deleted"), @@ -450,7 +450,7 @@ export const plugins = definePlugins( // tailordb/customer.ts export const customer = db - .type("Customer", { + .table("Customer", { name: db.string(), email: db.string(), }) diff --git a/packages/sdk/docs/plugin/index.md b/packages/sdk/docs/plugin/index.md index dbb2012a5..b382d8402 100644 --- a/packages/sdk/docs/plugin/index.md +++ b/packages/sdk/docs/plugin/index.md @@ -43,7 +43,7 @@ Use the `.plugin()` method to attach plugins to specific types: import { db } from "@tailor-platform/sdk"; export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string(), }) @@ -58,7 +58,7 @@ Some plugins accept per-type configuration: ```typescript export const customer = db - .type("Customer", { + .table("Customer", { name: db.string(), // ... }) diff --git a/packages/sdk/docs/services/auth.md b/packages/sdk/docs/services/auth.md index 32be2584d..fe71c60d8 100644 --- a/packages/sdk/docs/services/auth.md +++ b/packages/sdk/docs/services/auth.md @@ -86,7 +86,7 @@ Example TailorDB type for user profile: // tailordb/user.ts import { db } from "@tailor-platform/sdk"; -export const user = db.type("User", { +export const user = db.table("User", { email: db.string().unique(), // usernameField must have unique constraint role: db.enum(["admin", "user"]), ...db.fields.timestamps(), @@ -130,7 +130,7 @@ Example TailorDB type with UUID fields for attribute list: // tailordb/user.ts import { db } from "@tailor-platform/sdk"; -export const user = db.type("User", { +export const user = db.table("User", { email: db.string().unique(), role: db.enum(["admin", "user"]), organizationId: db.uuid(), // Can be used in attributeList diff --git a/packages/sdk/docs/services/resolver.md b/packages/sdk/docs/services/resolver.md index 51d2a6f11..cb4184e0b 100644 --- a/packages/sdk/docs/services/resolver.md +++ b/packages/sdk/docs/services/resolver.md @@ -108,7 +108,7 @@ Define input/output schemas using methods of `t` object. Basic usage and support You can reuse fields defined with `db` object, but note that unsupported options will be ignored: ```typescript -const user = db.type("User", { +const user = db.table("User", { name: db.string().unique(), age: db.int(), }); diff --git a/packages/sdk/docs/services/tailordb-migration.md b/packages/sdk/docs/services/tailordb-migration.md index 88fd4b080..61f753f48 100644 --- a/packages/sdk/docs/services/tailordb-migration.md +++ b/packages/sdk/docs/services/tailordb-migration.md @@ -69,7 +69,7 @@ A typical change cycle: ```typescript // tailordb/user.ts - export const user = db.type("User", { + export const user = db.table("User", { name: db.string(), email: db.string(), // ← new required field ...db.fields.timestamps(), diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index 36dc0ea9a..b8381a315 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -29,7 +29,7 @@ Define TailorDB Types in files matching glob patterns specified in `tailor.confi import { db } from "@tailor-platform/sdk"; // Export both value and type -export const user = db.type("User", { +export const user = db.table("User", { name: db.string(), email: db.string().unique(), age: db.int(), @@ -38,7 +38,7 @@ export const user = db.type("User", { export type user = typeof user; // You can define multiple types in the same file -export const role = db.type("Role", { +export const role = db.table("Role", { name: db.string().unique(), }); export type role = typeof role; @@ -47,7 +47,7 @@ export type role = typeof role; Specify plural form by passing an array as first argument: ```typescript -db.type(["User", "UserList"], { +db.table(["User", "UserList"], { name: db.string(), }); ``` @@ -55,7 +55,7 @@ db.type(["User", "UserList"], { Pass a description as second argument: ```typescript -db.type("User", "User in the system", { +db.table("User", "User in the system", { name: db.string(), }); ``` @@ -174,11 +174,11 @@ db.string().unique(); Add a relation to field with automatic index and foreign key constraint: ```typescript -const role = db.type("Role", { +const role = db.table("Role", { name: db.string(), }); -const user = db.type("User", { +const user = db.table("User", { name: db.string(), roleId: db.uuid().relation({ type: "n-1", @@ -190,7 +190,7 @@ const user = db.type("User", { For one-to-one relations, use `type: "1-1"`: ```typescript -const userProfile = db.type("UserProfile", { +const userProfile = db.table("UserProfile", { userId: db.uuid().relation({ type: "1-1", toward: { type: user }, @@ -202,7 +202,7 @@ const userProfile = db.type("UserProfile", { For foreign key constraint without creating a relation, use `type: "keyOnly"`: ```typescript -const user = db.type("User", { +const user = db.table("User", { roleId: db.uuid().relation({ type: "keyOnly", toward: { type: role }, @@ -213,11 +213,11 @@ const user = db.type("User", { Create relations against different fields using `toward.key`: ```typescript -const user = db.type("User", { +const user = db.table("User", { email: db.string().unique(), }); -const userProfile = db.type("UserProfile", { +const userProfile = db.table("UserProfile", { userEmail: db.string().relation({ type: "1-1", toward: { type: user, key: "email" }, @@ -228,7 +228,7 @@ const userProfile = db.type("UserProfile", { Customize relation names using `toward.as` / `backward` options: ```typescript -const userProfile = db.type("UserProfile", { +const userProfile = db.table("UserProfile", { userId: db.uuid().relation({ type: "1-1", toward: { type: user, as: "base" }, @@ -260,7 +260,7 @@ on the same type point to the same target type, because their default forward na from the target type name: ```typescript -const post = db.type("Post", { +const post = db.table("Post", { authorID: db.uuid().relation({ type: "n-1", toward: { type: user, as: "author" }, @@ -300,11 +300,11 @@ db.string().hooks({ #### Type-level Hooks -Set hooks for multiple fields at once using `db.type().hooks()`: +Set hooks for multiple fields at once using `db.table().hooks()`: ```typescript export const customer = db - .type("Customer", { + .table("Customer", { firstName: db.string(), lastName: db.string(), fullName: db.string(), @@ -322,7 +322,7 @@ export const customer = db ```typescript // Compile error - cannot set hooks on the same field twice export const user = db - .type("User", { + .table("User", { name: db.string().hooks({ create: ({ data }) => data.firstName }), // Field-level }) .hooks({ @@ -331,7 +331,7 @@ export const user = db // OK - set hooks on different fields export const user = db - .type("User", { + .table("User", { firstName: db.string().hooks({ create: () => "John" }), // Field-level on firstName lastName: db.string(), }) @@ -363,11 +363,11 @@ db.string().validate( #### Type-level Validation -Set validators for multiple fields at once using `db.type().validate()`: +Set validators for multiple fields at once using `db.table().validate()`: ```typescript export const user = db - .type("User", { + .table("User", { name: db.string(), email: db.string(), }) @@ -385,7 +385,7 @@ export const user = db ```typescript // Compile error - cannot set validation on the same field twice export const user = db - .type("User", { + .table("User", { name: db.string().validate(({ value }) => value.length > 0), // Field-level }) .validate({ @@ -394,7 +394,7 @@ export const user = db // OK - set validation on different fields export const user = db - .type("User", { + .table("User", { name: db.string().validate(({ value }) => value.length > 0), // Field-level on name email: db.string(), }) @@ -426,7 +426,7 @@ db.string().serial({ ### Common Fields ```typescript -export const user = db.type("User", { +export const user = db.table("User", { name: db.string(), ...db.fields.timestamps(), }); @@ -439,7 +439,7 @@ export const user = db.type("User", { ### Composite Indexes ```typescript -db.type("User", { +db.table("User", { firstName: db.string(), lastName: db.string(), }).indexes({ @@ -452,7 +452,7 @@ db.type("User", { ### File Fields ```typescript -db.type("User", { +db.table("User", { name: db.string(), }).files({ avatar: "profile image", @@ -462,7 +462,7 @@ db.type("User", { ### Features ```typescript -db.type("User", { +db.table("User", { name: db.string(), }).features({ aggregation: true, @@ -475,7 +475,7 @@ db.type("User", { Enable event publishing for a type to trigger executors on record changes: ```typescript -db.type("User", { +db.table("User", { name: db.string(), }).features({ publishEvents: true, @@ -494,7 +494,7 @@ db.type("User", { ```typescript // publishEvents is automatically enabled because an executor uses this type - export const order = db.type("Order", { + export const order = db.table("Order", { status: db.string(), }); @@ -508,7 +508,7 @@ db.type("User", { 2. **Manual enable**: Enable event publishing for external consumers or debugging ```typescript - db.type("AuditLog", { + db.table("AuditLog", { action: db.string(), }).features({ publishEvents: true, // Enable even without executor triggers @@ -518,7 +518,7 @@ db.type("User", { 3. **Explicit disable**: Disable event publishing for a type that doesn't need it (error if executor uses it) ```typescript - db.type("TempData", { + db.table("TempData", { data: db.string(), }).features({ publishEvents: false, // Explicitly disable @@ -534,7 +534,7 @@ Extract subsets of fields from a `TailorDBType` for reuse in resolvers, executor Select specific fields and optionally modify their properties: ```typescript -const user = db.type("User", { +const user = db.table("User", { id: db.uuid(), name: db.string(), email: db.string().unique(), @@ -604,7 +604,7 @@ Configure Permission and GQLPermission. For details, see the [TailorDB Permissio **Important**: Following the secure-by-default principle, all operations are denied if permissions are not configured. You must explicitly grant permissions for each operation (create, read, update, delete). ```typescript -db.type("User", { +db.table("User", { name: db.string(), role: db.enum(["admin", "user"]).index(), }) @@ -634,7 +634,7 @@ import { unsafeAllowAllGqlPermission, } from "@tailor-platform/sdk"; -db.type("User", { +db.table("User", { name: db.string(), }) .permission(unsafeAllowAllTypePermission) diff --git a/packages/sdk/e2e/deploy.test.ts b/packages/sdk/e2e/deploy.test.ts index fad7a44ff..c6a2d3c55 100644 --- a/packages/sdk/e2e/deploy.test.ts +++ b/packages/sdk/e2e/deploy.test.ts @@ -210,7 +210,7 @@ describe("E2E: Service deletion order", () => { ` import { db } from "@tailor-platform/sdk"; -export const user = db.type("User", { +export const user = db.table("User", { name: db.string(), email: db.string(), role: db.string({ optional: true }), @@ -232,7 +232,7 @@ export type user = typeof user; ` import { db } from "@tailor-platform/sdk"; -export const extraUser = db.type("ExtraUser", { +export const extraUser = db.table("ExtraUser", { name: db.string(), email: db.string(), }); @@ -547,7 +547,7 @@ export default defineConfig({ ` import { db } from "@tailor-platform/sdk"; -export const user = db.type("User", { +export const user = db.table("User", { name: db.string(), email: db.string(), role: db.string({ optional: true }), @@ -602,7 +602,7 @@ export default defineConfig({ ` import { db } from "@tailor-platform/sdk"; -export const user = db.type("User", { +export const user = db.table("User", { name: db.string(), email: db.string(), role: db.string({ optional: true }), diff --git a/packages/sdk/e2e/fixtures/migration/tailordb/post.ts b/packages/sdk/e2e/fixtures/migration/tailordb/post.ts index a258fb74e..67282b337 100644 --- a/packages/sdk/e2e/fixtures/migration/tailordb/post.ts +++ b/packages/sdk/e2e/fixtures/migration/tailordb/post.ts @@ -1,6 +1,6 @@ import { db } from "@tailor-platform/sdk"; -export const post = db.type("Post", { +export const post = db.table("Post", { title: db.string(), content: db.string({ optional: true }), }); diff --git a/packages/sdk/e2e/fixtures/migration/tailordb/user.ts b/packages/sdk/e2e/fixtures/migration/tailordb/user.ts index aaeea23cb..23e5218ca 100644 --- a/packages/sdk/e2e/fixtures/migration/tailordb/user.ts +++ b/packages/sdk/e2e/fixtures/migration/tailordb/user.ts @@ -1,6 +1,6 @@ import { db } from "@tailor-platform/sdk"; -export const user = db.type("User", { +export const user = db.table("User", { name: db.string(), email: db.string().unique(), role: db.string({ optional: true }), diff --git a/packages/sdk/e2e/migration.test.ts b/packages/sdk/e2e/migration.test.ts index 8779b08e4..fec557d1f 100644 --- a/packages/sdk/e2e/migration.test.ts +++ b/packages/sdk/e2e/migration.test.ts @@ -453,7 +453,7 @@ describe.sequential("E2E: TailorDB Migrations", () => { // Update type to add optional field updateTypeFile(`import { db } from "@tailor-platform/sdk"; -export const user = db.type("User", { +export const user = db.table("User", { name: db.string(), email: db.string().unique(), role: db.string({ optional: true }), @@ -504,7 +504,7 @@ export type user = typeof user; // Update type to add required field (breaking change) updateTypeFile(`import { db } from "@tailor-platform/sdk"; -export const user = db.type("User", { +export const user = db.table("User", { name: db.string(), email: db.string().unique(), role: db.string({ optional: true }), @@ -634,7 +634,7 @@ export type user = typeof user; // Update User type to remove requiredField updateTypeFile(`import { db } from "@tailor-platform/sdk"; -export const user = db.type("User", { +export const user = db.table("User", { name: db.string(), email: db.string().unique(), role: db.string({ optional: true }), @@ -686,7 +686,7 @@ export type user = typeof user; test("generates the breaking migration whose script will fail", async () => { updateTypeFile(`import { db } from "@tailor-platform/sdk"; -export const user = db.type("User", { +export const user = db.table("User", { name: db.string(), email: db.string().unique(), role: db.string({ optional: true }), diff --git a/packages/sdk/scripts/perf/features/executor-record.ts b/packages/sdk/scripts/perf/features/executor-record.ts index 2cdc2a01a..e5f392c43 100644 --- a/packages/sdk/scripts/perf/features/executor-record.ts +++ b/packages/sdk/scripts/perf/features/executor-record.ts @@ -12,7 +12,7 @@ import { db, } from "../../../src/configure"; -const dummyType = db.type("DummyType", { name: db.string() }); +const dummyType = db.table("DummyType", { name: db.string() }); export const executor0 = createExecutor({ name: "executor0", diff --git a/packages/sdk/scripts/perf/features/tailordb-basic.ts b/packages/sdk/scripts/perf/features/tailordb-basic.ts index 69421e307..7bca1c010 100644 --- a/packages/sdk/scripts/perf/features/tailordb-basic.ts +++ b/packages/sdk/scripts/perf/features/tailordb-basic.ts @@ -6,7 +6,7 @@ */ import { db } from "../../../src/configure"; -export const type0 = db.type("Type0", { +export const type0 = db.table("Type0", { stringField: db.string(), intField: db.int(), boolField: db.bool(), @@ -17,7 +17,7 @@ export const type0 = db.type("Type0", { timeField: db.time(), }); -export const type1 = db.type("Type1", { +export const type1 = db.table("Type1", { stringField: db.string(), intField: db.int(), boolField: db.bool(), @@ -28,7 +28,7 @@ export const type1 = db.type("Type1", { timeField: db.time(), }); -export const type2 = db.type("Type2", { +export const type2 = db.table("Type2", { stringField: db.string(), intField: db.int(), boolField: db.bool(), @@ -39,7 +39,7 @@ export const type2 = db.type("Type2", { timeField: db.time(), }); -export const type3 = db.type("Type3", { +export const type3 = db.table("Type3", { stringField: db.string(), intField: db.int(), boolField: db.bool(), @@ -50,7 +50,7 @@ export const type3 = db.type("Type3", { timeField: db.time(), }); -export const type4 = db.type("Type4", { +export const type4 = db.table("Type4", { stringField: db.string(), intField: db.int(), boolField: db.bool(), @@ -61,7 +61,7 @@ export const type4 = db.type("Type4", { timeField: db.time(), }); -export const type5 = db.type("Type5", { +export const type5 = db.table("Type5", { stringField: db.string(), intField: db.int(), boolField: db.bool(), @@ -72,7 +72,7 @@ export const type5 = db.type("Type5", { timeField: db.time(), }); -export const type6 = db.type("Type6", { +export const type6 = db.table("Type6", { stringField: db.string(), intField: db.int(), boolField: db.bool(), @@ -83,7 +83,7 @@ export const type6 = db.type("Type6", { timeField: db.time(), }); -export const type7 = db.type("Type7", { +export const type7 = db.table("Type7", { stringField: db.string(), intField: db.int(), boolField: db.bool(), @@ -94,7 +94,7 @@ export const type7 = db.type("Type7", { timeField: db.time(), }); -export const type8 = db.type("Type8", { +export const type8 = db.table("Type8", { stringField: db.string(), intField: db.int(), boolField: db.bool(), @@ -105,7 +105,7 @@ export const type8 = db.type("Type8", { timeField: db.time(), }); -export const type9 = db.type("Type9", { +export const type9 = db.table("Type9", { stringField: db.string(), intField: db.int(), boolField: db.bool(), diff --git a/packages/sdk/scripts/perf/features/tailordb-enum.ts b/packages/sdk/scripts/perf/features/tailordb-enum.ts index 9b0c94698..a5fdd4e54 100644 --- a/packages/sdk/scripts/perf/features/tailordb-enum.ts +++ b/packages/sdk/scripts/perf/features/tailordb-enum.ts @@ -5,70 +5,70 @@ */ import { db } from "../../../src/configure"; -export const type0 = db.type("Type0", { +export const type0 = db.table("Type0", { name: db.string(), status: db.enum(["ACTIVE", "INACTIVE", "PENDING", "ARCHIVED"]), priority: db.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]), category: db.enum(["A", "B", "C", "D", "E"]), }); -export const type1 = db.type("Type1", { +export const type1 = db.table("Type1", { name: db.string(), status: db.enum(["ACTIVE", "INACTIVE", "PENDING", "ARCHIVED"]), priority: db.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]), category: db.enum(["A", "B", "C", "D", "E"]), }); -export const type2 = db.type("Type2", { +export const type2 = db.table("Type2", { name: db.string(), status: db.enum(["ACTIVE", "INACTIVE", "PENDING", "ARCHIVED"]), priority: db.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]), category: db.enum(["A", "B", "C", "D", "E"]), }); -export const type3 = db.type("Type3", { +export const type3 = db.table("Type3", { name: db.string(), status: db.enum(["ACTIVE", "INACTIVE", "PENDING", "ARCHIVED"]), priority: db.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]), category: db.enum(["A", "B", "C", "D", "E"]), }); -export const type4 = db.type("Type4", { +export const type4 = db.table("Type4", { name: db.string(), status: db.enum(["ACTIVE", "INACTIVE", "PENDING", "ARCHIVED"]), priority: db.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]), category: db.enum(["A", "B", "C", "D", "E"]), }); -export const type5 = db.type("Type5", { +export const type5 = db.table("Type5", { name: db.string(), status: db.enum(["ACTIVE", "INACTIVE", "PENDING", "ARCHIVED"]), priority: db.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]), category: db.enum(["A", "B", "C", "D", "E"]), }); -export const type6 = db.type("Type6", { +export const type6 = db.table("Type6", { name: db.string(), status: db.enum(["ACTIVE", "INACTIVE", "PENDING", "ARCHIVED"]), priority: db.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]), category: db.enum(["A", "B", "C", "D", "E"]), }); -export const type7 = db.type("Type7", { +export const type7 = db.table("Type7", { name: db.string(), status: db.enum(["ACTIVE", "INACTIVE", "PENDING", "ARCHIVED"]), priority: db.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]), category: db.enum(["A", "B", "C", "D", "E"]), }); -export const type8 = db.type("Type8", { +export const type8 = db.table("Type8", { name: db.string(), status: db.enum(["ACTIVE", "INACTIVE", "PENDING", "ARCHIVED"]), priority: db.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]), category: db.enum(["A", "B", "C", "D", "E"]), }); -export const type9 = db.type("Type9", { +export const type9 = db.table("Type9", { name: db.string(), status: db.enum(["ACTIVE", "INACTIVE", "PENDING", "ARCHIVED"]), priority: db.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]), diff --git a/packages/sdk/scripts/perf/features/tailordb-hooks.ts b/packages/sdk/scripts/perf/features/tailordb-hooks.ts index 4247896b3..d3d78ed28 100644 --- a/packages/sdk/scripts/perf/features/tailordb-hooks.ts +++ b/packages/sdk/scripts/perf/features/tailordb-hooks.ts @@ -5,61 +5,61 @@ */ import { db } from "../../../src/configure"; -export const type0 = db.type("Type0", { +export const type0 = db.table("Type0", { name: db.string().hooks({ create: () => "default" }), createdAt: db.datetime().hooks({ create: () => new Date() }), updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), }); -export const type1 = db.type("Type1", { +export const type1 = db.table("Type1", { name: db.string().hooks({ create: () => "default" }), createdAt: db.datetime().hooks({ create: () => new Date() }), updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), }); -export const type2 = db.type("Type2", { +export const type2 = db.table("Type2", { name: db.string().hooks({ create: () => "default" }), createdAt: db.datetime().hooks({ create: () => new Date() }), updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), }); -export const type3 = db.type("Type3", { +export const type3 = db.table("Type3", { name: db.string().hooks({ create: () => "default" }), createdAt: db.datetime().hooks({ create: () => new Date() }), updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), }); -export const type4 = db.type("Type4", { +export const type4 = db.table("Type4", { name: db.string().hooks({ create: () => "default" }), createdAt: db.datetime().hooks({ create: () => new Date() }), updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), }); -export const type5 = db.type("Type5", { +export const type5 = db.table("Type5", { name: db.string().hooks({ create: () => "default" }), createdAt: db.datetime().hooks({ create: () => new Date() }), updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), }); -export const type6 = db.type("Type6", { +export const type6 = db.table("Type6", { name: db.string().hooks({ create: () => "default" }), createdAt: db.datetime().hooks({ create: () => new Date() }), updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), }); -export const type7 = db.type("Type7", { +export const type7 = db.table("Type7", { name: db.string().hooks({ create: () => "default" }), createdAt: db.datetime().hooks({ create: () => new Date() }), updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), }); -export const type8 = db.type("Type8", { +export const type8 = db.table("Type8", { name: db.string().hooks({ create: () => "default" }), createdAt: db.datetime().hooks({ create: () => new Date() }), updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), }); -export const type9 = db.type("Type9", { +export const type9 = db.table("Type9", { name: db.string().hooks({ create: () => "default" }), createdAt: db.datetime().hooks({ create: () => new Date() }), updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), diff --git a/packages/sdk/scripts/perf/features/tailordb-object.ts b/packages/sdk/scripts/perf/features/tailordb-object.ts index 4d7fe6997..d863c4829 100644 --- a/packages/sdk/scripts/perf/features/tailordb-object.ts +++ b/packages/sdk/scripts/perf/features/tailordb-object.ts @@ -5,7 +5,7 @@ */ import { db } from "../../../src/configure"; -export const type0 = db.type("Type0", { +export const type0 = db.table("Type0", { name: db.string(), address: db.object({ street: db.string(), @@ -19,7 +19,7 @@ export const type0 = db.type("Type0", { }), }); -export const type1 = db.type("Type1", { +export const type1 = db.table("Type1", { name: db.string(), address: db.object({ street: db.string(), @@ -33,7 +33,7 @@ export const type1 = db.type("Type1", { }), }); -export const type2 = db.type("Type2", { +export const type2 = db.table("Type2", { name: db.string(), address: db.object({ street: db.string(), @@ -47,7 +47,7 @@ export const type2 = db.type("Type2", { }), }); -export const type3 = db.type("Type3", { +export const type3 = db.table("Type3", { name: db.string(), address: db.object({ street: db.string(), @@ -61,7 +61,7 @@ export const type3 = db.type("Type3", { }), }); -export const type4 = db.type("Type4", { +export const type4 = db.table("Type4", { name: db.string(), address: db.object({ street: db.string(), @@ -75,7 +75,7 @@ export const type4 = db.type("Type4", { }), }); -export const type5 = db.type("Type5", { +export const type5 = db.table("Type5", { name: db.string(), address: db.object({ street: db.string(), @@ -89,7 +89,7 @@ export const type5 = db.type("Type5", { }), }); -export const type6 = db.type("Type6", { +export const type6 = db.table("Type6", { name: db.string(), address: db.object({ street: db.string(), @@ -103,7 +103,7 @@ export const type6 = db.type("Type6", { }), }); -export const type7 = db.type("Type7", { +export const type7 = db.table("Type7", { name: db.string(), address: db.object({ street: db.string(), @@ -117,7 +117,7 @@ export const type7 = db.type("Type7", { }), }); -export const type8 = db.type("Type8", { +export const type8 = db.table("Type8", { name: db.string(), address: db.object({ street: db.string(), @@ -131,7 +131,7 @@ export const type8 = db.type("Type8", { }), }); -export const type9 = db.type("Type9", { +export const type9 = db.table("Type9", { name: db.string(), address: db.object({ street: db.string(), diff --git a/packages/sdk/scripts/perf/features/tailordb-optional.ts b/packages/sdk/scripts/perf/features/tailordb-optional.ts index af2c38dd6..9bb44df3e 100644 --- a/packages/sdk/scripts/perf/features/tailordb-optional.ts +++ b/packages/sdk/scripts/perf/features/tailordb-optional.ts @@ -5,7 +5,7 @@ */ import { db } from "../../../src/configure"; -export const type0 = db.type("Type0", { +export const type0 = db.table("Type0", { requiredString: db.string(), optionalString: db.string({ optional: true }), optionalInt: db.int({ optional: true }), @@ -13,7 +13,7 @@ export const type0 = db.type("Type0", { optionalDate: db.date({ optional: true }), }); -export const type1 = db.type("Type1", { +export const type1 = db.table("Type1", { requiredString: db.string(), optionalString: db.string({ optional: true }), optionalInt: db.int({ optional: true }), @@ -21,7 +21,7 @@ export const type1 = db.type("Type1", { optionalDate: db.date({ optional: true }), }); -export const type2 = db.type("Type2", { +export const type2 = db.table("Type2", { requiredString: db.string(), optionalString: db.string({ optional: true }), optionalInt: db.int({ optional: true }), @@ -29,7 +29,7 @@ export const type2 = db.type("Type2", { optionalDate: db.date({ optional: true }), }); -export const type3 = db.type("Type3", { +export const type3 = db.table("Type3", { requiredString: db.string(), optionalString: db.string({ optional: true }), optionalInt: db.int({ optional: true }), @@ -37,7 +37,7 @@ export const type3 = db.type("Type3", { optionalDate: db.date({ optional: true }), }); -export const type4 = db.type("Type4", { +export const type4 = db.table("Type4", { requiredString: db.string(), optionalString: db.string({ optional: true }), optionalInt: db.int({ optional: true }), @@ -45,7 +45,7 @@ export const type4 = db.type("Type4", { optionalDate: db.date({ optional: true }), }); -export const type5 = db.type("Type5", { +export const type5 = db.table("Type5", { requiredString: db.string(), optionalString: db.string({ optional: true }), optionalInt: db.int({ optional: true }), @@ -53,7 +53,7 @@ export const type5 = db.type("Type5", { optionalDate: db.date({ optional: true }), }); -export const type6 = db.type("Type6", { +export const type6 = db.table("Type6", { requiredString: db.string(), optionalString: db.string({ optional: true }), optionalInt: db.int({ optional: true }), @@ -61,7 +61,7 @@ export const type6 = db.type("Type6", { optionalDate: db.date({ optional: true }), }); -export const type7 = db.type("Type7", { +export const type7 = db.table("Type7", { requiredString: db.string(), optionalString: db.string({ optional: true }), optionalInt: db.int({ optional: true }), @@ -69,7 +69,7 @@ export const type7 = db.type("Type7", { optionalDate: db.date({ optional: true }), }); -export const type8 = db.type("Type8", { +export const type8 = db.table("Type8", { requiredString: db.string(), optionalString: db.string({ optional: true }), optionalInt: db.int({ optional: true }), @@ -77,7 +77,7 @@ export const type8 = db.type("Type8", { optionalDate: db.date({ optional: true }), }); -export const type9 = db.type("Type9", { +export const type9 = db.table("Type9", { requiredString: db.string(), optionalString: db.string({ optional: true }), optionalInt: db.int({ optional: true }), diff --git a/packages/sdk/scripts/perf/features/tailordb-relation.ts b/packages/sdk/scripts/perf/features/tailordb-relation.ts index 94a8affc4..6e6ce0831 100644 --- a/packages/sdk/scripts/perf/features/tailordb-relation.ts +++ b/packages/sdk/scripts/perf/features/tailordb-relation.ts @@ -5,56 +5,56 @@ */ import { db } from "../../../src/configure"; -export const targetType = db.type("TargetType", { +export const targetType = db.table("TargetType", { name: db.string(), }); -export const type0 = db.type("Type0", { +export const type0 = db.table("Type0", { name: db.string(), targetId: db.uuid().relation({ type: "n-1", toward: { type: targetType } }), }); -export const type1 = db.type("Type1", { +export const type1 = db.table("Type1", { name: db.string(), targetId: db.uuid().relation({ type: "n-1", toward: { type: targetType } }), }); -export const type2 = db.type("Type2", { +export const type2 = db.table("Type2", { name: db.string(), targetId: db.uuid().relation({ type: "n-1", toward: { type: targetType } }), }); -export const type3 = db.type("Type3", { +export const type3 = db.table("Type3", { name: db.string(), targetId: db.uuid().relation({ type: "n-1", toward: { type: targetType } }), }); -export const type4 = db.type("Type4", { +export const type4 = db.table("Type4", { name: db.string(), targetId: db.uuid().relation({ type: "n-1", toward: { type: targetType } }), }); -export const type5 = db.type("Type5", { +export const type5 = db.table("Type5", { name: db.string(), targetId: db.uuid().relation({ type: "n-1", toward: { type: targetType } }), }); -export const type6 = db.type("Type6", { +export const type6 = db.table("Type6", { name: db.string(), targetId: db.uuid().relation({ type: "n-1", toward: { type: targetType } }), }); -export const type7 = db.type("Type7", { +export const type7 = db.table("Type7", { name: db.string(), targetId: db.uuid().relation({ type: "n-1", toward: { type: targetType } }), }); -export const type8 = db.type("Type8", { +export const type8 = db.table("Type8", { name: db.string(), targetId: db.uuid().relation({ type: "n-1", toward: { type: targetType } }), }); -export const type9 = db.type("Type9", { +export const type9 = db.table("Type9", { name: db.string(), targetId: db.uuid().relation({ type: "n-1", toward: { type: targetType } }), }); diff --git a/packages/sdk/scripts/perf/features/tailordb-validate.ts b/packages/sdk/scripts/perf/features/tailordb-validate.ts index e4eacf505..42422acf0 100644 --- a/packages/sdk/scripts/perf/features/tailordb-validate.ts +++ b/packages/sdk/scripts/perf/features/tailordb-validate.ts @@ -5,61 +5,61 @@ */ import { db } from "../../../src/configure"; -export const type0 = db.type("Type0", { +export const type0 = db.table("Type0", { name: db.string().validate(({ value }) => value.length > 0), email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), age: db.int().validate(({ value }) => value >= 0), }); -export const type1 = db.type("Type1", { +export const type1 = db.table("Type1", { name: db.string().validate(({ value }) => value.length > 0), email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), age: db.int().validate(({ value }) => value >= 0), }); -export const type2 = db.type("Type2", { +export const type2 = db.table("Type2", { name: db.string().validate(({ value }) => value.length > 0), email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), age: db.int().validate(({ value }) => value >= 0), }); -export const type3 = db.type("Type3", { +export const type3 = db.table("Type3", { name: db.string().validate(({ value }) => value.length > 0), email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), age: db.int().validate(({ value }) => value >= 0), }); -export const type4 = db.type("Type4", { +export const type4 = db.table("Type4", { name: db.string().validate(({ value }) => value.length > 0), email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), age: db.int().validate(({ value }) => value >= 0), }); -export const type5 = db.type("Type5", { +export const type5 = db.table("Type5", { name: db.string().validate(({ value }) => value.length > 0), email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), age: db.int().validate(({ value }) => value >= 0), }); -export const type6 = db.type("Type6", { +export const type6 = db.table("Type6", { name: db.string().validate(({ value }) => value.length > 0), email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), age: db.int().validate(({ value }) => value >= 0), }); -export const type7 = db.type("Type7", { +export const type7 = db.table("Type7", { name: db.string().validate(({ value }) => value.length > 0), email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), age: db.int().validate(({ value }) => value >= 0), }); -export const type8 = db.type("Type8", { +export const type8 = db.table("Type8", { name: db.string().validate(({ value }) => value.length > 0), email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), age: db.int().validate(({ value }) => value >= 0), }); -export const type9 = db.type("Type9", { +export const type9 = db.table("Type9", { name: db.string().validate(({ value }) => value.length > 0), email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), age: db.int().validate(({ value }) => value >= 0), diff --git a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailordb/order.ts b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailordb/order.ts index b85417787..15852b93e 100644 --- a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailordb/order.ts +++ b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailordb/order.ts @@ -1,7 +1,7 @@ import { db } from "@tailor-platform/sdk"; import { user } from "./user"; -export const order = db.type("Order", { +export const order = db.table("Order", { title: db.string(), amount: db.int(), userID: db.uuid().relation({ type: "n-1", toward: { type: user } }), diff --git a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailordb/user.ts b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailordb/user.ts index 06f07b47c..2bbb93ada 100644 --- a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailordb/user.ts +++ b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/tailordb/user.ts @@ -1,6 +1,6 @@ import { db } from "@tailor-platform/sdk"; -export const user = db.type("User", { +export const user = db.table("User", { name: db.string(), email: db.string().unique(), role: db.enum(["ADMIN", "MEMBER"]), diff --git a/packages/sdk/src/cli/commands/generate/plugin-type-generator.ts b/packages/sdk/src/cli/commands/generate/plugin-type-generator.ts index 44d9e032a..ee3bab176 100644 --- a/packages/sdk/src/cli/commands/generate/plugin-type-generator.ts +++ b/packages/sdk/src/cli/commands/generate/plugin-type-generator.ts @@ -106,7 +106,7 @@ function generateTypeFileContent(info: PluginGeneratedTypeInfo): string { */ import { db } from "@tailor-platform/sdk"; - export const ${variableName} = db.type(${JSON.stringify(type.name)}, ${fieldsCode}); + export const ${variableName} = db.table(${JSON.stringify(type.name)}, ${fieldsCode}); export type ${type.name} = typeof ${variableName}; `; diff --git a/packages/sdk/src/cli/query/type-field-order.test.ts b/packages/sdk/src/cli/query/type-field-order.test.ts index 1367c31b3..4819e2f48 100644 --- a/packages/sdk/src/cli/query/type-field-order.test.ts +++ b/packages/sdk/src/cli/query/type-field-order.test.ts @@ -23,12 +23,12 @@ describe("loadTypeFieldOrder", () => { return file; } - test("loads field order from db.type builder outputs", async () => { + test("loads field order from db.table builder outputs", async () => { const typeFile = writeTypeFile( "user.ts", ` import { db } from "@tailor-platform/sdk"; -export const user = db.type("User", { +export const user = db.table("User", { firstName: db.string(), lastName: db.string(), }); diff --git a/packages/sdk/src/cli/services/application.test.ts b/packages/sdk/src/cli/services/application.test.ts index 59e235ad4..83c4c5dd1 100644 --- a/packages/sdk/src/cli/services/application.test.ts +++ b/packages/sdk/src/cli/services/application.test.ts @@ -8,7 +8,7 @@ import { defineApplication } from "./application"; describe("defineAuth parse wiring", () => { test("preserves an explicit userProfile.namespace through AuthConfigSchema.parse", async () => { - const userType = db.type("User", { + const userType = db.table("User", { email: db.string().unique(), role: db.string(), }); diff --git a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.test.ts b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.test.ts index c3f67ef62..700682f5f 100644 --- a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.test.ts +++ b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.test.ts @@ -125,7 +125,7 @@ describe("collectSourceBindings", () => { `import { db } from "@tailor-platform/sdk";`, `import { formatAddress } from "./helpers";`, `const MAX = 100;`, - `export const customer = db.type("Customer", {`, + `export const customer = db.table("Customer", {`, ` name: db.string(),`, `}).hooks({ fullAddress: { create: ({ data }) => formatAddress(data) } });`, ``, diff --git a/packages/sdk/src/cli/services/tailordb/service.test.ts b/packages/sdk/src/cli/services/tailordb/service.test.ts index c03061dba..fdca2d252 100644 --- a/packages/sdk/src/cli/services/tailordb/service.test.ts +++ b/packages/sdk/src/cli/services/tailordb/service.test.ts @@ -33,7 +33,7 @@ describe("createTailorDBService.loadTypes", () => { "user.ts", ` import { db } from "@tailor-platform/sdk"; -export const user = db.type("User", { +export const user = db.table("User", { name: db.string(), }); `, @@ -42,7 +42,7 @@ export const user = db.type("User", { "account.ts", ` import { db } from "@tailor-platform/sdk"; -export const account = db.type("User", { +export const account = db.table("User", { email: db.string(), }); `, @@ -64,7 +64,7 @@ export const account = db.type("User", { "object-prototype.ts", ` import { db } from "@tailor-platform/sdk"; -export const objectPrototype = db.type("toString", { +export const objectPrototype = db.table("toString", { value: db.string(), }); `, @@ -85,7 +85,7 @@ export const objectPrototype = db.type("toString", { "proto.ts", ` import { db } from "@tailor-platform/sdk"; -export const proto = db.type("__proto__", { +export const proto = db.table("__proto__", { value: db.string(), }); `, @@ -107,7 +107,7 @@ export const proto = db.type("__proto__", { "overlapping-glob.ts", ` import { db } from "@tailor-platform/sdk"; -export const user = db.type("User", { +export const user = db.table("User", { name: db.string(), }); `, @@ -130,7 +130,7 @@ export const user = db.type("User", { importPath: "@example/namespace", onNamespaceLoaded: () => ({ types: { - auditLog: db.type("AuditLog", { + auditLog: db.table("AuditLog", { message: db.string(), }), }, diff --git a/packages/sdk/src/configure/services/auth/index.test.ts b/packages/sdk/src/configure/services/auth/index.test.ts index d04262be7..6578214ea 100644 --- a/packages/sdk/src/configure/services/auth/index.test.ts +++ b/packages/sdk/src/configure/services/auth/index.test.ts @@ -11,7 +11,7 @@ import type { } from "#/configure/services/auth/types"; import type { JsonObject } from "type-fest"; -const userType = db.type("User", { +const userType = db.table("User", { email: db.string().unique(), role: db.string(), isActive: db.bool(), diff --git a/packages/sdk/src/configure/services/executor/executor.test.ts b/packages/sdk/src/configure/services/executor/executor.test.ts index 1dcc063f4..8778d760a 100644 --- a/packages/sdk/src/configure/services/executor/executor.test.ts +++ b/packages/sdk/src/configure/services/executor/executor.test.ts @@ -20,7 +20,7 @@ import type { TailorPrincipal } from "#/runtime/types"; import type { Operation } from "./operation"; const createUserType = () => - db.type("User", { + db.table("User", { name: db.string(), age: db.int(), }); diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 8f7067a1d..58bf70fb1 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -8,9 +8,24 @@ import type { output, TypeLevelError } from "#/types/helpers"; import type { Hook } from "./types"; import type { StandardSchemaV1 } from "@standard-schema/spec"; +describe("TailorDB table builder", () => { + test("db.table creates a table with the generated id field", () => { + const user = db.table("User", { + name: db.string(), + }); + + expect(user.name).toBe("User"); + expect(user.fields.id).toBeDefined(); + expectTypeOf>().toEqualTypeOf<{ + id: string; + name: string; + }>(); + }); +}); + describe("TailorDBField basic field type tests", () => { test("string field outputs string type correctly", () => { - const _stringType = db.type("Test", { + const _stringType = db.table("Test", { name: db.string(), }); expectTypeOf>().toEqualTypeOf<{ @@ -20,7 +35,7 @@ describe("TailorDBField basic field type tests", () => { }); test("int field outputs number type correctly", () => { - const _intType = db.type("Test", { + const _intType = db.table("Test", { age: db.int(), }); expectTypeOf>().toEqualTypeOf<{ @@ -30,7 +45,7 @@ describe("TailorDBField basic field type tests", () => { }); test("bool field outputs boolean type correctly", () => { - const _boolType = db.type("Test", { + const _boolType = db.table("Test", { active: db.bool(), }); expectTypeOf>().toEqualTypeOf<{ @@ -40,7 +55,7 @@ describe("TailorDBField basic field type tests", () => { }); test("float field outputs number type correctly", () => { - const _floatType = db.type("Test", { + const _floatType = db.table("Test", { price: db.float(), }); expectTypeOf>().toEqualTypeOf<{ @@ -50,7 +65,7 @@ describe("TailorDBField basic field type tests", () => { }); test("uuid field outputs string type correctly", () => { - const _uuidType = db.type("Test", { + const _uuidType = db.table("Test", { uuid: db.uuid(), }); expectTypeOf>().toEqualTypeOf<{ @@ -60,7 +75,7 @@ describe("TailorDBField basic field type tests", () => { }); test("date field outputs string type correctly", () => { - const _dateType = db.type("Test", { + const _dateType = db.table("Test", { birthDate: db.date(), }); expectTypeOf>().toEqualTypeOf<{ @@ -70,7 +85,7 @@ describe("TailorDBField basic field type tests", () => { }); test("datetime field outputs string | Date type correctly", () => { - const _datetimeType = db.type("Test", { + const _datetimeType = db.table("Test", { timestamp: db.datetime(), }); expectTypeOf>().toMatchObjectType<{ @@ -80,7 +95,7 @@ describe("TailorDBField basic field type tests", () => { }); test("time field outputs string type correctly", () => { - const _timeType = db.type("Test", { + const _timeType = db.table("Test", { openingTime: db.time(), }); expectTypeOf>().toEqualTypeOf<{ @@ -92,7 +107,7 @@ describe("TailorDBField basic field type tests", () => { describe("TailorDBField optional option tests", () => { test("optional option generates nullable type", () => { - const _optionalType = db.type("Test", { + const _optionalType = db.table("Test", { description: db.string({ optional: true }), }); expectTypeOf>().toEqualTypeOf<{ @@ -102,7 +117,7 @@ describe("TailorDBField optional option tests", () => { }); test("multiple optional fields work correctly", () => { - const _multiOptionalType = db.type("Test", { + const _multiOptionalType = db.table("Test", { title: db.string(), description: db.string({ optional: true }), count: db.int({ optional: true }), @@ -118,7 +133,7 @@ describe("TailorDBField optional option tests", () => { describe("TailorDBField array option tests", () => { test("array option generates array type", () => { - const _arrayType = db.type("Test", { + const _arrayType = db.table("Test", { tags: db.string({ array: true }), }); expectTypeOf>().toEqualTypeOf<{ @@ -128,7 +143,7 @@ describe("TailorDBField array option tests", () => { }); test("optional array works correctly", () => { - const _optionalArrayType = db.type("Test", { + const _optionalArrayType = db.table("Test", { items: db.string({ optional: true, array: true }), }); expectTypeOf>().toEqualTypeOf<{ @@ -138,7 +153,7 @@ describe("TailorDBField array option tests", () => { }); test("multiple array fields work correctly", () => { - const _multiArrayType = db.type("Test", { + const _multiArrayType = db.table("Test", { tags: db.string({ array: true }), numbers: db.int({ array: true }), flags: db.bool({ array: true }), @@ -195,7 +210,7 @@ describe("TailorDBField enum field tests", () => { }); test("optional enum() works correctly", () => { - const _optionalEnumType = db.type("Test", { + const _optionalEnumType = db.table("Test", { priority: db.enum(["high", "medium", "low"], { optional: true }), }); expectTypeOf>().toEqualTypeOf<{ @@ -216,7 +231,7 @@ describe("TailorDBField enum field tests", () => { }); test("enum array works correctly", () => { - const _enumArrayType = db.type("Test", { + const _enumArrayType = db.table("Test", { categories: db.enum(["a", "b", "c"], { array: true }), }); expectTypeOf>().toEqualTypeOf<{ @@ -227,12 +242,12 @@ describe("TailorDBField enum field tests", () => { }); describe("TailorDBField RelationConfig option field tests", () => { - const User = db.type("User", { + const User = db.table("User", { name: db.string(), email: db.string(), }); - const Customer = db.type("Customer", { + const Customer = db.table("Customer", { name: db.string(), customerId: db.string(), }); @@ -358,7 +373,7 @@ describe("TailorDBField RelationConfig option field tests", () => { describe("TailorDBField modifier chain tests", () => { test("index() modifier does not affect type", () => { - const _indexType = db.type("Test", { + const _indexType = db.table("Test", { email: db.string().index(), }); expectTypeOf>().toEqualTypeOf<{ @@ -368,7 +383,7 @@ describe("TailorDBField modifier chain tests", () => { }); test("unique() modifier does not affect type", () => { - const _uniqueType = db.type("Test", { + const _uniqueType = db.table("Test", { username: db.string().unique(), }); expectTypeOf>().toEqualTypeOf<{ @@ -394,7 +409,7 @@ describe("TailorDBField type error message tests", () => { TypeLevelError<".description() has already been set"> >(); - const _userType = db.type("User", { + const _userType = db.table("User", { name: db.string(), }); const related = db.uuid().relation({ @@ -491,10 +506,10 @@ describe("TailorDBField type error message tests", () => { describe("TailorDBField relation modifier tests", () => { test("relation does not create reference type", () => { - const _userType = db.type("User", { + const _userType = db.table("User", { name: db.string(), }); - const _postType = db.type("Post", { + const _postType = db.table("Post", { title: db.string(), authorId: db.uuid().relation({ type: "oneToOne", @@ -510,7 +525,7 @@ describe("TailorDBField relation modifier tests", () => { }); test("attempting to set relation twice causes type error", () => { - const _userType = db.type("User", { + const _userType = db.table("User", { name: db.string(), }); @@ -528,7 +543,7 @@ describe("TailorDBField relation modifier tests", () => { describe("TailorDBField hooks modifier tests", () => { test("hooks modifier does not affect output type", () => { - const _hookType = db.type("Test", { + const _hookType = db.table("Test", { name: db.string().hooks({ create: () => "created", update: () => "updated", @@ -562,7 +577,7 @@ describe("TailorDBField hooks modifier tests", () => { describe("TailorDBField validate modifier tests", () => { test("validate modifier does not affect type", () => { - const _validateType = db.type("Test", { + const _validateType = db.table("Test", { email: db.string().validate(() => true), }); expectTypeOf>().toEqualTypeOf<{ @@ -572,7 +587,7 @@ describe("TailorDBField validate modifier tests", () => { }); test("validate modifier can receive object with message", () => { - const _validateType = db.type("Test", { + const _validateType = db.table("Test", { email: db.string().validate([({ value }) => value.includes("@"), "Email must contain @"]), }); expectTypeOf>().toEqualTypeOf<{ @@ -589,7 +604,7 @@ describe("TailorDBField validate modifier tests", () => { }); test("validate modifier can receive multiple validators", () => { - const _validateType = db.type("Test", { + const _validateType = db.table("Test", { password: db .string() .validate( @@ -706,7 +721,7 @@ describe("TailorDBField unique modifier tests", () => { describe("TailorDBType withTimestamps option tests", () => { test("withTimestamps: true adds timestamp fields", () => { - const _timestampType = db.type("TestWithTimestamp", { + const _timestampType = db.table("TestWithTimestamp", { name: db.string(), ...db.fields.timestamps(), }); @@ -782,7 +797,7 @@ describe("TailorDBType withTimestamps option tests", () => { describe("TailorDBType composite type tests", () => { test("type with multiple fields works correctly", () => { - const _complexType = db.type("User", { + const _complexType = db.table("User", { name: db.string(), email: db.string(), age: db.int({ optional: true }), @@ -812,7 +827,7 @@ describe("TailorDBType composite type tests", () => { describe("TailorDBType edge case tests", () => { test("type with single field works correctly", () => { - const _singleFieldType = db.type("Simple", { + const _singleFieldType = db.table("Simple", { value: db.string(), }); expectTypeOf>().toEqualTypeOf<{ @@ -822,7 +837,7 @@ describe("TailorDBType edge case tests", () => { }); test("type with all optional fields works correctly", () => { - const _allOptionalType = db.type("Optional", { + const _allOptionalType = db.table("Optional", { a: db.string({ optional: true }), b: db.int({ optional: true }), c: db.bool({ optional: true }), @@ -836,7 +851,7 @@ describe("TailorDBType edge case tests", () => { }); test("type with all array fields works correctly", () => { - const _allArrayType = db.type("Array", { + const _allArrayType = db.table("Array", { strings: db.string({ array: true }), numbers: db.int({ array: true }), booleans: db.bool({ array: true }), @@ -852,11 +867,11 @@ describe("TailorDBType edge case tests", () => { describe("TailorDBType type consistency tests", () => { test("same definition generates same type", () => { - const _type1 = db.type("Same", { + const _type1 = db.table("Same", { name: db.string(), age: db.int(), }); - const _type2 = db.type("Same", { + const _type2 = db.table("Same", { name: db.string(), age: db.int(), }); @@ -864,7 +879,7 @@ describe("TailorDBType type consistency tests", () => { }); test("id field is automatically added", () => { - const _typeWithoutId = db.type("Test", { + const _typeWithoutId = db.table("Test", { name: db.string(), }); expectTypeOf>().toEqualTypeOf<{ @@ -876,7 +891,7 @@ describe("TailorDBType type consistency tests", () => { describe("TailorDBType self relation tests", () => { test("when toward.type is self, rawRelation stores the config (processing happens in parser layer)", () => { - const TestType = db.type("TestType", { + const TestType = db.table("TestType", { name: db.string(), parentID: db.uuid().relation({ type: "n-1", @@ -912,7 +927,7 @@ describe("TailorDBType self relation tests", () => { }); test("when backward is not specified, undefined is stored in rawRelation (inflection happens in parser layer)", () => { - const A = db.type("Node", { + const A = db.table("Node", { // Many-to-one (non-unique): backward is plural (nodes) parentID: db.uuid().relation({ type: "n-1", toward: { type: "self" } }), // One-to-one (unique): backward is singular (node) @@ -929,7 +944,7 @@ describe("TailorDBType self relation tests", () => { describe("TailorDBType plural form tests", () => { test("when defining type with single name, pluralForm is not set in configure (inflection is executed at parser layer)", () => { - const _userType = db.type("User", { + const _userType = db.table("User", { name: db.string(), }); @@ -937,7 +952,7 @@ describe("TailorDBType plural form tests", () => { }); test("when specifying name and plural form as tuple, pluralForm is set", () => { - const _personType = db.type(["Person", "People"], { + const _personType = db.table(["Person", "People"], { name: db.string(), }); @@ -945,7 +960,7 @@ describe("TailorDBType plural form tests", () => { }); test("when plural form is empty string, it is not set in configure (inflection is executed at parser layer)", () => { - const _dataType = db.type(["Datum", ""], { + const _dataType = db.table(["Datum", ""], { value: db.string(), }); @@ -953,7 +968,7 @@ describe("TailorDBType plural form tests", () => { }); test("error when plural form is same as name (when explicitly specified in tuple format)", () => { - expect(() => db.type(["Data", "Data"], {})).toThrowError( + expect(() => db.table(["Data", "Data"], {})).toThrowError( "The name and the plural form must be different. name=Data", ); }); @@ -964,7 +979,7 @@ describe("TailorDBType plural form tests", () => { ["Item", "100Items"], ["Data", "DataSet"], ])("plural form %s/%s can be set via tuple format", (name, pluralForm) => { - const _type = db.type([name, pluralForm], { + const _type = db.table([name, pluralForm], { value: db.string(), }); @@ -972,7 +987,7 @@ describe("TailorDBType plural form tests", () => { }); test("all existing features work correctly with tuple format", () => { - const _postType = db.type(["Post", "Posts"], { + const _postType = db.table(["Post", "Posts"], { title: db.string(), content: db.string({ optional: true }), ...db.fields.timestamps(), @@ -992,7 +1007,7 @@ describe("TailorDBType plural form tests", () => { test("validation and plural form coexist in tuple format", () => { const _userType = db - .type(["User", "Users"], { + .table(["User", "Users"], { name: db.string(), email: db.string(), }) @@ -1011,11 +1026,11 @@ describe("TailorDBType plural form tests", () => { }); test("plural form works correctly for types with relations", () => { - const _categoryType = db.type(["Category", "Categories"], { + const _categoryType = db.table(["Category", "Categories"], { name: db.string(), }); - const _productType = db.type(["Product", "Products"], { + const _productType = db.table(["Product", "Products"], { name: db.string(), categoryId: db.uuid().relation({ type: "oneToOne", @@ -1031,7 +1046,7 @@ describe("TailorDBType plural form tests", () => { describe("TailorDBType hooks modifier tests", () => { test("hooks modifier does not affect output type", () => { const _hookType = db - .type("Test", { + .table("Test", { name: db.string(), }) .hooks({ @@ -1047,7 +1062,7 @@ describe("TailorDBType hooks modifier tests", () => { }); test("setting hooks on id causes type error", () => { - db.type("Test", { + db.table("Test", { name: db.string(), }).hooks({ // @ts-expect-error hooks() cannot be called on the "id" field @@ -1058,7 +1073,7 @@ describe("TailorDBType hooks modifier tests", () => { }); test("setting hooks on nested field causes type error", () => { - db.type("Test", { + db.table("Test", { name: db.object({ first: db.string(), last: db.string(), @@ -1072,7 +1087,7 @@ describe("TailorDBType hooks modifier tests", () => { }); test("hooks modifier on string field receives string", () => { - const testType = db.type("Test", { name: db.string() }); + const testType = db.table("Test", { name: db.string() }); const _hooks = testType.hooks; type ExpectedHooksParam = Parameters[0]; type ActualNameType = Exclude; @@ -1089,7 +1104,7 @@ describe("TailorDBType hooks modifier tests", () => { }); test("hooks modifier on optional field receives null", () => { - const testType = db.type("Test", { + const testType = db.table("Test", { name: db.string({ optional: true }), }); const _hooks = testType.hooks; @@ -1111,7 +1126,7 @@ describe("TailorDBType hooks modifier tests", () => { describe("TailorDBType validate modifier tests", () => { test("validate modifier can receive function", () => { const _validateType = db - .type("Test", { + .table("Test", { email: db.string(), }) .validate({ @@ -1128,7 +1143,7 @@ describe("TailorDBType validate modifier tests", () => { test("validate modifier can receive object with message", () => { const _validateType = db - .type("Test", { + .table("Test", { email: db.string(), }) .validate({ @@ -1143,7 +1158,7 @@ describe("TailorDBType validate modifier tests", () => { test("validate modifier can receive multiple validators", () => { const _validateType = db - .type("Test", { + .table("Test", { password: db.string(), }) .validate({ @@ -1162,7 +1177,7 @@ describe("TailorDBType validate modifier tests", () => { }); test("type error occurs when validate is already set on TailorDBField", () => { - db.type("Test", { + db.table("Test", { name: db.string().validate(() => true), // @ts-expect-error validate() cannot be called after validate() has already been called }).validate({ @@ -1171,7 +1186,7 @@ describe("TailorDBType validate modifier tests", () => { }); test("setting validate on id causes type error", () => { - db.type("Test", { + db.table("Test", { name: db.string(), }).validate({ // @ts-expect-error validate() cannot be called on the "id" field @@ -1180,14 +1195,14 @@ describe("TailorDBType validate modifier tests", () => { }); test("validate modifier on string field receives string", () => { - const _validate = db.type("Test", { name: db.string() }).validate; + const _validate = db.table("Test", { name: db.string() }).validate; expectTypeOf>().toExtend< Parameters[0]["name"] >(); }); test("validate modifier on optional field receives null", () => { - const _validate = db.type("Test", { + const _validate = db.table("Test", { name: db.string({ optional: true }), }).validate; expectTypeOf>().toExtend< @@ -1198,7 +1213,7 @@ describe("TailorDBType validate modifier tests", () => { describe("db.object tests", () => { test("correctly infers basic object type", () => { - const _objectType = db.type("Test", { + const _objectType = db.table("Test", { user: db.object({ name: db.string(), age: db.int(), @@ -1224,7 +1239,7 @@ describe("db.object tests", () => { }); test("correctly infers object type with optional fields", () => { - const _objectType = db.type("Test", { + const _objectType = db.table("Test", { user: db.object({ name: db.string(), age: db.int({ optional: true }), @@ -1242,7 +1257,7 @@ describe("db.object tests", () => { }); test("correctly infers object type with optional option", () => { - const _objectType = db.type("Test", { + const _objectType = db.table("Test", { user: db.object( { name: db.string(), @@ -1261,7 +1276,7 @@ describe("db.object tests", () => { }); test("correctly infers object type with array option", () => { - const _objectType = db.type("Test", { + const _objectType = db.table("Test", { users: db.object( { name: db.string(), @@ -1280,7 +1295,7 @@ describe("db.object tests", () => { }); test("correctly infers object type with array fields", () => { - const _objectType = db.type("Test", { + const _objectType = db.table("Test", { user: db.object({ name: db.string(), tags: db.string({ array: true }), @@ -1298,7 +1313,7 @@ describe("db.object tests", () => { }); test("correctly infers object type with multiple modifiers", () => { - const _objectType = db.type("Test", { + const _objectType = db.table("Test", { optionalUsers: db.object( { name: db.string(), @@ -1321,7 +1336,7 @@ describe("db.object tests", () => { }); test("correctly infers object type with bool type", () => { - const _objectType = db.type("Test", { + const _objectType = db.table("Test", { settings: db.object({ enabled: db.bool(), push: db.bool({ optional: true }), @@ -1337,7 +1352,7 @@ describe("db.object tests", () => { }); test("correctly infers object type with float and enum types", () => { - const _objectType = db.type("Test", { + const _objectType = db.table("Test", { product: db.object({ name: db.string(), price: db.float(), @@ -1370,7 +1385,7 @@ describe("TailorField/TailorType compatibility tests", () => { describe("TailorDBType/TailorDBField description support", () => { test("TailorDBField supports description", () => { - const userType = db.type("User", { + const userType = db.table("User", { name: db.string().description("User name"), age: db.int().description("User age"), }); @@ -1380,7 +1395,7 @@ describe("TailorDBType/TailorDBField description support", () => { }); test("TailorDBType description is set via second argument", () => { - const userType = db.type("User", "User profile type", { + const userType = db.table("User", "User profile type", { name: db.string(), }); @@ -1388,7 +1403,7 @@ describe("TailorDBType/TailorDBField description support", () => { }); test("TailorDBField nested object supports description", () => { - const profileType = db.type("Profile", { + const profileType = db.table("Profile", { userInfo: db .object({ name: db.string().description("Full name"), @@ -1403,7 +1418,7 @@ describe("TailorDBType/TailorDBField description support", () => { }); test("TailorDBType can be used in resolver with description preserved", () => { - const userType = db.type("User", "User type for resolver", { + const userType = db.table("User", "User type for resolver", { name: db.string().description("User name"), email: db.string().description("User email"), }); @@ -1447,7 +1462,7 @@ describe("TailorDBField fluent API type preservation", () => { }); test("relation() preserves uuid type", () => { - const User = db.type("User", { name: db.string() }); + const User = db.table("User", { name: db.string() }); const _field = db .uuid() .description("User reference") @@ -1459,7 +1474,7 @@ describe("TailorDBField fluent API type preservation", () => { describe("TailorDBType files method tests", () => { test("files method adds file fields to metadata", () => { const userType = db - .type("User", { + .table("User", { name: db.string(), }) .files({ @@ -1474,7 +1489,7 @@ describe("TailorDBType files method tests", () => { }); test("files field names cannot conflict with existing field names (type error)", () => { - const _userType = db.type("User", { + const _userType = db.table("User", { name: db.string(), avatar: db.string(), // existing field }); @@ -1492,7 +1507,7 @@ describe("TailorDBType files method tests", () => { }); test("files field names that do not conflict are allowed", () => { - const _userType = db.type("User", { + const _userType = db.table("User", { name: db.string(), }); @@ -1623,7 +1638,7 @@ describe("TailorDBField runtime validation tests", () => { describe("TailorDBType gqlOperations tests", () => { test("gqlOperations stores raw config via features()", () => { const orderType = db - .type("Order", { + .table("Order", { name: db.string(), }) .features({ @@ -1639,7 +1654,7 @@ describe("TailorDBType gqlOperations tests", () => { test("gqlOperations stores multiple operations config", () => { const archiveType = db - .type("Archive", { + .table("Archive", { data: db.string(), }) .features({ @@ -1656,7 +1671,7 @@ describe("TailorDBType gqlOperations tests", () => { test("gqlOperations stores read config", () => { const secretType = db - .type("Secret", { + .table("Secret", { value: db.string(), }) .features({ @@ -1671,7 +1686,7 @@ describe("TailorDBType gqlOperations tests", () => { test("gqlOperations works with other features", () => { const logType = db - .type("Log", { + .table("Log", { message: db.string(), }) .features({ @@ -1691,7 +1706,7 @@ describe("TailorDBType gqlOperations tests", () => { describe("TailorDBType gqlOperations alias tests", () => { test("gqlOperations: 'query' stores alias as raw value", () => { const readOnlyType = db - .type("ReadOnly", { + .table("ReadOnly", { data: db.string(), }) .features({ @@ -1705,7 +1720,7 @@ describe("TailorDBType gqlOperations alias tests", () => { test("gqlOperations: 'query' works with other features", () => { const auditType = db - .type("Audit", { + .table("Audit", { action: db.string(), }) .features({ @@ -1786,7 +1801,7 @@ describe("TailorDBField immutability", () => { }); test("field.relation() returns a new field without mutating the original", () => { - const User = db.type("User", { name: db.string() }); + const User = db.table("User", { name: db.string() }); const original = db.uuid(); const withRelation = original.relation({ type: "n-1", toward: { type: User } }); @@ -1812,8 +1827,8 @@ describe("TailorDBType does not mutate shared fields", () => { test("type.hooks() does not mutate the shared field", () => { const sharedField = db.string(); - const typeA = db.type("TypeA", { name: sharedField }).hooks({ name: { create: () => "A" } }); - const typeB = db.type("TypeB", { name: sharedField }); + const typeA = db.table("TypeA", { name: sharedField }).hooks({ name: { create: () => "A" } }); + const typeB = db.table("TypeB", { name: sharedField }); expect(typeA.fields.name.metadata.hooks).toBeDefined(); expect(typeB.fields.name.metadata.hooks).toBeUndefined(); @@ -1824,9 +1839,9 @@ describe("TailorDBType does not mutate shared fields", () => { const sharedField = db.string(); const typeA = db - .type("TypeA", { email: sharedField }) + .table("TypeA", { email: sharedField }) .validate({ email: ({ value }) => value.includes("@") }); - const typeB = db.type("TypeB", { email: sharedField }); + const typeB = db.table("TypeB", { email: sharedField }); expect(typeA.fields.email.metadata.validate).toBeDefined(); expect(typeB.fields.email.metadata.validate).toBeUndefined(); @@ -1837,7 +1852,7 @@ describe("TailorDBType does not mutate shared fields", () => { const nameField = db.string(); const fields = { name: nameField }; - db.type("TypeA", fields).hooks({ name: { create: () => "hooked" } }); + db.table("TypeA", fields).hooks({ name: { create: () => "hooked" } }); // The fields record should still reference the original field instance expect(fields.name).toBe(nameField); @@ -1847,7 +1862,7 @@ describe("TailorDBType does not mutate shared fields", () => { const emailField = db.string(); const fields = { email: emailField }; - db.type("TypeA", fields).validate({ email: ({ value }) => value.includes("@") }); + db.table("TypeA", fields).validate({ email: ({ value }) => value.includes("@") }); // The fields record should still reference the original field instance expect(fields.email).toBe(emailField); @@ -1919,7 +1934,7 @@ describe("TailorDBField clone tests", () => { }); test("pickFields with options preserves field output base type", () => { - const User = db.type("User", { + const User = db.table("User", { role: db.enum(["admin", "member"]), profile: db.object({ name: db.string() }, { optional: true }), }); @@ -1939,7 +1954,7 @@ describe("TailorDBField clone tests", () => { }); test("clones relation config correctly", () => { - const User = db.type("User", { name: db.string() }); + const User = db.table("User", { name: db.string() }); const original = db.uuid().relation({ type: "n-1", toward: { type: User, as: "author" }, @@ -2047,7 +2062,7 @@ describe("TailorDBField clone tests", () => { describe("TailorDBField decimal type tests", () => { test("decimal field outputs string type correctly", () => { - const _decimalType = db.type("Test", { + const _decimalType = db.table("Test", { price: db.decimal(), }); expectTypeOf>().toEqualTypeOf<{ @@ -2057,7 +2072,7 @@ describe("TailorDBField decimal type tests", () => { }); test("optional decimal field outputs string | null type correctly", () => { - const _decimalType = db.type("Test", { + const _decimalType = db.table("Test", { discount: db.decimal({ optional: true }), }); expectTypeOf>().toEqualTypeOf<{ diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 150436db3..c7bc8fde8 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -968,18 +968,18 @@ function createTailorDBType< const idField = uuid(); type idField = typeof idField; -type DBType> = TailorDBInstance< +type DBTable> = TailorDBInstance< { id: idField } & F >; /** - * Creates a new database type with the specified fields. - * An `id` field (UUID) is automatically added to every type. - * @param name - The name of the type, or a tuple of [name, pluralForm] - * @param fields - The field definitions for the type - * @returns A new TailorDBType instance + * Creates a new database table with the specified fields. + * An `id` field (UUID) is automatically added to every table. + * @param name - The name of the table, or a tuple of [name, pluralForm] + * @param fields - The field definitions for the table + * @returns A new TailorDB table instance * @example - * export const user = db.type("User", { + * export const user = db.table("User", { * name: db.string(), * email: db.string(), * age: db.int({ optional: true }), @@ -989,28 +989,28 @@ type DBType> = Tailo * // Always export both the value and type: * export type user = typeof user; */ -function dbType>( +function dbTable>( name: string | [string, string], fields: F, -): DBType; +): DBTable; /** - * Creates a new database type with the specified fields and description. - * An `id` field (UUID) is automatically added to every type. - * @param name - The name of the type, or a tuple of [name, pluralForm] - * @param description - A description of the type - * @param fields - The field definitions for the type - * @returns A new TailorDBType instance + * Creates a new database table with the specified fields and description. + * An `id` field (UUID) is automatically added to every table. + * @param name - The name of the table, or a tuple of [name, pluralForm] + * @param description - A description of the table + * @param fields - The field definitions for the table + * @returns A new TailorDB table instance */ -function dbType>( +function dbTable>( name: string | [string, string], description: string, fields: F, -): DBType; -function dbType>( +): DBTable; +function dbTable>( name: string | [string, string], fieldsOrDescription: string | F, fields?: F, -): DBType { +): DBTable { const typeName = Array.isArray(name) ? name[0] : name; const pluralForm = Array.isArray(name) ? name[1] : undefined; @@ -1029,12 +1029,12 @@ function dbType; + ) as DBTable; } -/** TailorDB schema builder utilities for defining types and fields. */ +/** TailorDB schema builder utilities for defining tables and fields. */ export const db = { - type: dbType, + table: dbTable, uuid, string, bool, @@ -1054,7 +1054,7 @@ export const db = { * historical records); the current time is used only when the value is omitted. * @returns An object with createdAt and updatedAt fields * @example - * const model = db.type("Model", { + * const model = db.table("Model", { * name: db.string(), * ...db.fields.timestamps(), * }); diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index bbde489da..adb69aaab 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -135,7 +135,7 @@ export interface TailorDBType< export type TailorAnyDBType = TailorDBType; export type TailorDBInstance< - // Default kept loose for convenience; callers still get fully inferred types from `db.type()`. + // Default kept loose for convenience; callers still get fully inferred types from `db.table()`. // oxlint-disable-next-line no-explicit-any Fields extends Record = any, User extends object = InferredAttributes, diff --git a/packages/sdk/src/parser/service/auth/index.test.ts b/packages/sdk/src/parser/service/auth/index.test.ts index a882352ab..8189d3ecf 100644 --- a/packages/sdk/src/parser/service/auth/index.test.ts +++ b/packages/sdk/src/parser/service/auth/index.test.ts @@ -9,7 +9,7 @@ import type { OptionalKeysOf } from "type-fest"; import type { z } from "zod"; // Define userType for type inference -const userType = db.type("User", { +const userType = db.table("User", { email: db.string().unique(), role: db.string(), isActive: db.bool(), diff --git a/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts b/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts index 87552f9be..226b4c072 100644 --- a/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts +++ b/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts @@ -9,7 +9,7 @@ describe("parseFieldConfig precompiled expressions", () => { const createHook = ({ value }: { value: string | null }) => value ?? "fallback"; setPrecompiledScriptExpr(createHook, "PRECOMPILED_HOOK_EXPR"); - const type = db.type("User", { + const type = db.table("User", { email: db.string().hooks({ create: createHook }), }); @@ -23,7 +23,7 @@ describe("parseFieldConfig precompiled expressions", () => { const validator = ({ value }: { value: string }) => value.length > 0; setPrecompiledScriptExpr(validator, "PRECOMPILED_VALIDATE_EXPR"); - const type = db.type("User", { + const type = db.table("User", { email: db.string().validate(validator), }); diff --git a/packages/sdk/src/parser/service/tailordb/field.test.ts b/packages/sdk/src/parser/service/tailordb/field.test.ts index e551aef14..a1b830b50 100644 --- a/packages/sdk/src/parser/service/tailordb/field.test.ts +++ b/packages/sdk/src/parser/service/tailordb/field.test.ts @@ -128,7 +128,7 @@ describe("parseFieldConfig validator expressions", () => { return [value].map((v) => v.includes("@"))[0] ?? false; }, }; - const type = db.type("User", { + const type = db.table("User", { email: db.string().validate(validators.isValid), }); @@ -150,7 +150,7 @@ describe("parseFieldConfig script expression validation", () => { return value ?? "generated"; }, }; - const type = db.type("User", { + const type = db.table("User", { email: db.string().hooks({ create: hooks[key] }), }); @@ -165,7 +165,7 @@ describe("parseFieldConfig script expression validation", () => { const check = function check({ value }: { value: string }) { return value.length > 0; }.bind(null); - const type = db.type("User", { + const type = db.table("User", { email: db.string().validate(check), }); @@ -180,7 +180,7 @@ describe("parseFieldConfig script expression validation", () => { const check = function check({ value }: { value: string }) { return value.length > 0; }.bind(null); - const type = db.type("User", { + const type = db.table("User", { email: db.string().validate(check), }); diff --git a/packages/sdk/src/parser/service/tailordb/type-parser.test.ts b/packages/sdk/src/parser/service/tailordb/type-parser.test.ts index 7c48ec048..c59e91788 100644 --- a/packages/sdk/src/parser/service/tailordb/type-parser.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-parser.test.ts @@ -5,7 +5,7 @@ import { parseTypes } from "./type-parser"; describe("parseTypes", () => { test("allows type names that match Object prototype properties", () => { - const testType = db.type("toString", { + const testType = db.table("toString", { value: db.string(), }); @@ -15,7 +15,7 @@ describe("parseTypes", () => { }); test("allows __proto__ as a type name", () => { - const testType = db.type("__proto__", { + const testType = db.table("__proto__", { value: db.string(), }); @@ -36,7 +36,7 @@ describe("parseTypes", () => { const field = db.string({ array: true }); (field as unknown as { _metadata: Record })._metadata[metadataKey] = true; - const testType = db.type("Test", { + const testType = db.table("Test", { tags: field, }); @@ -49,7 +49,7 @@ describe("parseTypes", () => { ["index", () => db.string().index()], ["unique", () => db.string().unique()], ] as const)("should allow %s on non-array fields", (metadataKey, buildField) => { - const testType = db.type("Test", { + const testType = db.table("Test", { email: buildField(), }); @@ -60,11 +60,11 @@ describe("parseTypes", () => { describe("buildBackwardRelationships", () => { test("should build backward relationships correctly", () => { - const employee = db.type("Employee", { + const employee = db.table("Employee", { name: db.string(), }); - const performanceReview = db.type("PerformanceReview", { + const performanceReview = db.table("PerformanceReview", { employeeId: db.uuid().relation({ type: "n-1", toward: { type: employee }, @@ -88,13 +88,13 @@ describe("parseTypes", () => { }); test("should throw error when backward relation names are duplicated", () => { - const employee = db.type("Employee", { + const employee = db.table("Employee", { name: db.string(), }); // Two fields referencing the same type without explicit backward names // Both will generate "performanceReviews" as the backward name - const performanceReview = db.type("PerformanceReview", { + const performanceReview = db.table("PerformanceReview", { targetEmployeeId: db.uuid().relation({ type: "n-1", toward: { type: employee, as: "targetEmployee" }, @@ -117,12 +117,12 @@ describe("parseTypes", () => { }); test("should not throw error when backward names are explicitly set to be unique", () => { - const employee = db.type("Employee", { + const employee = db.table("Employee", { name: db.string(), }); // Two fields referencing the same type with explicit unique backward names - const performanceReview = db.type("PerformanceReview", { + const performanceReview = db.table("PerformanceReview", { targetEmployeeId: db.uuid().relation({ type: "n-1", toward: { type: employee, as: "targetEmployee" }, @@ -155,11 +155,11 @@ describe("parseTypes", () => { }); test("should include source file information in error message when available", () => { - const employee = db.type("Employee", { + const employee = db.table("Employee", { name: db.string(), }); - const performanceReview = db.type("PerformanceReview", { + const performanceReview = db.table("PerformanceReview", { targetEmployeeId: db.uuid().relation({ type: "n-1", toward: { type: employee, as: "targetEmployee" }, @@ -187,12 +187,12 @@ describe("parseTypes", () => { }); test("should generate default backward names using inflection", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); // No explicit backward name, should generate "posts" (plural of "Post") - const post = db.type("Post", { + const post = db.table("Post", { userId: db.uuid().relation({ type: "n-1", toward: { type: user }, @@ -210,12 +210,12 @@ describe("parseTypes", () => { }); test("should generate singular backward name for unique relations", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); // Unique relation (1-1), should generate singular "profile" - const profile = db.type("Profile", { + const profile = db.table("Profile", { userId: db.uuid().relation({ type: "1-1", toward: { type: user }, @@ -239,7 +239,7 @@ describe("parseTypes", () => { [ "existing field", () => - db.type("User", { + db.table("User", { name: db.string(), posts: db.string({ array: true }), }), @@ -248,7 +248,7 @@ describe("parseTypes", () => { "files field", () => db - .type("User", { + .table("User", { name: db.string(), }) .files({ @@ -261,7 +261,7 @@ describe("parseTypes", () => { const user = buildUser(); // Post's backward relation will generate "posts" which conflicts - const post = db.type("Post", { + const post = db.table("Post", { userId: db.uuid().relation({ type: "n-1", toward: { type: user }, @@ -277,13 +277,13 @@ describe("parseTypes", () => { describe("forwardRelationships", () => { test("should throw error when forward relation names are duplicated", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); // Two fields referencing the same type without explicit forward names ("as") // Both will generate "user" as the forward name - const post = db.type("Post", { + const post = db.table("Post", { authorID: db.uuid().relation({ type: "n-1", toward: { type: user }, @@ -302,11 +302,11 @@ describe("parseTypes", () => { }); test("should not throw error when forward names are explicitly set to be unique", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); - const post = db.type("Post", { + const post = db.table("Post", { authorID: db.uuid().relation({ type: "n-1", toward: { type: user, as: "author" }, @@ -336,12 +336,12 @@ describe("parseTypes", () => { }); test("should throw error when forward name conflicts with existing field", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); // Post has a field named "user" - const post = db.type("Post", { + const post = db.table("Post", { user: db.string(), authorID: db.uuid().relation({ type: "n-1", @@ -355,12 +355,12 @@ describe("parseTypes", () => { }); test("should throw error when conflicting field is defined after the relation field", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); // The conflicting "user" field is defined after authorID in the object - const post = db.type("Post", { + const post = db.table("Post", { authorID: db.uuid().relation({ type: "n-1", toward: { type: user }, @@ -374,13 +374,13 @@ describe("parseTypes", () => { }); test("should throw error when forward name equals its own relation field name", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); // "as" is set to the same name as the relation field itself: the manifest // would end up with both a scalar field and a relationship named "authorID" - const post = db.type("Post", { + const post = db.table("Post", { authorID: db.uuid().relation({ type: "n-1", toward: { type: user, as: "authorID" }, @@ -393,13 +393,13 @@ describe("parseTypes", () => { }); test("should throw error when forward name conflicts with files field", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); // Post has a files field named "avatar" const post = db - .type("Post", { + .table("Post", { authorID: db.uuid().relation({ type: "n-1", toward: { type: user, as: "avatar" }, @@ -415,11 +415,11 @@ describe("parseTypes", () => { }); test("should throw error when forward name conflicts with backward name", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); - const post = db.type("Post", { + const post = db.table("Post", { authorID: db.uuid().relation({ type: "n-1", toward: { type: user }, @@ -427,7 +427,7 @@ describe("parseTypes", () => { }), }); - const comment = db.type("Comment", { + const comment = db.table("Comment", { postID: db.uuid().relation({ type: "n-1", toward: { type: post, as: "post" }, @@ -441,11 +441,11 @@ describe("parseTypes", () => { }); test("should include source file information in forward conflict error message", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); - const post = db.type("Post", { + const post = db.table("Post", { authorID: db.uuid().relation({ type: "n-1", toward: { type: user }, @@ -471,7 +471,7 @@ describe("parseTypes", () => { }); test("should throw error when self relation forward name is empty", () => { - const node = db.type("Node", { + const node = db.table("Node", { ID: db.uuid().relation({ type: "n-1", toward: { type: "self" }, @@ -486,12 +486,12 @@ describe("parseTypes", () => { describe("validateRelationType", () => { test("should throw error when relation type is missing", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); // Missing 'type' property - only TypeScript error, need runtime check - const post = db.type("Post", { + const post = db.table("Post", { // @ts-ignore - intentionally missing 'type' to test runtime validation (tsgo/tsc compat) userId: db.uuid().relation({ // @ts-ignore - ignore No overload matches this call error @@ -505,11 +505,11 @@ describe("parseTypes", () => { }); test("should throw error when relation type is invalid", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); - const post = db.type("Post", { + const post = db.table("Post", { userId: db.uuid().relation({ // @ts-ignore - intentionally invalid 'type' to test runtime validation (tsgo/tsc compat) type: "invalid-type", @@ -523,11 +523,11 @@ describe("parseTypes", () => { }); test("should throw error when target type does not exist", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); - const post = db.type("Post", { + const post = db.table("Post", { userId: db.uuid().relation({ type: "n-1", toward: { type: user }, @@ -543,11 +543,11 @@ describe("parseTypes", () => { test.each(["oneToOne", "1-1", "manyToOne", "n-1", "N-1", "keyOnly"] as const)( "should accept valid relation type %s", (relationType) => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); - const post = db.type("Post", { + const post = db.table("Post", { userId: db.uuid().relation({ type: relationType, toward: { type: user }, @@ -563,11 +563,11 @@ describe("parseTypes", () => { describe("processRelation", () => { test("should compute derived metadata for relations", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); - const post = db.type("Post", { + const post = db.table("Post", { authorId: db.uuid().relation({ type: "n-1", toward: { type: user, as: "author" }, @@ -589,7 +589,7 @@ describe("parseTypes", () => { test.each([ [ "relation only", - (user: ReturnType) => + (user: ReturnType) => db.uuid().relation({ type: "1-1", toward: { type: user }, @@ -597,7 +597,7 @@ describe("parseTypes", () => { ], [ "unique before relation", - (user: ReturnType) => + (user: ReturnType) => db .uuid() .unique() @@ -607,11 +607,11 @@ describe("parseTypes", () => { }), ], ] as const)("should set unique=true for oneToOne relations (%s)", (_label, buildUserId) => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); - const profile = db.type("Profile", { + const profile = db.table("Profile", { userId: buildUserId(user), }); @@ -624,7 +624,7 @@ describe("parseTypes", () => { }); test("should set unique=true for oneToOne relations (unique after relation)", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); const relatedUserId = db.uuid().relation({ @@ -634,7 +634,7 @@ describe("parseTypes", () => { // @ts-expect-error - Testing runtime behavior: 1-1 already implies unique, but we test the call order const userId = relatedUserId.unique(); - const profile = db.type("Profile", { + const profile = db.table("Profile", { userId, }); @@ -649,7 +649,7 @@ describe("parseTypes", () => { test.each([ [ "unique before relation", - (user: ReturnType) => + (user: ReturnType) => db .uuid() .unique() @@ -660,7 +660,7 @@ describe("parseTypes", () => { ], [ "unique after relation", - (user: ReturnType) => + (user: ReturnType) => db .uuid() .relation({ @@ -672,11 +672,11 @@ describe("parseTypes", () => { ] as const)( "should throw error when unique is set on n-1 relation (%s)", (_label, buildUserId) => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); - const employee = db.type("Employee", { + const employee = db.table("Employee", { userID: buildUserId(user), }); @@ -690,7 +690,7 @@ describe("parseTypes", () => { ); test("should handle self-referencing relations", () => { - const node = db.type("Node", { + const node = db.table("Node", { name: db.string(), parentId: db.uuid().relation({ type: "n-1", @@ -707,11 +707,11 @@ describe("parseTypes", () => { }); test("should not create forward/backward relationships for keyOnly relations", () => { - const user = db.type("User", { + const user = db.table("User", { name: db.string(), }); - const post = db.type("Post", { + const post = db.table("Post", { userId: db.uuid().relation({ type: "keyOnly", toward: { type: user }, diff --git a/packages/sdk/src/parser/service/tailordb/type-parser.ts b/packages/sdk/src/parser/service/tailordb/type-parser.ts index cd539376b..e47a3590b 100644 --- a/packages/sdk/src/parser/service/tailordb/type-parser.ts +++ b/packages/sdk/src/parser/service/tailordb/type-parser.ts @@ -352,7 +352,7 @@ function validatePluralFormUniqueness( const location = formatTypeSourceLocation(sourceInfo); errors.push( `Type "${parsedType.name}"${location} has identical singular and plural query names "${singularQuery}". ` + - `Use db.type(["${parsedType.name}", "UniquePluralForm"], {...}) to set a unique pluralForm.`, + `Use db.table(["${parsedType.name}", "UniquePluralForm"], {...}) to set a unique pluralForm.`, ); } } diff --git a/packages/sdk/src/plugin/builtin/enum-constants/index.test.ts b/packages/sdk/src/plugin/builtin/enum-constants/index.test.ts index 76c52dc6b..5247a7c42 100644 --- a/packages/sdk/src/plugin/builtin/enum-constants/index.test.ts +++ b/packages/sdk/src/plugin/builtin/enum-constants/index.test.ts @@ -27,7 +27,7 @@ describe("EnumConstantsPlugin", () => { describe("enum collection", () => { test("should collect top-level enum fields", async () => { - const type = db.type("User", { + const type = db.table("User", { role: db.enum(["ADMIN", "USER"]), status: db.enum(["ACTIVE", "INACTIVE"], { optional: true }), }); @@ -48,7 +48,7 @@ describe("EnumConstantsPlugin", () => { }); test("should collect enum fields from nested objects", async () => { - const type = db.type("PurchaseOrder", { + const type = db.table("PurchaseOrder", { attachedFiles: db.object( { id: db.uuid(), @@ -70,7 +70,7 @@ describe("EnumConstantsPlugin", () => { }); test("should return empty array when no enums are present", async () => { - const type = db.type("User", { + const type = db.table("User", { name: db.string(), age: db.int(), }); @@ -81,7 +81,7 @@ describe("EnumConstantsPlugin", () => { }); test("should collect enum values with descriptions", async () => { - const type = db.type("Invoice", { + const type = db.table("Invoice", { status: db.enum([ { value: "draft", description: "Draft invoice" }, { value: "sent", description: "Sent invoice" }, diff --git a/packages/sdk/src/plugin/builtin/file-utils/index.test.ts b/packages/sdk/src/plugin/builtin/file-utils/index.test.ts index 7971e0ec0..8cee1e06b 100644 --- a/packages/sdk/src/plugin/builtin/file-utils/index.test.ts +++ b/packages/sdk/src/plugin/builtin/file-utils/index.test.ts @@ -55,7 +55,7 @@ describe("FileUtilsPlugin", () => { ], ["should return empty array when no files are present", "User", undefined, []], ])("%s", async (_name, typeName, files, expectedFields) => { - let type = db.type(typeName, { name: db.string() }); + let type = db.table(typeName, { name: db.string() }); if (files) type = type.files(files); const result = await processFileType(parseTailorDBType(toSchemaOutput(type))); @@ -145,7 +145,7 @@ describe("FileUtilsPlugin", () => { describe("onTailorDBReady integration", () => { test("should generate file utils for types with file fields", async () => { const userType = db - .type("User", { + .table("User", { name: db.string(), }) .files({ @@ -153,7 +153,7 @@ describe("FileUtilsPlugin", () => { }); const salesOrderType = db - .type("SalesOrder", { + .table("SalesOrder", { name: db.string(), }) .files({ @@ -184,7 +184,7 @@ describe("FileUtilsPlugin", () => { }); test("should return empty files when no types have file fields", async () => { - const userType = db.type("User", { + const userType = db.table("User", { name: db.string(), }); @@ -205,7 +205,7 @@ describe("FileUtilsPlugin", () => { test("should handle multiple namespaces", async () => { const userType = db - .type("User", { + .table("User", { name: db.string(), }) .files({ @@ -213,7 +213,7 @@ describe("FileUtilsPlugin", () => { }); const customerType = db - .type("Customer", { + .table("Customer", { name: db.string(), }) .files({ diff --git a/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts index f9643c89b..73e24a717 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts @@ -13,7 +13,7 @@ function parseTailorDBType(type: TailorDBTypeSchemaOutput): TailorDBType { return types[type.name]!; } -const mockBasicType = db.type("User", { +const mockBasicType = db.table("User", { name: db.string().description("User name"), email: db.string().description("User email"), age: db.int({ optional: true }), @@ -25,12 +25,12 @@ const mockBasicType = db.type("User", { ...db.fields.timestamps(), }); -const mockEnumType = db.type("Status", { +const mockEnumType = db.table("Status", { status: db.enum([{ value: "active" }, { value: "inactive" }, { value: "pending" }]), priority: db.enum([{ value: "high" }, { value: "medium" }, { value: "low" }], { optional: true }), }); -const mockNestedType = db.type("ComplexUser", { +const mockNestedType = db.table("ComplexUser", { profile: db.object({ firstName: db.string(), lastName: db.string(), @@ -121,7 +121,7 @@ describe("KyselyTypePlugin integration tests", () => { }); test("correctly processes required/optional fields", async () => { - const testType = db.type("TestRequired", { + const testType = db.table("TestRequired", { requiredField: db.string(), optionalField: db.string({ optional: true }), undefinedRequiredField: db.string({ optional: true }), @@ -135,7 +135,7 @@ describe("KyselyTypePlugin integration tests", () => { }); test("correctly processes array types", async () => { - const arrayType = db.type("ArrayTest", { + const arrayType = db.table("ArrayTest", { stringArray: db.string({ array: true }), optionalIntArray: db.int({ optional: true, array: true }), }); @@ -214,7 +214,7 @@ describe("KyselyTypePlugin integration tests", () => { }); test("processes unknown type definitions as string type", async () => { - const unknownType = db.type("UnknownType", { + const unknownType = db.table("UnknownType", { unknownField: db.string(), }); @@ -226,8 +226,8 @@ describe("KyselyTypePlugin integration tests", () => { describe("multiple namespace support", () => { test("aggregates types from multiple namespaces", async () => { - const userType = db.type("User", { name: db.string() }); - const eventType = db.type("Event", { timestamp: db.datetime() }); + const userType = db.table("User", { name: db.string() }); + const eventType = db.table("Event", { timestamp: db.datetime() }); const result = await runOnTailorDBReady([ { @@ -252,7 +252,7 @@ describe("KyselyTypePlugin integration tests", () => { }); test("includes only necessary utility types", async () => { - const simpleType = db.type("Simple", { name: db.string() }); + const simpleType = db.table("Simple", { name: db.string() }); const result = await runOnTailorDBReady([ { diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts index a7286e774..33192ae0e 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts @@ -20,7 +20,7 @@ async function getTypeDef(type: TailorAnyDBType) { describe("Kysely TypeProcessor", () => { describe("basic types", () => { test("should propagate the type name into the result", async () => { - const type = db.type("User", { + const type = db.table("User", { name: db.string(), }); @@ -32,7 +32,7 @@ describe("Kysely TypeProcessor", () => { test.each([ { name: "string types", - type: db.type("User", { + type: db.table("User", { name: db.string(), nickname: db.string({ optional: true }), }), @@ -40,7 +40,7 @@ describe("Kysely TypeProcessor", () => { }, { name: "number types", - type: db.type("Product", { + type: db.table("Product", { quantity: db.int(), price: db.float(), discount: db.float({ optional: true }), @@ -49,7 +49,7 @@ describe("Kysely TypeProcessor", () => { }, { name: "boolean types", - type: db.type("Feature", { + type: db.table("Feature", { enabled: db.bool(), beta: db.bool({ optional: true }), }), @@ -57,7 +57,7 @@ describe("Kysely TypeProcessor", () => { }, { name: "date and datetime types", - type: db.type("Event", { + type: db.table("Event", { startDate: db.date(), endDate: db.datetime(), cancelledAt: db.datetime({ optional: true }), @@ -66,7 +66,7 @@ describe("Kysely TypeProcessor", () => { }, { name: "uuid types", - type: db.type("Session", { + type: db.table("Session", { userId: db.uuid(), deviceId: db.uuid({ optional: true }), }), @@ -81,7 +81,7 @@ describe("Kysely TypeProcessor", () => { describe("array types", () => { test("should handle array fields", async () => { const typeDef = await getTypeDef( - db.type("Post", { + db.table("Post", { tags: db.string({ array: true }), scores: db.int({ array: true, optional: true }), }), @@ -93,7 +93,7 @@ describe("Kysely TypeProcessor", () => { test("should use ArrayColumnType for datetime array fields", async () => { const typeDef = await getTypeDef( - db.type("Event", { + db.table("Event", { eventDates: db.datetime({ array: true }), optionalDates: db.date({ array: true, optional: true }), }), @@ -107,7 +107,7 @@ describe("Kysely TypeProcessor", () => { describe("enum types", () => { test("should handle enum types", async () => { const typeDef = await getTypeDef( - db.type("User", { + db.table("User", { role: db.enum([{ value: "admin" }, { value: "user" }]), status: db.enum([{ value: "active" }, { value: "inactive" }], { optional: true, @@ -121,7 +121,7 @@ describe("Kysely TypeProcessor", () => { test("should handle enum array types", async () => { const typeDef = await getTypeDef( - db.type("Article", { + db.table("Article", { categories: db.enum(["tech", "health", "finance"], { array: true }), authors: db.enum(["alice", "bob"], { array: true, optional: true }), }), @@ -134,7 +134,7 @@ describe("Kysely TypeProcessor", () => { describe("nested objects", () => { test("should handle single level nested objects", async () => { - const simpleNestedType = db.type("SimpleUser", { + const simpleNestedType = db.table("SimpleUser", { profile: db.object({ name: db.string(), email: db.string({ optional: true }), @@ -152,7 +152,7 @@ describe("Kysely TypeProcessor", () => { }); test("should handle multi-level nested objects", async () => { - const deepNestedType = db.type("Company", { + const deepNestedType = db.table("Company", { details: db.object({ // @ts-expect-error: Nested objects have complex type inference address: db.object({ @@ -181,7 +181,7 @@ describe("Kysely TypeProcessor", () => { }); test("should use Date | string instead of Timestamp for date fields inside nested objects", async () => { - const type = db.type("Receipt", { + const type = db.table("Receipt", { receiptDate: db.date(), dueSchedule: db.object({ dueDate: db.date(), @@ -201,7 +201,7 @@ describe("Kysely TypeProcessor", () => { test("should wrap nested object arrays with ArrayColumnType>", async () => { const typeDef = await getTypeDef( - db.type("Profile", { + db.table("Profile", { metadata: db.object( { created: db.datetime(), @@ -219,7 +219,7 @@ describe("Kysely TypeProcessor", () => { test("should handle optional nested object arrays", async () => { const typeDef = await getTypeDef( - db.type("Profile", { + db.table("Profile", { tags: db.object( { name: db.string(), @@ -236,7 +236,7 @@ describe("Kysely TypeProcessor", () => { test("should use plain array syntax for nested objects without ColumnType fields", async () => { const typeDef = await getTypeDef( - db.type("Profile", { + db.table("Profile", { tags: db.object( { name: db.string(), @@ -254,7 +254,7 @@ describe("Kysely TypeProcessor", () => { test("should handle optional nested objects", async () => { const typeDef = await getTypeDef( - db.type("User", { + db.table("User", { settings: db.object( { theme: db.string(), @@ -272,7 +272,7 @@ describe("Kysely TypeProcessor", () => { describe("special fields", () => { test("should process timestamp fields through normal field processing", async () => { - const typeWithTimestamps = db.type("UserWithTimestamp", { + const typeWithTimestamps = db.table("UserWithTimestamp", { name: db.string(), ...db.fields.timestamps(), }); @@ -288,7 +288,7 @@ describe("Kysely TypeProcessor", () => { test("should always include Generated for id field", async () => { const typeDef = await getTypeDef( - db.type("User", { + db.table("User", { name: db.string(), }), ); @@ -299,25 +299,25 @@ describe("Kysely TypeProcessor", () => { test.each([ { name: "basic types only", - type: db.type("User", { name: db.string(), age: db.int() }), + type: db.table("User", { name: db.string(), age: db.int() }), timestamp: false, serial: false, }, { name: "Timestamp", - type: db.type("User", { name: db.string(), ...db.fields.timestamps() }), + type: db.table("User", { name: db.string(), ...db.fields.timestamps() }), timestamp: true, serial: false, }, { name: "Serial", - type: db.type("Invoice", { invoiceNumber: db.string().serial({ start: 1000 }) }), + type: db.table("Invoice", { invoiceNumber: db.string().serial({ start: 1000 }) }), timestamp: false, serial: true, }, { name: "both", - type: db.type("Order", { + type: db.table("Order", { orderNumber: db.string().serial({ start: 1000 }), ...db.fields.timestamps(), }), diff --git a/packages/sdk/src/plugin/manager.test.ts b/packages/sdk/src/plugin/manager.test.ts index 1120052ac..8dbd63e31 100644 --- a/packages/sdk/src/plugin/manager.test.ts +++ b/packages/sdk/src/plugin/manager.test.ts @@ -3,7 +3,7 @@ import { db } from "#/configure/services/tailordb/index"; import { PluginManager } from "#/plugin/manager"; import type { Plugin } from "#/plugin/types"; -const orderType = () => db.type("Order", { name: db.string() }); +const orderType = () => db.table("Order", { name: db.string() }); describe("PluginManager", () => { test("collects namespace plugin-generated types", async () => { @@ -13,7 +13,7 @@ describe("PluginManager", () => { importPath: "@example/namespace", onNamespaceLoaded: () => ({ types: { - auditLog: db.type("AuditLog", { + auditLog: db.table("AuditLog", { message: db.string(), }), }, @@ -42,7 +42,7 @@ describe("PluginManager", () => { importPath: "@example/namespace", onNamespaceLoaded: () => ({ types: { - auditLog: db.type("AuditLog", { + auditLog: db.table("AuditLog", { message: db.string(), }), }, @@ -67,7 +67,7 @@ describe("PluginManager", () => { test("preserves pluralForm and plugin attachments when extending types", () => { const manager = new PluginManager(); const original = db - .type(["Person", "People"], { + .table(["Person", "People"], { name: db.string(), }) // PluginConfigs is open; use cast to attach plugin config in tests. @@ -116,7 +116,7 @@ describe("PluginManager", () => { importPath: "@example/schema-less", onTypeLoaded: (_context: Parameters>[0]) => ({ types: { - derived: db.type("Derived", { + derived: db.table("Derived", { sourceId: db.uuid(), customValue: db.string(), }), diff --git a/packages/sdk/src/plugin/manager.ts b/packages/sdk/src/plugin/manager.ts index f24428bd7..c32b1a780 100644 --- a/packages/sdk/src/plugin/manager.ts +++ b/packages/sdk/src/plugin/manager.ts @@ -509,7 +509,7 @@ export class PluginManager { /** * Extend a TailorDB type with new fields. - * This method handles the `db.type()` call and metadata copying internally. + * This method handles the `db.table()` call and metadata copying internally. * @param params - Parameters for type extension * @returns The extended TailorDB type */ @@ -536,7 +536,7 @@ export class PluginManager { const typeName = pluralForm ? ([originalType.name, pluralForm] as [string, string]) : originalType.name; - const extendedType = db.type(typeName, fieldsWithoutId); + const extendedType = db.table(typeName, fieldsWithoutId); return copyMetadataToExtendedType(originalType, extendedType); } } diff --git a/packages/sdk/src/utils/test/index.test.ts b/packages/sdk/src/utils/test/index.test.ts index c8e87b867..3cd0ce5a9 100644 --- a/packages/sdk/src/utils/test/index.test.ts +++ b/packages/sdk/src/utils/test/index.test.ts @@ -8,7 +8,7 @@ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12 describe("createTailorDBHook", () => { describe("id field", () => { test("uses existing id from data when provided", () => { - const type = db.type("Test", { name: db.string() }); + const type = db.table("Test", { name: db.string() }); const result = createTailorDBHook(type)({ id: "00000000-0000-0000-0000-000000000001", name: "a", @@ -21,7 +21,7 @@ describe("createTailorDBHook", () => { ["data is null", null], ["data is undefined", undefined], ])("generates a UUID when %s", (_label, data) => { - const type = db.type("Test", { name: db.string() }); + const type = db.table("Test", { name: db.string() }); const result = createTailorDBHook(type)(data); expect(result.id).toMatch(UUID_REGEX); }); @@ -29,7 +29,7 @@ describe("createTailorDBHook", () => { describe("plain field passthrough", () => { test("passes through scalar field values unchanged", () => { - const type = db.type("Test", { + const type = db.table("Test", { name: db.string(), age: db.int(), active: db.bool(), @@ -46,13 +46,13 @@ describe("createTailorDBHook", () => { ["data is null", null], ["data is a non-object primitive", "not-an-object"], ])("does not set scalar fields when %s", (_label, data) => { - const type = db.type("Test", { name: db.string() }); + const type = db.table("Test", { name: db.string() }); const result = createTailorDBHook(type)(data); expect(result.name).toBeUndefined(); }); test("preserves explicit null values from data", () => { - const type = db.type("Test", { nickname: db.string({ optional: true }) }); + const type = db.table("Test", { nickname: db.string({ optional: true }) }); const result = createTailorDBHook(type)({ nickname: null }); expect(result.nickname).toBeNull(); }); @@ -60,7 +60,7 @@ describe("createTailorDBHook", () => { describe("single nested object field", () => { test("recursively processes the nested object", () => { - const type = db.type("Test", { + const type = db.table("Test", { user: db.object({ name: db.string(), age: db.int() }), }); const result = createTailorDBHook(type)({ @@ -70,7 +70,7 @@ describe("createTailorDBHook", () => { }); test("generates a nested id when the nested object has an id field", () => { - const type = db.type("Test", { + const type = db.table("Test", { nested: db.object({ id: db.uuid(), name: db.string() }), }); const result = createTailorDBHook(type)({ nested: { name: "x" } }); @@ -78,7 +78,7 @@ describe("createTailorDBHook", () => { }); test("invokes nested sub-field hooks", () => { - const type = db.type("Test", { + const type = db.table("Test", { user: db.object({ name: db.string().hooks({ create: ({ value }) => `hooked:${value as string}`, @@ -92,7 +92,7 @@ describe("createTailorDBHook", () => { describe("nested object array field", () => { test("preserves array values when array is provided", () => { - const type = db.type("Test", { + const type = db.table("Test", { lines: db.object({ kind: db.string(), days: db.int() }, { array: true }), }); const value = [ @@ -104,7 +104,7 @@ describe("createTailorDBHook", () => { }); test("recursively processes each array element so nested ids are generated", () => { - const type = db.type("Test", { + const type = db.table("Test", { lines: db.object({ id: db.uuid(), kind: db.string() }, { array: true }), }); const result = createTailorDBHook(type)({ @@ -118,7 +118,7 @@ describe("createTailorDBHook", () => { test("invokes per-element sub-field hooks", () => { const calls: unknown[] = []; - const type = db.type("Test", { + const type = db.table("Test", { lines: db.object( { stamp: db.string().hooks({ @@ -139,7 +139,7 @@ describe("createTailorDBHook", () => { }); test("preserves an empty array as an empty array", () => { - const type = db.type("Test", { + const type = db.table("Test", { lines: db.object({ kind: db.string() }, { array: true }), }); const result = createTailorDBHook(type)({ lines: [] }); @@ -150,14 +150,14 @@ describe("createTailorDBHook", () => { ["passes through null for optional array field", { lines: null }, null], ["passes through undefined for omitted optional array field", {}, undefined], ])("%s", (_label, data, expected) => { - const type = db.type("Test", { + const type = db.table("Test", { lines: db.object({ kind: db.string() }, { optional: true, array: true }), }); expect(createTailorDBHook(type)(data).lines).toBe(expected); }); test("passes through non-array values without recursing (so the validator surfaces a clear error)", () => { - const type = db.type("Test", { + const type = db.table("Test", { lines: db.object({ kind: db.string() }, { array: true }), }); // Pass an object instead of an array; the hook must not corrupt it into a single @@ -171,7 +171,7 @@ describe("createTailorDBHook", () => { describe("create hook on a top-level field", () => { test("invokes the create hook with value, full data, and a null invoker", () => { const seen: { value: unknown; data: unknown; invoker: unknown }[] = []; - const type = db.type("Order", { total: db.float(), tax: db.float() }).hooks({ + const type = db.table("Order", { total: db.float(), tax: db.float() }).hooks({ tax: { create: ({ value, data, invoker }) => { seen.push({ value, data, invoker }); @@ -193,14 +193,14 @@ describe("createTailorDBHook", () => { test("normalizes a Date returned from the create hook to an ISO string", () => { const fixed = new Date("2026-04-15T00:00:00.000Z"); const type = db - .type("Test", { createdAt: db.datetime() }) + .table("Test", { createdAt: db.datetime() }) .hooks({ createdAt: { create: () => fixed } }); expect(createTailorDBHook(type)({}).createdAt).toBe("2026-04-15T00:00:00.000Z"); }); test("does not invoke a hook that only defines update (createTailorDBHook is create-only)", () => { let updateCalled = false; - const type = db.type("Test", { updatedAt: db.datetime() }).hooks({ + const type = db.table("Test", { updatedAt: db.datetime() }).hooks({ updatedAt: { update: () => { updateCalled = true; @@ -218,7 +218,7 @@ describe("createTailorDBHook", () => { describe("createStandardSchema", () => { const buildSchema = () => { - const type = db.type("PurchaseOrder", { + const type = db.table("PurchaseOrder", { paymentTermSnapshotLines: db.object( { kind: db.string(), days: db.int() }, { optional: true, array: true }, @@ -253,7 +253,7 @@ describe("createStandardSchema", () => { }); test("returns issues when the hooked data fails validation", () => { - const type = db.type("Test", { name: db.string() }); + const type = db.table("Test", { name: db.string() }); const schemaType = t.object({ id: t.uuid(), name: t.string() }); const schema = createStandardSchema(schemaType, createTailorDBHook(type)); diff --git a/packages/sdk/src/utils/test/internal.ts b/packages/sdk/src/utils/test/internal.ts index 3ab767429..eb5953f33 100644 --- a/packages/sdk/src/utils/test/internal.ts +++ b/packages/sdk/src/utils/test/internal.ts @@ -7,13 +7,13 @@ import { TailorDBTypeSchema } from "#/parser/service/tailordb/schema"; import type { TailorDBTypeRaw as TailorDBTypeSchemaOutput } from "#/types/tailordb.generated"; /** - * Converts a single db.type() result to schema-parsed output for testing. + * Converts a single db.table() result to schema-parsed output for testing. * In production, this conversion happens in application loader. - * @param type - The db.type() result to convert + * @param type - The db.table() result to convert * @returns Parsed TailorDB type schema output */ export function toSchemaOutput( - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accept any db.type() result for testing + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accept any db.table() result for testing type: any, ): TailorDBTypeSchemaOutput { const parsed = TailorDBTypeSchema.safeParse(stripTailorDBTypeBuilderHelpers(type)); @@ -24,13 +24,13 @@ export function toSchemaOutput( } /** - * Converts multiple db.type() results to schema-parsed outputs for testing. + * Converts multiple db.table() results to schema-parsed outputs for testing. * In production, this conversion happens in application loader. - * @param types - Record of db.type() results to convert + * @param types - Record of db.table() results to convert * @returns Record of parsed TailorDB type schema outputs */ export function toSchemaOutputs( - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accept any db.type() result for testing + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accept any db.table() result for testing types: Record, ): Record { const result = Object.create(null) as Record; From 1ab65dc0d007dcc35c012f332981d0b91a213194 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 13:05:25 +0900 Subject: [PATCH 455/618] fix: preserve local db shadows in db.table codemod --- .../v2/db-type-to-table/scripts/transform.ts | 109 ++++++++++++++++-- .../tests/sdk-import/expected.ts | 33 ++++++ .../tests/sdk-import/input.ts | 33 ++++++ 3 files changed, 168 insertions(+), 7 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts index 6f4097483..6ab006eea 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts @@ -30,6 +30,51 @@ function isInsideImportStatement(node: SgNode): boolean { return false; } +function isBindingLeafKind(kind: ReturnType): boolean { + return kind === "identifier" || kind === "shorthand_property_identifier_pattern"; +} + +function isBindingPatternKind(kind: ReturnType): boolean { + return ( + isBindingLeafKind(kind) || + kind === "object_pattern" || + kind === "array_pattern" || + kind === "rest_pattern" + ); +} + +function collectBindingNodes(node: SgNode, names: Set, result: SgNode[]): void { + if (isBindingLeafKind(node.kind())) { + if (names.has(node.text())) result.push(node); + return; + } + + for (const child of node.children()) { + if (child.kind() === "property_identifier") continue; + if (child.kind() === "=") break; + collectBindingNodes(child, names, result); + } +} + +function bindingNodes(node: SgNode, names: Set): SgNode[] { + const result: SgNode[] = []; + collectBindingNodes(node, names, result); + return result; +} + +function directBindingNodes(node: SgNode, names: Set): SgNode[] { + const result: SgNode[] = []; + for (const child of node.children()) { + if (child.kind() === "=") break; + if (isBindingPatternKind(child.kind())) collectBindingNodes(child, names, result); + } + return result; +} + +function firstDeclaratorChild(node: SgNode): SgNode | null { + return node.children().find((child) => child.kind() !== "=") ?? null; +} + function addShadowedRange( shadowedRanges: Map>, name: string, @@ -44,7 +89,12 @@ function nearestScope(node: SgNode): SgNode { let current: SgNode | null = node.parent(); while (current) { const kind = current.kind(); - if (kind === "statement_block" || kind === "program" || kind === "for_statement") { + if ( + kind === "statement_block" || + kind === "program" || + kind === "for_statement" || + kind === "for_in_statement" + ) { return current; } current = current.parent(); @@ -76,10 +126,24 @@ function parameterScope(node: SgNode): SgNode { function buildShadowedRanges(root: SgNode, names: Set) { const shadowedRanges = new Map>(); + for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { + if (isInsideImportStatement(decl)) continue; + const binding = firstDeclaratorChild(decl); + if (!binding) continue; + for (const name of bindingNodes(binding, names)) { + addShadowedRange(shadowedRanges, name.text(), nearestScope(decl)); + } + } + for (const decl of root.findAll({ - rule: { any: [{ kind: "function_declaration" }, { kind: "variable_declarator" }] }, + rule: { + any: [ + { kind: "function_declaration" }, + { kind: "class_declaration" }, + { kind: "enum_declaration" }, + ], + }, })) { - if (isInsideImportStatement(decl)) continue; const name = decl .children() .find((child) => child.kind() === "identifier" && names.has(child.text())); @@ -89,10 +153,41 @@ function buildShadowedRanges(root: SgNode, names: Set) { for (const param of root.findAll({ rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] }, })) { - const name = param - .children() - .find((child) => child.kind() === "identifier" && names.has(child.text())); - if (name) addShadowedRange(shadowedRanges, name.text(), parameterScope(param)); + for (const name of directBindingNodes(param, names)) { + addShadowedRange(shadowedRanges, name.text(), parameterScope(param)); + } + } + + for (const arrow of root.findAll({ rule: { kind: "arrow_function" } })) { + const children = arrow.children(); + const arrowIndex = children.findIndex((child) => child.kind() === "=>"); + if (arrowIndex === -1) continue; + for (const child of children.slice(0, arrowIndex)) { + if (child.kind() === "=") break; + if (!isBindingPatternKind(child.kind())) continue; + for (const name of bindingNodes(child, names)) { + addShadowedRange(shadowedRanges, name.text(), arrow); + } + } + } + + for (const catchClause of root.findAll({ rule: { kind: "catch_clause" } })) { + for (const name of directBindingNodes(catchClause, names)) { + addShadowedRange(shadowedRanges, name.text(), catchClause); + } + } + + for (const loop of root.findAll({ rule: { kind: "for_in_statement" } })) { + const children = loop.children(); + const keywordIndex = children.findIndex( + (child) => child.kind() === "in" || child.kind() === "of", + ); + if (keywordIndex === -1) continue; + for (const child of children.slice(0, keywordIndex)) { + for (const name of bindingNodes(child, names)) { + addShadowedRange(shadowedRanges, name.text(), loop); + } + } } return shadowedRanges; diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts index 7380b3221..b0211d9b7 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts @@ -14,6 +14,14 @@ export const team = sdk.db.table("Team", { label: sdk.db.string(), }); +export const optional = db.table?.("Optional", { + label: db.string(), +}); + +export const optionalTeam = sdk.db.table?.("OptionalTeam", { + label: sdk.db.string(), +}); + const local = { type: (name: string) => name, }; @@ -24,6 +32,10 @@ function useLocalDb(db: { type: (name: string) => string }) { return db.type("NoChange"); } +const useBareArrowDb = (db) => db.type("NoChange"); + +const useBareArrowNamespace = (sdk) => sdk.db.type("NoChange"); + { const schema = { type: (name: string) => name, @@ -31,4 +43,25 @@ function useLocalDb(db: { type: (name: string) => string }) { schema.type("NoChange"); } +{ + const { db } = { + db: { + type: (name: string) => name, + }, + }; + db.type("NoChange"); +} + +for (const db of [{ type: (name: string) => name }]) { + db.type("NoChange"); +} + +try { + throw { + type: (name: string) => name, + }; +} catch (db) { + db.type("NoChange"); +} + defineConfig({}); diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts index 32aa1ecfd..e4a9992db 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts @@ -14,6 +14,14 @@ export const team = sdk.db.type("Team", { label: sdk.db.string(), }); +export const optional = db.type?.("Optional", { + label: db.string(), +}); + +export const optionalTeam = sdk.db.type?.("OptionalTeam", { + label: sdk.db.string(), +}); + const local = { type: (name: string) => name, }; @@ -24,6 +32,10 @@ function useLocalDb(db: { type: (name: string) => string }) { return db.type("NoChange"); } +const useBareArrowDb = (db) => db.type("NoChange"); + +const useBareArrowNamespace = (sdk) => sdk.db.type("NoChange"); + { const schema = { type: (name: string) => name, @@ -31,4 +43,25 @@ function useLocalDb(db: { type: (name: string) => string }) { schema.type("NoChange"); } +{ + const { db } = { + db: { + type: (name: string) => name, + }, + }; + db.type("NoChange"); +} + +for (const db of [{ type: (name: string) => name }]) { + db.type("NoChange"); +} + +try { + throw { + type: (name: string) => name, + }; +} catch (db) { + db.type("NoChange"); +} + defineConfig({}); From 1b3240e46e4b1b1f6861c3cb90a39cb58d0fa5ec Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 13:18:15 +0900 Subject: [PATCH 456/618] fix: harden db.table codemod review coverage --- .../v2/db-type-to-table/scripts/transform.ts | 108 +++++++++++++++++- .../tests/sdk-import/expected.ts | 7 ++ .../tests/sdk-import/input.ts | 7 ++ .../src/db-type-to-table-review.test.ts | 65 +++++++++++ packages/sdk-codemod/src/registry.ts | 2 + packages/sdk/docs/migration/v2.md | 2 + 6 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 packages/sdk-codemod/src/db-type-to-table-review.test.ts diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts index 6ab006eea..56978f19c 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts @@ -4,6 +4,7 @@ import { importBindings, importSource, } from "../../../../src/ast-grep-helpers"; +import type { LlmReviewFinding } from "../../../../src/types"; import type { Edit, SgNode } from "@ast-grep/napi"; const SDK_MODULE = "@tailor-platform/sdk"; @@ -75,6 +76,13 @@ function firstDeclaratorChild(node: SgNode): SgNode | null { return node.children().find((child) => child.kind() !== "=") ?? null; } +function declaratorValue(node: SgNode): SgNode | null { + const children = node.children(); + const equalsIndex = children.findIndex((child) => child.kind() === "="); + if (equalsIndex === -1) return null; + return children.slice(equalsIndex + 1).find((child) => child.kind() !== "comment") ?? null; +} + function addShadowedRange( shadowedRanges: Map>, name: string, @@ -102,6 +110,30 @@ function nearestScope(node: SgNode): SgNode { return node; } +function functionScope(node: SgNode): SgNode { + let current: SgNode | null = node.parent(); + while (current) { + const kind = current.kind(); + if ( + kind === "function_declaration" || + kind === "function_expression" || + kind === "arrow_function" || + kind === "method_definition" || + kind === "program" + ) { + return current; + } + current = current.parent(); + } + return node; +} + +function variableDeclarationScope(node: SgNode): SgNode { + const declaration = node.parent(); + if (/^var\b/.test(declaration?.text().trimStart() ?? "")) return functionScope(node); + return nearestScope(node); +} + function parameterScope(node: SgNode): SgNode { let current: SgNode | null = node.parent(); while (current) { @@ -131,7 +163,7 @@ function buildShadowedRanges(root: SgNode, names: Set) { const binding = firstDeclaratorChild(decl); if (!binding) continue; for (const name of bindingNodes(binding, names)) { - addShadowedRange(shadowedRanges, name.text(), nearestScope(decl)); + addShadowedRange(shadowedRanges, name.text(), variableDeclarationScope(decl)); } } @@ -262,3 +294,77 @@ export default function transform(source: string, filePath: string): string | nu return edits.length > 0 ? root.commitEdits(edits) : null; } + +function lineForIndex(source: string, index: number): number { + return source.slice(0, index).split(/\r\n|\r|\n/).length; +} + +function excerptAtIndex(source: string, index: number): string { + const lineStart = Math.max(source.lastIndexOf("\n", index - 1) + 1, 0); + const lineEnd = source.indexOf("\n", index); + return source.slice(lineStart, lineEnd === -1 ? source.length : lineEnd).trim(); +} + +function objectPatternHasTypeProperty(pattern: SgNode): boolean { + return pattern + .findAll({ + rule: { + any: [ + { kind: "property_identifier", regex: "^type$" }, + { kind: "shorthand_property_identifier_pattern", regex: "^type$" }, + ], + }, + }) + .some((node) => node.text() === "type"); +} + +export function reviewFindings( + source: string, + filePath: string, + relativePath: string, +): LlmReviewFinding[] { + if (!source.includes("type") || !source.includes(SDK_MODULE)) return []; + + let root: SgNode; + try { + root = parse(sourceLang(filePath, source), source).root(); + } catch { + return []; + } + + const imports = findImportStatements(root).filter( + (importStmt) => importSource(importStmt) === SDK_MODULE, + ); + if (imports.length === 0) return []; + + const dbNames = new Set(); + const namespaceNames = new Set(); + for (const importStmt of imports) { + for (const binding of importBindings(importStmt)) { + if (binding.importedName === "db" && !binding.typeOnly) dbNames.add(binding.localName); + } + for (const name of namespaceImportNames(importStmt)) { + namespaceNames.add(name); + } + } + if (dbNames.size === 0 && namespaceNames.size === 0) return []; + + const shadowedRanges = buildShadowedRanges(root, new Set([...dbNames, ...namespaceNames])); + const findings: LlmReviewFinding[] = []; + + for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { + const binding = firstDeclaratorChild(decl); + if (binding?.kind() !== "object_pattern" || !objectPatternHasTypeProperty(binding)) continue; + const value = declaratorValue(decl); + if (!isSdkDbMember(value, dbNames, namespaceNames, shadowedRanges)) continue; + + findings.push({ + file: relativePath, + line: lineForIndex(source, binding.range().start.index), + message: "Review destructured db.type builder usage and migrate it to db.table.", + excerpt: excerptAtIndex(source, binding.range().start.index), + }); + } + + return findings; +} diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts index b0211d9b7..bb0370493 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts @@ -32,6 +32,13 @@ function useLocalDb(db: { type: (name: string) => string }) { return db.type("NoChange"); } +function useVarShadow(localDb: { type: (name: string) => string }) { + if (localDb) { + var db = localDb; + } + return db.type("NoChange"); +} + const useBareArrowDb = (db) => db.type("NoChange"); const useBareArrowNamespace = (sdk) => sdk.db.type("NoChange"); diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts index e4a9992db..87f342f0a 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts @@ -32,6 +32,13 @@ function useLocalDb(db: { type: (name: string) => string }) { return db.type("NoChange"); } +function useVarShadow(localDb: { type: (name: string) => string }) { + if (localDb) { + var db = localDb; + } + return db.type("NoChange"); +} + const useBareArrowDb = (db) => db.type("NoChange"); const useBareArrowNamespace = (sdk) => sdk.db.type("NoChange"); diff --git a/packages/sdk-codemod/src/db-type-to-table-review.test.ts b/packages/sdk-codemod/src/db-type-to-table-review.test.ts new file mode 100644 index 000000000..dc61016b2 --- /dev/null +++ b/packages/sdk-codemod/src/db-type-to-table-review.test.ts @@ -0,0 +1,65 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "pathe"; +import { afterEach, describe, expect, test } from "vitest"; +import { allCodemods } from "./registry"; +import { runCodemods } from "./runner"; + +const CODEMODS_DIR = path.resolve(__dirname, "../codemods"); + +const dbTypeToTable = allCodemods.find((codemod) => codemod.id === "v2/db-type-to-table"); + +if (!dbTypeToTable?.scriptPath) { + throw new Error("v2/db-type-to-table codemod is not registered with a script"); +} + +const dbTypeToTableEntry = { + codemod: dbTypeToTable, + scriptPath: path.join(CODEMODS_DIR, dbTypeToTable.scriptPath.replace(/\.js$/, ".ts")), +}; + +describe("db-type-to-table review findings", () => { + let tmpDir: string | undefined; + + afterEach(async () => { + if (tmpDir) { + await fs.promises.rm(tmpDir, { recursive: true, force: true }); + tmpDir = undefined; + } + }); + + test("reports destructured db.type builders for LLM review", async () => { + tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "db-type-review-test-")); + await fs.promises.writeFile( + path.join(tmpDir, "tailordb.ts"), + [ + 'import { db } from "@tailor-platform/sdk";', + "", + "const { type } = db;", + "", + 'export const user = type("User", {', + " name: db.string(),", + "});", + "", + ].join("\n"), + "utf-8", + ); + + const result = await runCodemods([dbTypeToTableEntry], tmpDir, false); + + expect(result.llmReviews).toEqual([ + expect.objectContaining({ + codemodId: "v2/db-type-to-table", + files: ["tailordb.ts"], + findings: [ + { + file: "tailordb.ts", + line: 3, + message: "Review destructured db.type builder usage and migrate it to db.table.", + excerpt: "const { type } = db;", + }, + ], + }), + ]); + }); +}); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index a5b2cb4c8..378118cb6 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -552,6 +552,8 @@ export const allCodemods: CodemodPackage[] = [ "In Tailor SDK v2, TailorDB schema definitions use db.table(...) instead of", "db.type(...). The codemod rewrites member accesses on db imported from", "@tailor-platform/sdk, including aliases such as `import { db as schema }`.", + "It flags destructured builder aliases such as `const { type } = db` for", + "manual review because the local alias may require call-site renaming.", "Review any remaining db.type references and rename SDK TailorDB schema builder", "calls to db.table. Leave unrelated local objects with a .type() method unchanged.", ].join("\n"), diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index fa8c923cc..6cd10a204 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -504,6 +504,8 @@ export const user = db.table("User", { In Tailor SDK v2, TailorDB schema definitions use db.table(...) instead of db.type(...). The codemod rewrites member accesses on db imported from @tailor-platform/sdk, including aliases such as `import { db as schema }`. +It flags destructured builder aliases such as `const { type } = db` for +manual review because the local alias may require call-site renaming. Review any remaining db.type references and rename SDK TailorDB schema builder calls to db.table. Leave unrelated local objects with a .type() method unchanged. ``` From 645a2917f04086c32e49bbf7d83237d8eebe6d02 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 13:29:50 +0900 Subject: [PATCH 457/618] fix: defer db.table codemod to stable boundary --- packages/sdk-codemod/src/registry.test.ts | 2 ++ packages/sdk-codemod/src/registry.ts | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index fb80aee1f..613b7c353 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -41,6 +41,7 @@ describe("getApplicableCodemods", () => { .map((codemod) => codemod.id), ); expect(prereleaseIds).not.toContain("v2/auth-attributes-rename"); + expect(prereleaseIds).not.toContain("v2/db-type-to-table"); expect(prereleaseIds).not.toContain("v2/env-var-rename"); expect(prereleaseIds).not.toContain("v2/rename-bin"); expect(prereleaseIds).not.toContain("v2/node-minimum-22-15-0"); @@ -91,6 +92,7 @@ describe("getApplicableCodemods", () => { const ids = getApplicableCodemods("2.0.0-next.2", "2.0.0").map((codemod) => codemod.id); expect(ids).toContain("v2/auth-attributes-rename"); + expect(ids).toContain("v2/db-type-to-table"); expect(ids).toContain("v2/env-var-rename"); expect(ids).toContain("v2/rename-bin"); expect(ids).toContain("v2/node-minimum-22-15-0"); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 378118cb6..6d4abf8fb 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -536,7 +536,6 @@ export const allCodemods: CodemodPackage[] = [ "Rename TailorDB schema builder calls from `db.type()` to `db.table()`. TailorDB schema definitions now use table terminology in SDK projects.", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_2, scriptPath: "v2/db-type-to-table/scripts/transform.js", filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], legacyPatterns: ["db.type"], From 5fdc1444d11c9065be7db7621e620d2a93561c43 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 13:42:53 +0900 Subject: [PATCH 458/618] fix: surface db.table codemod alias gaps --- .../v2/db-type-to-table/scripts/transform.ts | 54 ++++++++++++++++++- .../tests/sdk-import/expected.ts | 12 +++++ .../tests/sdk-import/input.ts | 12 +++++ .../src/db-type-to-table-review.test.ts | 47 ++++++++++++++++ 4 files changed, 124 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts index 56978f19c..d742f8e26 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts @@ -256,8 +256,37 @@ function isSdkDbMember( ); } +function typeStringLiteral(node: SgNode | null): SgNode | null { + if (node?.kind() !== "string") return null; + const fragments = node.children().filter((child) => child.kind() === "string_fragment"); + return fragments.length === 1 && fragments[0]!.text() === "type" ? node : null; +} + +function replaceStringLiteralValue(node: SgNode, value: string): Edit { + const quote = node.text().startsWith("'") ? "'" : '"'; + return node.replace(`${quote}${value}${quote}`); +} + +function hasTypeBuilderUse(root: SgNode, name: string, afterIndex: number): boolean { + for (const member of root.findAll({ rule: { kind: "member_expression" } })) { + if (member.range().start.index <= afterIndex) continue; + if (member.field("property")?.text() !== "type") continue; + const object = member.field("object"); + if (object?.kind() === "identifier" && object.text() === name) return true; + } + + for (const subscript of root.findAll({ rule: { kind: "subscript_expression" } })) { + if (subscript.range().start.index <= afterIndex) continue; + if (!typeStringLiteral(subscript.field("index"))) continue; + const object = subscript.field("object"); + if (object?.kind() === "identifier" && object.text() === name) return true; + } + + return false; +} + export default function transform(source: string, filePath: string): string | null { - if (!source.includes(".type") || !source.includes(SDK_MODULE)) return null; + if (!source.includes("type") || !source.includes(SDK_MODULE)) return null; let root: SgNode; try { @@ -291,6 +320,14 @@ export default function transform(source: string, filePath: string): string | nu if (!isSdkDbMember(member.field("object"), dbNames, namespaceNames, shadowedRanges)) continue; edits.push(property.replace("table")); } + for (const subscript of root.findAll({ rule: { kind: "subscript_expression" } })) { + const index = typeStringLiteral(subscript.field("index")); + if (!index) continue; + if (!isSdkDbMember(subscript.field("object"), dbNames, namespaceNames, shadowedRanges)) { + continue; + } + edits.push(replaceStringLiteralValue(index, "table")); + } return edits.length > 0 ? root.commitEdits(edits) : null; } @@ -366,5 +403,20 @@ export function reviewFindings( }); } + for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { + const binding = firstDeclaratorChild(decl); + if (binding?.kind() !== "identifier") continue; + const value = declaratorValue(decl); + if (!isSdkDbMember(value, dbNames, namespaceNames, shadowedRanges)) continue; + if (!hasTypeBuilderUse(root, binding.text(), decl.range().end.index)) continue; + + findings.push({ + file: relativePath, + line: lineForIndex(source, binding.range().start.index), + message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", + excerpt: excerptAtIndex(source, binding.range().start.index), + }); + } + return findings; } diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts index bb0370493..5be1289ba 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts @@ -22,6 +22,18 @@ export const optionalTeam = sdk.db.table?.("OptionalTeam", { label: sdk.db.string(), }); +export const computedUser = db["table"]("ComputedUser", { + label: db.string(), +}); + +export const computedProject = schema["table"]("ComputedProject", { + label: schema.string(), +}); + +export const computedTeam = sdk.db["table"]("ComputedTeam", { + label: sdk.db.string(), +}); + const local = { type: (name: string) => name, }; diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts index 87f342f0a..14161d4fa 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts @@ -22,6 +22,18 @@ export const optionalTeam = sdk.db.type?.("OptionalTeam", { label: sdk.db.string(), }); +export const computedUser = db["type"]("ComputedUser", { + label: db.string(), +}); + +export const computedProject = schema["type"]("ComputedProject", { + label: schema.string(), +}); + +export const computedTeam = sdk.db["type"]("ComputedTeam", { + label: sdk.db.string(), +}); + const local = { type: (name: string) => name, }; diff --git a/packages/sdk-codemod/src/db-type-to-table-review.test.ts b/packages/sdk-codemod/src/db-type-to-table-review.test.ts index dc61016b2..f0603d422 100644 --- a/packages/sdk-codemod/src/db-type-to-table-review.test.ts +++ b/packages/sdk-codemod/src/db-type-to-table-review.test.ts @@ -62,4 +62,51 @@ describe("db-type-to-table review findings", () => { }), ]); }); + + test("reports local SDK db aliases for LLM review", async () => { + tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "db-type-review-test-")); + await fs.promises.writeFile( + path.join(tmpDir, "tailordb.ts"), + [ + 'import { db } from "@tailor-platform/sdk";', + 'import * as sdk from "@tailor-platform/sdk";', + "", + "const schema = db;", + "const nsSchema = sdk.db;", + "", + 'export const user = schema.type("User", {', + " name: schema.string(),", + "});", + "", + 'export const team = nsSchema["type"]("Team", {', + " name: nsSchema.string(),", + "});", + "", + ].join("\n"), + "utf-8", + ); + + const result = await runCodemods([dbTypeToTableEntry], tmpDir, false); + + expect(result.llmReviews).toEqual([ + expect.objectContaining({ + codemodId: "v2/db-type-to-table", + files: ["tailordb.ts"], + findings: [ + { + file: "tailordb.ts", + line: 4, + message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", + excerpt: "const schema = db;", + }, + { + file: "tailordb.ts", + line: 5, + message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", + excerpt: "const nsSchema = sdk.db;", + }, + ], + }), + ]); + }); }); From a44d2980b030240f7e0372d18d4f9014f8d31849 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 13:57:27 +0900 Subject: [PATCH 459/618] fix: unwrap db expressions in db.table codemod --- .../v2/db-type-to-table/scripts/transform.ts | 44 +++++++++++++++---- .../tests/sdk-import/expected.ts | 8 ++++ .../tests/sdk-import/input.ts | 8 ++++ .../src/db-type-to-table-review.test.ts | 11 +++++ packages/sdk-codemod/src/registry.ts | 5 ++- packages/sdk/docs/migration/v2.md | 5 ++- 6 files changed, 68 insertions(+), 13 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts index d742f8e26..252a7fe58 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts @@ -235,19 +235,45 @@ function isShadowed( return ranges.some((range) => position >= range.start && position < range.end); } +function unwrapExpression(node: SgNode | null): SgNode | null { + let current = node; + while (current) { + const kind = current.kind(); + if (kind === "parenthesized_expression") { + current = current.children().find((child) => child.kind() !== "(" && child.kind() !== ")"); + continue; + } + if ( + kind === "as_expression" || + kind === "satisfies_expression" || + kind === "non_null_expression" + ) { + current = current.children()[0] ?? null; + continue; + } + if (kind === "type_assertion") { + current = current.children().find((child) => child.kind() !== "type_arguments"); + continue; + } + return current; + } + return null; +} + function isSdkDbMember( object: SgNode | null, dbNames: Set, namespaceNames: Set, shadowedRanges: Map>, ) { - if (!object) return false; - if (object.kind() === "identifier") - return dbNames.has(object.text()) && !isShadowed(object, shadowedRanges); - if (object.kind() !== "member_expression") return false; - - const base = object.field("object"); - const property = object.field("property"); + const unwrapped = unwrapExpression(object); + if (!unwrapped) return false; + if (unwrapped.kind() === "identifier") + return dbNames.has(unwrapped.text()) && !isShadowed(unwrapped, shadowedRanges); + if (unwrapped.kind() !== "member_expression") return false; + + const base = unwrapExpression(unwrapped.field("object")); + const property = unwrapped.field("property"); return ( base?.kind() === "identifier" && namespaceNames.has(base.text()) && @@ -271,14 +297,14 @@ function hasTypeBuilderUse(root: SgNode, name: string, afterIndex: number): bool for (const member of root.findAll({ rule: { kind: "member_expression" } })) { if (member.range().start.index <= afterIndex) continue; if (member.field("property")?.text() !== "type") continue; - const object = member.field("object"); + const object = unwrapExpression(member.field("object")); if (object?.kind() === "identifier" && object.text() === name) return true; } for (const subscript of root.findAll({ rule: { kind: "subscript_expression" } })) { if (subscript.range().start.index <= afterIndex) continue; if (!typeStringLiteral(subscript.field("index"))) continue; - const object = subscript.field("object"); + const object = unwrapExpression(subscript.field("object")); if (object?.kind() === "identifier" && object.text() === name) return true; } diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts index 5be1289ba..3121ce18a 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts @@ -34,6 +34,14 @@ export const computedTeam = sdk.db["table"]("ComputedTeam", { label: sdk.db.string(), }); +export const parenthesizedUser = (db).table("ParenthesizedUser", { + label: db.string(), +}); + +export const assertedUser = (db as any).table("AssertedUser", { + label: db.string(), +}); + const local = { type: (name: string) => name, }; diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts index 14161d4fa..54571e0cf 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts @@ -34,6 +34,14 @@ export const computedTeam = sdk.db["type"]("ComputedTeam", { label: sdk.db.string(), }); +export const parenthesizedUser = (db).type("ParenthesizedUser", { + label: db.string(), +}); + +export const assertedUser = (db as any).type("AssertedUser", { + label: db.string(), +}); + const local = { type: (name: string) => name, }; diff --git a/packages/sdk-codemod/src/db-type-to-table-review.test.ts b/packages/sdk-codemod/src/db-type-to-table-review.test.ts index f0603d422..f509ceb0e 100644 --- a/packages/sdk-codemod/src/db-type-to-table-review.test.ts +++ b/packages/sdk-codemod/src/db-type-to-table-review.test.ts @@ -73,6 +73,7 @@ describe("db-type-to-table review findings", () => { "", "const schema = db;", "const nsSchema = sdk.db;", + "const assertedSchema = db as const;", "", 'export const user = schema.type("User", {', " name: schema.string(),", @@ -82,6 +83,10 @@ describe("db-type-to-table review findings", () => { " name: nsSchema.string(),", "});", "", + 'export const asserted = assertedSchema.type("Asserted", {', + " name: assertedSchema.string(),", + "});", + "", ].join("\n"), "utf-8", ); @@ -105,6 +110,12 @@ describe("db-type-to-table review findings", () => { message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", excerpt: "const nsSchema = sdk.db;", }, + { + file: "tailordb.ts", + line: 6, + message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", + excerpt: "const assertedSchema = db as const;", + }, ], }), ]); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 6d4abf8fb..f8a3980bd 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -551,8 +551,9 @@ export const allCodemods: CodemodPackage[] = [ "In Tailor SDK v2, TailorDB schema definitions use db.table(...) instead of", "db.type(...). The codemod rewrites member accesses on db imported from", "@tailor-platform/sdk, including aliases such as `import { db as schema }`.", - "It flags destructured builder aliases such as `const { type } = db` for", - "manual review because the local alias may require call-site renaming.", + "It flags destructured builder aliases such as `const { type } = db` and", + "local builder aliases such as `const schema = db` for manual review because", + "the local alias may require call-site renaming.", "Review any remaining db.type references and rename SDK TailorDB schema builder", "calls to db.table. Leave unrelated local objects with a .type() method unchanged.", ].join("\n"), diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 6cd10a204..9fc721ea8 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -504,8 +504,9 @@ export const user = db.table("User", { In Tailor SDK v2, TailorDB schema definitions use db.table(...) instead of db.type(...). The codemod rewrites member accesses on db imported from @tailor-platform/sdk, including aliases such as `import { db as schema }`. -It flags destructured builder aliases such as `const { type } = db` for -manual review because the local alias may require call-site renaming. +It flags destructured builder aliases such as `const { type } = db` and +local builder aliases such as `const schema = db` for manual review because +the local alias may require call-site renaming. Review any remaining db.type references and rename SDK TailorDB schema builder calls to db.table. Leave unrelated local objects with a .type() method unchanged. ``` From 631d315bdfd028352f73a714635b7ca4aa939f9d Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 14:08:48 +0900 Subject: [PATCH 460/618] fix: respect switch scopes in db.table codemod --- .../v2/db-type-to-table/scripts/transform.ts | 6 ++- .../src/db-type-to-table-review.test.ts | 45 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts index 252a7fe58..5600a8b20 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts @@ -100,6 +100,7 @@ function nearestScope(node: SgNode): SgNode { if ( kind === "statement_block" || kind === "program" || + kind === "switch_body" || kind === "for_statement" || kind === "for_in_statement" ) { @@ -240,7 +241,8 @@ function unwrapExpression(node: SgNode | null): SgNode | null { while (current) { const kind = current.kind(); if (kind === "parenthesized_expression") { - current = current.children().find((child) => child.kind() !== "(" && child.kind() !== ")"); + current = + current.children().find((child) => child.kind() !== "(" && child.kind() !== ")") ?? null; continue; } if ( @@ -252,7 +254,7 @@ function unwrapExpression(node: SgNode | null): SgNode | null { continue; } if (kind === "type_assertion") { - current = current.children().find((child) => child.kind() !== "type_arguments"); + current = current.children().find((child) => child.kind() !== "type_arguments") ?? null; continue; } return current; diff --git a/packages/sdk-codemod/src/db-type-to-table-review.test.ts b/packages/sdk-codemod/src/db-type-to-table-review.test.ts index f509ceb0e..d0d61ceed 100644 --- a/packages/sdk-codemod/src/db-type-to-table-review.test.ts +++ b/packages/sdk-codemod/src/db-type-to-table-review.test.ts @@ -2,6 +2,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "pathe"; import { afterEach, describe, expect, test } from "vitest"; +import dbTypeToTableTransform from "../codemods/v2/db-type-to-table/scripts/transform"; import { allCodemods } from "./registry"; import { runCodemods } from "./runner"; @@ -28,6 +29,50 @@ describe("db-type-to-table review findings", () => { } }); + test("does not let switch-case local db declarations shadow imported db outside the switch", () => { + const input = [ + 'import { db } from "@tailor-platform/sdk";', + "", + 'export const beforeSwitch = db.type("BeforeSwitch", {', + " name: db.string(),", + "});", + "", + "switch (kind) {", + ' case "local":', + " const db = { type: (name: string) => name };", + ' db.type("NoChange");', + " break;", + "}", + "", + 'export const afterSwitch = db.type("AfterSwitch", {', + " name: db.string(),", + "});", + "", + ].join("\n"); + + const expected = [ + 'import { db } from "@tailor-platform/sdk";', + "", + 'export const beforeSwitch = db.table("BeforeSwitch", {', + " name: db.string(),", + "});", + "", + "switch (kind) {", + ' case "local":', + " const db = { type: (name: string) => name };", + ' db.type("NoChange");', + " break;", + "}", + "", + 'export const afterSwitch = db.table("AfterSwitch", {', + " name: db.string(),", + "});", + "", + ].join("\n"); + + expect(dbTypeToTableTransform(input, "tailordb.ts")).toBe(expected); + }); + test("reports destructured db.type builders for LLM review", async () => { tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "db-type-review-test-")); await fs.promises.writeFile( From 01df5e9c0272e051dfc8642a7c8a4dafa6d9f4e5 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 14:25:20 +0900 Subject: [PATCH 461/618] fix: handle template db.table codemod keys --- .../codemods/v2/db-type-to-table/scripts/transform.ts | 7 +++++-- .../v2/db-type-to-table/tests/sdk-import/expected.ts | 8 ++++++++ .../v2/db-type-to-table/tests/sdk-import/input.ts | 8 ++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts index 5600a8b20..edae8cece 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts @@ -285,13 +285,16 @@ function isSdkDbMember( } function typeStringLiteral(node: SgNode | null): SgNode | null { - if (node?.kind() !== "string") return null; + if (!node) return null; + const kind = node.kind(); + if (kind !== "string" && kind !== "template_string") return null; const fragments = node.children().filter((child) => child.kind() === "string_fragment"); return fragments.length === 1 && fragments[0]!.text() === "type" ? node : null; } function replaceStringLiteralValue(node: SgNode, value: string): Edit { - const quote = node.text().startsWith("'") ? "'" : '"'; + const text = node.text(); + const quote = text.startsWith("'") ? "'" : text.startsWith("`") ? "`" : '"'; return node.replace(`${quote}${value}${quote}`); } diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts index 3121ce18a..da10b1b26 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/expected.ts @@ -34,6 +34,14 @@ export const computedTeam = sdk.db["table"]("ComputedTeam", { label: sdk.db.string(), }); +export const computedTemplateUser = db[`table`]("ComputedTemplateUser", { + label: db.string(), +}); + +export const computedTemplateTeam = sdk.db[`table`]("ComputedTemplateTeam", { + label: sdk.db.string(), +}); + export const parenthesizedUser = (db).table("ParenthesizedUser", { label: db.string(), }); diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts index 54571e0cf..ca134dd4a 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/tests/sdk-import/input.ts @@ -34,6 +34,14 @@ export const computedTeam = sdk.db["type"]("ComputedTeam", { label: sdk.db.string(), }); +export const computedTemplateUser = db[`type`]("ComputedTemplateUser", { + label: db.string(), +}); + +export const computedTemplateTeam = sdk.db[`type`]("ComputedTemplateTeam", { + label: sdk.db.string(), +}); + export const parenthesizedUser = (db).type("ParenthesizedUser", { label: db.string(), }); From 75a0c4e25a9498d237e93544221894cbb6caa57b Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 14:37:24 +0900 Subject: [PATCH 462/618] fix: flag namespace db aliases in codemod --- .../v2/db-type-to-table/scripts/transform.ts | 46 +++++++++++++++++++ .../src/db-type-to-table-review.test.ts | 11 +++++ 2 files changed, 57 insertions(+) diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts index edae8cece..7ad388ba9 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts @@ -386,6 +386,35 @@ function objectPatternHasTypeProperty(pattern: SgNode): boolean { .some((node) => node.text() === "type"); } +function namespaceDbAliasBindings(pattern: SgNode): SgNode[] { + const aliases: SgNode[] = []; + for (const child of pattern.children()) { + if (child.kind() === "shorthand_property_identifier_pattern" && child.text() === "db") { + aliases.push(child); + continue; + } + if (child.kind() !== "pair_pattern") continue; + const key = child.field("key"); + if (key?.text() !== "db") continue; + const value = child.field("value"); + if (value?.kind() === "identifier") aliases.push(value); + } + return aliases; +} + +function isSdkNamespaceMember( + node: SgNode | null, + namespaceNames: Set, + shadowedRanges: Map>, +): boolean { + const unwrapped = unwrapExpression(node); + return ( + unwrapped?.kind() === "identifier" && + namespaceNames.has(unwrapped.text()) && + !isShadowed(unwrapped, shadowedRanges) + ); +} + export function reviewFindings( source: string, filePath: string, @@ -434,6 +463,23 @@ export function reviewFindings( }); } + for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { + const binding = firstDeclaratorChild(decl); + if (binding?.kind() !== "object_pattern") continue; + const value = declaratorValue(decl); + if (!isSdkNamespaceMember(value, namespaceNames, shadowedRanges)) continue; + + for (const alias of namespaceDbAliasBindings(binding)) { + if (!hasTypeBuilderUse(root, alias.text(), decl.range().end.index)) continue; + findings.push({ + file: relativePath, + line: lineForIndex(source, binding.range().start.index), + message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", + excerpt: excerptAtIndex(source, binding.range().start.index), + }); + } + } + for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { const binding = firstDeclaratorChild(decl); if (binding?.kind() !== "identifier") continue; diff --git a/packages/sdk-codemod/src/db-type-to-table-review.test.ts b/packages/sdk-codemod/src/db-type-to-table-review.test.ts index d0d61ceed..98cd4c95b 100644 --- a/packages/sdk-codemod/src/db-type-to-table-review.test.ts +++ b/packages/sdk-codemod/src/db-type-to-table-review.test.ts @@ -118,6 +118,7 @@ describe("db-type-to-table review findings", () => { "", "const schema = db;", "const nsSchema = sdk.db;", + "const { db: destructuredSchema } = sdk;", "const assertedSchema = db as const;", "", 'export const user = schema.type("User", {', @@ -128,6 +129,10 @@ describe("db-type-to-table review findings", () => { " name: nsSchema.string(),", "});", "", + 'export const destructured = destructuredSchema.type("Destructured", {', + " name: destructuredSchema.string(),", + "});", + "", 'export const asserted = assertedSchema.type("Asserted", {', " name: assertedSchema.string(),", "});", @@ -159,6 +164,12 @@ describe("db-type-to-table review findings", () => { file: "tailordb.ts", line: 6, message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", + excerpt: "const { db: destructuredSchema } = sdk;", + }, + { + file: "tailordb.ts", + line: 7, + message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", excerpt: "const assertedSchema = db as const;", }, ], From 7e3d6c1398732bbc986f9117194eb8129da7d9dc Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 14:53:28 +0900 Subject: [PATCH 463/618] fix: run db.table codemod for prereleases --- packages/sdk-codemod/src/registry.test.ts | 8 ++++++++ packages/sdk-codemod/src/registry.ts | 2 ++ 2 files changed, 10 insertions(+) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 613b7c353..9462eec49 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -47,6 +47,14 @@ describe("getApplicableCodemods", () => { expect(prereleaseIds).not.toContain("v2/node-minimum-22-15-0"); }); + test("returns db.type to db.table codemod for the prerelease that removes db.type", () => { + const prereleaseIds = getApplicableCodemods("1.67.1", "2.0.0-next.3").map( + (codemod) => codemod.id, + ); + + expect(prereleaseIds).toContain("v2/db-type-to-table"); + }); + test("returns empty when both versions are before the codemod boundary", () => { expect(getApplicableCodemods("1.0.0", "1.5.0")).toEqual([]); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index f8a3980bd..65d68a405 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -96,6 +96,7 @@ const RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN = new RegExp( ); const V2_NEXT_1 = "2.0.0-next.1"; const V2_NEXT_2 = "2.0.0-next.2"; +const V2_NEXT_3 = "2.0.0-next.3"; /** All registered codemods, in registration order. */ export const allCodemods: CodemodPackage[] = [ @@ -536,6 +537,7 @@ export const allCodemods: CodemodPackage[] = [ "Rename TailorDB schema builder calls from `db.type()` to `db.table()`. TailorDB schema definitions now use table terminology in SDK projects.", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_3, scriptPath: "v2/db-type-to-table/scripts/transform.js", filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], legacyPatterns: ["db.type"], From fe09dfc6a825426a223592a5cf7000f00da14b0c Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 15:09:01 +0900 Subject: [PATCH 464/618] fix: flag assigned db aliases in db.table codemod --- .../v2/db-type-to-table/scripts/transform.ts | 29 +++++++++++++++++++ .../src/db-type-to-table-review.test.ts | 24 +++++++++++++++ packages/sdk-codemod/src/registry.ts | 4 +-- packages/sdk/docs/migration/v2.md | 4 +-- 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts index 7ad388ba9..9938f2844 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts @@ -83,6 +83,20 @@ function declaratorValue(node: SgNode): SgNode | null { return children.slice(equalsIndex + 1).find((child) => child.kind() !== "comment") ?? null; } +function assignmentTarget(node: SgNode): SgNode | null { + const children = node.children(); + const equalsIndex = children.findIndex((child) => child.kind() === "="); + if (equalsIndex === -1) return null; + return children.slice(0, equalsIndex).find((child) => child.kind() !== "comment") ?? null; +} + +function assignmentValue(node: SgNode): SgNode | null { + const children = node.children(); + const equalsIndex = children.findIndex((child) => child.kind() === "="); + if (equalsIndex === -1) return null; + return children.slice(equalsIndex + 1).find((child) => child.kind() !== "comment") ?? null; +} + function addShadowedRange( shadowedRanges: Map>, name: string, @@ -495,5 +509,20 @@ export function reviewFindings( }); } + for (const assignment of root.findAll({ rule: { kind: "assignment_expression" } })) { + const target = assignmentTarget(assignment); + if (target?.kind() !== "identifier") continue; + const value = assignmentValue(assignment); + if (!isSdkDbMember(value, dbNames, namespaceNames, shadowedRanges)) continue; + if (!hasTypeBuilderUse(root, target.text(), assignment.range().end.index)) continue; + + findings.push({ + file: relativePath, + line: lineForIndex(source, assignment.range().start.index), + message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", + excerpt: excerptAtIndex(source, assignment.range().start.index), + }); + } + return findings; } diff --git a/packages/sdk-codemod/src/db-type-to-table-review.test.ts b/packages/sdk-codemod/src/db-type-to-table-review.test.ts index 98cd4c95b..e4d46931b 100644 --- a/packages/sdk-codemod/src/db-type-to-table-review.test.ts +++ b/packages/sdk-codemod/src/db-type-to-table-review.test.ts @@ -120,6 +120,10 @@ describe("db-type-to-table review findings", () => { "const nsSchema = sdk.db;", "const { db: destructuredSchema } = sdk;", "const assertedSchema = db as const;", + "let assignedSchema;", + "assignedSchema = db;", + "let nsAssignedSchema;", + "nsAssignedSchema = sdk.db;", "", 'export const user = schema.type("User", {', " name: schema.string(),", @@ -137,6 +141,14 @@ describe("db-type-to-table review findings", () => { " name: assertedSchema.string(),", "});", "", + 'export const assigned = assignedSchema.type("Assigned", {', + " name: assignedSchema.string(),", + "});", + "", + 'export const nsAssigned = nsAssignedSchema.type("NsAssigned", {', + " name: nsAssignedSchema.string(),", + "});", + "", ].join("\n"), "utf-8", ); @@ -172,6 +184,18 @@ describe("db-type-to-table review findings", () => { message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", excerpt: "const assertedSchema = db as const;", }, + { + file: "tailordb.ts", + line: 9, + message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", + excerpt: "assignedSchema = db;", + }, + { + file: "tailordb.ts", + line: 11, + message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", + excerpt: "nsAssignedSchema = sdk.db;", + }, ], }), ]); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 65d68a405..8f53acc49 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -554,8 +554,8 @@ export const allCodemods: CodemodPackage[] = [ "db.type(...). The codemod rewrites member accesses on db imported from", "@tailor-platform/sdk, including aliases such as `import { db as schema }`.", "It flags destructured builder aliases such as `const { type } = db` and", - "local builder aliases such as `const schema = db` for manual review because", - "the local alias may require call-site renaming.", + "local builder aliases such as `const schema = db` or `schema = db` for", + "manual review because the local alias may require call-site renaming.", "Review any remaining db.type references and rename SDK TailorDB schema builder", "calls to db.table. Leave unrelated local objects with a .type() method unchanged.", ].join("\n"), diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 9fc721ea8..9968c5a6e 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -505,8 +505,8 @@ In Tailor SDK v2, TailorDB schema definitions use db.table(...) instead of db.type(...). The codemod rewrites member accesses on db imported from @tailor-platform/sdk, including aliases such as `import { db as schema }`. It flags destructured builder aliases such as `const { type } = db` and -local builder aliases such as `const schema = db` for manual review because -the local alias may require call-site renaming. +local builder aliases such as `const schema = db` or `schema = db` for +manual review because the local alias may require call-site renaming. Review any remaining db.type references and rename SDK TailorDB schema builder calls to db.table. Leave unrelated local objects with a .type() method unchanged. ``` From 1bf18de530379debe4314c53ab2170dce922f874 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 15:21:45 +0900 Subject: [PATCH 465/618] fix: migrate type-only db builder references --- .../v2/db-type-to-table/scripts/transform.ts | 19 ++++++++++---- .../src/db-type-to-table-review.test.ts | 26 +++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts index 9938f2844..5c242c91b 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts @@ -289,7 +289,7 @@ function isSdkDbMember( if (unwrapped.kind() !== "member_expression") return false; const base = unwrapExpression(unwrapped.field("object")); - const property = unwrapped.field("property"); + const property = memberProperty(unwrapped); return ( base?.kind() === "identifier" && namespaceNames.has(base.text()) && @@ -298,6 +298,15 @@ function isSdkDbMember( ); } +function memberProperty(member: SgNode): SgNode | null { + const children = member.children(); + for (let index = children.length - 1; index >= 0; index -= 1) { + const child = children[index]!; + if (child.kind() === "property_identifier") return child; + } + return member.field("property"); +} + function typeStringLiteral(node: SgNode | null): SgNode | null { if (!node) return null; const kind = node.kind(); @@ -315,7 +324,7 @@ function replaceStringLiteralValue(node: SgNode, value: string): Edit { function hasTypeBuilderUse(root: SgNode, name: string, afterIndex: number): boolean { for (const member of root.findAll({ rule: { kind: "member_expression" } })) { if (member.range().start.index <= afterIndex) continue; - if (member.field("property")?.text() !== "type") continue; + if (memberProperty(member)?.text() !== "type") continue; const object = unwrapExpression(member.field("object")); if (object?.kind() === "identifier" && object.text() === name) return true; } @@ -349,7 +358,7 @@ export default function transform(source: string, filePath: string): string | nu const namespaceNames = new Set(); for (const importStmt of imports) { for (const binding of importBindings(importStmt)) { - if (binding.importedName === "db" && !binding.typeOnly) dbNames.add(binding.localName); + if (binding.importedName === "db") dbNames.add(binding.localName); } for (const name of namespaceImportNames(importStmt)) { namespaceNames.add(name); @@ -360,7 +369,7 @@ export default function transform(source: string, filePath: string): string | nu const shadowedRanges = buildShadowedRanges(root, new Set([...dbNames, ...namespaceNames])); const edits: Edit[] = []; for (const member of root.findAll({ rule: { kind: "member_expression" } })) { - const property = member.field("property"); + const property = memberProperty(member); if (property?.text() !== "type") continue; if (!isSdkDbMember(member.field("object"), dbNames, namespaceNames, shadowedRanges)) continue; edits.push(property.replace("table")); @@ -452,7 +461,7 @@ export function reviewFindings( const namespaceNames = new Set(); for (const importStmt of imports) { for (const binding of importBindings(importStmt)) { - if (binding.importedName === "db" && !binding.typeOnly) dbNames.add(binding.localName); + if (binding.importedName === "db") dbNames.add(binding.localName); } for (const name of namespaceImportNames(importStmt)) { namespaceNames.add(name); diff --git a/packages/sdk-codemod/src/db-type-to-table-review.test.ts b/packages/sdk-codemod/src/db-type-to-table-review.test.ts index e4d46931b..49769ecea 100644 --- a/packages/sdk-codemod/src/db-type-to-table-review.test.ts +++ b/packages/sdk-codemod/src/db-type-to-table-review.test.ts @@ -73,6 +73,32 @@ describe("db-type-to-table review findings", () => { expect(dbTypeToTableTransform(input, "tailordb.ts")).toBe(expected); }); + test("rewrites type-only db imports and namespace type queries", () => { + const input = [ + 'import type { db as schema } from "@tailor-platform/sdk";', + 'import * as sdk from "@tailor-platform/sdk";', + 'import type * as sdkTypes from "@tailor-platform/sdk";', + "", + "type NamedBuilder = typeof schema.type;", + "type NamespaceBuilder = typeof sdk.db.type;", + "type TypeNamespaceBuilder = typeof sdkTypes.db.type;", + "", + ].join("\n"); + + const expected = [ + 'import type { db as schema } from "@tailor-platform/sdk";', + 'import * as sdk from "@tailor-platform/sdk";', + 'import type * as sdkTypes from "@tailor-platform/sdk";', + "", + "type NamedBuilder = typeof schema.table;", + "type NamespaceBuilder = typeof sdk.db.table;", + "type TypeNamespaceBuilder = typeof sdkTypes.db.table;", + "", + ].join("\n"); + + expect(dbTypeToTableTransform(input, "tailordb.ts")).toBe(expected); + }); + test("reports destructured db.type builders for LLM review", async () => { tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "db-type-review-test-")); await fs.promises.writeFile( From 696d197e9a5db59cc018e32ccdd9b698a77aa53a Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 15:37:06 +0900 Subject: [PATCH 466/618] fix: flag parameter db aliases in db.table codemod --- .../v2/db-type-to-table/scripts/transform.ts | 31 ++++++++++++ .../src/db-type-to-table-review.test.ts | 47 +++++++++++++++++++ packages/sdk-codemod/src/registry.ts | 5 +- packages/sdk/docs/migration/v2.md | 5 +- 4 files changed, 84 insertions(+), 4 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts index 5c242c91b..b1a5fa87f 100644 --- a/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/db-type-to-table/scripts/transform.ts @@ -97,6 +97,20 @@ function assignmentValue(node: SgNode): SgNode | null { return children.slice(equalsIndex + 1).find((child) => child.kind() !== "comment") ?? null; } +function parameterDefaultTarget(node: SgNode): SgNode | null { + const children = node.children(); + const equalsIndex = children.findIndex((child) => child.kind() === "="); + if (equalsIndex === -1) return null; + return children.slice(0, equalsIndex).find((child) => isBindingPatternKind(child.kind())) ?? null; +} + +function parameterDefaultValue(node: SgNode): SgNode | null { + const children = node.children(); + const equalsIndex = children.findIndex((child) => child.kind() === "="); + if (equalsIndex === -1) return null; + return children.slice(equalsIndex + 1).find((child) => child.kind() !== "comment") ?? null; +} + function addShadowedRange( shadowedRanges: Map>, name: string, @@ -533,5 +547,22 @@ export function reviewFindings( }); } + for (const param of root.findAll({ + rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] }, + })) { + const target = parameterDefaultTarget(param); + if (target?.kind() !== "identifier") continue; + const value = parameterDefaultValue(param); + if (!isSdkDbMember(value, dbNames, namespaceNames, shadowedRanges)) continue; + if (!hasTypeBuilderUse(root, target.text(), param.range().end.index)) continue; + + findings.push({ + file: relativePath, + line: lineForIndex(source, param.range().start.index), + message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", + excerpt: excerptAtIndex(source, param.range().start.index), + }); + } + return findings; } diff --git a/packages/sdk-codemod/src/db-type-to-table-review.test.ts b/packages/sdk-codemod/src/db-type-to-table-review.test.ts index 49769ecea..0399d3c8f 100644 --- a/packages/sdk-codemod/src/db-type-to-table-review.test.ts +++ b/packages/sdk-codemod/src/db-type-to-table-review.test.ts @@ -226,4 +226,51 @@ describe("db-type-to-table review findings", () => { }), ]); }); + + test("reports default parameter SDK db aliases for LLM review", async () => { + tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "db-type-review-test-")); + await fs.promises.writeFile( + path.join(tmpDir, "tailordb.ts"), + [ + 'import { db } from "@tailor-platform/sdk";', + 'import * as sdk from "@tailor-platform/sdk";', + "", + "export function makeUser(schema = db) {", + ' return schema.type("User", {', + " name: schema.string(),", + " });", + "}", + "", + "export const makeTeam = (schema = sdk.db) =>", + ' schema.type("Team", {', + " name: schema.string(),", + " });", + "", + ].join("\n"), + "utf-8", + ); + + const result = await runCodemods([dbTypeToTableEntry], tmpDir, false); + + expect(result.llmReviews).toEqual([ + expect.objectContaining({ + codemodId: "v2/db-type-to-table", + files: ["tailordb.ts"], + findings: [ + { + file: "tailordb.ts", + line: 4, + message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", + excerpt: "export function makeUser(schema = db) {", + }, + { + file: "tailordb.ts", + line: 10, + message: "Review SDK db alias usage and migrate db.type builder calls to db.table.", + excerpt: "export const makeTeam = (schema = sdk.db) =>", + }, + ], + }), + ]); + }); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 8f53acc49..55a8cb49c 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -554,8 +554,9 @@ export const allCodemods: CodemodPackage[] = [ "db.type(...). The codemod rewrites member accesses on db imported from", "@tailor-platform/sdk, including aliases such as `import { db as schema }`.", "It flags destructured builder aliases such as `const { type } = db` and", - "local builder aliases such as `const schema = db` or `schema = db` for", - "manual review because the local alias may require call-site renaming.", + "local builder aliases such as `const schema = db`, `schema = db`, or", + "`function make(schema = db) { ... }` for manual review because the local", + "alias may require call-site renaming.", "Review any remaining db.type references and rename SDK TailorDB schema builder", "calls to db.table. Leave unrelated local objects with a .type() method unchanged.", ].join("\n"), diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 9968c5a6e..a3dfbb201 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -505,8 +505,9 @@ In Tailor SDK v2, TailorDB schema definitions use db.table(...) instead of db.type(...). The codemod rewrites member accesses on db imported from @tailor-platform/sdk, including aliases such as `import { db as schema }`. It flags destructured builder aliases such as `const { type } = db` and -local builder aliases such as `const schema = db` or `schema = db` for -manual review because the local alias may require call-site renaming. +local builder aliases such as `const schema = db`, `schema = db`, or +`function make(schema = db) { ... }` for manual review because the local +alias may require call-site renaming. Review any remaining db.type references and rename SDK TailorDB schema builder calls to db.table. Leave unrelated local objects with a .type() method unchanged. ``` From 62db39dea6a6204d10d45c4b02827c9cca5015cb Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 16:03:21 +0900 Subject: [PATCH 467/618] fix: migrate TailorDB schema test to db.table --- packages/sdk/src/configure/services/tailordb/schema.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 58bf70fb1..87a83152f 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -490,7 +490,7 @@ describe("TailorDBField type error message tests", () => { function withCustomFields< const F extends Record = Record, >(fields?: F) { - return db.type("WithCustomFields", { + return db.table("WithCustomFields", { tags: db.string({ array: true }).description("array field to catch array-widening bugs"), ...(fields ?? ({} as F)), }); From 1a8fd5d553fbf16a85e038262c5af1092b6462ae Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 8 Jul 2026 16:15:33 +0900 Subject: [PATCH 468/618] fix: remove stale file stream mock --- packages/sdk/src/vitest/mocks/file.ts | 85 --------------------------- 1 file changed, 85 deletions(-) diff --git a/packages/sdk/src/vitest/mocks/file.ts b/packages/sdk/src/vitest/mocks/file.ts index 10c501f0f..1264fd84e 100644 --- a/packages/sdk/src/vitest/mocks/file.ts +++ b/packages/sdk/src/vitest/mocks/file.ts @@ -31,83 +31,6 @@ const FILE_DEFAULTS: Record = { uploadStream: { metadata: { fileSize: 0, sha256sum: "" } }, }; -type FileStream = AsyncIterableIterator & { close(): Promise }; - -function toFileStream(value: unknown): FileStream { - if ( - value !== null && - typeof value === "object" && - Symbol.asyncIterator in value && - typeof (value as { close?: unknown }).close === "function" - ) { - return value as FileStream; - } - if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) { - throw new TypeError( - "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.", - ); - } - if ( - value !== null && - typeof value === "object" && - (Symbol.iterator in value || Symbol.asyncIterator in value) - ) { - const source = value as Iterable | AsyncIterable; - const inner = - Symbol.asyncIterator in source - ? (source as AsyncIterable)[Symbol.asyncIterator]() - : (source as Iterable)[Symbol.iterator](); - 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() {}, - [Symbol.asyncIterator]() { - return stream; - }, - }; - return stream; - } - const empty: FileStream = { - async next() { - return { done: true as const, value: undefined }; - }, - async close() {}, - [Symbol.asyncIterator]() { - return empty; - }, - }; - return empty; -} - -function assertStreamValue(v: unknown): void { - if (v === null || typeof v !== "object") { - throw new TypeError( - 'openDownloadStream expected a StreamValue item ({ type: "metadata" | "chunk" | "complete", ... }); ' + - `got ${typeof v === "object" ? "null" : typeof v}.`, - ); - } - if (v instanceof ArrayBuffer || ArrayBuffer.isView(v)) { - throw new TypeError( - "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( - 'openDownloadStream expected a StreamValue item with type "metadata" | "chunk" | "complete"; ' + - `got ${JSON.stringify(type)}.`, - ); - } -} - /** * Acquire a disposable mock for `tailordb.file`. Restored on dispose. * @returns Disposable File mock control object @@ -167,14 +90,6 @@ export function mockFile() { async getMetadata(namespace: string, typeName: string, fieldName: string, recordId: string) { return handle("getMetadata", namespace, typeName, fieldName, recordId); }, - async openDownloadStream( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ) { - return toFileStream(handle("openDownloadStream", namespace, typeName, fieldName, recordId)); - }, async downloadStream(namespace: string, typeName: string, fieldName: string, recordId: string) { const resolved = handle("downloadStream", namespace, typeName, fieldName, recordId); if (resolved != null) return resolved; From c139730d14753ef04604cd0f0abc3c9baaa11014 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 9 Jul 2026 11:38:37 +0900 Subject: [PATCH 469/618] feat(tailordb): split hook types to omit oldValue/oldRecord from create hooks --- packages/sdk-codemod/src/registry.ts | 1 + .../services/tailordb/schema.test.ts | 40 +++++++++++++++++-- .../src/configure/services/tailordb/types.ts | 27 +++++++++---- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 9590f1edf..837451907 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -96,6 +96,7 @@ const RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN = new RegExp( ); const V2_NEXT_1 = "2.0.0-next.1"; const V2_NEXT_2 = "2.0.0-next.2"; +const V2_NEXT_3 = "2.0.0-next.3"; /** All registered codemods, in registration order. */ export const allCodemods: CodemodPackage[] = [ diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 5e7b1dfcd..ae4ff5796 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -576,6 +576,28 @@ describe("TailorDBField hooks modifier tests", () => { const _hooks = db.string({ optional: true }).hooks; expectTypeOf[0]>().toEqualTypeOf>(); }); + + test("create hook args omit oldValue, update hook args include it", () => { + db.string().hooks({ + create: (args) => { + expectTypeOf(args).toEqualTypeOf<{ + value: string | null; + invoker: TailorPrincipal | null; + now: Date; + }>(); + return args.value ?? "default"; + }, + update: (args) => { + expectTypeOf(args).toEqualTypeOf<{ + value: string | null; + oldValue: string | null; + invoker: TailorPrincipal | null; + now: Date; + }>(); + return args.oldValue ?? args.value ?? "default"; + }, + }); + }); }); describe("TailorDBField validate modifier tests", () => { @@ -1090,12 +1112,24 @@ describe("TailorDBType hooks modifier tests", () => { }); }); - test("type hook input args receive correct types", () => { + test("type create hook input args receive correct types (no oldRecord)", () => { db.type("Test", { name: db.string(), age: db.int({ optional: true }) }).hooks({ - create: ({ input, oldRecord, invoker, now }) => { + create: ({ input, invoker, now }) => { expectTypeOf(input.name).toEqualTypeOf(); expectTypeOf(input.age).toEqualTypeOf(); - expectTypeOf(oldRecord).toBeNullable(); + expectTypeOf(invoker).toBeNullable(); + expectTypeOf(now).toEqualTypeOf(); + return {}; + }, + }); + }); + + test("type update hook input args include oldRecord", () => { + db.type("Test", { name: db.string(), age: db.int({ optional: true }) }).hooks({ + update: ({ input, oldRecord, invoker, now }) => { + expectTypeOf(input.name).toEqualTypeOf(); + expectTypeOf(oldRecord.name).toEqualTypeOf(); + expectTypeOf(oldRecord.age).toEqualTypeOf(); expectTypeOf(invoker).toBeNullable(); expectTypeOf(now).toEqualTypeOf(); return {}; diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index f7a92a269..b12324666 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -153,7 +153,13 @@ type HookArgs = ? { readonly [K in keyof TData]?: TData[K] | null | undefined } : unknown; -type HookFn = (args: { +type CreateHookFn = (args: { + value: TValue; + invoker: TailorPrincipal | null; + now: Date; +}) => TReturn; + +type UpdateHookFn = (args: { value: TValue; oldValue: TValue | null; invoker: TailorPrincipal | null; @@ -161,8 +167,8 @@ type HookFn = (args: { }) => TReturn; export type Hook = { - create?: HookFn; - update?: HookFn; + create?: CreateHookFn; + update?: UpdateHookFn; }; type DottedPaths = @@ -184,19 +190,26 @@ export type TypeValidateFn< issues: (field: DottedPaths>, message: string) => void, ) => void; -export type TypeHookFn< +type TypeCreateHookFn< + F extends Record, + TData = { [K in keyof F]: output }, +> = (args: { input: HookArgs; invoker: TailorPrincipal | null; now: Date }) => { + [K in Exclude]?: TData[K] | null | undefined; +}; + +type TypeUpdateHookFn< F extends Record, TData = { [K in keyof F]: output }, > = (args: { input: HookArgs; - oldRecord: HookArgs | null; + oldRecord: HookArgs; invoker: TailorPrincipal | null; now: Date; }) => { [K in Exclude]?: TData[K] | null | undefined }; export type TypeHook> = { - create?: TypeHookFn; - update?: TypeHookFn; + create?: TypeCreateHookFn; + update?: TypeUpdateHookFn; }; // --- Field helper types --- From ee35251e9211b35ce68f2ae5376c630f97662ddb Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 9 Jul 2026 11:52:28 +0900 Subject: [PATCH 470/618] fix(sdk): remove openDownloadStream from mockFile to match removed runtime API --- .../fix-mockfile-open-download-stream.md | 5 ++ packages/sdk/src/vitest/mocks/file.ts | 85 ------------------- 2 files changed, 5 insertions(+), 85 deletions(-) create mode 100644 .changeset/fix-mockfile-open-download-stream.md diff --git a/.changeset/fix-mockfile-open-download-stream.md b/.changeset/fix-mockfile-open-download-stream.md new file mode 100644 index 000000000..558d934dd --- /dev/null +++ b/.changeset/fix-mockfile-open-download-stream.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": patch +--- + +Remove the `openDownloadStream` method from `mockFile()` (`@tailor-platform/sdk/vitest`), matching the runtime file API, which no longer exposes it. Use `downloadStream` instead. diff --git a/packages/sdk/src/vitest/mocks/file.ts b/packages/sdk/src/vitest/mocks/file.ts index 10c501f0f..1264fd84e 100644 --- a/packages/sdk/src/vitest/mocks/file.ts +++ b/packages/sdk/src/vitest/mocks/file.ts @@ -31,83 +31,6 @@ const FILE_DEFAULTS: Record = { uploadStream: { metadata: { fileSize: 0, sha256sum: "" } }, }; -type FileStream = AsyncIterableIterator & { close(): Promise }; - -function toFileStream(value: unknown): FileStream { - if ( - value !== null && - typeof value === "object" && - Symbol.asyncIterator in value && - typeof (value as { close?: unknown }).close === "function" - ) { - return value as FileStream; - } - if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) { - throw new TypeError( - "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.", - ); - } - if ( - value !== null && - typeof value === "object" && - (Symbol.iterator in value || Symbol.asyncIterator in value) - ) { - const source = value as Iterable | AsyncIterable; - const inner = - Symbol.asyncIterator in source - ? (source as AsyncIterable)[Symbol.asyncIterator]() - : (source as Iterable)[Symbol.iterator](); - 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() {}, - [Symbol.asyncIterator]() { - return stream; - }, - }; - return stream; - } - const empty: FileStream = { - async next() { - return { done: true as const, value: undefined }; - }, - async close() {}, - [Symbol.asyncIterator]() { - return empty; - }, - }; - return empty; -} - -function assertStreamValue(v: unknown): void { - if (v === null || typeof v !== "object") { - throw new TypeError( - 'openDownloadStream expected a StreamValue item ({ type: "metadata" | "chunk" | "complete", ... }); ' + - `got ${typeof v === "object" ? "null" : typeof v}.`, - ); - } - if (v instanceof ArrayBuffer || ArrayBuffer.isView(v)) { - throw new TypeError( - "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( - 'openDownloadStream expected a StreamValue item with type "metadata" | "chunk" | "complete"; ' + - `got ${JSON.stringify(type)}.`, - ); - } -} - /** * Acquire a disposable mock for `tailordb.file`. Restored on dispose. * @returns Disposable File mock control object @@ -167,14 +90,6 @@ export function mockFile() { async getMetadata(namespace: string, typeName: string, fieldName: string, recordId: string) { return handle("getMetadata", namespace, typeName, fieldName, recordId); }, - async openDownloadStream( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ) { - return toFileStream(handle("openDownloadStream", namespace, typeName, fieldName, recordId)); - }, async downloadStream(namespace: string, typeName: string, fieldName: string, recordId: string) { const resolved = handle("downloadStream", namespace, typeName, fieldName, recordId); if (resolved != null) return resolved; From ad83d8f75a27c29c3b3bfcb5e599ded4657ee3a8 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, 9 Jul 2026 02:54:45 +0000 Subject: [PATCH 471/618] Version Packages (next) --- .changeset/pre.json | 29 +++++++++--- packages/create-sdk/CHANGELOG.md | 29 ++++++++++++ packages/create-sdk/package.json | 2 +- packages/sdk-codemod/CHANGELOG.md | 59 ++++++++++++++++++++++++ packages/sdk-codemod/package.json | 2 +- packages/sdk/CHANGELOG.md | 76 +++++++++++++++++++++++++++++++ packages/sdk/package.json | 2 +- 7 files changed, 189 insertions(+), 10 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index cdd1033bd..64c265ec6 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,39 +1,54 @@ { "mode": "pre", "tag": "next", - "initialVersions": { - "@tailor-platform/create-sdk": "1.64.0", - "@tailor-platform/sdk": "1.64.0", - "@tailor-platform/sdk-codemod": "0.2.7" - }, "changesets": [ "apply-deploy-source-strings", + "auth-attributes-rename", + "cli-plugins", "codemod-llm-review", "codemod-migration-docs", "codemod-residual-matching", + "codemod-runner-metadata", "execute-script-json-arg", "execute-script-review-scope", + "fix-mockfile-open-download-stream", + "fix-rename-bin-source-files", + "fix-v2-prerelease-codemods", "invoker-option-rename", "keyring-default-storage", "open-download-review-scope", + "parser-schema-strict", + "politty-skill-management", "principal-followup-review", "principal-unify-codemod", + "principal-unify-review-findings", + "principal-unify-review-followup", + "remove-auth-connection-token", "remove-auth-invoker-helper", "remove-define-generators", "remove-function-test-run-input-wrapper", "remove-open-download-stream", "remove-runtime-globals-compatibility", "remove-tailor-sdk-skills-shim", + "remove-tailorctl-config-migration", + "remove-tsx", "remove-v2-cli-aliases", "remove-workflow-test-env-fallback", - "renovate-1516", - "renovate-1525", + "rename-bin-command", + "rename-tailor-cli-env", "require-function-log-content-hash", + "revert-strict-scalar-strings", + "runtime-global-source-strings", "runtime-globals-import", + "runtime-idp-wrapper", + "share-codemod-runtime-imports", "store-cli-users-by-subject", + "tailor-output-ignore-dir", "tailor-principal-type", "timestamps-updated-at-create", + "user-profile-type-schema", "v2-baseline", + "wait-point-rename", "workflow-trigger-dispatch" ] } diff --git a/packages/create-sdk/CHANGELOG.md b/packages/create-sdk/CHANGELOG.md index b2bb71b41..6c3ada737 100644 --- a/packages/create-sdk/CHANGELOG.md +++ b/packages/create-sdk/CHANGELOG.md @@ -1,5 +1,34 @@ # @tailor-platform/create-sdk +## 2.0.0-next.3 + +### Major Changes + +- [#1684](https://github.com/tailor-platform/sdk/pull/1684) [`de3ef5e`](https://github.com/tailor-platform/sdk/commit/de3ef5e7421a998624154df5e90da62e17664524) Thanks [@dqn](https://github.com/dqn)! - Restore Tailor field outputs for UUID, date, datetime, time, and decimal fields to plain string-compatible types and remove the strict scalar string migration guidance. + +- [#1556](https://github.com/tailor-platform/sdk/pull/1556) [`645949e`](https://github.com/tailor-platform/sdk/commit/645949ed64bda8b82fc44c0db54928698b12a2eb) Thanks [@toiroakr](https://github.com/toiroakr)! - Rename `defineWaitPoint` and `defineWaitPoints` to `createWaitPoint` and `createWaitPoints`. + + These functions create runtime instances with `.wait()` and `.resolve()` methods that call the platform API at runtime, so the `create*` prefix is more accurate. Update any usages: + + ```diff + -import { defineWaitPoint, defineWaitPoints } from "@tailor-platform/sdk"; + +import { createWaitPoint, createWaitPoints } from "@tailor-platform/sdk"; + + -export const approval = defineWaitPoint("approval"); + +export const approval = createWaitPoint("approval"); + + -export const waitPoints = defineWaitPoints((define) => ({ ... })); + +export const waitPoints = createWaitPoints((define) => ({ ... })); + ``` + +### Patch Changes + +- [#1559](https://github.com/tailor-platform/sdk/pull/1559) [`ff8ef1c`](https://github.com/tailor-platform/sdk/commit/ff8ef1c1323daf81812c182e146fd53da20e676e) Thanks [@dqn](https://github.com/dqn)! - Rename auth attribute module augmentation from `AttributeMap` to `Attributes`. + +- [#1563](https://github.com/tailor-platform/sdk/pull/1563) [`501e8bf`](https://github.com/tailor-platform/sdk/commit/501e8bfdd2bca7201a1c9b036bf72087476da416) Thanks [@dqn](https://github.com/dqn)! - Standardize SDK-owned environment variables on the `TAILOR_*` namespace. + + Replace the removed SDK-specific environment variables with their new names: `TAILOR_CONFIG_PATH`, `TAILOR_DTS_PATH`, `TAILOR_CI_ALLOW_ID_INJECTION`, `TAILOR_DEPLOY_BUILD_ONLY`, `TAILOR_BUILD_OUTPUT_DIR`, `TAILOR_SKILLS_SOURCE`, `TAILOR_TEMPLATE_SDK_VERSION`, `TAILOR_PLATFORM_URL`, `TAILOR_PLATFORM_OAUTH2_CLIENT_ID`, `TAILOR_INLINE_SOURCEMAP`, `TAILOR_QUERY_NEWLINE_ON_ENTER`, and `TAILOR_APP_LOG_LEVEL`. The deprecated `TAILOR_TOKEN` fallback is removed; use `TAILOR_PLATFORM_TOKEN`. The v2 codemod rewrites unambiguous removed SDK environment variable names and flags generic names such as `LOG_LEVEL` and `PLATFORM_URL` for manual review. + ## 2.0.0-next.2 ### Major Changes diff --git a/packages/create-sdk/package.json b/packages/create-sdk/package.json index d3521bb13..e4ae09dea 100644 --- a/packages/create-sdk/package.json +++ b/packages/create-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/create-sdk", - "version": "2.0.0-next.2", + "version": "2.0.0-next.3", "description": "A CLI tool to quickly create a new Tailor Platform SDK project", "license": "MIT", "repository": { diff --git a/packages/sdk-codemod/CHANGELOG.md b/packages/sdk-codemod/CHANGELOG.md index d0e47e1d5..20ab87773 100644 --- a/packages/sdk-codemod/CHANGELOG.md +++ b/packages/sdk-codemod/CHANGELOG.md @@ -1,5 +1,64 @@ # @tailor-platform/sdk-codemod +## 0.3.0-next.3 + +### Patch Changes + +- [#1559](https://github.com/tailor-platform/sdk/pull/1559) [`ff8ef1c`](https://github.com/tailor-platform/sdk/commit/ff8ef1c1323daf81812c182e146fd53da20e676e) Thanks [@dqn](https://github.com/dqn)! - Rename auth attribute module augmentation from `AttributeMap` to `Attributes`. + +- [#1584](https://github.com/tailor-platform/sdk/pull/1584) [`7faff07`](https://github.com/tailor-platform/sdk/commit/7faff07982909b63b87185dc1186e2919a06d4bb) Thanks [@dqn](https://github.com/dqn)! - Report the codemod runner identity in the JSON summary, including the source checkout commit and local build command when run from a branch build, so prerelease migration validation can distinguish exact npm packages from branch-head behavior. + +- [#1599](https://github.com/tailor-platform/sdk/pull/1599) [`b88f6a2`](https://github.com/tailor-platform/sdk/commit/b88f6a2e1c6d8e25a797bec6ca90428f5be3b1b9) Thanks [@dqn](https://github.com/dqn)! - Apply the v2 `rename-bin` codemod to SDK CLI command strings in TypeScript and JavaScript source files. + +- [#1578](https://github.com/tailor-platform/sdk/pull/1578) [`579cb47`](https://github.com/tailor-platform/sdk/commit/579cb4705cb295c1fcf9bff948d205fb245ff4e5) Thanks [@dqn](https://github.com/dqn)! - Run v2 codemods when the target version is a v2 prerelease. + +- [#1686](https://github.com/tailor-platform/sdk/pull/1686) [`aecaf8c`](https://github.com/tailor-platform/sdk/commit/aecaf8c1bb7813a32e998ea7d034684541cb1c85) Thanks [@dqn](https://github.com/dqn)! - Replace `tailor skills install` with project-local `tailor skills add`, `list`, `remove`, and `sync` commands for bundled Tailor SDK agent skills. + +- [#1585](https://github.com/tailor-platform/sdk/pull/1585) [`1c1ca49`](https://github.com/tailor-platform/sdk/commit/1c1ca499b4fd55a616b1531ec7ab280ceed531d3) Thanks [@dqn](https://github.com/dqn)! - Report precise file-local findings for `principal-unify` review follow-ups, including nullable caller call sites and `context.user` helper adapters. + +- [#1601](https://github.com/tailor-platform/sdk/pull/1601) [`144f3e3`](https://github.com/tailor-platform/sdk/commit/144f3e30f2b0c5dac1a3288ff65c9dc5ca82c13b) Thanks [@dqn](https://github.com/dqn)! - Fix `v2/principal-unify` review findings for nested SDK field parser invoker values and destructured context helper messages. + +- [#1622](https://github.com/tailor-platform/sdk/pull/1622) [`0fe8bad`](https://github.com/tailor-platform/sdk/commit/0fe8bad9afbb7702bc067ac9635b77c0438497a6) Thanks [@dqn](https://github.com/dqn)! - Remove the deprecated `auth.getConnectionToken()` helper from values returned by `defineAuth()`. Use `authconnection.getConnectionToken(...)` from `@tailor-platform/sdk/runtime` in resolvers, executors, and workflows instead. The v2 codemod rewrites direct `auth.getConnectionToken(...)` calls when the `auth` binding is imported from `tailor.config`. + +- [#1557](https://github.com/tailor-platform/sdk/pull/1557) [`7ff575f`](https://github.com/tailor-platform/sdk/commit/7ff575fdfa15c00b5fc6282b28c0cb50bfdf927b) Thanks [@toiroakr](https://github.com/toiroakr)! - Rename the CLI binary from `tailor-sdk` to `tailor`. + + The output directory default changes from `.tailor-sdk` to `.tailor`, and the GitHub Actions lock file path changes from `.github/tailor-sdk.lock` to `.github/tailor.lock`. + + Run the `v2/rename-bin` codemod to migrate `tailor-sdk` invocations in package.json scripts, shell scripts, CI workflows, and documentation: + + ```sh + npx @tailor-platform/sdk-codemod --from 1.x --to 2.0.0 + ``` + +- [#1563](https://github.com/tailor-platform/sdk/pull/1563) [`501e8bf`](https://github.com/tailor-platform/sdk/commit/501e8bfdd2bca7201a1c9b036bf72087476da416) Thanks [@dqn](https://github.com/dqn)! - Standardize SDK-owned environment variables on the `TAILOR_*` namespace. + + Replace the removed SDK-specific environment variables with their new names: `TAILOR_CONFIG_PATH`, `TAILOR_DTS_PATH`, `TAILOR_CI_ALLOW_ID_INJECTION`, `TAILOR_DEPLOY_BUILD_ONLY`, `TAILOR_BUILD_OUTPUT_DIR`, `TAILOR_SKILLS_SOURCE`, `TAILOR_TEMPLATE_SDK_VERSION`, `TAILOR_PLATFORM_URL`, `TAILOR_PLATFORM_OAUTH2_CLIENT_ID`, `TAILOR_INLINE_SOURCEMAP`, `TAILOR_QUERY_NEWLINE_ON_ENTER`, and `TAILOR_APP_LOG_LEVEL`. The deprecated `TAILOR_TOKEN` fallback is removed; use `TAILOR_PLATFORM_TOKEN`. The v2 codemod rewrites unambiguous removed SDK environment variable names and flags generic names such as `LOG_LEVEL` and `PLATFORM_URL` for manual review. + +- [#1684](https://github.com/tailor-platform/sdk/pull/1684) [`de3ef5e`](https://github.com/tailor-platform/sdk/commit/de3ef5e7421a998624154df5e90da62e17664524) Thanks [@dqn](https://github.com/dqn)! - Restore Tailor field outputs for UUID, date, datetime, time, and decimal fields to plain string-compatible types and remove the strict scalar string migration guidance. + +- [#1582](https://github.com/tailor-platform/sdk/pull/1582) [`b8b48a3`](https://github.com/tailor-platform/sdk/commit/b8b48a379a73314c26fbf53c74c2181e77f0565b) Thanks [@dqn](https://github.com/dqn)! - Flag JavaScript files and embedded code strings that still reference ambient Tailor runtime globals during v2 migration review. + +- [#1583](https://github.com/tailor-platform/sdk/pull/1583) [`006a588`](https://github.com/tailor-platform/sdk/commit/006a5884583f23fc6852714c41e58a7ab6d65a5a) Thanks [@dqn](https://github.com/dqn)! - Automatically migrate simple direct `tailor.idp.Client` runtime global usage to the typed `idp.Client` wrapper during v2 upgrades. + +- [#1639](https://github.com/tailor-platform/sdk/pull/1639) [`6616674`](https://github.com/tailor-platform/sdk/commit/6616674eb41a53619138603405a4498e3f09d70b) Thanks [@dqn](https://github.com/dqn)! - Keep v2 codemods from reusing type-only runtime helper imports when adding runtime value imports. + +- [#1581](https://github.com/tailor-platform/sdk/pull/1581) [`79780bc`](https://github.com/tailor-platform/sdk/commit/79780bce19864a238602f4dd7a82fcc84e9f8501) Thanks [@dqn](https://github.com/dqn)! - Add the `v2/tailor-output-ignore-dir` codemod so SDK upgrades rewrite exact `.tailor-sdk/` ignore-file entries to `.tailor/` while leaving other `.tailor-sdk` paths unchanged. + +- [#1556](https://github.com/tailor-platform/sdk/pull/1556) [`645949e`](https://github.com/tailor-platform/sdk/commit/645949ed64bda8b82fc44c0db54928698b12a2eb) Thanks [@toiroakr](https://github.com/toiroakr)! - Rename `defineWaitPoint` and `defineWaitPoints` to `createWaitPoint` and `createWaitPoints`. + + These functions create runtime instances with `.wait()` and `.resolve()` methods that call the platform API at runtime, so the `create*` prefix is more accurate. Update any usages: + + ```diff + -import { defineWaitPoint, defineWaitPoints } from "@tailor-platform/sdk"; + +import { createWaitPoint, createWaitPoints } from "@tailor-platform/sdk"; + + -export const approval = defineWaitPoint("approval"); + +export const approval = createWaitPoint("approval"); + + -export const waitPoints = defineWaitPoints((define) => ({ ... })); + +export const waitPoints = createWaitPoints((define) => ({ ... })); + ``` + ## 0.3.0-next.2 ### Minor Changes diff --git a/packages/sdk-codemod/package.json b/packages/sdk-codemod/package.json index 1abab6c08..6fe095d9b 100644 --- a/packages/sdk-codemod/package.json +++ b/packages/sdk-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk-codemod", - "version": "0.3.0-next.2", + "version": "0.3.0-next.3", "description": "Codemod runner for Tailor Platform SDK upgrades", "license": "MIT", "repository": { diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index be5a4d1b4..daff0ee30 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,5 +1,81 @@ # @tailor-platform/sdk +## 2.0.0-next.3 + +### Major Changes + +- [#1559](https://github.com/tailor-platform/sdk/pull/1559) [`ff8ef1c`](https://github.com/tailor-platform/sdk/commit/ff8ef1c1323daf81812c182e146fd53da20e676e) Thanks [@dqn](https://github.com/dqn)! - Rename auth attribute module augmentation from `AttributeMap` to `Attributes`. + +- [#1529](https://github.com/tailor-platform/sdk/pull/1529) [`9ecb380`](https://github.com/tailor-platform/sdk/commit/9ecb380acdc1b37578c23c628ab46958663b4001) Thanks [@dqn](https://github.com/dqn)! - Reject unknown keys in SDK parser schemas instead of silently dropping them from application definitions. + +- [#1686](https://github.com/tailor-platform/sdk/pull/1686) [`aecaf8c`](https://github.com/tailor-platform/sdk/commit/aecaf8c1bb7813a32e998ea7d034684541cb1c85) Thanks [@dqn](https://github.com/dqn)! - Replace `tailor skills install` with project-local `tailor skills add`, `list`, `remove`, and `sync` commands for bundled Tailor SDK agent skills. + +- [#1622](https://github.com/tailor-platform/sdk/pull/1622) [`0fe8bad`](https://github.com/tailor-platform/sdk/commit/0fe8bad9afbb7702bc067ac9635b77c0438497a6) Thanks [@dqn](https://github.com/dqn)! - Remove the deprecated `auth.getConnectionToken()` helper from values returned by `defineAuth()`. Use `authconnection.getConnectionToken(...)` from `@tailor-platform/sdk/runtime` in resolvers, executors, and workflows instead. The v2 codemod rewrites direct `auth.getConnectionToken(...)` calls when the `auth` binding is imported from `tailor.config`. + +- [#1620](https://github.com/tailor-platform/sdk/pull/1620) [`1d71a52`](https://github.com/tailor-platform/sdk/commit/1d71a528ac57dd6fc0de2ab9b898b538608ac13e) Thanks [@dqn](https://github.com/dqn)! - Stop importing credentials and profiles from legacy `~/.tailorctl/config` when the platform config is missing. New CLI configs now start empty in the current platform config format. + +- [#1536](https://github.com/tailor-platform/sdk/pull/1536) [`84d9aba`](https://github.com/tailor-platform/sdk/commit/84d9aba843f14e8a7a43f0baff92dfc8afdf2821) Thanks [@toiroakr](https://github.com/toiroakr)! - Minimum Node.js version raised to 22.15.0; TypeScript loading switched from tsx to amaro + + Removes `tsx` (which pulled in esbuild's native binaries, ~10.5 MB) from + `dependencies` and replaces it with `amaro` (~3.8 MB, zero transitive deps). + + A small `ts-hook.mjs` provides the Node.js module hook with both a resolver + and a load hook (`amaro` for full TypeScript support including enums). + The resolver handles `.ts` extension fallback, directory barrel imports + (`./models` → `./models/index.ts`), and tsconfig `paths` aliases (reads + `tsconfig.json` following `extends` chains). + Dev-only scripts now use `node --experimental-strip-types` instead. + + Raises the minimum Node.js version to 22.15.0 (from >=22) to use + `module.registerHooks()`, which allows synchronous hook registration directly + in the main thread without a worker thread. + +- [#1557](https://github.com/tailor-platform/sdk/pull/1557) [`7ff575f`](https://github.com/tailor-platform/sdk/commit/7ff575fdfa15c00b5fc6282b28c0cb50bfdf927b) Thanks [@toiroakr](https://github.com/toiroakr)! - Rename the CLI binary from `tailor-sdk` to `tailor`. + + The output directory default changes from `.tailor-sdk` to `.tailor`, and the GitHub Actions lock file path changes from `.github/tailor-sdk.lock` to `.github/tailor.lock`. + + Run the `v2/rename-bin` codemod to migrate `tailor-sdk` invocations in package.json scripts, shell scripts, CI workflows, and documentation: + + ```sh + npx @tailor-platform/sdk-codemod --from 1.x --to 2.0.0 + ``` + +- [#1563](https://github.com/tailor-platform/sdk/pull/1563) [`501e8bf`](https://github.com/tailor-platform/sdk/commit/501e8bfdd2bca7201a1c9b036bf72087476da416) Thanks [@dqn](https://github.com/dqn)! - Standardize SDK-owned environment variables on the `TAILOR_*` namespace. + + Replace the removed SDK-specific environment variables with their new names: `TAILOR_CONFIG_PATH`, `TAILOR_DTS_PATH`, `TAILOR_CI_ALLOW_ID_INJECTION`, `TAILOR_DEPLOY_BUILD_ONLY`, `TAILOR_BUILD_OUTPUT_DIR`, `TAILOR_SKILLS_SOURCE`, `TAILOR_TEMPLATE_SDK_VERSION`, `TAILOR_PLATFORM_URL`, `TAILOR_PLATFORM_OAUTH2_CLIENT_ID`, `TAILOR_INLINE_SOURCEMAP`, `TAILOR_QUERY_NEWLINE_ON_ENTER`, and `TAILOR_APP_LOG_LEVEL`. The deprecated `TAILOR_TOKEN` fallback is removed; use `TAILOR_PLATFORM_TOKEN`. The v2 codemod rewrites unambiguous removed SDK environment variable names and flags generic names such as `LOG_LEVEL` and `PLATFORM_URL` for manual review. + +- [#1684](https://github.com/tailor-platform/sdk/pull/1684) [`de3ef5e`](https://github.com/tailor-platform/sdk/commit/de3ef5e7421a998624154df5e90da62e17664524) Thanks [@dqn](https://github.com/dqn)! - Restore Tailor field outputs for UUID, date, datetime, time, and decimal fields to plain string-compatible types and remove the strict scalar string migration guidance. + +- [#1556](https://github.com/tailor-platform/sdk/pull/1556) [`645949e`](https://github.com/tailor-platform/sdk/commit/645949ed64bda8b82fc44c0db54928698b12a2eb) Thanks [@toiroakr](https://github.com/toiroakr)! - Rename `defineWaitPoint` and `defineWaitPoints` to `createWaitPoint` and `createWaitPoints`. + + These functions create runtime instances with `.wait()` and `.resolve()` methods that call the platform API at runtime, so the `create*` prefix is more accurate. Update any usages: + + ```diff + -import { defineWaitPoint, defineWaitPoints } from "@tailor-platform/sdk"; + +import { createWaitPoint, createWaitPoints } from "@tailor-platform/sdk"; + + -export const approval = defineWaitPoint("approval"); + +export const approval = createWaitPoint("approval"); + + -export const waitPoints = defineWaitPoints((define) => ({ ... })); + +export const waitPoints = createWaitPoints((define) => ({ ... })); + ``` + +### Minor Changes + +- [#1501](https://github.com/tailor-platform/sdk/pull/1501) [`1e34d7a`](https://github.com/tailor-platform/sdk/commit/1e34d7a07acb5792f8e41b92b90cc339bf8cc73a) Thanks [@toiroakr](https://github.com/toiroakr)! - Add CLI plugin support (beta). Running `tailor ` for an unknown subcommand now executes an external `tailor-` executable found on your PATH or in `node_modules/.bin` (project-local takes precedence), forwarding all following arguments. This also works for unknown subcommands nested under a known command — e.g. `tailor tailordb erd` dispatches to `tailor-tailordb-erd`. Builtins always take precedence, matching stops at the first unknown segment, and a command that takes its own arguments is never replaced by a plugin. The plugin receives the current Tailor Platform context via environment variables (`TAILOR_PLATFORM_TOKEN`, `TAILOR_PLATFORM_URL`, `TAILOR_PLATFORM_OAUTH2_CLIENT_ID`, `TAILOR_PLATFORM_WORKSPACE_ID`, `TAILOR_PLATFORM_USER`, `TAILOR_CONFIG_PATH`, `TAILOR_VERSION`, `TAILOR_BIN`); token, workspace, and user are best-effort, so auth-free plugins still run when you are not logged in. + + Also adds: + + - `tailor auth token` — print a valid access token (refreshing it if expired) for use by plugins and scripts. + - `tailor plugin list` — list discovered plugins and their executable paths. + +### Patch Changes + +- [`ee35251`](https://github.com/tailor-platform/sdk/commit/ee35251e9211b35ce68f2ae5376c630f97662ddb) Thanks [@toiroakr](https://github.com/toiroakr)! - Remove the `openDownloadStream` method from `mockFile()` (`@tailor-platform/sdk/vitest`), matching the runtime file API, which no longer exposes it. Use `downloadStream` instead. + +- [#1629](https://github.com/tailor-platform/sdk/pull/1629) [`a0bc8e7`](https://github.com/tailor-platform/sdk/commit/a0bc8e7fe66147b5492bba358d7d2ec9b47c09be) Thanks [@dqn](https://github.com/dqn)! - Validate auth user profile TailorDB types with the strict TailorDB parser schema. + ## 2.0.0-next.2 ### Major Changes diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 70ffd7710..847a9617c 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk", - "version": "2.0.0-next.2", + "version": "2.0.0-next.3", "description": "Tailor Platform SDK - The SDK to work with Tailor Platform", "license": "MIT", "repository": { From 98cc98011e2f938d62441f57d7b5b5c4546c61f7 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 9 Jul 2026 15:40:30 +0900 Subject: [PATCH 472/618] fix(tailordb): align docs, changeset, and codemod with hook type split; collect all validator errors --- .changeset/tailordb-shared-now-hook.md | 11 +++++- packages/sdk-codemod/src/registry.ts | 11 +++--- packages/sdk/docs/migration/v2.md | 9 +++-- packages/sdk/docs/services/tailordb.md | 39 +++++++++++++++---- .../service/tailordb/type-script.test.ts | 5 ++- .../parser/service/tailordb/type-script.ts | 16 ++++---- 6 files changed, 63 insertions(+), 28 deletions(-) diff --git a/.changeset/tailordb-shared-now-hook.md b/.changeset/tailordb-shared-now-hook.md index ef18c37f9..8a9764600 100644 --- a/.changeset/tailordb-shared-now-hook.md +++ b/.changeset/tailordb-shared-now-hook.md @@ -2,6 +2,13 @@ "@tailor-platform/sdk": minor --- -Add a `now` argument to TailorDB hooks. `now` is the operation timestamp and is shared across every field hooked in the same create/update, so multiple fields can be stamped with an identical `Date`. Hooks and validators are now applied per type rather than per field, which is what makes the shared timestamp possible. +Redesign TailorDB hooks and validators with several breaking changes (pre-release): -As part of this, all of a type's hooks now run together and observe the same submitted input: a type-level hook's `input` reflects what the client sent and does not include other fields' hook results. +- Add shared `now` timestamp to all hooks — multiple fields stamped with the same `Date` +- Field-level hooks: `{ value, data, invoker }` → create `{ value, invoker, now }` / update `{ value, oldValue, invoker, now }` (`data` removed, `oldValue` added for update only) +- Type-level hooks: per-field mapping (`Hooks`) → single `{ create, update }` object (`TypeHook`) returning partial field overrides +- Type-level create hooks no longer receive `oldRecord`; update hooks receive non-nullable `oldRecord` +- Field-level validators: return type changed from `boolean` to `string | void` (return error message or void to pass); `[fn, message]` tuple form removed +- Type-level validators: `Validators` per-field record → `TypeValidateFn` single function with `issues(field, message)` callback +- Add `.default(value)` on fields to set a create-time default (makes required fields optional in create input) +- Remove exported types: `Hooks`, `Validators`, `ValidateConfig` diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 837451907..5eccfb500 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -868,11 +868,11 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/tailordb-hook-redesign", name: "TailorDB hook redesign: field-level args and type-level hooks", description: - "Field-level `HookFn` args change from `{ value, data, invoker }` to `{ value, oldValue, invoker, now }` — `data` (the full record) is replaced by `oldValue` (the previous field value) and `now` (operation timestamp). Type-level hooks on `db.type().hooks()` change from per-field mapping `{ fieldName: { create, update } }` (`Hooks`) to a single `{ create, update }` object (`TypeHook`) where each function takes `{ input, oldRecord, invoker, now }` and returns partial field overrides.", + "Field-level `HookFn` args change from `{ value, data, invoker }` to create `{ value, invoker, now }` / update `{ value, oldValue, invoker, now }` — `data` (the full record) is removed; `oldValue` (previous field value) is added for update hooks only; `now` (operation timestamp) is shared across all hooks. Type-level hooks on `db.type().hooks()` change from per-field mapping `{ fieldName: { create, update } }` (`Hooks`) to a single `{ create, update }` object (`TypeHook`) — create hooks take `{ input, invoker, now }`, update hooks take `{ input, oldRecord, invoker, now }` (oldRecord is always non-null). Both return partial field overrides.", since: "1.0.0", until: "2.0.0", prereleaseUntil: V2_NEXT_3, - suspiciousPatterns: ["Hooks<", "HookFn<"], + suspiciousPatterns: ["Hooks<", "HookFn<", "Hook<"], examples: [ { caption: @@ -894,8 +894,9 @@ export const allCodemods: CodemodPackage[] = [ "The v2 SDK redesigns TailorDB hooks at both field and type levels.", "", "Field-level `.hooks()` on individual fields:", - "- Args: `{ value, data, invoker }` → `{ value, oldValue, invoker, now }`", - "- `data` (full record) is removed; use `oldValue` (previous field value) instead", + "- Create args: `{ value, data, invoker }` → `{ value, invoker, now }` (no `oldValue`)", + "- Update args: `{ value, data, invoker }` → `{ value, oldValue, invoker, now }`", + "- `data` (full record) is removed; update hooks get `oldValue` (previous field value) instead", "- `now` provides the operation timestamp — use `now` instead of `new Date()`", "- If a field-level hook needs the full record (other fields), move it to a type-level hook", "", @@ -904,7 +905,7 @@ export const allCodemods: CodemodPackage[] = [ "- New: `.hooks({ create: fn, update: fn })` (single object, `TypeHook` type)", "- Each function: `({ input, oldRecord, invoker, now }) => ({ fieldName: value, ... })`", "- `input` is the pre-hook input (may have nullish values for optional/defaulted fields)", - "- `oldRecord` is null on create, the previous record on update", + "- Create hooks do not receive `oldRecord`; update hooks receive `oldRecord` (always non-null)", "- Return an object with only the fields to override; unmentioned fields are unchanged", "", "Migration steps for each `.hooks()` call on a `db.type()`:", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index e59d2e221..ca344a65c 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -791,7 +791,7 @@ For each remaining `ValidateConfig`, `Validators<`, or old-signature `.validate( **Migration:** Manual -Field-level `HookFn` args change from `{ value, data, invoker }` to `{ value, oldValue, invoker, now }` — `data` (the full record) is replaced by `oldValue` (the previous field value) and `now` (operation timestamp). Type-level hooks on `db.type().hooks()` change from per-field mapping `{ fieldName: { create, update } }` (`Hooks`) to a single `{ create, update }` object (`TypeHook`) where each function takes `{ input, oldRecord, invoker, now }` and returns partial field overrides. +Field-level `HookFn` args change from `{ value, data, invoker }` to create `{ value, invoker, now }` / update `{ value, oldValue, invoker, now }` — `data` (the full record) is removed; `oldValue` (previous field value) is added for update hooks only; `now` (operation timestamp) is shared across all hooks. Type-level hooks on `db.type().hooks()` change from per-field mapping `{ fieldName: { create, update } }` (`Hooks`) to a single `{ create, update }` object (`TypeHook`) — create hooks take `{ input, invoker, now }`, update hooks take `{ input, oldRecord, invoker, now }` (oldRecord is always non-null). Both return partial field overrides. Field-level hooks: `data` replaced by `oldValue` and `now`; use `now` instead of `new Date()`: @@ -846,8 +846,9 @@ After: The v2 SDK redesigns TailorDB hooks at both field and type levels. Field-level `.hooks()` on individual fields: -- Args: `{ value, data, invoker }` → `{ value, oldValue, invoker, now }` -- `data` (full record) is removed; use `oldValue` (previous field value) instead +- Create args: `{ value, data, invoker }` → `{ value, invoker, now }` (no `oldValue`) +- Update args: `{ value, data, invoker }` → `{ value, oldValue, invoker, now }` +- `data` (full record) is removed; update hooks get `oldValue` (previous field value) instead - `now` provides the operation timestamp — use `now` instead of `new Date()` - If a field-level hook needs the full record (other fields), move it to a type-level hook @@ -856,7 +857,7 @@ Type-level `.hooks()` on `db.type()`: - New: `.hooks({ create: fn, update: fn })` (single object, `TypeHook` type) - Each function: `({ input, oldRecord, invoker, now }) => ({ fieldName: value, ... })` - `input` is the pre-hook input (may have nullish values for optional/defaulted fields) -- `oldRecord` is null on create, the previous record on update +- Create hooks do not receive `oldRecord`; update hooks receive `oldRecord` (always non-null) - Return an object with only the fields to override; unmentioned fields are unchanged Migration steps for each `.hooks()` call on a `db.type()`: diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index 0bae371e7..6afe46773 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -283,17 +283,22 @@ Add hooks to execute functions during data creation or update. #### Field-level Hooks -Set hooks directly on individual fields. Each hook receives: +Set hooks directly on individual fields. -- `value`: The field value from the input (null on create when not provided) -- `oldValue`: The previous field value (null on create) +Create hooks receive: + +- `value`: The field value from the input (null when not provided) - `invoker`: Principal performing the operation - `now`: Operation timestamp (`Date`), shared across all hooks in the same operation +Update hooks receive the same arguments plus: + +- `oldValue`: The previous field value (may be null) + ```typescript db.string().hooks({ create: ({ invoker }) => invoker?.id ?? "", - update: ({ value }) => value, + update: ({ value, oldValue }) => value ?? oldValue, }); ``` @@ -301,14 +306,17 @@ Field-level hooks operate on a single field and cannot access other fields. Use #### Type-level Hooks -Set hooks across multiple fields using `db.type().hooks()`. Each hook receives: +Set hooks across multiple fields using `db.type().hooks()`. The hook returns an object with the fields to override. + +Create hooks receive: - `input`: The submitted record data (pre-hook values) -- `oldRecord`: The existing record (null on create) - `invoker`: Principal performing the operation - `now`: Operation timestamp (`Date`), shared across all hooks in the same operation -The hook returns an object with the fields to override: +Update hooks receive the same arguments plus: + +- `oldRecord`: The existing record (non-null) ```typescript export const customer = db @@ -321,7 +329,7 @@ export const customer = db create: ({ input }) => ({ fullName: `${input.firstName} ${input.lastName}`, }), - update: ({ input }) => ({ + update: ({ input, oldRecord }) => ({ fullName: `${input.firstName} ${input.lastName}`, }), }); @@ -376,6 +384,21 @@ export const user = db }); ``` +### Defaults + +Set a default value for a required field on create. The field becomes optional in the create input — the default fills in when no value is provided: + +```typescript +db.int().default(0); +db.string().default("pending"); +``` + +For datetime/date/time fields, pass `"now"` to use the operation timestamp: + +```typescript +db.datetime().default("now"); +``` + ### Vector Search ```typescript diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index 21c103f90..e1331377f 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -113,7 +113,7 @@ describe("buildTypeScripts", () => { expect(createExpr).toContain('"label": _input["label"] ?? "now"'); }); - test("builds a validate script with ?? chain and typeof string check", () => { + test("builds a validate script that runs all validators and collects all errors", () => { const fields: Record = { age: { type: "integer", @@ -132,7 +132,8 @@ describe("buildTypeScripts", () => { expect(createExpr).toContain("const __errs = {}"); expect(createExpr).toContain('const _value = _newRecord["age"]'); expect(createExpr).not.toContain("_oldValue"); - expect(createExpr).toContain("(_value >= 0) ?? (_value < 200)"); + expect(createExpr).toContain("(_value >= 0)"); + expect(createExpr).toContain("(_value < 200)"); expect(createExpr).toContain('if (typeof __r === "string") { __errs["age"] = __r; }'); expect(createExpr).toContain("return __errs"); expect(createExpr).not.toContain("new Date()"); diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index cd8e6ba04..fd5356bab 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -97,8 +97,8 @@ function buildHookObject( /** * Build validation statements for one record level. - * Each leaf field with validators contributes a block that records the first - * failing message keyed by its dotted field path. + * Each leaf field with validators contributes a block that runs every + * validator and records all failing messages keyed by dotted field path. * @param {Record} fields - Field configurations * @param {string} accessExpr - JS expression to access the parent object * @param {string} keyPrefix - Dotted path prefix for error keys @@ -122,11 +122,13 @@ function buildValidateStatements( const validators = (config.validate ?? []).filter((v) => v.script?.expr); if (validators.length > 0) { - const chain = validators.map((v) => `(${v.script?.expr})`).join(" ?? "); - statements.push( - `{ const _value = ${access};` + - ` const __r = ${chain}; if (typeof __r === "string") { __errs[${key(fieldPath)}] = __r; } }`, - ); + const checks = validators + .map( + (v) => + `{ const __r = (${v.script?.expr}); if (typeof __r === "string") { __errs[${key(fieldPath)}] = __r; } }`, + ) + .join(" "); + statements.push(`{ const _value = ${access}; ${checks} }`); } } From 0063115f567ba7e73c0b679a392d5983869e8ac4 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 9 Jul 2026 15:54:35 +0900 Subject: [PATCH 473/618] fix(cli): resolve extensionless imports of dotted basenames in TS loader hook The ts-hook resolver treated any dot in the last path segment as evidence the specifier already had an extension, skipping the .ts/.mts extension retry. Basenames like `permissions.generated` have no extension but contain a dot, so the retry never ran and resolution failed with ERR_MODULE_NOT_FOUND. Check against a list of known extensions instead of a bare dot. --- .changeset/fix-ts-hook-dotted-basename.md | 5 +++++ packages/sdk/src/cli/ts-hook.mjs | 5 +++-- packages/sdk/src/cli/ts-hook.test.ts | 24 +++++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-ts-hook-dotted-basename.md diff --git a/.changeset/fix-ts-hook-dotted-basename.md b/.changeset/fix-ts-hook-dotted-basename.md new file mode 100644 index 000000000..25337b2d2 --- /dev/null +++ b/.changeset/fix-ts-hook-dotted-basename.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": patch +--- + +Fix `tailor` CLI failing with `ERR_MODULE_NOT_FOUND` when resolving extensionless relative imports of files whose basename contains a dot (e.g. `./permissions.generated`). diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index ee48593b1..7ad97696c 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -12,6 +12,7 @@ const JS_TO_TS = new Map([ [".js", ".ts"], [".mjs", ".mts"], ]); +const KNOWN_EXTENSIONS = [".ts", ".tsx", ".mts", ".js", ".mjs", ".json"]; // --- tsconfig paths resolution --- @@ -175,7 +176,7 @@ export async function resolve(specifier, context, nextResolve) { } const lastSegment = specifier.split("/").pop() ?? ""; - if (!lastSegment.includes(".")) { + if (!KNOWN_EXTENSIONS.some((ext) => lastSegment.endsWith(ext))) { for (const ext of TS_EXTENSIONS) { try { return await nextResolve(specifier + ext, context); @@ -259,7 +260,7 @@ export function resolveSync(specifier, context, nextResolve) { } const lastSegment = specifier.split("/").pop() ?? ""; - if (!lastSegment.includes(".")) { + if (!KNOWN_EXTENSIONS.some((ext) => lastSegment.endsWith(ext))) { for (const ext of TS_EXTENSIONS) { try { return nextResolve(specifier + ext, context); diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index 261d001ae..18fe3d30f 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -131,6 +131,17 @@ describe("resolve", () => { expect(nextResolve).toHaveBeenCalledTimes(1); }); + test("retries with .ts extension for extensionless specifier whose basename contains a dot", async () => { + const resolved = { url: "file:///path/to/permissions.generated.ts" }; + const nextResolve = vi + .fn() + .mockRejectedValueOnce(notFound("./permissions.generated")) + .mockResolvedValueOnce(resolved); + const result = await resolve("./permissions.generated", {}, nextResolve); + expect(result).toEqual(resolved); + expect(nextResolve).toHaveBeenCalledWith("./permissions.generated.ts", {}); + }); + test("rethrows ERR_MODULE_NOT_FOUND for non-relative specifiers without retrying", async () => { const nextResolve = vi.fn().mockRejectedValue(notFound("some-package")); await expect(resolve("some-package", {}, nextResolve)).rejects.toMatchObject({ @@ -317,6 +328,19 @@ describe("resolveSync", () => { expect(nextResolve).toHaveBeenCalledTimes(1); }); + test("retries with .ts extension for extensionless specifier whose basename contains a dot", () => { + const resolved = { url: "file:///path/to/permissions.generated.ts" }; + const nextResolve = vi + .fn() + .mockImplementationOnce(() => { + throw notFound("./permissions.generated"); + }) + .mockReturnValueOnce(resolved); + const result = resolveSync("./permissions.generated", {}, nextResolve); + expect(result).toEqual(resolved); + expect(nextResolve).toHaveBeenCalledWith("./permissions.generated.ts", {}); + }); + test("resolves non-relative specifier via tsconfig path alias", () => { const tsconfig = JSON.stringify({ compilerOptions: { baseUrl: ".", paths: { "@/*": ["./*"] } }, From 9c81d9c18b1d29b3e9307ea17fe54c8ce55f4dda Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 18:01:20 +0900 Subject: [PATCH 474/618] feat: unify runtime subpath namespace exports --- .changeset/runtime-subpath-namespace.md | 8 + example/generated/files.ts | 2 +- example/tests/fixtures/expected/files.ts | 2 +- .../v2/runtime-subpath-namespace/codemod.yaml | 7 + .../scripts/transform.ts | 302 ++++++++++++++++++ .../tests/aliased-flat-import/expected.ts | 4 + .../tests/aliased-flat-import/input.ts | 4 + .../tests/delete-file-import/expected.ts | 3 + .../tests/delete-file-import/input.ts | 3 + .../tests/flat-named-import/expected.ts | 3 + .../tests/flat-named-import/input.ts | 3 + .../tests/namespace-import/expected.ts | 3 + .../tests/namespace-import/input.ts | 3 + .../tests/type-only-import/input.ts | 3 + packages/sdk-codemod/src/registry.ts | 44 +++ packages/sdk-codemod/src/transform.test.ts | 4 + packages/sdk-codemod/tsdown.config.ts | 2 + packages/sdk/docs/migration/v2.md | 49 +++ packages/sdk/docs/runtime.md | 4 +- packages/sdk/knip.ts | 5 + .../builtin/file-utils/generate-file-utils.ts | 2 +- packages/sdk/src/runtime/aigateway.test.ts | 8 +- packages/sdk/src/runtime/aigateway.ts | 16 +- .../sdk/src/runtime/authconnection.test.ts | 6 +- packages/sdk/src/runtime/authconnection.ts | 16 +- packages/sdk/src/runtime/context.test.ts | 8 +- packages/sdk/src/runtime/context.ts | 7 +- packages/sdk/src/runtime/file.test.ts | 18 +- packages/sdk/src/runtime/file.ts | 38 ++- packages/sdk/src/runtime/iconv.test.ts | 6 +- packages/sdk/src/runtime/iconv.ts | 54 ++-- packages/sdk/src/runtime/idp.test.ts | 6 +- packages/sdk/src/runtime/idp.ts | 7 +- packages/sdk/src/runtime/index.ts | 16 +- .../sdk/src/runtime/secretmanager.test.ts | 6 +- packages/sdk/src/runtime/secretmanager.ts | 27 +- packages/sdk/src/runtime/workflow.test.ts | 6 +- packages/sdk/src/runtime/workflow.ts | 52 +-- 38 files changed, 602 insertions(+), 155 deletions(-) create mode 100644 .changeset/runtime-subpath-namespace.md create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aliased-flat-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aliased-flat-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-file-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-file-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-named-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-named-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-import/input.ts diff --git a/.changeset/runtime-subpath-namespace.md b/.changeset/runtime-subpath-namespace.md new file mode 100644 index 000000000..b3d80ee6a --- /dev/null +++ b/.changeset/runtime-subpath-namespace.md @@ -0,0 +1,8 @@ +--- +"@tailor-platform/sdk": major +"@tailor-platform/sdk-codemod": patch +--- + +Remove flat value exports from `@tailor-platform/sdk/runtime/*` subpath modules. Import each subpath through its default export or self-named namespace export instead, for example `import iconv from "@tailor-platform/sdk/runtime/iconv"` or `import { iconv } from "@tailor-platform/sdk/runtime/iconv"`. + +The aggregate `@tailor-platform/sdk/runtime` namespace imports are unchanged. The v2 codemod rewrites straightforward namespace-star subpath imports and flat named value imports to the new namespace-object style. diff --git a/example/generated/files.ts b/example/generated/files.ts index b7e393b3d..54ebb3a5a 100644 --- a/example/generated/files.ts +++ b/example/generated/files.ts @@ -1,4 +1,4 @@ -import * as file from "@tailor-platform/sdk/runtime/file"; +import { file } from "@tailor-platform/sdk/runtime/file"; import type { FileUploadOptions, FileUploadResponse, diff --git a/example/tests/fixtures/expected/files.ts b/example/tests/fixtures/expected/files.ts index b7e393b3d..54ebb3a5a 100644 --- a/example/tests/fixtures/expected/files.ts +++ b/example/tests/fixtures/expected/files.ts @@ -1,4 +1,4 @@ -import * as file from "@tailor-platform/sdk/runtime/file"; +import { file } from "@tailor-platform/sdk/runtime/file"; import type { FileUploadOptions, FileUploadResponse, diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/codemod.yaml b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/codemod.yaml new file mode 100644 index 000000000..b446fb35c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/runtime-subpath-namespace" +version: "1.0.0" +description: "Rewrite runtime subpath namespace-star and flat value imports to v2 namespace object imports" +engine: jssg +language: typescript +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts new file mode 100644 index 000000000..27613eb92 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -0,0 +1,302 @@ +import { parse, Lang } from "@ast-grep/napi"; +import { + findImportStatements, + importBindings, + importSource, + importSpecNames, + isTypeOnlyImport, + localDeclarationNames, +} from "../../../../src/ast-grep-helpers"; +import type { Edit, SgNode } from "@ast-grep/napi"; + +interface RuntimeModule { + namespace: string; + source: string; + members: Record; +} + +interface FlatImport { + localName: string; + memberName: string; +} + +interface ImportReplacement { + edit: Edit; + flatImports: FlatImport[]; + namespaceLocal: string; +} + +const RUNTIME_MODULES: RuntimeModule[] = [ + { + namespace: "iconv", + source: "@tailor-platform/sdk/runtime/iconv", + members: { + convert: "convert", + convertBuffer: "convertBuffer", + decode: "decode", + encode: "encode", + encodings: "encodings", + Iconv: "Iconv", + }, + }, + { + namespace: "secretmanager", + source: "@tailor-platform/sdk/runtime/secretmanager", + members: { getSecrets: "getSecrets", getSecret: "getSecret" }, + }, + { + namespace: "authconnection", + source: "@tailor-platform/sdk/runtime/authconnection", + members: { getConnectionToken: "getConnectionToken" }, + }, + { + namespace: "idp", + source: "@tailor-platform/sdk/runtime/idp", + members: { Client: "Client" }, + }, + { + namespace: "workflow", + source: "@tailor-platform/sdk/runtime/workflow", + members: { + triggerWorkflow: "triggerWorkflow", + resumeWorkflow: "resumeWorkflow", + triggerJobFunction: "triggerJobFunction", + wait: "wait", + resolve: "resolve", + }, + }, + { + namespace: "context", + source: "@tailor-platform/sdk/runtime/context", + members: { getInvoker: "getInvoker" }, + }, + { + namespace: "file", + source: "@tailor-platform/sdk/runtime/file", + members: { + upload: "upload", + download: "download", + downloadAsBase64: "downloadAsBase64", + delete: "delete", + deleteFile: "delete", + getMetadata: "getMetadata", + downloadStream: "downloadStream", + uploadStream: "uploadStream", + }, + }, + { + namespace: "aigateway", + source: "@tailor-platform/sdk/runtime/aigateway", + members: { get: "get" }, + }, +]; + +const MODULES_BY_SOURCE = new Map(RUNTIME_MODULES.map((mod) => [mod.source, mod])); +const JSX_FILE_EXTENSIONS = new Set([".tsx", ".jsx"]); +const JS_FILE_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]); + +function quickFilter(source: string): boolean { + return source.includes("@tailor-platform/sdk/runtime/"); +} + +function sourceLang(filePath: string, source: string): Lang { + const lower = filePath.toLowerCase(); + const extension = lower.slice(lower.lastIndexOf(".")); + if (JSX_FILE_EXTENSIONS.has(extension)) return Lang.Tsx; + if (JS_FILE_EXTENSIONS.has(extension) && /<>|<\/>|<[A-Za-z][\w.$:-]/.test(source)) { + return Lang.Tsx; + } + return Lang.TypeScript; +} + +function importClause(importStmt: SgNode): SgNode | null { + return importStmt.children().find((child) => child.kind() === "import_clause") ?? null; +} + +function defaultImportName(importStmt: SgNode): string | null { + return ( + importClause(importStmt) + ?.children() + .find((child) => child.kind() === "identifier") + ?.text() ?? null + ); +} + +function namespaceImportName(importStmt: SgNode): string | null { + return ( + importClause(importStmt) + ?.children() + .find((child) => child.kind() === "namespace_import") + ?.children() + .find((child) => child.kind() === "identifier") + ?.text() ?? null + ); +} + +function formatImport(source: string, defaultName: string | null, namedSpecs: string[]): string { + const named = namedSpecs.length > 0 ? `{ ${namedSpecs.join(", ")} }` : null; + if (defaultName && named) return `import ${defaultName}, ${named} from "${source}";`; + if (defaultName) return `import ${defaultName} from "${source}";`; + if (named) return `import ${named} from "${source}";`; + return ""; +} + +function isInsideImportStatement(node: SgNode): boolean { + let current = node.parent(); + while (current) { + if (current.kind() === "import_statement") return true; + current = current.parent(); + } + return false; +} + +function usedNames(root: SgNode, imports: SgNode[], removedNames: Set): Set { + const names = localDeclarationNames(root); + for (const importStmt of imports) { + for (const binding of importBindings(importStmt)) { + if (!removedNames.has(binding.localName)) names.add(binding.localName); + } + } + return names; +} + +function uniqueNamespaceLocal( + mod: RuntimeModule, + root: SgNode, + imports: SgNode[], + removedNames: Set, +): string { + const names = usedNames(root, imports, removedNames); + if (!names.has(mod.namespace)) return mod.namespace; + + const base = `${mod.namespace}Runtime`; + if (!names.has(base)) return base; + + for (let i = 2; ; i++) { + const candidate = `${base}${i}`; + if (!names.has(candidate)) return candidate; + } +} + +function selfNamespaceSpec(mod: RuntimeModule, localName: string): string { + return localName === mod.namespace ? mod.namespace : `${mod.namespace} as ${localName}`; +} + +function existingSelfNamespaceLocal(importStmt: SgNode, mod: RuntimeModule): string | null { + for (const spec of importStmt.findAll({ rule: { kind: "import_specifier" } })) { + const names = importSpecNames(spec); + if (!names || names.typeOnly || names.importedName !== mod.namespace) continue; + return names.localName; + } + return null; +} + +function buildImportReplacement( + importStmt: SgNode, + mod: RuntimeModule, + root: SgNode, + imports: SgNode[], +): ImportReplacement | null { + const source = importSource(importStmt); + if (!source || isTypeOnlyImport(importStmt)) return null; + + const namespaceName = namespaceImportName(importStmt); + if (namespaceName) { + return { + edit: importStmt.replace(formatImport(source, namespaceName, [])), + flatImports: [], + namespaceLocal: namespaceName, + }; + } + + const defaultName = defaultImportName(importStmt); + const existingSelfLocal = existingSelfNamespaceLocal(importStmt, mod); + const flatImports: FlatImport[] = []; + const keptSpecs: string[] = []; + + for (const spec of importStmt.findAll({ rule: { kind: "import_specifier" } })) { + const names = importSpecNames(spec); + if (!names) continue; + + const memberName = mod.members[names.importedName]; + if (!names.typeOnly && memberName) { + flatImports.push({ localName: names.localName, memberName }); + continue; + } + + keptSpecs.push(spec.text()); + } + + if (flatImports.length === 0) return null; + + const removedNames = new Set(flatImports.map((binding) => binding.localName)); + const declaredNames = localDeclarationNames(root); + if (flatImports.some((binding) => declaredNames.has(binding.localName))) return null; + + const namespaceLocal = + defaultName ?? existingSelfLocal ?? uniqueNamespaceLocal(mod, root, imports, removedNames); + const nextNamedSpecs = + defaultName || existingSelfLocal + ? keptSpecs + : [selfNamespaceSpec(mod, namespaceLocal), ...keptSpecs]; + + return { + edit: importStmt.replace(formatImport(source, defaultName, nextNamedSpecs)), + flatImports, + namespaceLocal, + }; +} + +function referenceEdits(root: SgNode, replacements: ImportReplacement[]): Edit[] { + const byLocalName = new Map(); + for (const replacement of replacements) { + for (const binding of replacement.flatImports) { + byLocalName.set(binding.localName, { + namespaceLocal: replacement.namespaceLocal, + memberName: binding.memberName, + }); + } + } + + const edits: Edit[] = []; + for (const node of root.findAll({ rule: { kind: "identifier" } })) { + if (isInsideImportStatement(node)) continue; + const binding = byLocalName.get(node.text()); + if (!binding) continue; + edits.push(node.replace(`${binding.namespaceLocal}.${binding.memberName}`)); + } + return edits; +} + +/** + * Rewrite v1 runtime subpath imports to the v2 namespace object exports. + * @param source - File contents + * @param filePath - Absolute path to the file + * @returns Transformed source or null when nothing matched. + */ +export default function transform(source: string, filePath: string): string | null { + if (!quickFilter(source)) return null; + + const root = parse(sourceLang(filePath, source), source).root(); + const imports = findImportStatements(root); + const replacements: ImportReplacement[] = []; + + for (const importStmt of imports) { + const sourceName = importSource(importStmt); + if (!sourceName) continue; + const mod = MODULES_BY_SOURCE.get(sourceName); + if (!mod) continue; + + const replacement = buildImportReplacement(importStmt, mod, root, imports); + if (replacement) replacements.push(replacement); + } + + if (replacements.length === 0) return null; + + const edits = [ + ...replacements.map((replacement) => replacement.edit), + ...referenceEdits(root, replacements), + ]; + const result = root.commitEdits(edits); + return result === source ? null : result; +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aliased-flat-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aliased-flat-import/expected.ts new file mode 100644 index 000000000..c91d25944 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aliased-flat-import/expected.ts @@ -0,0 +1,4 @@ +import { idp } from "@tailor-platform/sdk/runtime/idp"; + +const ClientRef = idp.Client; +const client = new idp.Client({ namespace: "default" }); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aliased-flat-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aliased-flat-import/input.ts new file mode 100644 index 000000000..6ab1b4d58 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aliased-flat-import/input.ts @@ -0,0 +1,4 @@ +import { Client as IdpClient } from "@tailor-platform/sdk/runtime/idp"; + +const ClientRef = IdpClient; +const client = new IdpClient({ namespace: "default" }); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-file-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-file-import/expected.ts new file mode 100644 index 000000000..707446513 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-file-import/expected.ts @@ -0,0 +1,3 @@ +import { file } from "@tailor-platform/sdk/runtime/file"; + +await file.delete("ns", "Doc", "blob", "rec-1"); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-file-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-file-import/input.ts new file mode 100644 index 000000000..cfa8d789b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-file-import/input.ts @@ -0,0 +1,3 @@ +import { deleteFile } from "@tailor-platform/sdk/runtime/file"; + +await deleteFile("ns", "Doc", "blob", "rec-1"); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-named-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-named-import/expected.ts new file mode 100644 index 000000000..84b81357e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-named-import/expected.ts @@ -0,0 +1,3 @@ +import { aigateway, type GetAIGatewayResult } from "@tailor-platform/sdk/runtime/aigateway"; + +const result: Promise = aigateway.get("main"); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-named-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-named-import/input.ts new file mode 100644 index 000000000..eb3c718f0 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-named-import/input.ts @@ -0,0 +1,3 @@ +import { get, type GetAIGatewayResult } from "@tailor-platform/sdk/runtime/aigateway"; + +const result: Promise = get("main"); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/expected.ts new file mode 100644 index 000000000..edfb8745a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/expected.ts @@ -0,0 +1,3 @@ +import iconv from "@tailor-platform/sdk/runtime/iconv"; + +const out = iconv.convert("a", "UTF-8", "Shift_JIS"); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/input.ts new file mode 100644 index 000000000..171ffbe9c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/input.ts @@ -0,0 +1,3 @@ +import * as iconv from "@tailor-platform/sdk/runtime/iconv"; + +const out = iconv.convert("a", "UTF-8", "Shift_JIS"); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-import/input.ts new file mode 100644 index 000000000..726a939c5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-import/input.ts @@ -0,0 +1,3 @@ +import type { ClientConfig } from "@tailor-platform/sdk/runtime/idp"; + +const config: ClientConfig = { namespace: "default" }; diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index f0822c3d8..159bb0311 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -501,6 +501,50 @@ export const allCodemods: CodemodPackage[] = [ "wrapper or global tailor.authconnection.", ].join("\n"), }, + { + id: "v2/runtime-subpath-namespace", + name: "Runtime subpath imports use namespace objects", + description: + "Rewrite `@tailor-platform/sdk/runtime/*` namespace-star imports and flat value imports to the v2 default or self-named namespace object imports.", + since: "1.0.0", + until: "2.0.0", + prereleaseUntil: V2_NEXT_3, + scriptPath: "v2/runtime-subpath-namespace/scripts/transform.js", + filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], + legacyPatterns: [ + "@tailor-platform/sdk/runtime/iconv", + "@tailor-platform/sdk/runtime/secretmanager", + "@tailor-platform/sdk/runtime/authconnection", + "@tailor-platform/sdk/runtime/idp", + "@tailor-platform/sdk/runtime/workflow", + "@tailor-platform/sdk/runtime/context", + "@tailor-platform/sdk/runtime/file", + "@tailor-platform/sdk/runtime/aigateway", + ], + examples: [ + { + before: + 'import * as iconv from "@tailor-platform/sdk/runtime/iconv";\niconv.convert(value, "UTF-8", "Shift_JIS");', + after: + 'import iconv from "@tailor-platform/sdk/runtime/iconv";\niconv.convert(value, "UTF-8", "Shift_JIS");', + }, + { + before: + 'import { get } from "@tailor-platform/sdk/runtime/aigateway";\nconst gateway = await get("main");', + after: + 'import { aigateway } from "@tailor-platform/sdk/runtime/aigateway";\nconst gateway = await aigateway.get("main");', + }, + ], + prompt: [ + "In Tailor SDK v2, runtime subpath modules export a default namespace object and", + "a self-named namespace object (for example, `iconv` from", + "`@tailor-platform/sdk/runtime/iconv`). Flat value imports such as", + '`import { get } from "@tailor-platform/sdk/runtime/aigateway"` are removed.', + "The codemod rewrites straightforward namespace-star imports and flat named value", + "imports. Review any remaining runtime subpath imports manually, especially when", + "a local binding or nested scope shadows the imported flat value.", + ].join("\n"), + }, { id: "v2/tailordb-namespace", name: "Tailordb → tailordb (lowercase ambient namespace)", diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index c69f26ce6..590b858d5 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -107,6 +107,10 @@ describe("codemod transforms", () => { await expect(runFixtureCases("v2/auth-connection-token-helper")).resolves.toBeUndefined(); }); + test("v2/runtime-subpath-namespace transforms correctly", async () => { + await expect(runFixtureCases("v2/runtime-subpath-namespace")).resolves.toBeUndefined(); + }); + test("v2/tailordb-namespace transforms correctly", async () => { await expect(runFixtureCases("v2/tailordb-namespace")).resolves.toBeUndefined(); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index 8f8d5d132..9d8b346ff 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -34,6 +34,8 @@ export default defineConfig([ "codemods/v2/auth-invoker-unwrap/scripts/transform.ts", "v2/auth-connection-token-helper/scripts/transform": "codemods/v2/auth-connection-token-helper/scripts/transform.ts", + "v2/runtime-subpath-namespace/scripts/transform": + "codemods/v2/runtime-subpath-namespace/scripts/transform.ts", "v2/tailordb-namespace/scripts/transform": "codemods/v2/tailordb-namespace/scripts/transform.ts", "v2/db-type-to-table/scripts/transform": "codemods/v2/db-type-to-table/scripts/transform.ts", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index bc799247f..bbc122c6e 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -436,6 +436,55 @@ wrapper or global tailor.authconnection. +## Runtime subpath imports use namespace objects + +**Migration:** Partially automatic + +Rewrite `@tailor-platform/sdk/runtime/*` namespace-star imports and flat value imports to the v2 default or self-named namespace object imports. + +Before: + +```ts +import * as iconv from "@tailor-platform/sdk/runtime/iconv"; +iconv.convert(value, "UTF-8", "Shift_JIS"); +``` + +After: + +```ts +import iconv from "@tailor-platform/sdk/runtime/iconv"; +iconv.convert(value, "UTF-8", "Shift_JIS"); +``` + +Before: + +```ts +import { get } from "@tailor-platform/sdk/runtime/aigateway"; +const gateway = await get("main"); +``` + +After: + +```ts +import { aigateway } from "@tailor-platform/sdk/runtime/aigateway"; +const gateway = await aigateway.get("main"); +``` + +
+Prompt for an AI agent (to finish the cases the codemod could not migrate) + +```text +In Tailor SDK v2, runtime subpath modules export a default namespace object and +a self-named namespace object (for example, `iconv` from +`@tailor-platform/sdk/runtime/iconv`). Flat value imports such as +`import { get } from "@tailor-platform/sdk/runtime/aigateway"` are removed. +The codemod rewrites straightforward namespace-star imports and flat named value +imports. Review any remaining runtime subpath imports manually, especially when +a local binding or nested scope shadows the imported flat value. +``` + +
+ ## Tailordb → tailordb (lowercase ambient namespace) **Migration:** Partially automatic diff --git a/packages/sdk/docs/runtime.md b/packages/sdk/docs/runtime.md index 68cc2ee73..939c4bd5d 100644 --- a/packages/sdk/docs/runtime.md +++ b/packages/sdk/docs/runtime.md @@ -45,8 +45,8 @@ const { url } = await aigateway.get("my-aigateway"); Each namespace can also be imported individually so you only pull what you need: ```ts -import * as iconv from "@tailor-platform/sdk/runtime/iconv"; -import type { ListUsersResponse, ClientConfig } from "@tailor-platform/sdk/runtime/idp"; +import iconv from "@tailor-platform/sdk/runtime/iconv"; +import { idp, type ListUsersResponse, type ClientConfig } from "@tailor-platform/sdk/runtime/idp"; ``` ## Activating the global types diff --git a/packages/sdk/knip.ts b/packages/sdk/knip.ts index c28bae0cc..139a519cc 100644 --- a/packages/sdk/knip.ts +++ b/packages/sdk/knip.ts @@ -15,6 +15,11 @@ export default { "src/vitest/integration/vitest.config.ts", "zinfer.config.ts", ], + ignoreIssues: { + "src/runtime/{aigateway,authconnection,context,file,iconv,idp,secretmanager,workflow}.ts": [ + "duplicates", + ], + }, ignoreDependencies: ["undici", "vite"], ignoreBinaries: ["knip", "publint", "actionlint"], } satisfies KnipConfig; 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 1c227327c..64fc6b0ab 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 @@ -38,7 +38,7 @@ export function generateUnifiedFileUtils( const importStatement = multiline /* ts */ ` - import * as file from "@tailor-platform/sdk/runtime/file"; + import { file } from "@tailor-platform/sdk/runtime/file"; import type { FileUploadOptions, FileUploadResponse, diff --git a/packages/sdk/src/runtime/aigateway.test.ts b/packages/sdk/src/runtime/aigateway.test.ts index 70c49c32a..aabd385d3 100644 --- a/packages/sdk/src/runtime/aigateway.test.ts +++ b/packages/sdk/src/runtime/aigateway.test.ts @@ -2,7 +2,7 @@ * Tests for `@tailor-platform/sdk/runtime/aigateway` typed wrappers. */ import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; -import * as aigateway from "#/runtime/aigateway"; +import defaultAigateway, { aigateway, type GetAIGatewayResult } from "#/runtime/aigateway"; import { cleanupMocks, injectMocks, mockAigateway } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/aigateway", () => { @@ -14,13 +14,17 @@ describe("@tailor-platform/sdk/runtime/aigateway", () => { cleanupMocks(globalThis); }); + test("exports matching default and named namespace objects", () => { + expect(defaultAigateway).toBe(aigateway); + }); + test("get forwards to global and returns Promise<{ url: string }>", async () => { using ag = mockAigateway(); ag.setUrls({ "my-aigateway": "https://my-aigateway.example.com" }); const result = aigateway.get("my-aigateway"); - expectTypeOf(result).toEqualTypeOf>(); + expectTypeOf(result).toEqualTypeOf>(); await expect(result).resolves.toEqual({ url: "https://my-aigateway.example.com" }); expect(ag.calls).toEqual([{ name: "my-aigateway" }]); }); diff --git a/packages/sdk/src/runtime/aigateway.ts b/packages/sdk/src/runtime/aigateway.ts index 2d21b0514..f15f3a09f 100644 --- a/packages/sdk/src/runtime/aigateway.ts +++ b/packages/sdk/src/runtime/aigateway.ts @@ -28,10 +28,6 @@ export interface GetAIGatewayResult { /** * Platform API surface for `tailor.aigateway`. Describes the shape the * platform runtime injects on `globalThis.tailor.aigateway`. - * - * Each method below is also re-exported as a top-level named export from this - * module so callers can either `import * as aigateway from - * "@tailor-platform/sdk/runtime/aigateway"` or pick individual methods. */ export interface TailorAigatewayAPI { /** @@ -45,9 +41,9 @@ export interface TailorAigatewayAPI { const api = (): TailorAigatewayAPI => (globalThis as { tailor: { aigateway: TailorAigatewayAPI } }).tailor.aigateway; -/** - * See {@link TailorAigatewayAPI.get}. - * @param args - Forwarded to {@link TailorAigatewayAPI.get} - * @returns The resolved AI Gateway's platform-assigned URL - */ -export const get: TailorAigatewayAPI["get"] = (...args) => api().get(...args); +const get: TailorAigatewayAPI["get"] = (...args) => api().get(...args); + +/** Runtime wrapper namespace for `tailor.aigateway`. */ +export const aigateway = { get } as const satisfies TailorAigatewayAPI; + +export default aigateway; diff --git a/packages/sdk/src/runtime/authconnection.test.ts b/packages/sdk/src/runtime/authconnection.test.ts index a27bd41a3..c18f061d5 100644 --- a/packages/sdk/src/runtime/authconnection.test.ts +++ b/packages/sdk/src/runtime/authconnection.test.ts @@ -2,7 +2,7 @@ * Tests for `@tailor-platform/sdk/runtime/authconnection` typed wrappers. */ import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import * as authconnection from "#/runtime/authconnection"; +import defaultAuthconnection, { authconnection } from "#/runtime/authconnection"; import { mockAuthconnection, cleanupMocks, injectMocks } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/authconnection", () => { @@ -14,6 +14,10 @@ describe("@tailor-platform/sdk/runtime/authconnection", () => { cleanupMocks(globalThis); }); + test("exports matching default and named namespace objects", () => { + expect(defaultAuthconnection).toBe(authconnection); + }); + test("getConnectionToken forwards to global and records call", async () => { using ac = mockAuthconnection(); ac.setTokens({ diff --git a/packages/sdk/src/runtime/authconnection.ts b/packages/sdk/src/runtime/authconnection.ts index 1a9cf36a1..15269b116 100644 --- a/packages/sdk/src/runtime/authconnection.ts +++ b/packages/sdk/src/runtime/authconnection.ts @@ -22,10 +22,6 @@ import type { ConnectionName } from "@tailor-platform/sdk"; /** * Platform API surface for `tailor.authconnection`. Describes the shape the * platform runtime injects on `globalThis.tailor.authconnection`. - * - * 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 { /** @@ -39,10 +35,10 @@ export interface TailorAuthconnectionAPI { const api = (): TailorAuthconnectionAPI => (globalThis as { tailor: { authconnection: TailorAuthconnectionAPI } }).tailor.authconnection; -/** - * See {@link TailorAuthconnectionAPI.getConnectionToken}. - * @param args - Forwarded to {@link TailorAuthconnectionAPI.getConnectionToken} - * @returns Token payload - */ -export const getConnectionToken: TailorAuthconnectionAPI["getConnectionToken"] = (...args) => +const getConnectionToken: TailorAuthconnectionAPI["getConnectionToken"] = (...args) => api().getConnectionToken(...args); + +/** Runtime wrapper namespace for `tailor.authconnection`. */ +export const authconnection = { getConnectionToken } as const satisfies TailorAuthconnectionAPI; + +export default authconnection; diff --git a/packages/sdk/src/runtime/context.test.ts b/packages/sdk/src/runtime/context.test.ts index 3de72d57e..51a1a4a8d 100644 --- a/packages/sdk/src/runtime/context.test.ts +++ b/packages/sdk/src/runtime/context.test.ts @@ -2,7 +2,7 @@ * Tests for `@tailor-platform/sdk/runtime/context` typed wrappers. */ import { afterEach, beforeEach, describe, expect, expectTypeOf, test, vi } from "vitest"; -import * as context from "#/runtime/context"; +import defaultContext, { context, type Invoker } from "#/runtime/context"; import { cleanupMocks, injectMocks } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/context", () => { @@ -14,10 +14,14 @@ describe("@tailor-platform/sdk/runtime/context", () => { cleanupMocks(globalThis); }); + test("exports matching default and named namespace objects", () => { + expect(defaultContext).toBe(context); + }); + test("getInvoker returns null for anonymous invocations", () => { const result = context.getInvoker(); - expectTypeOf(result).toEqualTypeOf(); + expectTypeOf(result).toEqualTypeOf(); expect(result).toBeNull(); }); diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index 203ae6863..2b0f09d41 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -54,7 +54,7 @@ export interface TailorContextAPI { * or `null` for anonymous invocations. * @returns Invoker details, or `null` when the call is anonymous */ -export function getInvoker(): Invoker | null { +function getInvoker(): Invoker | null { const raw = (globalThis as { tailor: { context: TailorContextAPI } }).tailor.context.getInvoker(); if (!raw) return null; return { @@ -65,3 +65,8 @@ export function getInvoker(): Invoker | null { attributeList: raw.attributes as Invoker["attributeList"], }; } + +/** Runtime wrapper namespace for `tailor.context`. */ +export const context = { getInvoker } as const; + +export default context; diff --git a/packages/sdk/src/runtime/file.test.ts b/packages/sdk/src/runtime/file.test.ts index a1dd23bfd..0b5fcf748 100644 --- a/packages/sdk/src/runtime/file.test.ts +++ b/packages/sdk/src/runtime/file.test.ts @@ -2,7 +2,11 @@ * Tests for `@tailor-platform/sdk/runtime/file` typed wrappers. */ import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import * as file from "#/runtime/file"; +import defaultFile, { + file, + type TailorDBFileError, + type TailorDBFileErrorCode, +} from "#/runtime/file"; import { cleanupMocks, mockFile, injectMocks } from "#/vitest/mock"; const args = ["ns", "Doc", "blob", "rec-1"] as const; @@ -23,6 +27,10 @@ describe("@tailor-platform/sdk/runtime/file", () => { cleanupMocks(globalThis); }); + test("exports matching default and named namespace objects", () => { + expect(defaultFile).toBe(file); + }); + test("upload forwards args and records the call", async () => { using fileM = mockFile(); fileM.enqueueResult({ metadata: { fileSize: 4, sha256sum: "abc" } }); @@ -84,7 +92,7 @@ describe("@tailor-platform/sdk/runtime/file", () => { expect(fileM.calls[0]?.method).toBe("getMetadata"); }); - test("delete forwards (re-exported from deleteFile)", async () => { + test("delete forwards", async () => { using fileM = mockFile(); await file.delete(...args); @@ -141,8 +149,8 @@ describe("@tailor-platform/sdk/runtime/file", () => { globalThis as unknown as { TailorDBFileError: new ( m: string, - c?: file.TailorDBFileErrorCode, - ) => Error & { code?: file.TailorDBFileErrorCode }; + c?: TailorDBFileErrorCode, + ) => Error & { code?: TailorDBFileErrorCode }; } ).TailorDBFileError; const err = new TailorDBFileError("operation failed", "OPERATION_FAILED"); @@ -150,7 +158,7 @@ describe("@tailor-platform/sdk/runtime/file", () => { expect(err.code).toBe("OPERATION_FAILED"); // 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; + const _typed: TailorDBFileError = err as TailorDBFileError; expect(_typed).toBe(err); }); }); diff --git a/packages/sdk/src/runtime/file.ts b/packages/sdk/src/runtime/file.ts index 1ea2b0e31..a7f133e72 100644 --- a/packages/sdk/src/runtime/file.ts +++ b/packages/sdk/src/runtime/file.ts @@ -30,7 +30,7 @@ export interface DownloadMetadata { lastUploadedAt: string; } -/** File metadata (for {@link getMetadata}). */ +/** File metadata (for {@link TailorDBFileAPI.getMetadata}). */ export interface FileMetadata { contentType: string; fileSize: number; @@ -99,11 +99,6 @@ export interface TailorDBFileError extends Error { /** * Platform API surface for `tailordb.file`. Describes the shape the platform * runtime injects on `globalThis.tailordb.file`. - * - * 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 { /** @@ -162,8 +157,7 @@ export interface TailorDBFileAPI { ): Promise; /** - * Delete a file from TailorDB. Exported as `deleteFile` (and aliased as - * `delete`) so it can be used both with named and namespace imports. + * Delete a file from TailorDB. * @param namespace - TailorDB namespace * @param typeName - TailorDB type name * @param fieldName - File field name on the type @@ -230,21 +224,21 @@ const api = (): TailorDBFileAPI => * @param args - Forwarded to {@link TailorDBFileAPI.upload} * @returns Upload response containing the file metadata */ -export const upload: TailorDBFileAPI["upload"] = (...args) => api().upload(...args); +const upload: TailorDBFileAPI["upload"] = (...args) => api().upload(...args); /** * See {@link TailorDBFileAPI.download}. * @param args - Forwarded to {@link TailorDBFileAPI.download} * @returns Bytes and metadata for the file */ -export const download: TailorDBFileAPI["download"] = (...args) => api().download(...args); +const download: TailorDBFileAPI["download"] = (...args) => api().download(...args); /** * See {@link TailorDBFileAPI.downloadAsBase64}. * @param args - Forwarded to {@link TailorDBFileAPI.downloadAsBase64} * @returns Base64-encoded contents and metadata for the file */ -export const downloadAsBase64: TailorDBFileAPI["downloadAsBase64"] = (...args) => +const downloadAsBase64: TailorDBFileAPI["downloadAsBase64"] = (...args) => api().downloadAsBase64(...args); /** @@ -252,21 +246,21 @@ export const downloadAsBase64: TailorDBFileAPI["downloadAsBase64"] = (...args) = * @param args - Forwarded to {@link TailorDBFileAPI.delete} * @returns Resolves once the file has been deleted */ -export const deleteFile: TailorDBFileAPI["delete"] = (...args) => api().delete(...args); +const deleteFile: TailorDBFileAPI["delete"] = (...args) => api().delete(...args); /** * See {@link TailorDBFileAPI.getMetadata}. * @param args - Forwarded to {@link TailorDBFileAPI.getMetadata} * @returns Metadata for the stored file */ -export const getMetadata: TailorDBFileAPI["getMetadata"] = (...args) => api().getMetadata(...args); +const getMetadata: TailorDBFileAPI["getMetadata"] = (...args) => api().getMetadata(...args); /** * See {@link TailorDBFileAPI.downloadStream}. * @param args - Forwarded to {@link TailorDBFileAPI.downloadStream} * @returns ReadableStream body and metadata for the file */ -export const downloadStream: TailorDBFileAPI["downloadStream"] = (...args) => +const downloadStream: TailorDBFileAPI["downloadStream"] = (...args) => api().downloadStream(...args); /** @@ -274,7 +268,17 @@ export const downloadStream: TailorDBFileAPI["downloadStream"] = (...args) => * @param args - Forwarded to {@link TailorDBFileAPI.uploadStream} * @returns Upload response containing the file metadata */ -export const uploadStream: TailorDBFileAPI["uploadStream"] = (...args) => - api().uploadStream(...args); +const uploadStream: TailorDBFileAPI["uploadStream"] = (...args) => api().uploadStream(...args); + +/** Runtime wrapper namespace for `tailordb.file`. */ +export const file = { + upload, + download, + downloadAsBase64, + delete: deleteFile, + getMetadata, + downloadStream, + uploadStream, +} as const satisfies TailorDBFileAPI; -export { deleteFile as delete }; +export default file; diff --git a/packages/sdk/src/runtime/iconv.test.ts b/packages/sdk/src/runtime/iconv.test.ts index b641f9593..9d1a29968 100644 --- a/packages/sdk/src/runtime/iconv.test.ts +++ b/packages/sdk/src/runtime/iconv.test.ts @@ -6,7 +6,7 @@ * `string`, otherwise `Uint8Array`) holds at the type level. */ import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; -import * as iconv from "#/runtime/iconv"; +import defaultIconv, { iconv } from "#/runtime/iconv"; import { cleanupMocks, mockIconv, injectMocks } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/iconv", () => { @@ -18,6 +18,10 @@ describe("@tailor-platform/sdk/runtime/iconv", () => { cleanupMocks(globalThis); }); + test("exports matching default and named namespace objects", () => { + expect(defaultIconv).toBe(iconv); + }); + test("convert forwards args and returns string for UTF-8 target", () => { using iconvM = mockIconv(); iconvM.setResolver((method, args) => { diff --git a/packages/sdk/src/runtime/iconv.ts b/packages/sdk/src/runtime/iconv.ts index ebeb732e1..dc6c94ba6 100644 --- a/packages/sdk/src/runtime/iconv.ts +++ b/packages/sdk/src/runtime/iconv.ts @@ -31,11 +31,6 @@ 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. - * - * 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 { /** @@ -96,46 +91,21 @@ export interface TailorIconvAPI { const api = (): TailorIconvAPI => (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv; -/** - * See {@link TailorIconvAPI.convert}. - * @param args - Forwarded to {@link TailorIconvAPI.convert} - * @returns `string` when `toEncoding` is `"UTF8"` or `"UTF-8"`, otherwise `Uint8Array`. - */ -export const convert: TailorIconvAPI["convert"] = (...args) => api().convert(...args); +const convert: TailorIconvAPI["convert"] = (...args) => api().convert(...args); -/** - * See {@link TailorIconvAPI.convertBuffer}. - * @param args - Forwarded to {@link TailorIconvAPI.convertBuffer} - * @returns `string` when `toEncoding` is `"UTF8"` or `"UTF-8"`, otherwise `Uint8Array`. - */ -export const convertBuffer: TailorIconvAPI["convertBuffer"] = (...args) => - api().convertBuffer(...args); +const convertBuffer: TailorIconvAPI["convertBuffer"] = (...args) => api().convertBuffer(...args); -/** - * See {@link TailorIconvAPI.decode}. - * @param args - Forwarded to {@link TailorIconvAPI.decode} - * @returns Decoded UTF-8 string - */ -export const decode: TailorIconvAPI["decode"] = (...args) => api().decode(...args); +const decode: TailorIconvAPI["decode"] = (...args) => api().decode(...args); -/** - * See {@link TailorIconvAPI.encode}. - * @param args - Forwarded to {@link TailorIconvAPI.encode} - * @returns `string` when `encoding` is `"UTF8"` or `"UTF-8"`, otherwise `Uint8Array`. - */ -export const encode: TailorIconvAPI["encode"] = (...args) => api().encode(...args); +const encode: TailorIconvAPI["encode"] = (...args) => api().encode(...args); -/** - * See {@link TailorIconvAPI.encodings}. - * @returns Array of encoding names supported by the platform iconv runtime - */ -export const encodings: TailorIconvAPI["encodings"] = () => api().encodings(); +const encodings: TailorIconvAPI["encodings"] = () => api().encodings(); /** * Stateful converter for repeated conversions between a fixed encoding pair. * Compatible with the `node-iconv` API surface. */ -export class Iconv { +class Iconv { private impl: IconvInstance; constructor(fromEncoding: string, toEncoding: string) { @@ -151,3 +121,15 @@ export class Iconv { return this.impl.convert(input); } } + +/** Runtime wrapper namespace for `tailor.iconv`. */ +export const iconv = { + convert, + convertBuffer, + decode, + encode, + encodings, + Iconv, +} as const satisfies TailorIconvAPI; + +export default iconv; diff --git a/packages/sdk/src/runtime/idp.test.ts b/packages/sdk/src/runtime/idp.test.ts index 0c32ccdaa..17e93c22b 100644 --- a/packages/sdk/src/runtime/idp.test.ts +++ b/packages/sdk/src/runtime/idp.test.ts @@ -5,7 +5,7 @@ * `tailor.idp.Client` and records calls with method, args, and namespace. */ import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import * as idp from "#/runtime/idp"; +import defaultIdp, { idp } from "#/runtime/idp"; import { cleanupMocks, mockIdp, injectMocks } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/idp", () => { @@ -17,6 +17,10 @@ describe("@tailor-platform/sdk/runtime/idp", () => { cleanupMocks(globalThis); }); + test("exports matching default and named namespace objects", () => { + expect(defaultIdp).toBe(idp); + }); + test("Client.user forwards args and namespace", async () => { using idpM = mockIdp(); idpM.enqueueResult({ id: "u-1", name: "alice", disabled: false }); diff --git a/packages/sdk/src/runtime/idp.ts b/packages/sdk/src/runtime/idp.ts index c707de793..5407f0be1 100644 --- a/packages/sdk/src/runtime/idp.ts +++ b/packages/sdk/src/runtime/idp.ts @@ -140,7 +140,7 @@ export interface TailorIdpAPI { * * Wraps the platform-provided `tailor.idp.Client` and exposes the same surface. */ -export class Client { +class Client { #impl: IdpClientInstance; constructor(config: ClientConfig) { @@ -219,3 +219,8 @@ export class Client { return this.#impl.unenrollMfa(input); } } + +/** Runtime wrapper namespace for `tailor.idp`. */ +export const idp = { Client } as const satisfies TailorIdpAPI; + +export default idp; diff --git a/packages/sdk/src/runtime/index.ts b/packages/sdk/src/runtime/index.ts index f8d6f2602..5d66f997f 100644 --- a/packages/sdk/src/runtime/index.ts +++ b/packages/sdk/src/runtime/index.ts @@ -24,14 +24,14 @@ 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"; -export * as idp from "./idp"; -export * as workflow from "./workflow"; -export * as context from "./context"; -export * as file from "./file"; -export * as aigateway from "./aigateway"; +export { iconv } from "./iconv"; +export { secretmanager } from "./secretmanager"; +export { authconnection } from "./authconnection"; +export { idp } from "./idp"; +export { workflow } from "./workflow"; +export { context } from "./context"; +export { file } from "./file"; +export { aigateway } from "./aigateway"; /** SQL command type recorded on a {@link TailordbQueryResult}. */ export type TailordbCommandType = diff --git a/packages/sdk/src/runtime/secretmanager.test.ts b/packages/sdk/src/runtime/secretmanager.test.ts index 40edcf7e1..46b16cc28 100644 --- a/packages/sdk/src/runtime/secretmanager.test.ts +++ b/packages/sdk/src/runtime/secretmanager.test.ts @@ -2,7 +2,7 @@ * Tests for `@tailor-platform/sdk/runtime/secretmanager` typed wrappers. */ import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; -import * as secretmanager from "#/runtime/secretmanager"; +import defaultSecretmanager, { secretmanager } from "#/runtime/secretmanager"; import { cleanupMocks, injectMocks, mockSecretmanager } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/secretmanager", () => { @@ -14,6 +14,10 @@ describe("@tailor-platform/sdk/runtime/secretmanager", () => { cleanupMocks(globalThis); }); + test("exports matching default and named namespace objects", () => { + expect(defaultSecretmanager).toBe(secretmanager); + }); + test("getSecret forwards to global and returns Promise", async () => { using sm = mockSecretmanager(); sm.setSecrets({ vault: { API_KEY: "sk-123" } }); diff --git a/packages/sdk/src/runtime/secretmanager.ts b/packages/sdk/src/runtime/secretmanager.ts index 82ceb1744..c36834f24 100644 --- a/packages/sdk/src/runtime/secretmanager.ts +++ b/packages/sdk/src/runtime/secretmanager.ts @@ -15,10 +15,6 @@ /** * Platform API surface for `tailor.secretmanager`. Describes the shape the * platform runtime injects on `globalThis.tailor.secretmanager`. - * - * 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 { /** @@ -44,17 +40,14 @@ export interface TailorSecretmanagerAPI { const api = (): TailorSecretmanagerAPI => (globalThis as { tailor: { secretmanager: TailorSecretmanagerAPI } }).tailor.secretmanager; -/** - * See {@link TailorSecretmanagerAPI.getSecrets}. - * @param args - Forwarded to {@link TailorSecretmanagerAPI.getSecrets} - * @returns Partial record keyed by the requested names - */ -export const getSecrets: TailorSecretmanagerAPI["getSecrets"] = (...args) => - api().getSecrets(...args); +const getSecrets: TailorSecretmanagerAPI["getSecrets"] = (...args) => api().getSecrets(...args); -/** - * See {@link TailorSecretmanagerAPI.getSecret}. - * @param args - Forwarded to {@link TailorSecretmanagerAPI.getSecret} - * @returns The secret value, or `undefined` if not present - */ -export const getSecret: TailorSecretmanagerAPI["getSecret"] = (...args) => api().getSecret(...args); +const getSecret: TailorSecretmanagerAPI["getSecret"] = (...args) => api().getSecret(...args); + +/** Runtime wrapper namespace for `tailor.secretmanager`. */ +export const secretmanager = { + getSecrets, + getSecret, +} as const satisfies TailorSecretmanagerAPI; + +export default secretmanager; diff --git a/packages/sdk/src/runtime/workflow.test.ts b/packages/sdk/src/runtime/workflow.test.ts index 2dcde19be..49d219f20 100644 --- a/packages/sdk/src/runtime/workflow.test.ts +++ b/packages/sdk/src/runtime/workflow.test.ts @@ -2,7 +2,7 @@ * Tests for `@tailor-platform/sdk/runtime/workflow` typed wrappers. */ import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; -import * as workflow from "#/runtime/workflow"; +import defaultWorkflow, { workflow } from "#/runtime/workflow"; import { cleanupMocks, injectMocks, mockWorkflow } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/workflow", () => { @@ -14,6 +14,10 @@ describe("@tailor-platform/sdk/runtime/workflow", () => { cleanupMocks(globalThis); }); + test("exports matching default and named namespace objects", () => { + expect(defaultWorkflow).toBe(workflow); + }); + test("triggerWorkflow forwards args and returns Promise", async () => { using wf = mockWorkflow(); wf.setTriggerHandler("exec-42"); diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index 629f359c5..554fa2025 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -36,10 +36,6 @@ export interface PlatformTriggerWorkflowOptions { /** * Platform API surface for `tailor.workflow`. Describes the shape the platform * runtime injects on `globalThis.tailor.workflow`. - * - * 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 { /** @@ -91,14 +87,7 @@ export interface TailorWorkflowAPI { const api = (): TailorWorkflowAPI => (globalThis as { tailor: { workflow: TailorWorkflowAPI } }).tailor.workflow; -/** - * See {@link TailorWorkflowAPI.triggerWorkflow}. - * @param workflowName - Workflow name as defined in tailor.config - * @param args - Arguments forwarded to the workflow's main job - * @param options - Optional trigger options - * @returns The execution ID of the triggered workflow - */ -export function triggerWorkflow( +function triggerWorkflow( workflowName: string, args?: any, options?: TriggerWorkflowOptions, @@ -109,32 +98,23 @@ export function triggerWorkflow( return api().triggerWorkflow(workflowName, args, { authInvoker: options.invoker }); } -/** - * See {@link TailorWorkflowAPI.resumeWorkflow}. - * @param args - Forwarded to {@link TailorWorkflowAPI.resumeWorkflow} - * @returns The execution ID of the resumed workflow - */ -export const resumeWorkflow: TailorWorkflowAPI["resumeWorkflow"] = (...args) => +const resumeWorkflow: TailorWorkflowAPI["resumeWorkflow"] = (...args) => api().resumeWorkflow(...args); -/** - * See {@link TailorWorkflowAPI.triggerJobFunction}. - * @param args - Forwarded to {@link TailorWorkflowAPI.triggerJobFunction} - * @returns The job's return value - */ -export const triggerJobFunction: TailorWorkflowAPI["triggerJobFunction"] = (...args) => +const triggerJobFunction: TailorWorkflowAPI["triggerJobFunction"] = (...args) => api().triggerJobFunction(...args); -/** - * See {@link TailorWorkflowAPI.wait}. - * @param args - Forwarded to {@link TailorWorkflowAPI.wait} - * @returns The payload supplied by the corresponding `resolve` call - */ -export const wait: TailorWorkflowAPI["wait"] = (...args) => api().wait(...args); +const wait: TailorWorkflowAPI["wait"] = (...args) => api().wait(...args); -/** - * See {@link TailorWorkflowAPI.resolve}. - * @param args - Forwarded to {@link TailorWorkflowAPI.resolve} - * @returns A promise that resolves once the resolve has been recorded - */ -export const resolve: TailorWorkflowAPI["resolve"] = (...args) => api().resolve(...args); +const resolve: TailorWorkflowAPI["resolve"] = (...args) => api().resolve(...args); + +/** Runtime wrapper namespace for `tailor.workflow`. */ +export const workflow = { + triggerWorkflow, + resumeWorkflow, + triggerJobFunction, + wait, + resolve, +} as const; + +export default workflow; From 243e62ff00c23be765eb6ee78555a80187712607 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 18:08:51 +0900 Subject: [PATCH 475/618] fix: handle runtime codemod shorthand properties --- .../scripts/transform.ts | 17 ++++++++++++++--- .../tests/shorthand-property/expected.ts | 3 +++ .../tests/shorthand-property/input.ts | 3 +++ 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/shorthand-property/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/shorthand-property/input.ts diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index 27613eb92..82b3f563a 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -259,11 +259,22 @@ function referenceEdits(root: SgNode, replacements: ImportReplacement[]): Edit[] } const edits: Edit[] = []; + const replacementFor = (name: string): string | null => { + const binding = byLocalName.get(name); + return binding ? `${binding.namespaceLocal}.${binding.memberName}` : null; + }; + for (const node of root.findAll({ rule: { kind: "identifier" } })) { if (isInsideImportStatement(node)) continue; - const binding = byLocalName.get(node.text()); - if (!binding) continue; - edits.push(node.replace(`${binding.namespaceLocal}.${binding.memberName}`)); + const replacement = replacementFor(node.text()); + if (!replacement) continue; + edits.push(node.replace(replacement)); + } + + for (const node of root.findAll({ rule: { kind: "shorthand_property_identifier" } })) { + const replacement = replacementFor(node.text()); + if (!replacement) continue; + edits.push(node.replace(`${node.text()}: ${replacement}`)); } return edits; } diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/shorthand-property/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/shorthand-property/expected.ts new file mode 100644 index 000000000..50af99ed6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/shorthand-property/expected.ts @@ -0,0 +1,3 @@ +import { aigateway } from "@tailor-platform/sdk/runtime/aigateway"; + +export const helpers = { get: aigateway.get }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/shorthand-property/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/shorthand-property/input.ts new file mode 100644 index 000000000..58d8d3db4 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/shorthand-property/input.ts @@ -0,0 +1,3 @@ +import { get } from "@tailor-platform/sdk/runtime/aigateway"; + +export const helpers = { get }; From cc469cff59daf32282e195c612e7d9ebf503d67d Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 18:30:41 +0900 Subject: [PATCH 476/618] fix: preserve runtime namespace compatibility --- .../generators/src/generated/files.ts | 2 +- .../scripts/transform.ts | 125 ++++++++++++++++-- .../tests/export-specifier/input.ts | 3 + .../tests/flat-type-reference/expected.ts | 4 + .../tests/flat-type-reference/input.ts | 4 + .../tests/namespace-import-type/expected.ts | 3 + .../tests/namespace-import-type/input.ts | 3 + .../tests/type-only-flat-import/expected.ts | 4 + .../tests/type-only-flat-import/input.ts | 4 + packages/sdk-codemod/src/runner.test.ts | 39 ++++++ packages/sdk/src/runtime/aigateway.ts | 10 ++ packages/sdk/src/runtime/authconnection.ts | 8 ++ packages/sdk/src/runtime/context.ts | 12 ++ packages/sdk/src/runtime/file.ts | 33 ++++- packages/sdk/src/runtime/iconv.ts | 14 ++ packages/sdk/src/runtime/idp.ts | 32 +++++ packages/sdk/src/runtime/index.test.ts | 44 ++++++ packages/sdk/src/runtime/secretmanager.ts | 8 ++ packages/sdk/src/runtime/workflow.ts | 14 ++ 19 files changed, 352 insertions(+), 14 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/export-specifier/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-type-reference/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-type-reference/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import-type/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import-type/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-flat-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-flat-import/input.ts create mode 100644 packages/sdk/src/runtime/index.test.ts diff --git a/packages/create-sdk/templates/generators/src/generated/files.ts b/packages/create-sdk/templates/generators/src/generated/files.ts index a18d7ada1..d27231a13 100644 --- a/packages/create-sdk/templates/generators/src/generated/files.ts +++ b/packages/create-sdk/templates/generators/src/generated/files.ts @@ -1,4 +1,4 @@ -import * as file from "@tailor-platform/sdk/runtime/file"; +import { file } from "@tailor-platform/sdk/runtime/file"; import type { FileUploadOptions, FileUploadResponse, diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index 82b3f563a..1b5611cbe 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -7,6 +7,7 @@ import { isTypeOnlyImport, localDeclarationNames, } from "../../../../src/ast-grep-helpers"; +import type { LlmReviewFinding } from "../../../../src/types"; import type { Edit, SgNode } from "@ast-grep/napi"; interface RuntimeModule { @@ -18,6 +19,7 @@ interface RuntimeModule { interface FlatImport { localName: string; memberName: string; + typeOnly: boolean; } interface ImportReplacement { @@ -133,11 +135,17 @@ function namespaceImportName(importStmt: SgNode): string | null { ); } -function formatImport(source: string, defaultName: string | null, namedSpecs: string[]): string { +function formatImport( + source: string, + defaultName: string | null, + namedSpecs: string[], + typeOnly = false, +): string { + const importKeyword = typeOnly ? "import type" : "import"; const named = namedSpecs.length > 0 ? `{ ${namedSpecs.join(", ")} }` : null; - if (defaultName && named) return `import ${defaultName}, ${named} from "${source}";`; - if (defaultName) return `import ${defaultName} from "${source}";`; - if (named) return `import ${named} from "${source}";`; + if (defaultName && named) return `${importKeyword} ${defaultName}, ${named} from "${source}";`; + if (defaultName) return `${importKeyword} ${defaultName} from "${source}";`; + if (named) return `${importKeyword} ${named} from "${source}";`; return ""; } @@ -150,6 +158,26 @@ function isInsideImportStatement(node: SgNode): boolean { return false; } +function isInsideExportSpecifier(node: SgNode): boolean { + let current = node.parent(); + while (current) { + if (current.kind() === "export_specifier") return true; + if (current.kind() === "export_statement") return false; + current = current.parent(); + } + return false; +} + +function hasExportSpecifierReference(root: SgNode, names: Set): boolean { + return root + .findAll({ rule: { kind: "export_specifier" } }) + .some((specifier) => + specifier + .children() + .some((child) => child.kind() === "identifier" && names.has(child.text())), + ); +} + function usedNames(root: SgNode, imports: SgNode[], removedNames: Set): Set { const names = localDeclarationNames(root); for (const importStmt of imports) { @@ -198,18 +226,26 @@ function buildImportReplacement( imports: SgNode[], ): ImportReplacement | null { const source = importSource(importStmt); - if (!source || isTypeOnlyImport(importStmt)) return null; + if (!source) return null; + const statementTypeOnly = isTypeOnlyImport(importStmt); const namespaceName = namespaceImportName(importStmt); if (namespaceName) { + const edit = statementTypeOnly + ? importStmt.replace( + formatImport(source, null, [selfNamespaceSpec(mod, namespaceName)], true), + ) + : importStmt.replace(formatImport(source, namespaceName, [])); return { - edit: importStmt.replace(formatImport(source, namespaceName, [])), + edit, flatImports: [], namespaceLocal: namespaceName, }; } const defaultName = defaultImportName(importStmt); + if (statementTypeOnly && defaultName) return null; + const existingSelfLocal = existingSelfNamespaceLocal(importStmt, mod); const flatImports: FlatImport[] = []; const keptSpecs: string[] = []; @@ -219,8 +255,12 @@ function buildImportReplacement( if (!names) continue; const memberName = mod.members[names.importedName]; - if (!names.typeOnly && memberName) { - flatImports.push({ localName: names.localName, memberName }); + if (memberName) { + flatImports.push({ + localName: names.localName, + memberName, + typeOnly: statementTypeOnly || names.typeOnly, + }); continue; } @@ -232,28 +272,42 @@ function buildImportReplacement( const removedNames = new Set(flatImports.map((binding) => binding.localName)); const declaredNames = localDeclarationNames(root); if (flatImports.some((binding) => declaredNames.has(binding.localName))) return null; + if (hasExportSpecifierReference(root, removedNames)) return null; const namespaceLocal = - defaultName ?? existingSelfLocal ?? uniqueNamespaceLocal(mod, root, imports, removedNames); + !statementTypeOnly && defaultName + ? defaultName + : (existingSelfLocal ?? uniqueNamespaceLocal(mod, root, imports, removedNames)); const nextNamedSpecs = - defaultName || existingSelfLocal + (!statementTypeOnly && defaultName) || existingSelfLocal ? keptSpecs : [selfNamespaceSpec(mod, namespaceLocal), ...keptSpecs]; return { - edit: importStmt.replace(formatImport(source, defaultName, nextNamedSpecs)), + edit: importStmt.replace( + formatImport( + source, + statementTypeOnly ? null : defaultName, + nextNamedSpecs, + statementTypeOnly, + ), + ), flatImports, namespaceLocal, }; } function referenceEdits(root: SgNode, replacements: ImportReplacement[]): Edit[] { - const byLocalName = new Map(); + const byLocalName = new Map< + string, + { namespaceLocal: string; memberName: string; typeOnly: boolean } + >(); for (const replacement of replacements) { for (const binding of replacement.flatImports) { byLocalName.set(binding.localName, { namespaceLocal: replacement.namespaceLocal, memberName: binding.memberName, + typeOnly: binding.typeOnly, }); } } @@ -266,12 +320,23 @@ function referenceEdits(root: SgNode, replacements: ImportReplacement[]): Edit[] for (const node of root.findAll({ rule: { kind: "identifier" } })) { if (isInsideImportStatement(node)) continue; + if (isInsideExportSpecifier(node)) continue; + if (byLocalName.get(node.text())?.typeOnly) continue; + const replacement = replacementFor(node.text()); + if (!replacement) continue; + edits.push(node.replace(replacement)); + } + + for (const node of root.findAll({ rule: { kind: "type_identifier" } })) { + if (isInsideImportStatement(node)) continue; + if (isInsideExportSpecifier(node)) continue; const replacement = replacementFor(node.text()); if (!replacement) continue; edits.push(node.replace(replacement)); } for (const node of root.findAll({ rule: { kind: "shorthand_property_identifier" } })) { + if (byLocalName.get(node.text())?.typeOnly) continue; const replacement = replacementFor(node.text()); if (!replacement) continue; edits.push(node.replace(`${node.text()}: ${replacement}`)); @@ -311,3 +376,39 @@ export default function transform(source: string, filePath: string): string | nu const result = root.commitEdits(edits); return result === source ? null : result; } + +export function reviewFindings( + source: string, + filePath: string, + relativePath: string, +): LlmReviewFinding[] { + if (!quickFilter(source)) return []; + + const root = parse(sourceLang(filePath, source), source).root(); + const findings: LlmReviewFinding[] = []; + + for (const importStmt of findImportStatements(root)) { + const sourceName = importSource(importStmt); + if (!sourceName) continue; + const mod = MODULES_BY_SOURCE.get(sourceName); + if (!mod) continue; + + const hasNamespaceImport = namespaceImportName(importStmt) != null; + const hasRemovedFlatImport = importStmt + .findAll({ rule: { kind: "import_specifier" } }) + .some((spec) => { + const names = importSpecNames(spec); + return names != null && mod.members[names.importedName] != null; + }); + if (!hasNamespaceImport && !hasRemovedFlatImport) continue; + + findings.push({ + file: relativePath, + line: importStmt.range().start.line + 1, + message: "Runtime subpath import still uses a removed namespace-star or flat value import.", + excerpt: importStmt.text().trim(), + }); + } + + return findings; +} diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/export-specifier/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/export-specifier/input.ts new file mode 100644 index 000000000..e9af0607b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/export-specifier/input.ts @@ -0,0 +1,3 @@ +import { get } from "@tailor-platform/sdk/runtime/aigateway"; + +export { get }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-type-reference/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-type-reference/expected.ts new file mode 100644 index 000000000..ead8c6d52 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-type-reference/expected.ts @@ -0,0 +1,4 @@ +import { idp } from "@tailor-platform/sdk/runtime/idp"; + +let client: idp.Client; +client = new idp.Client({ namespace: "default" }); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-type-reference/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-type-reference/input.ts new file mode 100644 index 000000000..5c5126a77 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-type-reference/input.ts @@ -0,0 +1,4 @@ +import { Client as IdpClient } from "@tailor-platform/sdk/runtime/idp"; + +let client: IdpClient; +client = new IdpClient({ namespace: "default" }); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import-type/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import-type/expected.ts new file mode 100644 index 000000000..dc7ee8e28 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import-type/expected.ts @@ -0,0 +1,3 @@ +import idp from "@tailor-platform/sdk/runtime/idp"; + +const config: idp.ClientConfig = { namespace: "default" }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import-type/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import-type/input.ts new file mode 100644 index 000000000..44b654299 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import-type/input.ts @@ -0,0 +1,3 @@ +import * as idp from "@tailor-platform/sdk/runtime/idp"; + +const config: idp.ClientConfig = { namespace: "default" }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-flat-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-flat-import/expected.ts new file mode 100644 index 000000000..7666e0cbd --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-flat-import/expected.ts @@ -0,0 +1,4 @@ +import type { idp, ClientConfig } from "@tailor-platform/sdk/runtime/idp"; + +const config: ClientConfig = { namespace: "default" }; +type ClientRef = idp.Client; diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-flat-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-flat-import/input.ts new file mode 100644 index 000000000..eb967a6b6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-flat-import/input.ts @@ -0,0 +1,4 @@ +import type { Client, ClientConfig } from "@tailor-platform/sdk/runtime/idp"; + +const config: ClientConfig = { namespace: "default" }; +type ClientRef = Client; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 7e426414f..a2dde1a9f 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1041,6 +1041,45 @@ describe("runCodemods", () => { ]); }); + test("flags runtime subpath imports left after conservative skips", async () => { + const codemod = allCodemods.find((entry) => entry.id === "v2/runtime-subpath-namespace"); + if (!codemod?.scriptPath) throw new Error("runtime subpath codemod missing script"); + const scriptPath = path.resolve( + __dirname, + "../codemods", + codemod.scriptPath.replace(/\.js$/, ".ts"), + ); + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "runner-runtime-test-")); + tmpDir = dir; + await fs.promises.writeFile( + path.join(dir, "exports.ts"), + [ + 'import { get } from "@tailor-platform/sdk/runtime/aigateway";', + "", + "export { get };", + "", + ].join("\n"), + ); + + const result = await runCodemods([{ codemod, scriptPath }], dir, true); + + expect(result.changed).toBe(false); + expect(result.llmReviews).toEqual([ + { + codemodId: "v2/runtime-subpath-namespace", + prompt: codemod.prompt, + files: ["exports.ts"], + findings: [ + expect.objectContaining({ + file: "exports.ts", + line: 1, + excerpt: 'import { get } from "@tailor-platform/sdk/runtime/aigateway";', + }), + ], + }, + ]); + }); + test("flags unresolved auth connection token helper usages for LLM review", async () => { const codemod = allCodemods.find((entry) => entry.id === "v2/auth-connection-token-helper"); if (!codemod?.scriptPath) throw new Error("auth connection token codemod missing script"); diff --git a/packages/sdk/src/runtime/aigateway.ts b/packages/sdk/src/runtime/aigateway.ts index f15f3a09f..134e81ea5 100644 --- a/packages/sdk/src/runtime/aigateway.ts +++ b/packages/sdk/src/runtime/aigateway.ts @@ -46,4 +46,14 @@ const get: TailorAigatewayAPI["get"] = (...args) => api().get(...args); /** Runtime wrapper namespace for `tailor.aigateway`. */ export const aigateway = { get } as const satisfies TailorAigatewayAPI; +type AigatewayGetAIGatewayResult = GetAIGatewayResult; +type AigatewayTailorAigatewayAPI = TailorAigatewayAPI; + +// Type-only namespace merge preserves namespace type access without restoring flat value exports. +// oxlint-disable-next-line typescript/no-namespace +export namespace aigateway { + export type GetAIGatewayResult = AigatewayGetAIGatewayResult; + export type TailorAigatewayAPI = AigatewayTailorAigatewayAPI; +} + export default aigateway; diff --git a/packages/sdk/src/runtime/authconnection.ts b/packages/sdk/src/runtime/authconnection.ts index 15269b116..8c7e53b08 100644 --- a/packages/sdk/src/runtime/authconnection.ts +++ b/packages/sdk/src/runtime/authconnection.ts @@ -41,4 +41,12 @@ const getConnectionToken: TailorAuthconnectionAPI["getConnectionToken"] = (...ar /** Runtime wrapper namespace for `tailor.authconnection`. */ export const authconnection = { getConnectionToken } as const satisfies TailorAuthconnectionAPI; +type AuthconnectionTailorAuthconnectionAPI = TailorAuthconnectionAPI; + +// Type-only namespace merge preserves namespace type access without restoring flat value exports. +// oxlint-disable-next-line typescript/no-namespace +export namespace authconnection { + export type TailorAuthconnectionAPI = AuthconnectionTailorAuthconnectionAPI; +} + export default authconnection; diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index 2b0f09d41..e1f3e1d00 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -69,4 +69,16 @@ function getInvoker(): Invoker | null { /** Runtime wrapper namespace for `tailor.context`. */ export const context = { getInvoker } as const; +type ContextInvokerResult = Invoker; +type RuntimeContextInvoker = ContextInvoker; +type ContextTailorContextAPI = TailorContextAPI; + +// Type-only namespace merge preserves namespace type access without restoring flat value exports. +// oxlint-disable-next-line typescript/no-namespace +export namespace context { + export type Invoker = ContextInvokerResult; + export type ContextInvoker = RuntimeContextInvoker; + export type TailorContextAPI = ContextTailorContextAPI; +} + export default context; diff --git a/packages/sdk/src/runtime/file.ts b/packages/sdk/src/runtime/file.ts index a7f133e72..80c766147 100644 --- a/packages/sdk/src/runtime/file.ts +++ b/packages/sdk/src/runtime/file.ts @@ -276,9 +276,40 @@ export const file = { download, downloadAsBase64, delete: deleteFile, + deleteFile, getMetadata, downloadStream, uploadStream, -} as const satisfies TailorDBFileAPI; +} as const satisfies TailorDBFileAPI & { deleteFile: TailorDBFileAPI["delete"] }; + +type FileUploadMetadata = UploadMetadata; +type FileDownloadMetadata = DownloadMetadata; +type RuntimeFileMetadata = FileMetadata; +type RuntimeFileUploadOptions = FileUploadOptions; +type RuntimeFileUploadStreamOptions = FileUploadStreamOptions; +type RuntimeFileUploadResponse = FileUploadResponse; +type RuntimeFileDownloadResponse = FileDownloadResponse; +type RuntimeFileDownloadAsBase64Response = FileDownloadAsBase64Response; +type RuntimeFileDownloadStreamResponse = FileDownloadStreamResponse; +type RuntimeTailorDBFileErrorCode = TailorDBFileErrorCode; +type RuntimeTailorDBFileError = TailorDBFileError; +type RuntimeTailorDBFileAPI = TailorDBFileAPI; + +// Type-only namespace merge preserves namespace type access without restoring flat value exports. +// oxlint-disable-next-line typescript/no-namespace +export namespace file { + export type UploadMetadata = FileUploadMetadata; + export type DownloadMetadata = FileDownloadMetadata; + export type FileMetadata = RuntimeFileMetadata; + export type FileUploadOptions = RuntimeFileUploadOptions; + export type FileUploadStreamOptions = RuntimeFileUploadStreamOptions; + export type FileUploadResponse = RuntimeFileUploadResponse; + export type FileDownloadResponse = RuntimeFileDownloadResponse; + export type FileDownloadAsBase64Response = RuntimeFileDownloadAsBase64Response; + export type FileDownloadStreamResponse = RuntimeFileDownloadStreamResponse; + export type TailorDBFileErrorCode = RuntimeTailorDBFileErrorCode; + export type TailorDBFileError = RuntimeTailorDBFileError; + export type TailorDBFileAPI = RuntimeTailorDBFileAPI; +} export default file; diff --git a/packages/sdk/src/runtime/iconv.ts b/packages/sdk/src/runtime/iconv.ts index dc6c94ba6..6a0f9f0ca 100644 --- a/packages/sdk/src/runtime/iconv.ts +++ b/packages/sdk/src/runtime/iconv.ts @@ -132,4 +132,18 @@ export const iconv = { Iconv, } as const satisfies TailorIconvAPI; +type RuntimeIconvInstance = IconvInstance; +type RuntimeIconvConstructor = IconvConstructor; +type RuntimeTailorIconvAPI = TailorIconvAPI; +type RuntimeIconv = InstanceType; + +// Type-only namespace merge preserves namespace type access without restoring flat value exports. +// oxlint-disable-next-line typescript/no-namespace +export namespace iconv { + export type IconvInstance = RuntimeIconvInstance; + export type IconvConstructor = RuntimeIconvConstructor; + export type TailorIconvAPI = RuntimeTailorIconvAPI; + export type Iconv = RuntimeIconv; +} + export default iconv; diff --git a/packages/sdk/src/runtime/idp.ts b/packages/sdk/src/runtime/idp.ts index 5407f0be1..99ad444e5 100644 --- a/packages/sdk/src/runtime/idp.ts +++ b/packages/sdk/src/runtime/idp.ts @@ -223,4 +223,36 @@ class Client { /** Runtime wrapper namespace for `tailor.idp`. */ export const idp = { Client } as const satisfies TailorIdpAPI; +type RuntimeClientConfig = ClientConfig; +type RuntimeUser = User; +type RuntimeUserQuery = UserQuery; +type RuntimeListUsersOptions = ListUsersOptions; +type RuntimeListUsersResponse = ListUsersResponse; +type RuntimeCreateUserInput = CreateUserInput; +type RuntimeUpdateUserInput = UpdateUserInput; +type RuntimeSendPasswordResetEmailInput = SendPasswordResetEmailInput; +type RuntimeUnenrollMfaInput = UnenrollMfaInput; +type RuntimeIdpClientInstance = IdpClientInstance; +type RuntimeIdpClientConstructor = IdpClientConstructor; +type RuntimeTailorIdpAPI = TailorIdpAPI; +type RuntimeClient = InstanceType; + +// Type-only namespace merge preserves namespace type access without restoring flat value exports. +// oxlint-disable-next-line typescript/no-namespace +export namespace idp { + export type ClientConfig = RuntimeClientConfig; + export type User = RuntimeUser; + export type UserQuery = RuntimeUserQuery; + export type ListUsersOptions = RuntimeListUsersOptions; + export type ListUsersResponse = RuntimeListUsersResponse; + export type CreateUserInput = RuntimeCreateUserInput; + export type UpdateUserInput = RuntimeUpdateUserInput; + export type SendPasswordResetEmailInput = RuntimeSendPasswordResetEmailInput; + export type UnenrollMfaInput = RuntimeUnenrollMfaInput; + export type IdpClientInstance = RuntimeIdpClientInstance; + export type IdpClientConstructor = RuntimeIdpClientConstructor; + export type TailorIdpAPI = RuntimeTailorIdpAPI; + export type Client = RuntimeClient; +} + export default idp; diff --git a/packages/sdk/src/runtime/index.test.ts b/packages/sdk/src/runtime/index.test.ts new file mode 100644 index 000000000..5113e8be7 --- /dev/null +++ b/packages/sdk/src/runtime/index.test.ts @@ -0,0 +1,44 @@ +/** + * Tests for the aggregate `@tailor-platform/sdk/runtime` entry point. + */ +import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; +import { file, type iconv, type idp } from "#/runtime/index"; +import { cleanupMocks, injectMocks, mockFile } from "#/vitest/mock"; +import type { TailorDBFileErrorCode } from "#/runtime/file"; +import type { IconvInstance } from "#/runtime/iconv"; +import type { ClientConfig } from "#/runtime/idp"; + +const fileArgs = ["ns", "Doc", "blob", "rec-1"] as const; + +describe("@tailor-platform/sdk/runtime aggregate exports", () => { + beforeEach(() => { + injectMocks(globalThis); + }); + + afterEach(() => { + cleanupMocks(globalThis); + }); + + test("preserves namespace type access for aggregate imports", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); + + test("keeps the file.deleteFile alias on the aggregate file namespace", async () => { + using fileM = mockFile(); + + await file.deleteFile(...fileArgs); + + expect(file.deleteFile).toBe(file.delete); + expect(fileM.calls).toEqual([ + { + method: "delete", + namespace: "ns", + typeName: "Doc", + fieldName: "blob", + recordId: "rec-1", + }, + ]); + }); +}); diff --git a/packages/sdk/src/runtime/secretmanager.ts b/packages/sdk/src/runtime/secretmanager.ts index c36834f24..2eff863cf 100644 --- a/packages/sdk/src/runtime/secretmanager.ts +++ b/packages/sdk/src/runtime/secretmanager.ts @@ -50,4 +50,12 @@ export const secretmanager = { getSecret, } as const satisfies TailorSecretmanagerAPI; +type SecretmanagerTailorSecretmanagerAPI = TailorSecretmanagerAPI; + +// Type-only namespace merge preserves namespace type access without restoring flat value exports. +// oxlint-disable-next-line typescript/no-namespace +export namespace secretmanager { + export type TailorSecretmanagerAPI = SecretmanagerTailorSecretmanagerAPI; +} + export default secretmanager; diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index 554fa2025..14519f9a0 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -117,4 +117,18 @@ export const workflow = { resolve, } as const; +type WorkflowInvoker = Invoker; +type WorkflowTriggerWorkflowOptions = TriggerWorkflowOptions; +type WorkflowPlatformTriggerWorkflowOptions = PlatformTriggerWorkflowOptions; +type WorkflowTailorWorkflowAPI = TailorWorkflowAPI; + +// Type-only namespace merge preserves namespace type access without restoring flat value exports. +// oxlint-disable-next-line typescript/no-namespace +export namespace workflow { + export type Invoker = WorkflowInvoker; + export type TriggerWorkflowOptions = WorkflowTriggerWorkflowOptions; + export type PlatformTriggerWorkflowOptions = WorkflowPlatformTriggerWorkflowOptions; + export type TailorWorkflowAPI = WorkflowTailorWorkflowAPI; +} + export default workflow; From 9e2965651c26bb04d28741d24dc6b1929d5a036f Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 18:47:32 +0900 Subject: [PATCH 477/618] fix: cover runtime codemod review gaps --- .changeset/runtime-subpath-namespace.md | 1 + .../scripts/transform.ts | 49 ++++++++++++++++--- .../tests/type-only-typeof/expected.ts | 3 ++ .../tests/type-only-typeof/input.ts | 3 ++ packages/sdk-codemod/src/runner.test.ts | 11 ++++- 5 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-typeof/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-typeof/input.ts diff --git a/.changeset/runtime-subpath-namespace.md b/.changeset/runtime-subpath-namespace.md index b3d80ee6a..55613eeeb 100644 --- a/.changeset/runtime-subpath-namespace.md +++ b/.changeset/runtime-subpath-namespace.md @@ -1,6 +1,7 @@ --- "@tailor-platform/sdk": major "@tailor-platform/sdk-codemod": patch +"@tailor-platform/create-sdk": patch --- Remove flat value exports from `@tailor-platform/sdk/runtime/*` subpath modules. Import each subpath through its default export or self-named namespace export instead, for example `import iconv from "@tailor-platform/sdk/runtime/iconv"` or `import { iconv } from "@tailor-platform/sdk/runtime/iconv"`. diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index 1b5611cbe..41a686846 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -168,6 +168,16 @@ function isInsideExportSpecifier(node: SgNode): boolean { return false; } +function isInsideTypeQuery(node: SgNode): boolean { + let current = node.parent(); + while (current) { + if (current.kind() === "type_query") return true; + if (current.kind() === "statement_block" || current.kind() === "program") return false; + current = current.parent(); + } + return false; +} + function hasExportSpecifierReference(root: SgNode, names: Set): boolean { return root .findAll({ rule: { kind: "export_specifier" } }) @@ -178,6 +188,13 @@ function hasExportSpecifierReference(root: SgNode, names: Set): boolean ); } +function findExportStatements(root: SgNode): SgNode[] { + return root + .findAll({ rule: { kind: "export_statement" } }) + .filter((stmt) => stmt.parent()?.kind() === "program") + .toSorted((a, b) => a.range().start.index - b.range().start.index); +} + function usedNames(root: SgNode, imports: SgNode[], removedNames: Set): Set { const names = localDeclarationNames(root); for (const importStmt of imports) { @@ -321,7 +338,7 @@ function referenceEdits(root: SgNode, replacements: ImportReplacement[]): Edit[] for (const node of root.findAll({ rule: { kind: "identifier" } })) { if (isInsideImportStatement(node)) continue; if (isInsideExportSpecifier(node)) continue; - if (byLocalName.get(node.text())?.typeOnly) continue; + if (byLocalName.get(node.text())?.typeOnly && !isInsideTypeQuery(node)) continue; const replacement = replacementFor(node.text()); if (!replacement) continue; edits.push(node.replace(replacement)); @@ -377,6 +394,15 @@ export default function transform(source: string, filePath: string): string | nu return result === source ? null : result; } +function hasRemovedFlatSpecifier(node: SgNode, mod: RuntimeModule): boolean { + return node + .findAll({ rule: { any: [{ kind: "import_specifier" }, { kind: "export_specifier" }] } }) + .some((spec) => { + const names = importSpecNames(spec); + return names != null && mod.members[names.importedName] != null; + }); +} + export function reviewFindings( source: string, filePath: string, @@ -394,12 +420,7 @@ export function reviewFindings( if (!mod) continue; const hasNamespaceImport = namespaceImportName(importStmt) != null; - const hasRemovedFlatImport = importStmt - .findAll({ rule: { kind: "import_specifier" } }) - .some((spec) => { - const names = importSpecNames(spec); - return names != null && mod.members[names.importedName] != null; - }); + const hasRemovedFlatImport = hasRemovedFlatSpecifier(importStmt, mod); if (!hasNamespaceImport && !hasRemovedFlatImport) continue; findings.push({ @@ -410,5 +431,19 @@ export function reviewFindings( }); } + for (const exportStmt of findExportStatements(root)) { + const sourceName = importSource(exportStmt); + if (!sourceName) continue; + const mod = MODULES_BY_SOURCE.get(sourceName); + if (!mod || !hasRemovedFlatSpecifier(exportStmt, mod)) continue; + + findings.push({ + file: relativePath, + line: exportStmt.range().start.line + 1, + message: "Runtime subpath re-export still uses a removed flat value export.", + excerpt: exportStmt.text().trim(), + }); + } + return findings; } diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-typeof/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-typeof/expected.ts new file mode 100644 index 000000000..2509f9174 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-typeof/expected.ts @@ -0,0 +1,3 @@ +import type { idp } from "@tailor-platform/sdk/runtime/idp"; + +type ClientConstructor = typeof idp.Client; diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-typeof/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-typeof/input.ts new file mode 100644 index 000000000..ba9d6c173 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-typeof/input.ts @@ -0,0 +1,3 @@ +import type { Client } from "@tailor-platform/sdk/runtime/idp"; + +type ClientConstructor = typeof Client; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index a2dde1a9f..5ad669172 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1060,6 +1060,10 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "reexport.ts"), + ['export { get } from "@tailor-platform/sdk/runtime/aigateway";', ""].join("\n"), + ); const result = await runCodemods([{ codemod, scriptPath }], dir, true); @@ -1068,13 +1072,18 @@ describe("runCodemods", () => { { codemodId: "v2/runtime-subpath-namespace", prompt: codemod.prompt, - files: ["exports.ts"], + files: ["exports.ts", "reexport.ts"], findings: [ expect.objectContaining({ file: "exports.ts", line: 1, excerpt: 'import { get } from "@tailor-platform/sdk/runtime/aigateway";', }), + expect.objectContaining({ + file: "reexport.ts", + line: 1, + excerpt: 'export { get } from "@tailor-platform/sdk/runtime/aigateway";', + }), ], }, ]); From fadde55d715236fc81d679de2c69f2570c9622d3 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 19:01:13 +0900 Subject: [PATCH 478/618] test: cover runtime codemod delete import --- .../tests/delete-keyword-import/expected.ts | 3 +++ .../tests/delete-keyword-import/input.ts | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-keyword-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-keyword-import/input.ts diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-keyword-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-keyword-import/expected.ts new file mode 100644 index 000000000..707446513 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-keyword-import/expected.ts @@ -0,0 +1,3 @@ +import { file } from "@tailor-platform/sdk/runtime/file"; + +await file.delete("ns", "Doc", "blob", "rec-1"); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-keyword-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-keyword-import/input.ts new file mode 100644 index 000000000..32580750c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/delete-keyword-import/input.ts @@ -0,0 +1,3 @@ +import { delete as del } from "@tailor-platform/sdk/runtime/file"; + +await del("ns", "Doc", "blob", "rec-1"); From 91384e0bf4d60f9bc5805ee89a6e242c4c9cb240 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 19:14:10 +0900 Subject: [PATCH 479/618] fix: preserve runtime codemod type-only imports --- .../scripts/transform.ts | 44 ++++++++++++++----- .../tests/inline-type-flat-import/expected.ts | 3 ++ .../tests/inline-type-flat-import/input.ts | 3 ++ packages/sdk-codemod/src/runner.test.ts | 11 ++++- 4 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/inline-type-flat-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/inline-type-flat-import/input.ts diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index 41a686846..6775d6cc2 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -28,6 +28,11 @@ interface ImportReplacement { namespaceLocal: string; } +interface SelfNamespaceImport { + localName: string; + typeOnly: boolean; +} + const RUNTIME_MODULES: RuntimeModule[] = [ { namespace: "iconv", @@ -227,11 +232,19 @@ function selfNamespaceSpec(mod: RuntimeModule, localName: string): string { return localName === mod.namespace ? mod.namespace : `${mod.namespace} as ${localName}`; } -function existingSelfNamespaceLocal(importStmt: SgNode, mod: RuntimeModule): string | null { +function typeOnlySelfNamespaceSpec(mod: RuntimeModule, localName: string): string { + return `type ${selfNamespaceSpec(mod, localName)}`; +} + +function existingSelfNamespaceImport( + importStmt: SgNode, + mod: RuntimeModule, + statementTypeOnly: boolean, +): SelfNamespaceImport | null { for (const spec of importStmt.findAll({ rule: { kind: "import_specifier" } })) { const names = importSpecNames(spec); - if (!names || names.typeOnly || names.importedName !== mod.namespace) continue; - return names.localName; + if (!names || names.importedName !== mod.namespace) continue; + return { localName: names.localName, typeOnly: statementTypeOnly || names.typeOnly }; } return null; } @@ -263,7 +276,7 @@ function buildImportReplacement( const defaultName = defaultImportName(importStmt); if (statementTypeOnly && defaultName) return null; - const existingSelfLocal = existingSelfNamespaceLocal(importStmt, mod); + const existingSelf = existingSelfNamespaceImport(importStmt, mod, statementTypeOnly); const flatImports: FlatImport[] = []; const keptSpecs: string[] = []; @@ -291,14 +304,21 @@ function buildImportReplacement( if (flatImports.some((binding) => declaredNames.has(binding.localName))) return null; if (hasExportSpecifierReference(root, removedNames)) return null; + const flatImportsAreTypeOnly = flatImports.every((binding) => binding.typeOnly); + const canUseExistingSelf = + existingSelf != null && (!existingSelf.typeOnly || flatImportsAreTypeOnly); const namespaceLocal = !statementTypeOnly && defaultName ? defaultName - : (existingSelfLocal ?? uniqueNamespaceLocal(mod, root, imports, removedNames)); - const nextNamedSpecs = - (!statementTypeOnly && defaultName) || existingSelfLocal - ? keptSpecs - : [selfNamespaceSpec(mod, namespaceLocal), ...keptSpecs]; + : canUseExistingSelf + ? existingSelf.localName + : uniqueNamespaceLocal(mod, root, imports, removedNames); + const needsNamespaceSpecifier = !((!statementTypeOnly && defaultName) || canUseExistingSelf); + const namespaceSpecifier = + flatImportsAreTypeOnly && !statementTypeOnly + ? typeOnlySelfNamespaceSpec(mod, namespaceLocal) + : selfNamespaceSpec(mod, namespaceLocal); + const nextNamedSpecs = needsNamespaceSpecifier ? [namespaceSpecifier, ...keptSpecs] : keptSpecs; return { edit: importStmt.replace( @@ -403,6 +423,10 @@ function hasRemovedFlatSpecifier(node: SgNode, mod: RuntimeModule): boolean { }); } +function isExportStar(node: SgNode): boolean { + return node.children().some((child) => child.kind() === "*"); +} + export function reviewFindings( source: string, filePath: string, @@ -435,7 +459,7 @@ export function reviewFindings( const sourceName = importSource(exportStmt); if (!sourceName) continue; const mod = MODULES_BY_SOURCE.get(sourceName); - if (!mod || !hasRemovedFlatSpecifier(exportStmt, mod)) continue; + if (!mod || (!hasRemovedFlatSpecifier(exportStmt, mod) && !isExportStar(exportStmt))) continue; findings.push({ file: relativePath, diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/inline-type-flat-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/inline-type-flat-import/expected.ts new file mode 100644 index 000000000..b81b2be17 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/inline-type-flat-import/expected.ts @@ -0,0 +1,3 @@ +import { type idp } from "@tailor-platform/sdk/runtime/idp"; + +type ClientRef = idp.Client; diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/inline-type-flat-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/inline-type-flat-import/input.ts new file mode 100644 index 000000000..9fd6d1a87 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/inline-type-flat-import/input.ts @@ -0,0 +1,3 @@ +import { type Client } from "@tailor-platform/sdk/runtime/idp"; + +type ClientRef = Client; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 5ad669172..9733e03e7 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1064,6 +1064,10 @@ describe("runCodemods", () => { path.join(dir, "reexport.ts"), ['export { get } from "@tailor-platform/sdk/runtime/aigateway";', ""].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "reexport-all.ts"), + ['export * from "@tailor-platform/sdk/runtime/aigateway";', ""].join("\n"), + ); const result = await runCodemods([{ codemod, scriptPath }], dir, true); @@ -1072,13 +1076,18 @@ describe("runCodemods", () => { { codemodId: "v2/runtime-subpath-namespace", prompt: codemod.prompt, - files: ["exports.ts", "reexport.ts"], + files: ["exports.ts", "reexport-all.ts", "reexport.ts"], findings: [ expect.objectContaining({ file: "exports.ts", line: 1, excerpt: 'import { get } from "@tailor-platform/sdk/runtime/aigateway";', }), + expect.objectContaining({ + file: "reexport-all.ts", + line: 1, + excerpt: 'export * from "@tailor-platform/sdk/runtime/aigateway";', + }), expect.objectContaining({ file: "reexport.ts", line: 1, From 74d92ac0b4f40c37ca2b5cc3e19d35a4d0061dc3 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 19:46:11 +0900 Subject: [PATCH 480/618] fix: flag dynamic runtime subpath imports --- .../scripts/transform.ts | 53 +++++++++++++++++++ packages/sdk-codemod/src/runner.test.ts | 20 ++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index 6775d6cc2..0d3c0e7d5 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -427,6 +427,58 @@ function isExportStar(node: SgNode): boolean { return node.children().some((child) => child.kind() === "*"); } +function dynamicImportCallFor(sourceNode: SgNode): SgNode | null { + let current = sourceNode.parent(); + while (current) { + if (current.kind() === "import_statement" || current.kind() === "export_statement") { + return null; + } + if ( + current.kind() === "call_expression" && + current.children().some((child) => child.kind() === "import") + ) { + return current; + } + current = current.parent(); + } + return null; +} + +function dynamicImportExcerptNode(callExpression: SgNode): SgNode { + let current = callExpression; + while (current.parent()) { + const parent = current.parent(); + if (!parent) break; + if ( + parent.kind() !== "await_expression" && + parent.kind() !== "parenthesized_expression" && + parent.kind() !== "member_expression" + ) { + break; + } + current = parent; + } + return current; +} + +function dynamicRuntimeImportFindings(root: SgNode, relativePath: string): LlmReviewFinding[] { + const findings: LlmReviewFinding[] = []; + for (const sourceNode of root.findAll({ rule: { kind: "string" } })) { + const sourceName = importSource(sourceNode); + if (!sourceName || !MODULES_BY_SOURCE.has(sourceName)) continue; + const callExpression = dynamicImportCallFor(sourceNode); + if (!callExpression) continue; + const excerptNode = dynamicImportExcerptNode(callExpression); + findings.push({ + file: relativePath, + line: excerptNode.range().start.line + 1, + message: "Dynamic runtime subpath import may still access a removed flat value export.", + excerpt: excerptNode.text().trim(), + }); + } + return findings; +} + export function reviewFindings( source: string, filePath: string, @@ -436,6 +488,7 @@ export function reviewFindings( const root = parse(sourceLang(filePath, source), source).root(); const findings: LlmReviewFinding[] = []; + findings.push(...dynamicRuntimeImportFindings(root, relativePath)); for (const importStmt of findImportStatements(root)) { const sourceName = importSource(importStmt); diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 9733e03e7..c66a9bd86 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1068,6 +1068,14 @@ describe("runCodemods", () => { path.join(dir, "reexport-all.ts"), ['export * from "@tailor-platform/sdk/runtime/aigateway";', ""].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "dynamic.ts"), + [ + 'type ClientRef = import("@tailor-platform/sdk/runtime/idp").Client;', + 'const getGateway = (await import("@tailor-platform/sdk/runtime/aigateway")).get;', + "", + ].join("\n"), + ); const result = await runCodemods([{ codemod, scriptPath }], dir, true); @@ -1076,8 +1084,18 @@ describe("runCodemods", () => { { codemodId: "v2/runtime-subpath-namespace", prompt: codemod.prompt, - files: ["exports.ts", "reexport-all.ts", "reexport.ts"], + files: ["dynamic.ts", "exports.ts", "reexport-all.ts", "reexport.ts"], findings: [ + expect.objectContaining({ + file: "dynamic.ts", + line: 1, + excerpt: 'import("@tailor-platform/sdk/runtime/idp").Client', + }), + expect.objectContaining({ + file: "dynamic.ts", + line: 2, + excerpt: '(await import("@tailor-platform/sdk/runtime/aigateway")).get', + }), expect.objectContaining({ file: "exports.ts", line: 1, From 3d6948b9f6669d0c300aad73e1d7a020580c1485 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 19:58:53 +0900 Subject: [PATCH 481/618] fix: hide runtime namespace constructor internals --- packages/sdk/src/runtime/iconv.ts | 6 +-- packages/sdk/src/runtime/idp.ts | 6 ++- packages/sdk/src/runtime/index.test.ts | 75 +++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/packages/sdk/src/runtime/iconv.ts b/packages/sdk/src/runtime/iconv.ts index 6a0f9f0ca..ca023d60e 100644 --- a/packages/sdk/src/runtime/iconv.ts +++ b/packages/sdk/src/runtime/iconv.ts @@ -89,7 +89,7 @@ export interface TailorIconvAPI { } const api = (): TailorIconvAPI => - (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv; + (globalThis as unknown as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv; const convert: TailorIconvAPI["convert"] = (...args) => api().convert(...args); @@ -123,14 +123,14 @@ class Iconv { } /** Runtime wrapper namespace for `tailor.iconv`. */ -export const iconv = { +export const iconv: TailorIconvAPI = { convert, convertBuffer, decode, encode, encodings, Iconv, -} as const satisfies TailorIconvAPI; +}; type RuntimeIconvInstance = IconvInstance; type RuntimeIconvConstructor = IconvConstructor; diff --git a/packages/sdk/src/runtime/idp.ts b/packages/sdk/src/runtime/idp.ts index 99ad444e5..760d72e12 100644 --- a/packages/sdk/src/runtime/idp.ts +++ b/packages/sdk/src/runtime/idp.ts @@ -144,7 +144,9 @@ class Client { #impl: IdpClientInstance; constructor(config: ClientConfig) { - this.#impl = new (globalThis as { tailor: { idp: TailorIdpAPI } }).tailor.idp.Client(config); + this.#impl = new (globalThis as unknown as { tailor: { idp: TailorIdpAPI } }).tailor.idp.Client( + config, + ); } /** @@ -221,7 +223,7 @@ class Client { } /** Runtime wrapper namespace for `tailor.idp`. */ -export const idp = { Client } as const satisfies TailorIdpAPI; +export const idp: TailorIdpAPI = { Client }; type RuntimeClientConfig = ClientConfig; type RuntimeUser = User; diff --git a/packages/sdk/src/runtime/index.test.ts b/packages/sdk/src/runtime/index.test.ts index 5113e8be7..3079eb35b 100644 --- a/packages/sdk/src/runtime/index.test.ts +++ b/packages/sdk/src/runtime/index.test.ts @@ -1,14 +1,68 @@ /** * Tests for the aggregate `@tailor-platform/sdk/runtime` entry point. */ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import * as path from "node:path"; +import ts from "typescript"; import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; -import { file, type iconv, type idp } from "#/runtime/index"; +import { + file, + type iconv as runtimeIconv, + type idp as runtimeIdp, + type iconv, + type idp, +} from "#/runtime/index"; import { cleanupMocks, injectMocks, mockFile } from "#/vitest/mock"; import type { TailorDBFileErrorCode } from "#/runtime/file"; import type { IconvInstance } from "#/runtime/iconv"; -import type { ClientConfig } from "#/runtime/idp"; +import type { ClientConfig, IdpClientInstance } from "#/runtime/idp"; const fileArgs = ["ns", "Doc", "blob", "rec-1"] as const; +const packageRoot = path.resolve(import.meta.dirname, "../.."); + +function declarationEmitDiagnostics(source: string): string { + const tmpDir = mkdtempSync(path.join(packageRoot, ".tmp-runtime-declaration-")); + try { + const tsconfigPath = path.join(tmpDir, "tsconfig.json"); + writeFileSync(path.join(tmpDir, "index.ts"), source, "utf8"); + writeFileSync( + tsconfigPath, + JSON.stringify({ + extends: "../tsconfig.json", + compilerOptions: { + declaration: true, + emitDeclarationOnly: true, + noEmit: false, + incremental: false, + outDir: "dist", + }, + include: ["index.ts"], + }), + "utf8", + ); + + const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + if (configFile.error) { + return ts.formatDiagnosticsWithColorAndContext([configFile.error], diagnosticHost(tmpDir)); + } + + const config = ts.parseJsonConfigFileContent(configFile.config, ts.sys, tmpDir); + const program = ts.createProgram(config.fileNames, config.options); + const emit = program.emit(); + const diagnostics = [...ts.getPreEmitDiagnostics(program), ...emit.diagnostics]; + return ts.formatDiagnosticsWithColorAndContext(diagnostics, diagnosticHost(tmpDir)); + } finally { + rmSync(tmpDir, { force: true, recursive: true }); + } +} + +function diagnosticHost(cwd: string): ts.FormatDiagnosticsHost { + return { + getCanonicalFileName: (fileName) => fileName, + getCurrentDirectory: () => cwd, + getNewLine: () => "\n", + }; +} describe("@tailor-platform/sdk/runtime aggregate exports", () => { beforeEach(() => { @@ -25,6 +79,23 @@ describe("@tailor-platform/sdk/runtime aggregate exports", () => { expectTypeOf().toEqualTypeOf(); }); + test("exposes named instance types for namespace constructors", () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + + test("emits declarations for exported namespace constructor instances", () => { + const diagnostics = declarationEmitDiagnostics(` + import { idp } from "#/runtime/idp"; + import iconv from "#/runtime/iconv"; + + export const makeClient = () => new idp.Client({ namespace: "default" }); + export const makeConverter = () => new iconv.Iconv("UTF-8", "Shift_JIS"); + `); + + expect(diagnostics).toBe(""); + }); + test("keeps the file.deleteFile alias on the aggregate file namespace", async () => { using fileM = mockFile(); From 2f145d5f69515cf55727c2b9de2b08fe853bdab3 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 20:15:21 +0900 Subject: [PATCH 482/618] fix: avoid duplicate runtime codemod imports --- .../scripts/transform.ts | 39 +++++++++++++++++-- .../tests/repeated-flat-imports/expected.ts | 4 ++ .../tests/repeated-flat-imports/input.ts | 5 +++ 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/repeated-flat-imports/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/repeated-flat-imports/input.ts diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index 0d3c0e7d5..6679d91a0 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -154,6 +154,19 @@ function formatImport( return ""; } +function replaceImportStatement(importStmt: SgNode, nextText: string, sourceText: string): Edit { + if (nextText !== "") return importStmt.replace(nextText); + + const range = importStmt.range(); + let endPos = range.end.index; + if (sourceText[endPos] === "\r" && sourceText[endPos + 1] === "\n") { + endPos += 2; + } else if (sourceText[endPos] === "\n") { + endPos += 1; + } + return { startPos: range.start.index, endPos, insertedText: "" }; +} + function isInsideImportStatement(node: SgNode): boolean { let current = node.parent(); while (current) { @@ -254,6 +267,8 @@ function buildImportReplacement( mod: RuntimeModule, root: SgNode, imports: SgNode[], + sourceText: string, + emittedNamespaceSpecifiers: Set, ): ImportReplacement | null { const source = importSource(importStmt); if (!source) return null; @@ -313,7 +328,15 @@ function buildImportReplacement( : canUseExistingSelf ? existingSelf.localName : uniqueNamespaceLocal(mod, root, imports, removedNames); - const needsNamespaceSpecifier = !((!statementTypeOnly && defaultName) || canUseExistingSelf); + const namespaceSpecifierKey = [ + source, + namespaceLocal, + flatImportsAreTypeOnly ? "type" : "value", + ].join("\0"); + const namespaceAlreadyEmitted = emittedNamespaceSpecifiers.has(namespaceSpecifierKey); + const needsNamespaceSpecifier = + !((!statementTypeOnly && defaultName) || canUseExistingSelf) && !namespaceAlreadyEmitted; + if (needsNamespaceSpecifier) emittedNamespaceSpecifiers.add(namespaceSpecifierKey); const namespaceSpecifier = flatImportsAreTypeOnly && !statementTypeOnly ? typeOnlySelfNamespaceSpec(mod, namespaceLocal) @@ -321,13 +344,15 @@ function buildImportReplacement( const nextNamedSpecs = needsNamespaceSpecifier ? [namespaceSpecifier, ...keptSpecs] : keptSpecs; return { - edit: importStmt.replace( + edit: replaceImportStatement( + importStmt, formatImport( source, statementTypeOnly ? null : defaultName, nextNamedSpecs, statementTypeOnly, ), + sourceText, ), flatImports, namespaceLocal, @@ -393,6 +418,7 @@ export default function transform(source: string, filePath: string): string | nu const root = parse(sourceLang(filePath, source), source).root(); const imports = findImportStatements(root); const replacements: ImportReplacement[] = []; + const emittedNamespaceSpecifiers = new Set(); for (const importStmt of imports) { const sourceName = importSource(importStmt); @@ -400,7 +426,14 @@ export default function transform(source: string, filePath: string): string | nu const mod = MODULES_BY_SOURCE.get(sourceName); if (!mod) continue; - const replacement = buildImportReplacement(importStmt, mod, root, imports); + const replacement = buildImportReplacement( + importStmt, + mod, + root, + imports, + source, + emittedNamespaceSpecifiers, + ); if (replacement) replacements.push(replacement); } diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/repeated-flat-imports/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/repeated-flat-imports/expected.ts new file mode 100644 index 000000000..9c4fb137b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/repeated-flat-imports/expected.ts @@ -0,0 +1,4 @@ +import { aigateway } from "@tailor-platform/sdk/runtime/aigateway"; + +const first = await aigateway.get("main"); +const second = await aigateway.get("other"); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/repeated-flat-imports/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/repeated-flat-imports/input.ts new file mode 100644 index 000000000..18643314c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/repeated-flat-imports/input.ts @@ -0,0 +1,5 @@ +import { get } from "@tailor-platform/sdk/runtime/aigateway"; +import { get as getAgain } from "@tailor-platform/sdk/runtime/aigateway"; + +const first = await get("main"); +const second = await getAgain("other"); From 41c6e05f6b0cc9422cfac6b9e3dfdf39c5c3da77 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 20:30:21 +0900 Subject: [PATCH 483/618] fix: preserve scoped runtime codemod types --- .../scripts/transform.ts | 48 +++++++++++++++++++ .../tests/scoped-type-identifiers/expected.ts | 10 ++++ .../tests/scoped-type-identifiers/input.ts | 10 ++++ 3 files changed, 68 insertions(+) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/input.ts diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index 6679d91a0..a2f85063d 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -196,6 +196,52 @@ function isInsideTypeQuery(node: SgNode): boolean { return false; } +function sameNode(left: SgNode | null | undefined, right: SgNode): boolean { + if (!left) return false; + const leftRange = left.range(); + const rightRange = right.range(); + return ( + leftRange.start.index === rightRange.start.index && leftRange.end.index === rightRange.end.index + ); +} + +function typeParameterName(typeParameter: SgNode): SgNode | null { + return typeParameter.children().find((child) => child.kind() === "type_identifier") ?? null; +} + +function typeParametersDeclare(typeParameters: SgNode, name: string): boolean { + return typeParameters + .children() + .some( + (child) => child.kind() === "type_parameter" && typeParameterName(child)?.text() === name, + ); +} + +function isTypeParameterScoped(node: SgNode): boolean { + let current = node.parent(); + while (current) { + if (current.kind() === "type_parameter" && sameNode(typeParameterName(current), node)) { + return true; + } + + const typeParameters = current.children().find((child) => child.kind() === "type_parameters"); + if (typeParameters && typeParametersDeclare(typeParameters, node.text())) return true; + + current = current.parent(); + } + return false; +} + +function isNestedTypeMember(node: SgNode): boolean { + const parent = node.parent(); + if (parent?.kind() !== "nested_type_identifier") return false; + + const firstNamedChild = parent + .children() + .find((child) => child.kind() === "identifier" || child.kind() === "type_identifier"); + return !sameNode(firstNamedChild, node); +} + function hasExportSpecifierReference(root: SgNode, names: Set): boolean { return root .findAll({ rule: { kind: "export_specifier" } }) @@ -392,6 +438,8 @@ function referenceEdits(root: SgNode, replacements: ImportReplacement[]): Edit[] for (const node of root.findAll({ rule: { kind: "type_identifier" } })) { if (isInsideImportStatement(node)) continue; if (isInsideExportSpecifier(node)) continue; + if (isTypeParameterScoped(node)) continue; + if (isNestedTypeMember(node)) continue; const replacement = replacementFor(node.text()); if (!replacement) continue; edits.push(node.replace(replacement)); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/expected.ts new file mode 100644 index 000000000..2ca3e9050 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/expected.ts @@ -0,0 +1,10 @@ +import type { External } from "./external"; +import { idp } from "@tailor-platform/sdk/runtime/idp"; + +type Wrapper = { + value: Client; + external: External.Client; + runtime: import("@tailor-platform/sdk/runtime/idp").Client; +}; + +type RuntimeClient = idp.Client; diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/input.ts new file mode 100644 index 000000000..3a2c92438 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/input.ts @@ -0,0 +1,10 @@ +import type { External } from "./external"; +import { Client } from "@tailor-platform/sdk/runtime/idp"; + +type Wrapper = { + value: Client; + external: External.Client; + runtime: import("@tailor-platform/sdk/runtime/idp").Client; +}; + +type RuntimeClient = Client; From 42739586187d86b425ebb91625f2394f5f18cc5b Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 20:45:18 +0900 Subject: [PATCH 484/618] fix: avoid runtime codemod scoped references --- .../scripts/transform.ts | 28 ++++++++++++++++++- .../tests/jsx-tag-name/expected.tsx | 8 ++++++ .../tests/jsx-tag-name/input.tsx | 8 ++++++ .../tests/scoped-type-identifiers/expected.ts | 3 +- .../tests/scoped-type-identifiers/input.ts | 3 +- .../tests/type-scope-shadow/input.ts | 7 +++++ 6 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/jsx-tag-name/expected.tsx create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/jsx-tag-name/input.tsx create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-scope-shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index a2f85063d..88efeaaff 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -196,6 +196,15 @@ function isInsideTypeQuery(node: SgNode): boolean { return false; } +function isJsxTagName(node: SgNode): boolean { + const parentKind = node.parent()?.kind(); + return ( + parentKind === "jsx_opening_element" || + parentKind === "jsx_self_closing_element" || + parentKind === "jsx_closing_element" + ); +} + function sameNode(left: SgNode | null | undefined, right: SgNode): boolean { if (!left) return false; const leftRange = left.range(); @@ -242,6 +251,19 @@ function isNestedTypeMember(node: SgNode): boolean { return !sameNode(firstNamedChild, node); } +function localTypeScopeDeclarationNames(root: SgNode): Set { + const names = new Set(); + for (const node of root.findAll({ + rule: { + any: [{ kind: "type_parameter" }, { kind: "infer_type" }, { kind: "mapped_type_clause" }], + }, + })) { + const name = node.children().find((child) => child.kind() === "type_identifier"); + if (name) names.add(name.text()); + } + return names; +} + function hasExportSpecifierReference(root: SgNode, names: Set): boolean { return root .findAll({ rule: { kind: "export_specifier" } }) @@ -361,7 +383,10 @@ function buildImportReplacement( if (flatImports.length === 0) return null; const removedNames = new Set(flatImports.map((binding) => binding.localName)); - const declaredNames = localDeclarationNames(root); + const declaredNames = new Set([ + ...localDeclarationNames(root), + ...localTypeScopeDeclarationNames(root), + ]); if (flatImports.some((binding) => declaredNames.has(binding.localName))) return null; if (hasExportSpecifierReference(root, removedNames)) return null; @@ -429,6 +454,7 @@ function referenceEdits(root: SgNode, replacements: ImportReplacement[]): Edit[] for (const node of root.findAll({ rule: { kind: "identifier" } })) { if (isInsideImportStatement(node)) continue; if (isInsideExportSpecifier(node)) continue; + if (isJsxTagName(node)) continue; if (byLocalName.get(node.text())?.typeOnly && !isInsideTypeQuery(node)) continue; const replacement = replacementFor(node.text()); if (!replacement) continue; diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/jsx-tag-name/expected.tsx b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/jsx-tag-name/expected.tsx new file mode 100644 index 000000000..57d993ff2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/jsx-tag-name/expected.tsx @@ -0,0 +1,8 @@ +import { aigateway } from "@tailor-platform/sdk/runtime/aigateway"; + +const gateway = aigateway.get("main"); +const element = ( + + + +); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/jsx-tag-name/input.tsx b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/jsx-tag-name/input.tsx new file mode 100644 index 000000000..7a04a382c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/jsx-tag-name/input.tsx @@ -0,0 +1,8 @@ +import { get } from "@tailor-platform/sdk/runtime/aigateway"; + +const gateway = get("main"); +const element = ( + + + +); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/expected.ts index 2ca3e9050..f4af1ce7d 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/expected.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/expected.ts @@ -1,8 +1,7 @@ import type { External } from "./external"; import { idp } from "@tailor-platform/sdk/runtime/idp"; -type Wrapper = { - value: Client; +type Wrapper = { external: External.Client; runtime: import("@tailor-platform/sdk/runtime/idp").Client; }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/input.ts index 3a2c92438..8220e4889 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/input.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/input.ts @@ -1,8 +1,7 @@ import type { External } from "./external"; import { Client } from "@tailor-platform/sdk/runtime/idp"; -type Wrapper = { - value: Client; +type Wrapper = { external: External.Client; runtime: import("@tailor-platform/sdk/runtime/idp").Client; }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-scope-shadow/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-scope-shadow/input.ts new file mode 100644 index 000000000..2829e98da --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-scope-shadow/input.ts @@ -0,0 +1,7 @@ +import { Client } from "@tailor-platform/sdk/runtime/idp"; + +type Wrapper = Client; +type Unwrap = T extends Promise ? Client : never; +type MapClient = { [Client in Keys]: Client }; + +const client = new Client({ namespace: "default" }); From 18fd1e607cd0da24f9386f8bc69fb5fdafd320ac Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 21:12:20 +0900 Subject: [PATCH 485/618] fix: flag runtime codemod namespace reexports --- .../scripts/transform.ts | 19 ++++++++++-- packages/sdk-codemod/src/runner.test.ts | 30 ++++++++++++++++++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index 88efeaaff..59e005260 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -531,7 +531,18 @@ function hasRemovedFlatSpecifier(node: SgNode, mod: RuntimeModule): boolean { } function isExportStar(node: SgNode): boolean { - return node.children().some((child) => child.kind() === "*"); + if (node.children().some((child) => child.kind() === "*")) return true; + const namespaceExport = node.children().find((child) => child.kind() === "namespace_export"); + return namespaceExport?.children().some((child) => child.kind() === "*") ?? false; +} + +function literalModuleSource(node: SgNode): string | null { + if (node.kind() === "string") return importSource(node); + if (node.kind() !== "template_string") return null; + if (node.children().some((child) => child.kind() === "template_substitution")) return null; + + const text = node.text(); + return text.startsWith("`") && text.endsWith("`") ? text.slice(1, -1) : null; } function dynamicImportCallFor(sourceNode: SgNode): SgNode | null { @@ -570,8 +581,10 @@ function dynamicImportExcerptNode(callExpression: SgNode): SgNode { function dynamicRuntimeImportFindings(root: SgNode, relativePath: string): LlmReviewFinding[] { const findings: LlmReviewFinding[] = []; - for (const sourceNode of root.findAll({ rule: { kind: "string" } })) { - const sourceName = importSource(sourceNode); + for (const sourceNode of root.findAll({ + rule: { any: [{ kind: "string" }, { kind: "template_string" }] }, + })) { + const sourceName = literalModuleSource(sourceNode); if (!sourceName || !MODULES_BY_SOURCE.has(sourceName)) continue; const callExpression = dynamicImportCallFor(sourceNode); if (!callExpression) continue; diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index c66a9bd86..8e15ec528 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1068,6 +1068,10 @@ describe("runCodemods", () => { path.join(dir, "reexport-all.ts"), ['export * from "@tailor-platform/sdk/runtime/aigateway";', ""].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "reexport-namespace.ts"), + ['export * as aigateway from "@tailor-platform/sdk/runtime/aigateway";', ""].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "dynamic.ts"), [ @@ -1076,6 +1080,13 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "dynamic-template.ts"), + [ + "const getGateway = (await import(`@tailor-platform/sdk/runtime/aigateway`)).get;", + "", + ].join("\n"), + ); const result = await runCodemods([{ codemod, scriptPath }], dir, true); @@ -1084,8 +1095,20 @@ describe("runCodemods", () => { { codemodId: "v2/runtime-subpath-namespace", prompt: codemod.prompt, - files: ["dynamic.ts", "exports.ts", "reexport-all.ts", "reexport.ts"], + files: [ + "dynamic-template.ts", + "dynamic.ts", + "exports.ts", + "reexport-all.ts", + "reexport-namespace.ts", + "reexport.ts", + ], findings: [ + expect.objectContaining({ + file: "dynamic-template.ts", + line: 1, + excerpt: "(await import(`@tailor-platform/sdk/runtime/aigateway`)).get", + }), expect.objectContaining({ file: "dynamic.ts", line: 1, @@ -1106,6 +1129,11 @@ describe("runCodemods", () => { line: 1, excerpt: 'export * from "@tailor-platform/sdk/runtime/aigateway";', }), + expect.objectContaining({ + file: "reexport-namespace.ts", + line: 1, + excerpt: 'export * as aigateway from "@tailor-platform/sdk/runtime/aigateway";', + }), expect.objectContaining({ file: "reexport.ts", line: 1, From 3cb2da8b41e35ebadceb86a20e0944e8daa4efec Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 21:29:42 +0900 Subject: [PATCH 486/618] fix: merge runtime codemod type namespaces --- .../scripts/transform.ts | 68 ++++++++++++++++++- .../split-type-value-imports/expected.ts | 6 ++ .../tests/split-type-value-imports/input.ts | 6 ++ .../split-value-type-imports/expected.ts | 4 ++ .../tests/split-value-type-imports/input.ts | 5 ++ 5 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-type-value-imports/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-type-value-imports/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-value-type-imports/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-value-type-imports/input.ts diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index 59e005260..ea305f4c3 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -330,6 +330,62 @@ function existingSelfNamespaceImport( return null; } +function flatImportsFor(importStmt: SgNode, mod: RuntimeModule): FlatImport[] { + const statementTypeOnly = isTypeOnlyImport(importStmt); + const flatImports: FlatImport[] = []; + for (const spec of importStmt.findAll({ rule: { kind: "import_specifier" } })) { + const names = importSpecNames(spec); + if (!names) continue; + const memberName = mod.members[names.importedName]; + if (!memberName) continue; + flatImports.push({ + localName: names.localName, + memberName, + typeOnly: statementTypeOnly || names.typeOnly, + }); + } + return flatImports; +} + +function plannedValueNamespaceLocal( + importStmt: SgNode, + mod: RuntimeModule, + root: SgNode, + imports: SgNode[], +): string | null { + const source = importSource(importStmt); + if (!source) return null; + + for (const candidate of imports) { + if (sameNode(candidate, importStmt)) continue; + if (importSource(candidate) !== source || isTypeOnlyImport(candidate)) continue; + + const namespaceName = namespaceImportName(candidate); + if (namespaceName) return namespaceName; + + const defaultName = defaultImportName(candidate); + if (defaultName) return defaultName; + + const existingSelf = existingSelfNamespaceImport(candidate, mod, false); + if (existingSelf && !existingSelf.typeOnly) return existingSelf.localName; + + const valueFlatImports = flatImportsFor(candidate, mod).filter((binding) => !binding.typeOnly); + if (valueFlatImports.length === 0) continue; + + const removedNames = new Set(valueFlatImports.map((binding) => binding.localName)); + const declaredNames = new Set([ + ...localDeclarationNames(root), + ...localTypeScopeDeclarationNames(root), + ]); + if (valueFlatImports.some((binding) => declaredNames.has(binding.localName))) continue; + if (hasExportSpecifierReference(root, removedNames)) continue; + + return uniqueNamespaceLocal(mod, root, imports, removedNames); + } + + return null; +} + function buildImportReplacement( importStmt: SgNode, mod: RuntimeModule, @@ -393,12 +449,16 @@ function buildImportReplacement( const flatImportsAreTypeOnly = flatImports.every((binding) => binding.typeOnly); const canUseExistingSelf = existingSelf != null && (!existingSelf.typeOnly || flatImportsAreTypeOnly); + const plannedValueLocal = flatImportsAreTypeOnly + ? plannedValueNamespaceLocal(importStmt, mod, root, imports) + : null; const namespaceLocal = - !statementTypeOnly && defaultName + plannedValueLocal ?? + (!statementTypeOnly && defaultName ? defaultName : canUseExistingSelf ? existingSelf.localName - : uniqueNamespaceLocal(mod, root, imports, removedNames); + : uniqueNamespaceLocal(mod, root, imports, removedNames)); const namespaceSpecifierKey = [ source, namespaceLocal, @@ -406,7 +466,9 @@ function buildImportReplacement( ].join("\0"); const namespaceAlreadyEmitted = emittedNamespaceSpecifiers.has(namespaceSpecifierKey); const needsNamespaceSpecifier = - !((!statementTypeOnly && defaultName) || canUseExistingSelf) && !namespaceAlreadyEmitted; + plannedValueLocal == null && + !((!statementTypeOnly && defaultName) || canUseExistingSelf) && + !namespaceAlreadyEmitted; if (needsNamespaceSpecifier) emittedNamespaceSpecifiers.add(namespaceSpecifierKey); const namespaceSpecifier = flatImportsAreTypeOnly && !statementTypeOnly diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-type-value-imports/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-type-value-imports/expected.ts new file mode 100644 index 000000000..be61056d7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-type-value-imports/expected.ts @@ -0,0 +1,6 @@ +import type { ClientConfig } from "@tailor-platform/sdk/runtime/idp"; +import { idp } from "@tailor-platform/sdk/runtime/idp"; + +const config: ClientConfig = { namespace: "default" }; +type ClientRef = idp.Client; +const client = new idp.Client(config); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-type-value-imports/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-type-value-imports/input.ts new file mode 100644 index 000000000..e7874ec0e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-type-value-imports/input.ts @@ -0,0 +1,6 @@ +import type { Client, ClientConfig } from "@tailor-platform/sdk/runtime/idp"; +import { Client as IdpClient } from "@tailor-platform/sdk/runtime/idp"; + +const config: ClientConfig = { namespace: "default" }; +type ClientRef = Client; +const client = new IdpClient(config); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-value-type-imports/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-value-type-imports/expected.ts new file mode 100644 index 000000000..fa52648ad --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-value-type-imports/expected.ts @@ -0,0 +1,4 @@ +import { idp } from "@tailor-platform/sdk/runtime/idp"; + +type ClientRef = idp.Client; +const client = new idp.Client({ namespace: "default" }); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-value-type-imports/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-value-type-imports/input.ts new file mode 100644 index 000000000..d7ee5f78f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-value-type-imports/input.ts @@ -0,0 +1,5 @@ +import { Client as IdpClient } from "@tailor-platform/sdk/runtime/idp"; +import type { Client } from "@tailor-platform/sdk/runtime/idp"; + +type ClientRef = Client; +const client = new IdpClient({ namespace: "default" }); From d1da4486bbe6915df7f2177fdb5699fb3d4e514b Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 9 Jul 2026 21:41:11 +0900 Subject: [PATCH 487/618] ci(erd): handle base schema incompatibility in ERD viewer preview --- .github/workflows/erd-viewer-preview.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/erd-viewer-preview.yml b/.github/workflows/erd-viewer-preview.yml index 0a81fa8ab..2bf3f8a5a 100644 --- a/.github/workflows/erd-viewer-preview.yml +++ b/.github/workflows/erd-viewer-preview.yml @@ -97,12 +97,14 @@ jobs: TAILOR_PLATFORM_SDK_DTS_PATH="$RUNNER_TEMP/erd-dts/base-$NAMESPACE.d.ts" \ "$GITHUB_WORKSPACE/node_modules/.bin/tailor" tailordb erd export --namespace "$NAMESPACE" --output "$base_output" ) >"$base_log" 2>&1; then + cat "$base_log" if grep -q 'not found in local config.db' "$base_log"; then - cat "$base_log" echo "Base ERD namespace '$NAMESPACE' not found; rendering current objects as added." base_missing="true" + elif grep -qE 'Failed to load type|Invalid input' "$base_log"; then + echo "::warning::Base schema is incompatible with the current parser (likely a breaking change). Rendering current objects as added." + base_missing="true" else - cat "$base_log" exit 1 fi elif [ -s "$base_log" ]; then From bd337c3415a001ee4bc9af9ec3332a22a44ad169 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 21:52:10 +0900 Subject: [PATCH 488/618] fix: stabilize runtime global casts --- packages/sdk/src/runtime/aigateway.ts | 2 +- packages/sdk/src/runtime/authconnection.ts | 3 ++- packages/sdk/src/runtime/context.ts | 4 +++- packages/sdk/src/runtime/file.ts | 2 +- packages/sdk/src/runtime/secretmanager.ts | 3 ++- packages/sdk/src/runtime/workflow.ts | 2 +- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/sdk/src/runtime/aigateway.ts b/packages/sdk/src/runtime/aigateway.ts index 134e81ea5..333a72ab2 100644 --- a/packages/sdk/src/runtime/aigateway.ts +++ b/packages/sdk/src/runtime/aigateway.ts @@ -39,7 +39,7 @@ export interface TailorAigatewayAPI { } const api = (): TailorAigatewayAPI => - (globalThis as { tailor: { aigateway: TailorAigatewayAPI } }).tailor.aigateway; + (globalThis as unknown as { tailor: { aigateway: TailorAigatewayAPI } }).tailor.aigateway; const get: TailorAigatewayAPI["get"] = (...args) => api().get(...args); diff --git a/packages/sdk/src/runtime/authconnection.ts b/packages/sdk/src/runtime/authconnection.ts index 8c7e53b08..c436b13c0 100644 --- a/packages/sdk/src/runtime/authconnection.ts +++ b/packages/sdk/src/runtime/authconnection.ts @@ -33,7 +33,8 @@ export interface TailorAuthconnectionAPI { } const api = (): TailorAuthconnectionAPI => - (globalThis as { tailor: { authconnection: TailorAuthconnectionAPI } }).tailor.authconnection; + (globalThis as unknown as { tailor: { authconnection: TailorAuthconnectionAPI } }).tailor + .authconnection; const getConnectionToken: TailorAuthconnectionAPI["getConnectionToken"] = (...args) => api().getConnectionToken(...args); diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index e1f3e1d00..a4a789bc0 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -55,7 +55,9 @@ export interface TailorContextAPI { * @returns Invoker details, or `null` when the call is anonymous */ function getInvoker(): Invoker | null { - const raw = (globalThis as { tailor: { context: TailorContextAPI } }).tailor.context.getInvoker(); + const raw = ( + globalThis as unknown 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 80c766147..eacdf72c8 100644 --- a/packages/sdk/src/runtime/file.ts +++ b/packages/sdk/src/runtime/file.ts @@ -217,7 +217,7 @@ export interface TailorDBFileAPI { } const api = (): TailorDBFileAPI => - (globalThis as { tailordb: { file: TailorDBFileAPI } }).tailordb.file; + (globalThis as unknown as { tailordb: { file: TailorDBFileAPI } }).tailordb.file; /** * See {@link TailorDBFileAPI.upload}. diff --git a/packages/sdk/src/runtime/secretmanager.ts b/packages/sdk/src/runtime/secretmanager.ts index 2eff863cf..524b6bb6d 100644 --- a/packages/sdk/src/runtime/secretmanager.ts +++ b/packages/sdk/src/runtime/secretmanager.ts @@ -38,7 +38,8 @@ export interface TailorSecretmanagerAPI { } const api = (): TailorSecretmanagerAPI => - (globalThis as { tailor: { secretmanager: TailorSecretmanagerAPI } }).tailor.secretmanager; + (globalThis as unknown as { tailor: { secretmanager: TailorSecretmanagerAPI } }).tailor + .secretmanager; const getSecrets: TailorSecretmanagerAPI["getSecrets"] = (...args) => api().getSecrets(...args); diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index 14519f9a0..e4a1441d4 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -85,7 +85,7 @@ export interface TailorWorkflowAPI { } const api = (): TailorWorkflowAPI => - (globalThis as { tailor: { workflow: TailorWorkflowAPI } }).tailor.workflow; + (globalThis as unknown as { tailor: { workflow: TailorWorkflowAPI } }).tailor.workflow; function triggerWorkflow( workflowName: string, From 46817786354665cb97d0de63b78fa83e64096e03 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 9 Jul 2026 22:00:51 +0900 Subject: [PATCH 489/618] fix(sdk): restore Timestamp Kysely type for date fields case "date" was left as a separate switch case emitting a bare "string" Kysely type when PR #1684 reverted the "strict field scalar types" experiment. Before that experiment, "date" and "datetime" were grouped in the same case and both mapped to Timestamp; the revert should have merged "date" back into "datetime" instead of leaving it on its own. Merge the case so db.date() fields generate Timestamp again, consistent with db.datetime(). --- .changeset/kysely-date-timestamp.md | 5 +++++ .../src/plugin/builtin/kysely-type/index.test.ts | 2 +- .../builtin/kysely-type/type-processor.test.ts | 14 +++++++++----- .../plugin/builtin/kysely-type/type-processor.ts | 2 -- 4 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 .changeset/kysely-date-timestamp.md diff --git a/.changeset/kysely-date-timestamp.md b/.changeset/kysely-date-timestamp.md new file mode 100644 index 000000000..d38ee46d5 --- /dev/null +++ b/.changeset/kysely-date-timestamp.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": patch +--- + +Fix generated Kysely types for `db.date()` fields to use `Timestamp` instead of `string`, matching `db.datetime()` and allowing `insertInto`/`updateTable` calls to accept a `Date` value. diff --git a/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts index 73e24a717..82a0f7517 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts @@ -84,7 +84,7 @@ describe("KyselyTypePlugin integration tests", () => { expect(result.typeDef).toContain("age: number | null;"); expect(result.typeDef).toContain("isActive: boolean;"); expect(result.typeDef).toContain("score: number | null;"); - expect(result.typeDef).toContain("birthDate: string | null;"); + expect(result.typeDef).toContain("birthDate: Timestamp | null;"); expect(result.typeDef).toContain("lastLogin: Timestamp | null;"); expect(result.typeDef).toContain("tags: string[];"); expect(result.typeDef).toContain("createdAt: Generated;"); diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts index 33192ae0e..4f4cb117b 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts @@ -62,7 +62,11 @@ describe("Kysely TypeProcessor", () => { endDate: db.datetime(), cancelledAt: db.datetime({ optional: true }), }), - expected: ["startDate: string;", "endDate: Timestamp;", "cancelledAt: Timestamp | null;"], + expected: [ + "startDate: Timestamp;", + "endDate: Timestamp;", + "cancelledAt: Timestamp | null;", + ], }, { name: "uuid types", @@ -100,7 +104,7 @@ describe("Kysely TypeProcessor", () => { ); expect(typeDef).toContain("eventDates: ArrayColumnType;"); - expect(typeDef).toContain("optionalDates: string[] | null;"); + expect(typeDef).toContain("optionalDates: ArrayColumnType | null;"); }); }); @@ -180,7 +184,7 @@ describe("Kysely TypeProcessor", () => { expect(typeDef).toContain("phone?: string | null"); }); - test("should use Date | string instead of Timestamp for date fields inside nested objects", async () => { + test("should use Timestamp for date fields inside nested objects", async () => { const type = db.table("Receipt", { receiptDate: db.date(), dueSchedule: db.object({ @@ -191,10 +195,10 @@ describe("Kysely TypeProcessor", () => { const result = await processKyselyType(parseTailorDBType(toSchemaOutput(type))); - expect(result.typeDef).toContain("receiptDate: string;"); + expect(result.typeDef).toContain("receiptDate: Timestamp;"); // Nested object with datetime is wrapped in ObjectColumnType expect(result.typeDef).toContain("ObjectColumnType<"); - expect(result.typeDef).toContain("dueDate: string"); + expect(result.typeDef).toContain("dueDate: Timestamp"); expect(result.typeDef).toContain("reminderAt?: Timestamp | null"); expect(result.usedUtilityTypes.Timestamp).toBe(true); }); diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts index da3ec72ca..024487e05 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts @@ -90,8 +90,6 @@ function getBaseType(fieldConfig: OperatorFieldConfig): FieldTypeResult { type = "number"; break; case "date": - type = "string"; - break; case "datetime": usedUtilityTypes.Timestamp = true; type = "Timestamp"; From 122c72588d03f2fd9c628598edef62c53c653ed3 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 22:12:37 +0900 Subject: [PATCH 490/618] fix: flag indirect runtime dynamic imports --- .../scripts/transform.ts | 157 ++++++++++++++++-- packages/sdk-codemod/src/runner.test.ts | 14 ++ 2 files changed, 155 insertions(+), 16 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index ea305f4c3..4032096d6 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -1,5 +1,6 @@ import { parse, Lang } from "@ast-grep/napi"; import { + collectBindingNames, findImportStatements, importBindings, importSource, @@ -607,23 +608,150 @@ function literalModuleSource(node: SgNode): string | null { return text.startsWith("`") && text.endsWith("`") ? text.slice(1, -1) : null; } -function dynamicImportCallFor(sourceNode: SgNode): SgNode | null { - let current = sourceNode.parent(); +function isSourceScopeNode(node: SgNode): boolean { + const kind = node.kind(); + return ( + kind === "program" || + kind === "statement_block" || + kind === "function_declaration" || + kind === "arrow_function" || + kind === "method_definition" + ); +} + +function nearestSourceScope(node: SgNode): SgNode | null { + let current = node.parent(); while (current) { - if (current.kind() === "import_statement" || current.kind() === "export_statement") { - return null; - } - if ( - current.kind() === "call_expression" && - current.children().some((child) => child.kind() === "import") - ) { - return current; + if (isSourceScopeNode(current)) return current; + current = current.parent(); + } + return null; +} + +function sameRange(left: SgNode | null, right: SgNode): boolean { + if (!left) return false; + const leftRange = left.range(); + const rightRange = right.range(); + return ( + leftRange.start.index === rightRange.start.index && leftRange.end.index === rightRange.end.index + ); +} + +function isConstVariableDeclarator(node: SgNode): boolean { + return ( + node + .parent() + ?.children() + .some((child) => child.kind() === "const") ?? false + ); +} + +function sourceConstInitializerContent(node: SgNode): string | null { + const directValue = literalModuleSource(node); + if (directValue != null) return directValue; + if ( + node.kind() !== "as_expression" && + node.kind() !== "satisfies_expression" && + node.kind() !== "parenthesized_expression" + ) { + return null; + } + for (const child of node.children()) { + const childValue = sourceConstInitializerContent(child); + if (childValue != null) return childValue; + } + return null; +} + +function sourceConstVariableDeclaratorContent(node: SgNode, name: string): string | null { + if (!isConstVariableDeclarator(node)) return null; + const names = new Set(); + collectBindingNames(node, names); + if (!names.has(name)) return null; + + const initializer = node + .children() + .findLast((child) => sourceConstInitializerContent(child) != null); + return initializer == null ? null : sourceConstInitializerContent(initializer); +} + +function bindingNames(node: SgNode): Set { + const names = new Set(); + collectBindingNames(node, names); + return names; +} + +function sourceStringVariableInScope( + scope: SgNode, + name: string, + before: number, +): string | null | undefined { + const bindings = scope + .findAll({ + rule: { + any: [ + { kind: "variable_declarator" }, + { kind: "required_parameter" }, + { kind: "optional_parameter" }, + { kind: "catch_clause" }, + ], + }, + }) + .filter( + (node) => node.range().start.index < before && sameRange(nearestSourceScope(node), scope), + ) + .toSorted((a, b) => b.range().start.index - a.range().start.index); + + for (const binding of bindings) { + if (!bindingNames(binding).has(name)) continue; + return binding.kind() === "variable_declarator" + ? sourceConstVariableDeclaratorContent(binding, name) + : null; + } + + return undefined; +} + +function sourceScopedStringVariableContent(identifier: SgNode): string | null { + const name = identifier.text(); + const before = identifier.range().start.index; + let current = identifier.parent(); + while (current) { + if (isSourceScopeNode(current)) { + const value = sourceStringVariableInScope(current, name, before); + if (value !== undefined) return value; } current = current.parent(); } return null; } +function isDynamicImportCall(node: SgNode): boolean { + return ( + node.kind() === "call_expression" && node.children().some((child) => child.kind() === "import") + ); +} + +function isArgumentSyntaxNode(node: SgNode): boolean { + const kind = node.kind(); + return kind === "(" || kind === ")" || kind === "," || kind === "comment"; +} + +function firstCallArgument(callExpression: SgNode): SgNode | null { + const args = callExpression.children().find((child) => child.kind() === "arguments"); + return args?.children().find((child) => !isArgumentSyntaxNode(child)) ?? null; +} + +function dynamicImportSourceName(callExpression: SgNode): string | null { + const sourceArg = firstCallArgument(callExpression); + if (!sourceArg) return null; + + const sourceName = literalModuleSource(sourceArg); + if (sourceName != null) return sourceName; + + return sourceArg.kind() === "identifier" ? sourceScopedStringVariableContent(sourceArg) : null; +} + function dynamicImportExcerptNode(callExpression: SgNode): SgNode { let current = callExpression; while (current.parent()) { @@ -643,13 +771,10 @@ function dynamicImportExcerptNode(callExpression: SgNode): SgNode { function dynamicRuntimeImportFindings(root: SgNode, relativePath: string): LlmReviewFinding[] { const findings: LlmReviewFinding[] = []; - for (const sourceNode of root.findAll({ - rule: { any: [{ kind: "string" }, { kind: "template_string" }] }, - })) { - const sourceName = literalModuleSource(sourceNode); + for (const callExpression of root.findAll({ rule: { kind: "call_expression" } })) { + if (!isDynamicImportCall(callExpression)) continue; + const sourceName = dynamicImportSourceName(callExpression); if (!sourceName || !MODULES_BY_SOURCE.has(sourceName)) continue; - const callExpression = dynamicImportCallFor(sourceNode); - if (!callExpression) continue; const excerptNode = dynamicImportExcerptNode(callExpression); findings.push({ file: relativePath, diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 8e15ec528..5f3d4e52d 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1087,6 +1087,14 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "dynamic-const.ts"), + [ + 'const runtimeModule = "@tailor-platform/sdk/runtime/aigateway";', + "const getGateway = (await import(runtimeModule)).get;", + "", + ].join("\n"), + ); const result = await runCodemods([{ codemod, scriptPath }], dir, true); @@ -1096,6 +1104,7 @@ describe("runCodemods", () => { codemodId: "v2/runtime-subpath-namespace", prompt: codemod.prompt, files: [ + "dynamic-const.ts", "dynamic-template.ts", "dynamic.ts", "exports.ts", @@ -1104,6 +1113,11 @@ describe("runCodemods", () => { "reexport.ts", ], findings: [ + expect.objectContaining({ + file: "dynamic-const.ts", + line: 2, + excerpt: "(await import(runtimeModule)).get", + }), expect.objectContaining({ file: "dynamic-template.ts", line: 1, From f79f4df9254f869b9ab26bcea1727c32ee133cd8 Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 22:29:37 +0900 Subject: [PATCH 491/618] fix: preserve runtime codemod import attributes --- .../scripts/transform.ts | 25 +++++++++++++++---- .../tests/import-attributes/expected.cts | 5 ++++ .../tests/import-attributes/input.cts | 5 ++++ 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/import-attributes/expected.cts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/import-attributes/input.cts diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index 4032096d6..a59d75191 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -146,15 +146,28 @@ function formatImport( defaultName: string | null, namedSpecs: string[], typeOnly = false, + attributeText = "", ): string { const importKeyword = typeOnly ? "import type" : "import"; const named = namedSpecs.length > 0 ? `{ ${namedSpecs.join(", ")} }` : null; - if (defaultName && named) return `${importKeyword} ${defaultName}, ${named} from "${source}";`; - if (defaultName) return `${importKeyword} ${defaultName} from "${source}";`; - if (named) return `${importKeyword} ${named} from "${source}";`; + const attributes = attributeText === "" ? "" : ` ${attributeText}`; + if (defaultName && named) { + return `${importKeyword} ${defaultName}, ${named} from "${source}"${attributes};`; + } + if (defaultName) return `${importKeyword} ${defaultName} from "${source}"${attributes};`; + if (named) return `${importKeyword} ${named} from "${source}"${attributes};`; return ""; } +function importAttributeText(importStmt: SgNode): string { + return ( + importStmt + .children() + .find((child) => child.kind() === "import_attribute") + ?.text() ?? "" + ); +} + function replaceImportStatement(importStmt: SgNode, nextText: string, sourceText: string): Edit { if (nextText !== "") return importStmt.replace(nextText); @@ -399,13 +412,14 @@ function buildImportReplacement( if (!source) return null; const statementTypeOnly = isTypeOnlyImport(importStmt); + const attributes = importAttributeText(importStmt); const namespaceName = namespaceImportName(importStmt); if (namespaceName) { const edit = statementTypeOnly ? importStmt.replace( - formatImport(source, null, [selfNamespaceSpec(mod, namespaceName)], true), + formatImport(source, null, [selfNamespaceSpec(mod, namespaceName)], true, attributes), ) - : importStmt.replace(formatImport(source, namespaceName, [])); + : importStmt.replace(formatImport(source, namespaceName, [], false, attributes)); return { edit, flatImports: [], @@ -485,6 +499,7 @@ function buildImportReplacement( statementTypeOnly ? null : defaultName, nextNamedSpecs, statementTypeOnly, + attributes, ), sourceText, ), diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/import-attributes/expected.cts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/import-attributes/expected.cts new file mode 100644 index 000000000..df6392fb7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/import-attributes/expected.cts @@ -0,0 +1,5 @@ +import type { idp } from "@tailor-platform/sdk/runtime/idp" with { "resolution-mode": "import" }; +import { aigateway } from "@tailor-platform/sdk/runtime/aigateway" assert { type: "json" }; + +type ClientRef = idp.Client; +const gateway = aigateway.get("main"); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/import-attributes/input.cts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/import-attributes/input.cts new file mode 100644 index 000000000..8d8293b85 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/import-attributes/input.cts @@ -0,0 +1,5 @@ +import type { Client } from "@tailor-platform/sdk/runtime/idp" with { "resolution-mode": "import" }; +import { get } from "@tailor-platform/sdk/runtime/aigateway" assert { type: "json" }; + +type ClientRef = Client; +const gateway = get("main"); From 1ec488aab7d86b4936f6cefb6afdacbe024c8ebd Mon Sep 17 00:00:00 2001 From: dqn Date: Thu, 9 Jul 2026 22:49:24 +0900 Subject: [PATCH 492/618] fix: flag runtime codemod require imports --- .../scripts/transform.ts | 42 +++++++++++++++++-- packages/sdk-codemod/src/runner.test.ts | 29 +++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index a59d75191..83ec89316 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -747,6 +747,13 @@ function isDynamicImportCall(node: SgNode): boolean { ); } +function isRequireCall(node: SgNode): boolean { + return ( + node.kind() === "call_expression" && + node.children().some((child) => child.kind() === "identifier" && child.text() === "require") + ); +} + function isArgumentSyntaxNode(node: SgNode): boolean { const kind = node.kind(); return kind === "(" || kind === ")" || kind === "," || kind === "comment"; @@ -757,7 +764,7 @@ function firstCallArgument(callExpression: SgNode): SgNode | null { return args?.children().find((child) => !isArgumentSyntaxNode(child)) ?? null; } -function dynamicImportSourceName(callExpression: SgNode): string | null { +function moduleCallSourceName(callExpression: SgNode): string | null { const sourceArg = firstCallArgument(callExpression); if (!sourceArg) return null; @@ -767,6 +774,10 @@ function dynamicImportSourceName(callExpression: SgNode): string | null { return sourceArg.kind() === "identifier" ? sourceScopedStringVariableContent(sourceArg) : null; } +function dynamicImportSourceName(callExpression: SgNode): string | null { + return moduleCallSourceName(callExpression); +} + function dynamicImportExcerptNode(callExpression: SgNode): SgNode { let current = callExpression; while (current.parent()) { @@ -801,6 +812,27 @@ function dynamicRuntimeImportFindings(root: SgNode, relativePath: string): LlmRe return findings; } +function runtimeRequireFindings(root: SgNode, relativePath: string): LlmReviewFinding[] { + const findings: LlmReviewFinding[] = []; + for (const callExpression of root.findAll({ rule: { kind: "call_expression" } })) { + if (isInsideImportStatement(callExpression)) continue; + if (!isRequireCall(callExpression)) continue; + const sourceName = moduleCallSourceName(callExpression); + if (!sourceName || !MODULES_BY_SOURCE.has(sourceName)) continue; + findings.push({ + file: relativePath, + line: callExpression.range().start.line + 1, + message: "CommonJS runtime subpath require may still access a removed flat value export.", + excerpt: callExpression.text().trim(), + }); + } + return findings; +} + +function isImportRequireStatement(importStmt: SgNode): boolean { + return importStmt.children().some((child) => child.kind() === "import_require_clause"); +} + export function reviewFindings( source: string, filePath: string, @@ -811,6 +843,7 @@ export function reviewFindings( const root = parse(sourceLang(filePath, source), source).root(); const findings: LlmReviewFinding[] = []; findings.push(...dynamicRuntimeImportFindings(root, relativePath)); + findings.push(...runtimeRequireFindings(root, relativePath)); for (const importStmt of findImportStatements(root)) { const sourceName = importSource(importStmt); @@ -818,14 +851,17 @@ export function reviewFindings( const mod = MODULES_BY_SOURCE.get(sourceName); if (!mod) continue; + const hasImportRequire = isImportRequireStatement(importStmt); const hasNamespaceImport = namespaceImportName(importStmt) != null; const hasRemovedFlatImport = hasRemovedFlatSpecifier(importStmt, mod); - if (!hasNamespaceImport && !hasRemovedFlatImport) continue; + if (!hasImportRequire && !hasNamespaceImport && !hasRemovedFlatImport) continue; findings.push({ file: relativePath, line: importStmt.range().start.line + 1, - message: "Runtime subpath import still uses a removed namespace-star or flat value import.", + message: hasImportRequire + ? "TypeScript runtime subpath import-equals may still access a removed flat value export." + : "Runtime subpath import still uses a removed namespace-star or flat value import.", excerpt: importStmt.text().trim(), }); } diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 5f3d4e52d..95284ece7 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1095,6 +1095,23 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "require.cjs"), + [ + 'const { get } = require("@tailor-platform/sdk/runtime/aigateway");', + "module.exports = get;", + "", + ].join("\n"), + ); + await fs.promises.writeFile( + path.join(dir, "import-equals.cts"), + [ + 'import iconv = require("@tailor-platform/sdk/runtime/iconv");', + "", + "export const encode = iconv.encode;", + "", + ].join("\n"), + ); const result = await runCodemods([{ codemod, scriptPath }], dir, true); @@ -1108,9 +1125,11 @@ describe("runCodemods", () => { "dynamic-template.ts", "dynamic.ts", "exports.ts", + "import-equals.cts", "reexport-all.ts", "reexport-namespace.ts", "reexport.ts", + "require.cjs", ], findings: [ expect.objectContaining({ @@ -1138,6 +1157,11 @@ describe("runCodemods", () => { line: 1, excerpt: 'import { get } from "@tailor-platform/sdk/runtime/aigateway";', }), + expect.objectContaining({ + file: "import-equals.cts", + line: 1, + excerpt: 'import iconv = require("@tailor-platform/sdk/runtime/iconv");', + }), expect.objectContaining({ file: "reexport-all.ts", line: 1, @@ -1153,6 +1177,11 @@ describe("runCodemods", () => { line: 1, excerpt: 'export { get } from "@tailor-platform/sdk/runtime/aigateway";', }), + expect.objectContaining({ + file: "require.cjs", + line: 1, + excerpt: 'require("@tailor-platform/sdk/runtime/aigateway")', + }), ], }, ]); From 7dfd6917e70b520074c17cf3396f7ce1f81e638e Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 10 Jul 2026 00:41:18 +0900 Subject: [PATCH 493/618] refactor: remove runtime namespace type merges --- .../scripts/transform.ts | 23 ++++++++ .../tests/flat-type-reference/expected.ts | 4 -- .../tests/import-attributes/expected.cts | 4 +- .../tests/inline-type-flat-import/expected.ts | 3 -- .../tests/namespace-import-type/expected.ts | 3 -- .../tests/scoped-type-identifiers/expected.ts | 9 ---- .../split-type-value-imports/expected.ts | 4 +- .../split-value-type-imports/expected.ts | 3 +- .../tests/type-only-flat-import/expected.ts | 4 -- packages/sdk-codemod/src/registry.ts | 3 +- packages/sdk-codemod/src/runner.test.ts | 30 +++++++++++ packages/sdk/docs/migration/v2.md | 3 +- packages/sdk/src/runtime/aigateway.ts | 10 ---- packages/sdk/src/runtime/authconnection.ts | 8 --- packages/sdk/src/runtime/context.ts | 12 ----- packages/sdk/src/runtime/file.ts | 30 ----------- packages/sdk/src/runtime/iconv.ts | 19 ++----- packages/sdk/src/runtime/idp.ts | 53 ++++--------------- packages/sdk/src/runtime/index.test.ts | 19 ++----- packages/sdk/src/runtime/secretmanager.ts | 8 --- packages/sdk/src/runtime/workflow.ts | 14 ----- 21 files changed, 80 insertions(+), 186 deletions(-) delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-type-reference/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/inline-type-flat-import/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import-type/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/expected.ts delete mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-flat-import/expected.ts diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index 83ec89316..5a1531c72 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -288,6 +288,26 @@ function hasExportSpecifierReference(root: SgNode, names: Set): boolean ); } +function hasNonTypeQueryTypeReference(root: SgNode, names: Set): boolean { + return root.findAll({ rule: { kind: "type_identifier" } }).some((node) => { + if (!names.has(node.text())) return false; + if (isInsideImportStatement(node)) return false; + if (isInsideExportSpecifier(node)) return false; + if (isTypeParameterScoped(node)) return false; + if (isNestedTypeMember(node)) return false; + return !isInsideTypeQuery(node); + }); +} + +function hasNamespaceTypeMemberReference(root: SgNode, namespaceLocal: string): boolean { + return root.findAll({ rule: { kind: "nested_type_identifier" } }).some((node) => { + const firstNamedChild = node + .children() + .find((child) => child.kind() === "identifier" || child.kind() === "type_identifier"); + return firstNamedChild?.text() === namespaceLocal; + }); +} + function findExportStatements(root: SgNode): SgNode[] { return root .findAll({ rule: { kind: "export_statement" } }) @@ -415,6 +435,7 @@ function buildImportReplacement( const attributes = importAttributeText(importStmt); const namespaceName = namespaceImportName(importStmt); if (namespaceName) { + if (hasNamespaceTypeMemberReference(root, namespaceName)) return null; const edit = statementTypeOnly ? importStmt.replace( formatImport(source, null, [selfNamespaceSpec(mod, namespaceName)], true, attributes), @@ -460,6 +481,7 @@ function buildImportReplacement( ]); if (flatImports.some((binding) => declaredNames.has(binding.localName))) return null; if (hasExportSpecifierReference(root, removedNames)) return null; + if (hasNonTypeQueryTypeReference(root, removedNames)) return null; const flatImportsAreTypeOnly = flatImports.every((binding) => binding.typeOnly); const canUseExistingSelf = @@ -542,6 +564,7 @@ function referenceEdits(root: SgNode, replacements: ImportReplacement[]): Edit[] for (const node of root.findAll({ rule: { kind: "type_identifier" } })) { if (isInsideImportStatement(node)) continue; if (isInsideExportSpecifier(node)) continue; + if (!isInsideTypeQuery(node)) continue; if (isTypeParameterScoped(node)) continue; if (isNestedTypeMember(node)) continue; const replacement = replacementFor(node.text()); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-type-reference/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-type-reference/expected.ts deleted file mode 100644 index ead8c6d52..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/flat-type-reference/expected.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { idp } from "@tailor-platform/sdk/runtime/idp"; - -let client: idp.Client; -client = new idp.Client({ namespace: "default" }); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/import-attributes/expected.cts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/import-attributes/expected.cts index df6392fb7..4765c2698 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/import-attributes/expected.cts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/import-attributes/expected.cts @@ -1,5 +1,5 @@ -import type { idp } from "@tailor-platform/sdk/runtime/idp" with { "resolution-mode": "import" }; +import type { Client } from "@tailor-platform/sdk/runtime/idp" with { "resolution-mode": "import" }; import { aigateway } from "@tailor-platform/sdk/runtime/aigateway" assert { type: "json" }; -type ClientRef = idp.Client; +type ClientRef = Client; const gateway = aigateway.get("main"); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/inline-type-flat-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/inline-type-flat-import/expected.ts deleted file mode 100644 index b81b2be17..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/inline-type-flat-import/expected.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { type idp } from "@tailor-platform/sdk/runtime/idp"; - -type ClientRef = idp.Client; diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import-type/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import-type/expected.ts deleted file mode 100644 index dc7ee8e28..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import-type/expected.ts +++ /dev/null @@ -1,3 +0,0 @@ -import idp from "@tailor-platform/sdk/runtime/idp"; - -const config: idp.ClientConfig = { namespace: "default" }; diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/expected.ts deleted file mode 100644 index f4af1ce7d..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/scoped-type-identifiers/expected.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { External } from "./external"; -import { idp } from "@tailor-platform/sdk/runtime/idp"; - -type Wrapper = { - external: External.Client; - runtime: import("@tailor-platform/sdk/runtime/idp").Client; -}; - -type RuntimeClient = idp.Client; diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-type-value-imports/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-type-value-imports/expected.ts index be61056d7..263144644 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-type-value-imports/expected.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-type-value-imports/expected.ts @@ -1,6 +1,6 @@ -import type { ClientConfig } from "@tailor-platform/sdk/runtime/idp"; +import type { Client, ClientConfig } from "@tailor-platform/sdk/runtime/idp"; import { idp } from "@tailor-platform/sdk/runtime/idp"; const config: ClientConfig = { namespace: "default" }; -type ClientRef = idp.Client; +type ClientRef = Client; const client = new idp.Client(config); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-value-type-imports/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-value-type-imports/expected.ts index fa52648ad..ef5d6d49b 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-value-type-imports/expected.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/split-value-type-imports/expected.ts @@ -1,4 +1,5 @@ import { idp } from "@tailor-platform/sdk/runtime/idp"; +import type { Client } from "@tailor-platform/sdk/runtime/idp"; -type ClientRef = idp.Client; +type ClientRef = Client; const client = new idp.Client({ namespace: "default" }); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-flat-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-flat-import/expected.ts deleted file mode 100644 index 7666e0cbd..000000000 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/type-only-flat-import/expected.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { idp, ClientConfig } from "@tailor-platform/sdk/runtime/idp"; - -const config: ClientConfig = { namespace: "default" }; -type ClientRef = idp.Client; diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 159bb0311..790ecd305 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -542,7 +542,8 @@ export const allCodemods: CodemodPackage[] = [ '`import { get } from "@tailor-platform/sdk/runtime/aigateway"` are removed.', "The codemod rewrites straightforward namespace-star imports and flat named value", "imports. Review any remaining runtime subpath imports manually, especially when", - "a local binding or nested scope shadows the imported flat value.", + "a local binding or nested scope shadows the imported flat value, or when", + "type-position namespace member references need explicit top-level type imports.", ].join("\n"), }, { diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 95284ece7..1b9055381 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1095,6 +1095,24 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "type-reference.ts"), + [ + 'import type { Client } from "@tailor-platform/sdk/runtime/idp";', + "", + "type RuntimeClient = Client;", + "", + ].join("\n"), + ); + await fs.promises.writeFile( + path.join(dir, "namespace-type.ts"), + [ + 'import * as idp from "@tailor-platform/sdk/runtime/idp";', + "", + "type RuntimeConfig = idp.ClientConfig;", + "", + ].join("\n"), + ); await fs.promises.writeFile( path.join(dir, "require.cjs"), [ @@ -1126,10 +1144,12 @@ describe("runCodemods", () => { "dynamic.ts", "exports.ts", "import-equals.cts", + "namespace-type.ts", "reexport-all.ts", "reexport-namespace.ts", "reexport.ts", "require.cjs", + "type-reference.ts", ], findings: [ expect.objectContaining({ @@ -1162,6 +1182,11 @@ describe("runCodemods", () => { line: 1, excerpt: 'import iconv = require("@tailor-platform/sdk/runtime/iconv");', }), + expect.objectContaining({ + file: "namespace-type.ts", + line: 1, + excerpt: 'import * as idp from "@tailor-platform/sdk/runtime/idp";', + }), expect.objectContaining({ file: "reexport-all.ts", line: 1, @@ -1182,6 +1207,11 @@ describe("runCodemods", () => { line: 1, excerpt: 'require("@tailor-platform/sdk/runtime/aigateway")', }), + expect.objectContaining({ + file: "type-reference.ts", + line: 1, + excerpt: 'import type { Client } from "@tailor-platform/sdk/runtime/idp";', + }), ], }, ]); diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index bbc122c6e..025bf8930 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -480,7 +480,8 @@ a self-named namespace object (for example, `iconv` from `import { get } from "@tailor-platform/sdk/runtime/aigateway"` are removed. The codemod rewrites straightforward namespace-star imports and flat named value imports. Review any remaining runtime subpath imports manually, especially when -a local binding or nested scope shadows the imported flat value. +a local binding or nested scope shadows the imported flat value, or when +type-position namespace member references need explicit top-level type imports. ``` diff --git a/packages/sdk/src/runtime/aigateway.ts b/packages/sdk/src/runtime/aigateway.ts index 333a72ab2..eb1a25906 100644 --- a/packages/sdk/src/runtime/aigateway.ts +++ b/packages/sdk/src/runtime/aigateway.ts @@ -46,14 +46,4 @@ const get: TailorAigatewayAPI["get"] = (...args) => api().get(...args); /** Runtime wrapper namespace for `tailor.aigateway`. */ export const aigateway = { get } as const satisfies TailorAigatewayAPI; -type AigatewayGetAIGatewayResult = GetAIGatewayResult; -type AigatewayTailorAigatewayAPI = TailorAigatewayAPI; - -// Type-only namespace merge preserves namespace type access without restoring flat value exports. -// oxlint-disable-next-line typescript/no-namespace -export namespace aigateway { - export type GetAIGatewayResult = AigatewayGetAIGatewayResult; - export type TailorAigatewayAPI = AigatewayTailorAigatewayAPI; -} - export default aigateway; diff --git a/packages/sdk/src/runtime/authconnection.ts b/packages/sdk/src/runtime/authconnection.ts index c436b13c0..d4e85d333 100644 --- a/packages/sdk/src/runtime/authconnection.ts +++ b/packages/sdk/src/runtime/authconnection.ts @@ -42,12 +42,4 @@ const getConnectionToken: TailorAuthconnectionAPI["getConnectionToken"] = (...ar /** Runtime wrapper namespace for `tailor.authconnection`. */ export const authconnection = { getConnectionToken } as const satisfies TailorAuthconnectionAPI; -type AuthconnectionTailorAuthconnectionAPI = TailorAuthconnectionAPI; - -// Type-only namespace merge preserves namespace type access without restoring flat value exports. -// oxlint-disable-next-line typescript/no-namespace -export namespace authconnection { - export type TailorAuthconnectionAPI = AuthconnectionTailorAuthconnectionAPI; -} - export default authconnection; diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index a4a789bc0..021fa15be 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -71,16 +71,4 @@ function getInvoker(): Invoker | null { /** Runtime wrapper namespace for `tailor.context`. */ export const context = { getInvoker } as const; -type ContextInvokerResult = Invoker; -type RuntimeContextInvoker = ContextInvoker; -type ContextTailorContextAPI = TailorContextAPI; - -// Type-only namespace merge preserves namespace type access without restoring flat value exports. -// oxlint-disable-next-line typescript/no-namespace -export namespace context { - export type Invoker = ContextInvokerResult; - export type ContextInvoker = RuntimeContextInvoker; - export type TailorContextAPI = ContextTailorContextAPI; -} - export default context; diff --git a/packages/sdk/src/runtime/file.ts b/packages/sdk/src/runtime/file.ts index eacdf72c8..7469739fc 100644 --- a/packages/sdk/src/runtime/file.ts +++ b/packages/sdk/src/runtime/file.ts @@ -282,34 +282,4 @@ export const file = { uploadStream, } as const satisfies TailorDBFileAPI & { deleteFile: TailorDBFileAPI["delete"] }; -type FileUploadMetadata = UploadMetadata; -type FileDownloadMetadata = DownloadMetadata; -type RuntimeFileMetadata = FileMetadata; -type RuntimeFileUploadOptions = FileUploadOptions; -type RuntimeFileUploadStreamOptions = FileUploadStreamOptions; -type RuntimeFileUploadResponse = FileUploadResponse; -type RuntimeFileDownloadResponse = FileDownloadResponse; -type RuntimeFileDownloadAsBase64Response = FileDownloadAsBase64Response; -type RuntimeFileDownloadStreamResponse = FileDownloadStreamResponse; -type RuntimeTailorDBFileErrorCode = TailorDBFileErrorCode; -type RuntimeTailorDBFileError = TailorDBFileError; -type RuntimeTailorDBFileAPI = TailorDBFileAPI; - -// Type-only namespace merge preserves namespace type access without restoring flat value exports. -// oxlint-disable-next-line typescript/no-namespace -export namespace file { - export type UploadMetadata = FileUploadMetadata; - export type DownloadMetadata = FileDownloadMetadata; - export type FileMetadata = RuntimeFileMetadata; - export type FileUploadOptions = RuntimeFileUploadOptions; - export type FileUploadStreamOptions = RuntimeFileUploadStreamOptions; - export type FileUploadResponse = RuntimeFileUploadResponse; - export type FileDownloadResponse = RuntimeFileDownloadResponse; - export type FileDownloadAsBase64Response = RuntimeFileDownloadAsBase64Response; - export type FileDownloadStreamResponse = RuntimeFileDownloadStreamResponse; - export type TailorDBFileErrorCode = RuntimeTailorDBFileErrorCode; - export type TailorDBFileError = RuntimeTailorDBFileError; - export type TailorDBFileAPI = RuntimeTailorDBFileAPI; -} - export default file; diff --git a/packages/sdk/src/runtime/iconv.ts b/packages/sdk/src/runtime/iconv.ts index ca023d60e..301f6804b 100644 --- a/packages/sdk/src/runtime/iconv.ts +++ b/packages/sdk/src/runtime/iconv.ts @@ -84,7 +84,7 @@ export interface TailorIconvAPI { */ encodings(): string[]; - /** Constructor for the stateful {@link Iconv} converter. */ + /** Constructor for the stateful converter. */ Iconv: IconvConstructor; } @@ -122,7 +122,8 @@ class Iconv { } } -/** Runtime wrapper namespace for `tailor.iconv`. */ +// Keep the object typed to the public API so the private wrapper class does not leak into d.ts. +/** Runtime API for `tailor.iconv`. */ export const iconv: TailorIconvAPI = { convert, convertBuffer, @@ -132,18 +133,4 @@ export const iconv: TailorIconvAPI = { Iconv, }; -type RuntimeIconvInstance = IconvInstance; -type RuntimeIconvConstructor = IconvConstructor; -type RuntimeTailorIconvAPI = TailorIconvAPI; -type RuntimeIconv = InstanceType; - -// Type-only namespace merge preserves namespace type access without restoring flat value exports. -// oxlint-disable-next-line typescript/no-namespace -export namespace iconv { - export type IconvInstance = RuntimeIconvInstance; - export type IconvConstructor = RuntimeIconvConstructor; - export type TailorIconvAPI = RuntimeTailorIconvAPI; - export type Iconv = RuntimeIconv; -} - export default iconv; diff --git a/packages/sdk/src/runtime/idp.ts b/packages/sdk/src/runtime/idp.ts index 760d72e12..19314de70 100644 --- a/packages/sdk/src/runtime/idp.ts +++ b/packages/sdk/src/runtime/idp.ts @@ -11,7 +11,7 @@ * const { users } = await client.users({ first: 10 }); */ -/** Configuration object for {@link Client}. */ +/** Configuration object for `idp.Client`. */ export interface ClientConfig { namespace: string; } @@ -29,12 +29,12 @@ export interface User { mfaEnrolled: boolean; /** * Enrolled MFA second factor IDs. Pass an entry into - * {@link Client.unenrollMfa} to remove that factor. + * `idp.Client.unenrollMfa()` to remove that factor. */ mfaFactorIds: string[]; } -/** Filter options for {@link Client.users}. */ +/** Filter options for `idp.Client.users()`. */ export interface UserQuery { /** Filter by user IDs */ ids?: string[]; @@ -42,7 +42,7 @@ export interface UserQuery { names?: string[]; } -/** Pagination/filter options for {@link Client.users}. */ +/** Pagination/filter options for `idp.Client.users()`. */ export interface ListUsersOptions { /** Maximum number of users to return */ first?: number; @@ -52,14 +52,14 @@ export interface ListUsersOptions { query?: UserQuery; } -/** Response shape for {@link Client.users}. */ +/** Response shape for `idp.Client.users()`. */ export interface ListUsersResponse { users: User[]; nextPageToken: string | null; totalCount: number; } -/** Input for {@link Client.createUser}. */ +/** Input for `idp.Client.createUser()`. */ export interface CreateUserInput { /** The user's name (typically email) */ name: string; @@ -69,7 +69,7 @@ export interface CreateUserInput { disabled?: boolean; } -/** Input for {@link Client.updateUser}. */ +/** Input for `idp.Client.updateUser()`. */ export interface UpdateUserInput { /** The user's ID */ id: string; @@ -83,7 +83,7 @@ export interface UpdateUserInput { disabled?: boolean; } -/** Input for {@link Client.sendPasswordResetEmail}. */ +/** Input for `idp.Client.sendPasswordResetEmail()`. */ export interface SendPasswordResetEmailInput { /** The ID of the user */ userId: string; @@ -95,7 +95,7 @@ export interface SendPasswordResetEmailInput { subject?: string; } -/** Input for {@link Client.unenrollMfa}. */ +/** Input for `idp.Client.unenrollMfa()`. */ export interface UnenrollMfaInput { /** The ID of the user whose factor will be unenrolled. */ userId: string; @@ -222,39 +222,8 @@ class Client { } } -/** Runtime wrapper namespace for `tailor.idp`. */ +// Keep the object typed to the public API so the private wrapper class does not leak into d.ts. +/** Runtime API for `tailor.idp`. */ export const idp: TailorIdpAPI = { Client }; -type RuntimeClientConfig = ClientConfig; -type RuntimeUser = User; -type RuntimeUserQuery = UserQuery; -type RuntimeListUsersOptions = ListUsersOptions; -type RuntimeListUsersResponse = ListUsersResponse; -type RuntimeCreateUserInput = CreateUserInput; -type RuntimeUpdateUserInput = UpdateUserInput; -type RuntimeSendPasswordResetEmailInput = SendPasswordResetEmailInput; -type RuntimeUnenrollMfaInput = UnenrollMfaInput; -type RuntimeIdpClientInstance = IdpClientInstance; -type RuntimeIdpClientConstructor = IdpClientConstructor; -type RuntimeTailorIdpAPI = TailorIdpAPI; -type RuntimeClient = InstanceType; - -// Type-only namespace merge preserves namespace type access without restoring flat value exports. -// oxlint-disable-next-line typescript/no-namespace -export namespace idp { - export type ClientConfig = RuntimeClientConfig; - export type User = RuntimeUser; - export type UserQuery = RuntimeUserQuery; - export type ListUsersOptions = RuntimeListUsersOptions; - export type ListUsersResponse = RuntimeListUsersResponse; - export type CreateUserInput = RuntimeCreateUserInput; - export type UpdateUserInput = RuntimeUpdateUserInput; - export type SendPasswordResetEmailInput = RuntimeSendPasswordResetEmailInput; - export type UnenrollMfaInput = RuntimeUnenrollMfaInput; - export type IdpClientInstance = RuntimeIdpClientInstance; - export type IdpClientConstructor = RuntimeIdpClientConstructor; - export type TailorIdpAPI = RuntimeTailorIdpAPI; - export type Client = RuntimeClient; -} - export default idp; diff --git a/packages/sdk/src/runtime/index.test.ts b/packages/sdk/src/runtime/index.test.ts index 3079eb35b..88506910a 100644 --- a/packages/sdk/src/runtime/index.test.ts +++ b/packages/sdk/src/runtime/index.test.ts @@ -5,17 +5,10 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import * as path from "node:path"; import ts from "typescript"; import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; -import { - file, - type iconv as runtimeIconv, - type idp as runtimeIdp, - type iconv, - type idp, -} from "#/runtime/index"; +import { file, type iconv as runtimeIconv, type idp as runtimeIdp } from "#/runtime/index"; import { cleanupMocks, injectMocks, mockFile } from "#/vitest/mock"; -import type { TailorDBFileErrorCode } from "#/runtime/file"; import type { IconvInstance } from "#/runtime/iconv"; -import type { ClientConfig, IdpClientInstance } from "#/runtime/idp"; +import type { IdpClientInstance } from "#/runtime/idp"; const fileArgs = ["ns", "Doc", "blob", "rec-1"] as const; const packageRoot = path.resolve(import.meta.dirname, "../.."); @@ -73,13 +66,7 @@ describe("@tailor-platform/sdk/runtime aggregate exports", () => { cleanupMocks(globalThis); }); - test("preserves namespace type access for aggregate imports", () => { - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - }); - - test("exposes named instance types for namespace constructors", () => { + test("exposes constructor instance types through namespace object values", () => { expectTypeOf>().toEqualTypeOf(); expectTypeOf>().toEqualTypeOf(); }); diff --git a/packages/sdk/src/runtime/secretmanager.ts b/packages/sdk/src/runtime/secretmanager.ts index 524b6bb6d..1e442768f 100644 --- a/packages/sdk/src/runtime/secretmanager.ts +++ b/packages/sdk/src/runtime/secretmanager.ts @@ -51,12 +51,4 @@ export const secretmanager = { getSecret, } as const satisfies TailorSecretmanagerAPI; -type SecretmanagerTailorSecretmanagerAPI = TailorSecretmanagerAPI; - -// Type-only namespace merge preserves namespace type access without restoring flat value exports. -// oxlint-disable-next-line typescript/no-namespace -export namespace secretmanager { - export type TailorSecretmanagerAPI = SecretmanagerTailorSecretmanagerAPI; -} - export default secretmanager; diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index e4a1441d4..95c22175c 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -117,18 +117,4 @@ export const workflow = { resolve, } as const; -type WorkflowInvoker = Invoker; -type WorkflowTriggerWorkflowOptions = TriggerWorkflowOptions; -type WorkflowPlatformTriggerWorkflowOptions = PlatformTriggerWorkflowOptions; -type WorkflowTailorWorkflowAPI = TailorWorkflowAPI; - -// Type-only namespace merge preserves namespace type access without restoring flat value exports. -// oxlint-disable-next-line typescript/no-namespace -export namespace workflow { - export type Invoker = WorkflowInvoker; - export type TriggerWorkflowOptions = WorkflowTriggerWorkflowOptions; - export type PlatformTriggerWorkflowOptions = WorkflowPlatformTriggerWorkflowOptions; - export type TailorWorkflowAPI = WorkflowTailorWorkflowAPI; -} - export default workflow; From 2da9b376ae6aa114385b20a5571c597b25277fd8 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 07:26:25 +0900 Subject: [PATCH 494/618] fix(cli): resolve tsconfig path aliases without baseUrl in ts-hook collectPathsInto only applied `paths` when `baseUrl` was also set, silently dropping alias resolution for tsconfig.json files that omit baseUrl (the standard TS 5.0+ style, resolved relative to the tsconfig directory). --- packages/sdk/src/cli/ts-hook.mjs | 4 ++-- packages/sdk/src/cli/ts-hook.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 7ad97696c..675e07242 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -38,8 +38,8 @@ function collectPathsInto(out, configFilePath, content, visited) { const opts = content.compilerOptions ?? {}; const rawPaths = opts.paths; - const baseUrl = opts.baseUrl; - if (rawPaths && baseUrl) { + const baseUrl = opts.baseUrl ?? "."; + if (rawPaths) { const absBase = resolvePath(baseDir, baseUrl); for (const [alias, targets] of Object.entries(rawPaths)) { out[alias] = targets.map((t) => { diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index 18fe3d30f..d9776e80f 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -172,6 +172,28 @@ describe("resolve", () => { vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + test("resolves non-relative specifier via tsconfig path alias without baseUrl", async () => { + const tsconfig = JSON.stringify({ + compilerOptions: { paths: { "@/*": ["./*"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///alias-project-no-baseurl/tailordb/user.ts" }; + const nextResolve = vi + .fn() + .mockRejectedValueOnce(notFound("@/tailordb/user")) + .mockResolvedValueOnce(resolved); + const result = await resolve( + "@/tailordb/user", + { parentURL: "file:///alias-project-no-baseurl/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + test("resolves tsconfig path alias when parentURL has tailorImportNonce query string", async () => { const tsconfig = JSON.stringify({ compilerOptions: { baseUrl: ".", paths: { "@/*": ["./*"] } }, From 958d571555a5c66e2e3b9beb3d2247f63cbb8f2c Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 08:38:40 +0900 Subject: [PATCH 495/618] fix(cli): guard non-string tsconfig baseUrl in ts-hook path alias resolution `baseUrl ?? "."` left non-string values (e.g. a boolean) from a malformed tsconfig.json passed straight to path.resolve(), which throws a TypeError. Fall back to "." unless baseUrl is actually a string. Also add a matching resolveSync regression test for the baseUrl- omitted case, and a changeset for the fix. --- .../fix-ts-hook-paths-without-baseurl.md | 5 ++ packages/sdk/src/cli/ts-hook.mjs | 2 +- packages/sdk/src/cli/ts-hook.test.ts | 46 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-ts-hook-paths-without-baseurl.md diff --git a/.changeset/fix-ts-hook-paths-without-baseurl.md b/.changeset/fix-ts-hook-paths-without-baseurl.md new file mode 100644 index 000000000..950c017ed --- /dev/null +++ b/.changeset/fix-ts-hook-paths-without-baseurl.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": patch +--- + +Fix `tailor` CLI failing with `ERR_MODULE_NOT_FOUND` when resolving `tsconfig.json` path aliases (`compilerOptions.paths`) in projects that omit `baseUrl`, which is the standard style since TypeScript 5.0. diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 675e07242..30e6d3cf9 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -38,7 +38,7 @@ function collectPathsInto(out, configFilePath, content, visited) { const opts = content.compilerOptions ?? {}; const rawPaths = opts.paths; - const baseUrl = opts.baseUrl ?? "."; + const baseUrl = typeof opts.baseUrl === "string" ? opts.baseUrl : "."; if (rawPaths) { const absBase = resolvePath(baseDir, baseUrl); for (const [alias, targets] of Object.entries(rawPaths)) { diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index d9776e80f..138af2bc3 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -194,6 +194,28 @@ describe("resolve", () => { vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + test("falls back to tsconfig directory when baseUrl is a non-string value", async () => { + const tsconfig = JSON.stringify({ + compilerOptions: { baseUrl: true, paths: { "@/*": ["./*"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///alias-project-bad-baseurl/tailordb/user.ts" }; + const nextResolve = vi + .fn() + .mockRejectedValueOnce(notFound("@/tailordb/user")) + .mockResolvedValueOnce(resolved); + const result = await resolve( + "@/tailordb/user", + { parentURL: "file:///alias-project-bad-baseurl/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + test("resolves tsconfig path alias when parentURL has tailorImportNonce query string", async () => { const tsconfig = JSON.stringify({ compilerOptions: { baseUrl: ".", paths: { "@/*": ["./*"] } }, @@ -410,4 +432,28 @@ describe("resolveSync", () => { expect(result).toEqual(resolved); vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + + test("resolves non-relative specifier via tsconfig path alias without baseUrl", () => { + const tsconfig = JSON.stringify({ + compilerOptions: { paths: { "@/*": ["./*"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///alias-sync-project-no-baseurl/tailordb/user.ts" }; + const nextResolve = vi + .fn() + .mockImplementationOnce(() => { + throw notFound("@/tailordb/user"); + }) + .mockReturnValueOnce(resolved); + const result = resolveSync( + "@/tailordb/user", + { parentURL: "file:///alias-sync-project-no-baseurl/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); }); From 1b35df21876490a75c10bacc697bf9286a236005 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 10 Jul 2026 13:06:46 +0900 Subject: [PATCH 496/618] refactor(runtime): separate wrapper API contracts --- .../configure/services/workflow/wait-point.ts | 4 +-- packages/sdk/src/runtime/context.test.ts | 7 +++- packages/sdk/src/runtime/context.ts | 15 +++++++-- packages/sdk/src/runtime/index.ts | 8 ++--- packages/sdk/src/runtime/workflow.test.ts | 13 +++++++- packages/sdk/src/runtime/workflow.ts | 32 ++++++++++++++----- packages/sdk/src/vitest/workflow-local.ts | 8 ++--- 7 files changed, 64 insertions(+), 23 deletions(-) diff --git a/packages/sdk/src/configure/services/workflow/wait-point.ts b/packages/sdk/src/configure/services/workflow/wait-point.ts index e3b7cf858..99c44c945 100644 --- a/packages/sdk/src/configure/services/workflow/wait-point.ts +++ b/packages/sdk/src/configure/services/workflow/wait-point.ts @@ -1,5 +1,5 @@ import { brandValue } from "#/utils/brand"; -import type { TailorWorkflowAPI } from "#/runtime/workflow"; +import type { PlatformWorkflowAPI } from "#/runtime/workflow"; import type { JsonCompatible, TypeLevelError } from "#/types/helpers"; /** @@ -37,7 +37,7 @@ interface WaitPointWithSetter { } function getPlatformWorkflow() { - const platform = globalThis as { tailor?: { workflow?: TailorWorkflowAPI } }; + const platform = globalThis as { tailor?: { workflow?: PlatformWorkflowAPI } }; const workflow = platform.tailor?.workflow; if (!workflow) { throw new Error( diff --git a/packages/sdk/src/runtime/context.test.ts b/packages/sdk/src/runtime/context.test.ts index 51a1a4a8d..8b220233f 100644 --- a/packages/sdk/src/runtime/context.test.ts +++ b/packages/sdk/src/runtime/context.test.ts @@ -2,7 +2,7 @@ * Tests for `@tailor-platform/sdk/runtime/context` typed wrappers. */ import { afterEach, beforeEach, describe, expect, expectTypeOf, test, vi } from "vitest"; -import defaultContext, { context, type Invoker } from "#/runtime/context"; +import defaultContext, { context, type Invoker, type TailorContextAPI } from "#/runtime/context"; import { cleanupMocks, injectMocks } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/context", () => { @@ -18,6 +18,11 @@ describe("@tailor-platform/sdk/runtime/context", () => { expect(defaultContext).toBe(context); }); + test("exposes the normalized wrapper contract", () => { + expectTypeOf(context).toExtend(); + expectTypeOf>().toEqualTypeOf(); + }); + test("getInvoker returns null for anonymous invocations", () => { const result = context.getInvoker(); diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index 021fa15be..e05c736a9 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -45,10 +45,19 @@ export interface ContextInvoker { * runtime injects on `globalThis.tailor.context`. * @internal */ -export interface TailorContextAPI { +export interface PlatformContextAPI { getInvoker(): ContextInvoker | null; } +/** Runtime wrapper API for execution context utilities. */ +export interface TailorContextAPI { + /** + * Returns information about the current invoker. + * @returns Invoker details, or `null` for anonymous invocations + */ + getInvoker(): Invoker | null; +} + /** * Returns information about the invoker of the current function execution, * or `null` for anonymous invocations. @@ -56,7 +65,7 @@ export interface TailorContextAPI { */ function getInvoker(): Invoker | null { const raw = ( - globalThis as unknown as { tailor: { context: TailorContextAPI } } + globalThis as unknown as { tailor: { context: PlatformContextAPI } } ).tailor.context.getInvoker(); if (!raw) return null; return { @@ -69,6 +78,6 @@ function getInvoker(): Invoker | null { } /** Runtime wrapper namespace for `tailor.context`. */ -export const context = { getInvoker } as const; +export const context = { getInvoker } as const satisfies TailorContextAPI; export default context; diff --git a/packages/sdk/src/runtime/index.ts b/packages/sdk/src/runtime/index.ts index 5d66f997f..366b579af 100644 --- a/packages/sdk/src/runtime/index.ts +++ b/packages/sdk/src/runtime/index.ts @@ -17,12 +17,12 @@ import type { TailorAigatewayAPI } from "./aigateway"; import type { TailorAuthconnectionAPI } from "./authconnection"; -import type { TailorContextAPI } from "./context"; +import type { PlatformContextAPI } 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"; +import type { PlatformWorkflowAPI } from "./workflow"; export { iconv } from "./iconv"; export { secretmanager } from "./secretmanager"; @@ -69,8 +69,8 @@ export interface TailorRuntime { authconnection: TailorAuthconnectionAPI; iconv: TailorIconvAPI; idp: TailorIdpAPI; - workflow: TailorWorkflowAPI; - context: TailorContextAPI; + workflow: PlatformWorkflowAPI; + context: PlatformContextAPI; aigateway: TailorAigatewayAPI; } diff --git a/packages/sdk/src/runtime/workflow.test.ts b/packages/sdk/src/runtime/workflow.test.ts index 49d219f20..76d7af9ae 100644 --- a/packages/sdk/src/runtime/workflow.test.ts +++ b/packages/sdk/src/runtime/workflow.test.ts @@ -2,7 +2,11 @@ * Tests for `@tailor-platform/sdk/runtime/workflow` typed wrappers. */ import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; -import defaultWorkflow, { workflow } from "#/runtime/workflow"; +import defaultWorkflow, { + workflow, + type TailorWorkflowAPI, + type TriggerWorkflowOptions, +} from "#/runtime/workflow"; import { cleanupMocks, injectMocks, mockWorkflow } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/workflow", () => { @@ -18,6 +22,13 @@ describe("@tailor-platform/sdk/runtime/workflow", () => { expect(defaultWorkflow).toBe(workflow); }); + test("exposes the wrapper trigger options", () => { + expectTypeOf(workflow).toExtend(); + expectTypeOf[2]>().toEqualTypeOf< + TriggerWorkflowOptions | undefined + >(); + }); + test("triggerWorkflow forwards args and returns Promise", async () => { using wf = mockWorkflow(); wf.setTriggerHandler("exec-42"); diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index 95c22175c..08f522f34 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -37,7 +37,7 @@ export interface PlatformTriggerWorkflowOptions { * Platform API surface for `tailor.workflow`. Describes the shape the platform * runtime injects on `globalThis.tailor.workflow`. */ -export interface TailorWorkflowAPI { +export interface PlatformWorkflowAPI { /** * Triggers a workflow and returns its execution ID. * @param workflowName - Workflow name as defined in tailor.config @@ -84,8 +84,24 @@ export interface TailorWorkflowAPI { resolve(executionId: string, key: string, callback: (waitPayload: any) => any): Promise; } -const api = (): TailorWorkflowAPI => - (globalThis as unknown as { tailor: { workflow: TailorWorkflowAPI } }).tailor.workflow; +/** Runtime wrapper API for workflow and job control. */ +export interface TailorWorkflowAPI extends Omit { + /** + * 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 SDK trigger options + * @returns The execution ID of the triggered workflow + */ + triggerWorkflow( + workflowName: string, + args?: any, + options?: TriggerWorkflowOptions, + ): Promise; +} + +const api = (): PlatformWorkflowAPI => + (globalThis as unknown as { tailor: { workflow: PlatformWorkflowAPI } }).tailor.workflow; function triggerWorkflow( workflowName: string, @@ -98,15 +114,15 @@ function triggerWorkflow( return api().triggerWorkflow(workflowName, args, { authInvoker: options.invoker }); } -const resumeWorkflow: TailorWorkflowAPI["resumeWorkflow"] = (...args) => +const resumeWorkflow: PlatformWorkflowAPI["resumeWorkflow"] = (...args) => api().resumeWorkflow(...args); -const triggerJobFunction: TailorWorkflowAPI["triggerJobFunction"] = (...args) => +const triggerJobFunction: PlatformWorkflowAPI["triggerJobFunction"] = (...args) => api().triggerJobFunction(...args); -const wait: TailorWorkflowAPI["wait"] = (...args) => api().wait(...args); +const wait: PlatformWorkflowAPI["wait"] = (...args) => api().wait(...args); -const resolve: TailorWorkflowAPI["resolve"] = (...args) => api().resolve(...args); +const resolve: PlatformWorkflowAPI["resolve"] = (...args) => api().resolve(...args); /** Runtime wrapper namespace for `tailor.workflow`. */ export const workflow = { @@ -115,6 +131,6 @@ export const workflow = { triggerJobFunction, wait, resolve, -} as const; +} as const satisfies TailorWorkflowAPI; export default workflow; diff --git a/packages/sdk/src/vitest/workflow-local.ts b/packages/sdk/src/vitest/workflow-local.ts index deda5c1d3..956566738 100644 --- a/packages/sdk/src/vitest/workflow-local.ts +++ b/packages/sdk/src/vitest/workflow-local.ts @@ -9,7 +9,7 @@ import { import { platformSerialize } from "../utils/test/platform-serialize"; import type { Workflow, WorkflowJob } from "../configure/services/workflow"; import type { TailorEnv } from "../runtime/types"; -import type { TailorWorkflowAPI } from "../runtime/workflow"; +import type { PlatformWorkflowAPI } from "../runtime/workflow"; type AnyWorkflowJob = WorkflowJob; type AnyWorkflow = Workflow; @@ -21,7 +21,7 @@ type WorkflowOutput = type GlobalWithTailor = { tailor?: { - workflow?: TailorWorkflowAPI; + workflow?: PlatformWorkflowAPI; }; }; @@ -240,9 +240,9 @@ function assertSameTrigger(record: TriggerRecord, jobName: string, args: unknown } function createLocalWorkflowRuntime( - previous: TailorWorkflowAPI | undefined, + previous: PlatformWorkflowAPI | undefined, triggerJobFunction: (name: string, args?: unknown) => unknown, -): TailorWorkflowAPI { +): PlatformWorkflowAPI { return { triggerJobFunction, triggerWorkflow: async (name, args, options) => { From 420748155572c337494f4b07a28e5c2917fb4880 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 13:28:31 +0900 Subject: [PATCH 497/618] fix(cli): inherit extended baseUrl and replace paths in ts-hook collectPathsInto resolved a child config's paths against its own directory even when baseUrl was only defined in an extended parent config, and merged inherited paths instead of replacing them the way TypeScript does when a child config defines its own paths. Verified both behaviors against tsc directly before fixing: baseUrl resolves relative to the config file that defines it, and a child's own paths replaces (not merges with) inherited paths. Propagate the effective baseUrl up through the extends recursion so a child without its own baseUrl can still use one inherited from an extended config, and clear previously collected paths before writing a config's own paths. --- .../fix-ts-hook-paths-without-baseurl.md | 2 +- packages/sdk/src/cli/ts-hook.mjs | 21 +++++-- packages/sdk/src/cli/ts-hook.test.ts | 55 +++++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/.changeset/fix-ts-hook-paths-without-baseurl.md b/.changeset/fix-ts-hook-paths-without-baseurl.md index 950c017ed..4c6e0d51b 100644 --- a/.changeset/fix-ts-hook-paths-without-baseurl.md +++ b/.changeset/fix-ts-hook-paths-without-baseurl.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk": patch --- -Fix `tailor` CLI failing with `ERR_MODULE_NOT_FOUND` when resolving `tsconfig.json` path aliases (`compilerOptions.paths`) in projects that omit `baseUrl`, which is the standard style since TypeScript 5.0. +Fix `tailor` CLI failing with `ERR_MODULE_NOT_FOUND` when resolving `tsconfig.json` path aliases (`compilerOptions.paths`) in projects that omit `baseUrl`, which is the standard style since TypeScript 5.0. Also fix path alias resolution to match TypeScript's `extends` behavior: a child config's `baseUrl` is now correctly inherited from an extended config, and a child config's own `paths` now replaces (rather than merges with) inherited `paths`. diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 30e6d3cf9..8b54c2b3a 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -18,29 +18,38 @@ const KNOWN_EXTENSIONS = [".ts", ".tsx", ".mts", ".js", ".mjs", ".json"]; const tsconfigPathsCache = new Map(); +// Returns the effective (own or inherited) baseUrl, so a child config whose +// own `paths` omits `baseUrl` can still resolve against a base defined by +// whatever config it `extends`, rather than always falling back to its own +// directory. function collectPathsInto(out, configFilePath, content, visited) { - if (visited.has(configFilePath)) return; + if (visited.has(configFilePath)) return undefined; visited.add(configFilePath); const baseDir = dirname(configFilePath); + const opts = content.compilerOptions ?? {}; + const ownBaseUrl = typeof opts.baseUrl === "string" ? resolvePath(baseDir, opts.baseUrl) : undefined; + let inheritedBaseUrl; const extendsField = content.extends; if (typeof extendsField === "string") { const base = resolvePath(baseDir, extendsField); const extendsPath = base.endsWith(".json") ? base : base + ".json"; try { const sub = JSON.parse(readFileSync(extendsPath, "utf-8")); - collectPathsInto(out, extendsPath, sub, visited); + inheritedBaseUrl = collectPathsInto(out, extendsPath, sub, visited); } catch (e) { if (e?.code !== "ENOENT" && !(e instanceof SyntaxError)) throw e; } } - const opts = content.compilerOptions ?? {}; + const effectiveBaseUrl = ownBaseUrl ?? inheritedBaseUrl; + const rawPaths = opts.paths; - const baseUrl = typeof opts.baseUrl === "string" ? opts.baseUrl : "."; if (rawPaths) { - const absBase = resolvePath(baseDir, baseUrl); + // TypeScript replaces (not merges) inherited `paths` when a config defines its own. + for (const key of Object.keys(out)) delete out[key]; + const absBase = effectiveBaseUrl ?? baseDir; for (const [alias, targets] of Object.entries(rawPaths)) { out[alias] = targets.map((t) => { const isWildcard = t.endsWith("/*"); @@ -49,6 +58,8 @@ function collectPathsInto(out, configFilePath, content, visited) { }); } } + + return effectiveBaseUrl; } function loadTsconfigPaths(startDir) { diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index 138af2bc3..5a16c8875 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -263,6 +263,61 @@ describe("resolve", () => { vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + test("resolves child paths using the extended config's baseUrl directory, not the child config's directory", async () => { + const baseConfig = JSON.stringify({ + compilerOptions: { baseUrl: "shared-base" }, + }); + const rootConfig = JSON.stringify({ + extends: "./tsconfig.base.json", + compilerOptions: { paths: { "@app/*": ["./*"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + const p = String(path); + if (p.endsWith("tsconfig.base.json")) return baseConfig as unknown as string; + if (p.endsWith("tsconfig.json")) return rootConfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const nextResolve = vi.fn().mockRejectedValue(notFound("@app/tailordb/user")); + await expect( + resolve( + "@app/tailordb/user", + { parentURL: "file:///inherited-baseurl-project/tailor.config.ts" }, + nextResolve, + ), + ).rejects.toMatchObject({ code: "ERR_MODULE_NOT_FOUND" }); + expect(nextResolve).toHaveBeenCalledWith( + expect.stringContaining("inherited-baseurl-project/shared-base/tailordb/user.ts"), + expect.anything(), + ); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + + test("replaces inherited paths instead of merging when child config defines its own paths", async () => { + const baseConfig = JSON.stringify({ + compilerOptions: { baseUrl: ".", paths: { "@parent/*": ["./parent-src/*"] } }, + }); + const rootConfig = JSON.stringify({ + extends: "./tsconfig.base.json", + compilerOptions: { baseUrl: ".", paths: { "@child/*": ["./child-src/*"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + const p = String(path); + if (p.endsWith("tsconfig.base.json")) return baseConfig as unknown as string; + if (p.endsWith("tsconfig.json")) return rootConfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const nextResolve = vi.fn().mockRejectedValue(notFound("@parent/foo")); + await expect( + resolve( + "@parent/foo", + { parentURL: "file:///replace-paths-project/tailor.config.ts" }, + nextResolve, + ), + ).rejects.toMatchObject({ code: "ERR_MODULE_NOT_FOUND" }); + expect(nextResolve).toHaveBeenCalledTimes(1); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + test("prefers more specific wildcard alias over less specific", async () => { const tsconfig = JSON.stringify({ compilerOptions: { From 58d82ea1b5c8874eac7a2fad1cff13ec1787c711 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 10 Jul 2026 13:20:26 +0900 Subject: [PATCH 498/618] feat(runtime): use named-only namespace exports --- .changeset/runtime-subpath-namespace.md | 6 +- .../v2/runtime-subpath-namespace/codemod.yaml | 2 +- .../scripts/transform.ts | 394 ++++++++++++++++-- .../tests/aggregate-file-delete/expected.ts | 23 + .../tests/aggregate-file-delete/input.ts | 23 + .../tests/namespace-import/expected.ts | 4 +- .../tests/namespace-import/input.ts | 4 +- packages/sdk-codemod/src/registry.ts | 24 +- packages/sdk-codemod/src/runner.test.ts | 41 ++ packages/sdk/docs/migration/v2.md | 32 +- packages/sdk/docs/runtime.md | 4 +- packages/sdk/src/runtime/aigateway.test.ts | 6 +- packages/sdk/src/runtime/aigateway.ts | 2 - .../sdk/src/runtime/authconnection.test.ts | 6 +- packages/sdk/src/runtime/authconnection.ts | 2 - packages/sdk/src/runtime/context.test.ts | 6 +- packages/sdk/src/runtime/context.ts | 2 - packages/sdk/src/runtime/file.test.ts | 10 +- packages/sdk/src/runtime/file.ts | 5 +- packages/sdk/src/runtime/iconv.test.ts | 6 +- packages/sdk/src/runtime/iconv.ts | 2 - packages/sdk/src/runtime/idp.test.ts | 6 +- packages/sdk/src/runtime/idp.ts | 2 - packages/sdk/src/runtime/index.test.ts | 48 +-- .../sdk/src/runtime/secretmanager.test.ts | 6 +- packages/sdk/src/runtime/secretmanager.ts | 2 - packages/sdk/src/runtime/workflow.test.ts | 10 +- packages/sdk/src/runtime/workflow.ts | 2 - 28 files changed, 529 insertions(+), 151 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aggregate-file-delete/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aggregate-file-delete/input.ts diff --git a/.changeset/runtime-subpath-namespace.md b/.changeset/runtime-subpath-namespace.md index 55613eeeb..3d02f66b0 100644 --- a/.changeset/runtime-subpath-namespace.md +++ b/.changeset/runtime-subpath-namespace.md @@ -4,6 +4,8 @@ "@tailor-platform/create-sdk": patch --- -Remove flat value exports from `@tailor-platform/sdk/runtime/*` subpath modules. Import each subpath through its default export or self-named namespace export instead, for example `import iconv from "@tailor-platform/sdk/runtime/iconv"` or `import { iconv } from "@tailor-platform/sdk/runtime/iconv"`. +Remove flat value and default exports from `@tailor-platform/sdk/runtime/*` subpath modules. Import each subpath through its self-named namespace export instead, for example `import { iconv } from "@tailor-platform/sdk/runtime/iconv"`. -The aggregate `@tailor-platform/sdk/runtime` namespace imports are unchanged. The v2 codemod rewrites straightforward namespace-star subpath imports and flat named value imports to the new namespace-object style. +The aggregate `@tailor-platform/sdk/runtime` entry remains named-only, and its deprecated `file.deleteFile` alias is removed in favor of `file.delete`. The v2 codemod rewrites straightforward namespace-star subpath imports, flat named value imports, and aggregate `file.deleteFile` calls to the new namespace-object style. + +`TailorContextAPI` and `TailorWorkflowAPI` now describe the SDK wrapper objects. Code that types the platform-provided `globalThis.tailor.context` or `globalThis.tailor.workflow` objects directly must use `PlatformContextAPI` or `PlatformWorkflowAPI` instead. diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/codemod.yaml b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/codemod.yaml index b446fb35c..64ea37424 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/codemod.yaml +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/codemod.yaml @@ -1,6 +1,6 @@ name: "@tailor-platform/runtime-subpath-namespace" version: "1.0.0" -description: "Rewrite runtime subpath namespace-star and flat value imports to v2 namespace object imports" +description: "Rewrite runtime namespace imports and aggregate file.deleteFile calls to v2 APIs" engine: jssg language: typescript since: "1.0.0" diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts index 5a1531c72..791a79526 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/scripts/transform.ts @@ -100,11 +100,12 @@ const RUNTIME_MODULES: RuntimeModule[] = [ ]; const MODULES_BY_SOURCE = new Map(RUNTIME_MODULES.map((mod) => [mod.source, mod])); +const AGGREGATE_RUNTIME_SOURCE = "@tailor-platform/sdk/runtime"; const JSX_FILE_EXTENSIONS = new Set([".tsx", ".jsx"]); const JS_FILE_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]); function quickFilter(source: string): boolean { - return source.includes("@tailor-platform/sdk/runtime/"); + return source.includes(AGGREGATE_RUNTIME_SOURCE); } function sourceLang(filePath: string, source: string): Lang { @@ -121,12 +122,11 @@ function importClause(importStmt: SgNode): SgNode | null { return importStmt.children().find((child) => child.kind() === "import_clause") ?? null; } -function defaultImportName(importStmt: SgNode): string | null { +function hasDefaultImport(importStmt: SgNode): boolean { return ( importClause(importStmt) ?.children() - .find((child) => child.kind() === "identifier") - ?.text() ?? null + .some((child) => child.kind() === "identifier") ?? false ); } @@ -143,7 +143,6 @@ function namespaceImportName(importStmt: SgNode): string | null { function formatImport( source: string, - defaultName: string | null, namedSpecs: string[], typeOnly = false, attributeText = "", @@ -151,10 +150,6 @@ function formatImport( const importKeyword = typeOnly ? "import type" : "import"; const named = namedSpecs.length > 0 ? `{ ${namedSpecs.join(", ")} }` : null; const attributes = attributeText === "" ? "" : ` ${attributeText}`; - if (defaultName && named) { - return `${importKeyword} ${defaultName}, ${named} from "${source}"${attributes};`; - } - if (defaultName) return `${importKeyword} ${defaultName} from "${source}"${attributes};`; if (named) return `${importKeyword} ${named} from "${source}"${attributes};`; return ""; } @@ -315,6 +310,325 @@ function findExportStatements(root: SgNode): SgNode[] { .toSorted((a, b) => a.range().start.index - b.range().start.index); } +function aggregateFileImportLocals(imports: SgNode[]): Set { + const locals = new Set(); + for (const importStmt of imports) { + if (importSource(importStmt) !== AGGREGATE_RUNTIME_SOURCE) continue; + for (const binding of importBindings(importStmt)) { + if (!binding.typeOnly && binding.importedName === "file") locals.add(binding.localName); + } + } + return locals; +} + +function isValueBindingLeafKind(kind: ReturnType): boolean { + return kind === "identifier" || kind === "shorthand_property_identifier_pattern"; +} + +function isValueBindingPatternKind(kind: ReturnType): boolean { + return ( + isValueBindingLeafKind(kind) || + kind === "object_pattern" || + kind === "array_pattern" || + kind === "rest_pattern" + ); +} + +function collectValueBindingNames(node: SgNode, names: Set, result: Set): void { + if (isValueBindingLeafKind(node.kind())) { + if (names.has(node.text())) result.add(node.text()); + return; + } + + for (const child of node.children()) { + if (child.kind() === "property_identifier") continue; + if (child.kind() === "=") break; + collectValueBindingNames(child, names, result); + } +} + +function valueBindingNames(node: SgNode, names: Set): Set { + const result = new Set(); + collectValueBindingNames(node, names, result); + return result; +} + +function directValueBindingNames(node: SgNode, names: Set): Set { + const result = new Set(); + for (const child of node.children()) { + if (child.kind() === "=") break; + if (isValueBindingPatternKind(child.kind())) { + collectValueBindingNames(child, names, result); + } + } + return result; +} + +function firstDeclaratorChild(node: SgNode): SgNode | null { + return node.children().find((child) => child.kind() !== "=") ?? null; +} + +type ShadowedRanges = Map>; + +function addShadowedRange(ranges: ShadowedRanges, name: string, scope: SgNode): void { + const range = scope.range(); + const existing = ranges.get(name) ?? []; + existing.push({ start: range.start.index, end: range.end.index }); + ranges.set(name, existing); +} + +function nearestValueScope(node: SgNode): SgNode { + let current = node.parent(); + while (current) { + const kind = current.kind(); + if ( + kind === "statement_block" || + kind === "program" || + kind === "switch_body" || + kind === "for_statement" || + kind === "for_in_statement" + ) { + return current; + } + current = current.parent(); + } + return node; +} + +function functionValueScope(node: SgNode): SgNode { + let current = node.parent(); + while (current) { + const kind = current.kind(); + if ( + kind === "function_declaration" || + kind === "function_expression" || + kind === "arrow_function" || + kind === "method_definition" || + kind === "program" + ) { + return current; + } + current = current.parent(); + } + return node; +} + +function variableValueScope(node: SgNode): SgNode { + const declaration = node.parent(); + return /^var\b/.test(declaration?.text().trimStart() ?? "") + ? functionValueScope(node) + : nearestValueScope(node); +} + +function parameterValueScope(node: SgNode): SgNode { + let current = node.parent(); + while (current) { + const kind = current.kind(); + if (kind === "formal_parameters") { + current = current.parent(); + continue; + } + if ( + kind === "function_declaration" || + kind === "function_expression" || + kind === "arrow_function" || + kind === "method_definition" + ) { + return current; + } + break; + } + return nearestValueScope(node); +} + +function buildShadowedRanges(root: SgNode, names: Set): ShadowedRanges { + const ranges: ShadowedRanges = new Map(); + + for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) { + if (isInsideImportStatement(decl)) continue; + const binding = firstDeclaratorChild(decl); + if (!binding) continue; + for (const name of valueBindingNames(binding, names)) { + addShadowedRange(ranges, name, variableValueScope(decl)); + } + } + + for (const decl of root.findAll({ + rule: { + any: [ + { kind: "function_declaration" }, + { kind: "class_declaration" }, + { kind: "enum_declaration" }, + ], + }, + })) { + const name = decl + .children() + .find((child) => child.kind() === "identifier" && names.has(child.text())); + if (name) addShadowedRange(ranges, name.text(), nearestValueScope(decl)); + } + + for (const expression of root.findAll({ + rule: { any: [{ kind: "function_expression" }, { kind: "class" }] }, + })) { + const name = expression + .children() + .find( + (child) => + (child.kind() === "identifier" || child.kind() === "type_identifier") && + names.has(child.text()), + ); + if (name) addShadowedRange(ranges, name.text(), expression); + } + + for (const param of root.findAll({ + rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] }, + })) { + for (const name of directValueBindingNames(param, names)) { + addShadowedRange(ranges, name, parameterValueScope(param)); + } + } + + for (const arrow of root.findAll({ rule: { kind: "arrow_function" } })) { + const children = arrow.children(); + const arrowIndex = children.findIndex((child) => child.kind() === "=>"); + if (arrowIndex === -1) continue; + for (const child of children.slice(0, arrowIndex)) { + if (child.kind() === "=") break; + if (!isValueBindingPatternKind(child.kind())) continue; + for (const name of valueBindingNames(child, names)) { + addShadowedRange(ranges, name, arrow); + } + } + } + + for (const catchClause of root.findAll({ rule: { kind: "catch_clause" } })) { + for (const name of directValueBindingNames(catchClause, names)) { + addShadowedRange(ranges, name, catchClause); + } + } + + for (const loop of root.findAll({ rule: { kind: "for_in_statement" } })) { + const children = loop.children(); + const keywordIndex = children.findIndex( + (child) => child.kind() === "in" || child.kind() === "of", + ); + if (keywordIndex === -1) continue; + for (const child of children.slice(0, keywordIndex)) { + for (const name of valueBindingNames(child, names)) { + addShadowedRange(ranges, name, loop); + } + } + } + + return ranges; +} + +function isShadowed(node: SgNode, ranges: ShadowedRanges): boolean { + const candidates = ranges.get(node.text()); + if (!candidates) return false; + const position = node.range().start.index; + return candidates.some((range) => position >= range.start && position < range.end); +} + +interface AggregateFileDeleteAccess { + node: SgNode; + property: SgNode; + computed: boolean; +} + +function isAggregateFileReceiver( + node: SgNode | null, + fileLocals: Set, + shadowedRanges: ShadowedRanges, +): node is SgNode { + return ( + node?.kind() === "identifier" && + fileLocals.has(node.text()) && + !isShadowed(node, shadowedRanges) + ); +} + +function aggregateFileDeleteAccesses(root: SgNode, imports: SgNode[]): AggregateFileDeleteAccess[] { + const fileLocals = aggregateFileImportLocals(imports); + if (fileLocals.size === 0) return []; + const shadowedRanges = buildShadowedRanges(root, fileLocals); + const accesses: AggregateFileDeleteAccess[] = []; + + for (const member of root.findAll({ rule: { kind: "member_expression" } })) { + const receiver = member.field("object"); + const property = member.children().find((child) => child.kind() === "property_identifier"); + if ( + isAggregateFileReceiver(receiver, fileLocals, shadowedRanges) && + property?.text() === "deleteFile" + ) { + accesses.push({ node: member, property, computed: false }); + } + } + + for (const subscript of root.findAll({ rule: { kind: "subscript_expression" } })) { + const receiver = subscript.field("object"); + const property = subscript.field("index"); + if ( + isAggregateFileReceiver(receiver, fileLocals, shadowedRanges) && + property?.kind() === "string" && + property.text().slice(1, -1) === "deleteFile" + ) { + accesses.push({ node: subscript, property, computed: true }); + } + } + + return accesses; +} + +function aggregateFileDeleteEdits(root: SgNode, imports: SgNode[]): Edit[] { + return aggregateFileDeleteAccesses(root, imports).map(({ property, computed }) => { + if (!computed) return property.replace("delete"); + const quote = property.text()[0] ?? '"'; + return property.replace(`${quote}delete${quote}`); + }); +} + +function declaratorSides(node: SgNode): { binding: SgNode; value: SgNode } | null { + const children = node.children(); + const equalsIndex = children.findIndex((child) => child.kind() === "="); + if (equalsIndex === -1) return null; + const binding = children.slice(0, equalsIndex).find((child) => child.kind() !== "comment"); + const value = children.slice(equalsIndex + 1).find((child) => child.kind() !== "comment"); + return binding && value ? { binding, value } : null; +} + +function aggregateFileDeleteDestructures(root: SgNode, imports: SgNode[]): SgNode[] { + const fileLocals = aggregateFileImportLocals(imports); + if (fileLocals.size === 0) return []; + const shadowedRanges = buildShadowedRanges(root, fileLocals); + + return root.findAll({ rule: { kind: "variable_declarator" } }).filter((declarator) => { + const sides = declaratorSides(declarator); + if ( + !sides || + sides.binding.kind() !== "object_pattern" || + !isAggregateFileReceiver(sides.value, fileLocals, shadowedRanges) + ) { + return false; + } + return sides.binding + .findAll({ + rule: { + any: [{ kind: "property_identifier" }, { kind: "shorthand_property_identifier_pattern" }], + }, + }) + .some((property) => property.text() === "deleteFile"); + }); +} + +function aggregateFileDeleteFindingNodes(root: SgNode, imports: SgNode[]): SgNode[] { + return [ + ...aggregateFileDeleteAccesses(root, imports).map((access) => access.node), + ...aggregateFileDeleteDestructures(root, imports), + ]; +} + function usedNames(root: SgNode, imports: SgNode[], removedNames: Set): Set { const names = localDeclarationNames(root); for (const importStmt of imports) { @@ -397,9 +711,6 @@ function plannedValueNamespaceLocal( const namespaceName = namespaceImportName(candidate); if (namespaceName) return namespaceName; - const defaultName = defaultImportName(candidate); - if (defaultName) return defaultName; - const existingSelf = existingSelfNamespaceImport(candidate, mod, false); if (existingSelf && !existingSelf.typeOnly) return existingSelf.localName; @@ -436,11 +747,9 @@ function buildImportReplacement( const namespaceName = namespaceImportName(importStmt); if (namespaceName) { if (hasNamespaceTypeMemberReference(root, namespaceName)) return null; - const edit = statementTypeOnly - ? importStmt.replace( - formatImport(source, null, [selfNamespaceSpec(mod, namespaceName)], true, attributes), - ) - : importStmt.replace(formatImport(source, namespaceName, [], false, attributes)); + const edit = importStmt.replace( + formatImport(source, [selfNamespaceSpec(mod, namespaceName)], statementTypeOnly, attributes), + ); return { edit, flatImports: [], @@ -448,8 +757,7 @@ function buildImportReplacement( }; } - const defaultName = defaultImportName(importStmt); - if (statementTypeOnly && defaultName) return null; + if (hasDefaultImport(importStmt)) return null; const existingSelf = existingSelfNamespaceImport(importStmt, mod, statementTypeOnly); const flatImports: FlatImport[] = []; @@ -491,11 +799,9 @@ function buildImportReplacement( : null; const namespaceLocal = plannedValueLocal ?? - (!statementTypeOnly && defaultName - ? defaultName - : canUseExistingSelf - ? existingSelf.localName - : uniqueNamespaceLocal(mod, root, imports, removedNames)); + (canUseExistingSelf + ? existingSelf.localName + : uniqueNamespaceLocal(mod, root, imports, removedNames)); const namespaceSpecifierKey = [ source, namespaceLocal, @@ -503,9 +809,7 @@ function buildImportReplacement( ].join("\0"); const namespaceAlreadyEmitted = emittedNamespaceSpecifiers.has(namespaceSpecifierKey); const needsNamespaceSpecifier = - plannedValueLocal == null && - !((!statementTypeOnly && defaultName) || canUseExistingSelf) && - !namespaceAlreadyEmitted; + plannedValueLocal == null && !canUseExistingSelf && !namespaceAlreadyEmitted; if (needsNamespaceSpecifier) emittedNamespaceSpecifiers.add(namespaceSpecifierKey); const namespaceSpecifier = flatImportsAreTypeOnly && !statementTypeOnly @@ -516,13 +820,7 @@ function buildImportReplacement( return { edit: replaceImportStatement( importStmt, - formatImport( - source, - statementTypeOnly ? null : defaultName, - nextNamedSpecs, - statementTypeOnly, - attributes, - ), + formatImport(source, nextNamedSpecs, statementTypeOnly, attributes), sourceText, ), flatImports, @@ -612,11 +910,13 @@ export default function transform(source: string, filePath: string): string | nu if (replacement) replacements.push(replacement); } - if (replacements.length === 0) return null; + const aggregateEdits = aggregateFileDeleteEdits(root, imports); + if (replacements.length === 0 && aggregateEdits.length === 0) return null; const edits = [ ...replacements.map((replacement) => replacement.edit), ...referenceEdits(root, replacements), + ...aggregateEdits, ]; const result = root.commitEdits(edits); return result === source ? null : result; @@ -864,27 +1164,45 @@ export function reviewFindings( if (!quickFilter(source)) return []; const root = parse(sourceLang(filePath, source), source).root(); + const imports = findImportStatements(root); const findings: LlmReviewFinding[] = []; findings.push(...dynamicRuntimeImportFindings(root, relativePath)); findings.push(...runtimeRequireFindings(root, relativePath)); - for (const importStmt of findImportStatements(root)) { + for (const access of aggregateFileDeleteFindingNodes(root, imports)) { + findings.push({ + file: relativePath, + line: access.range().start.line + 1, + message: "Aggregate runtime file namespace still uses the removed deleteFile alias.", + excerpt: access.text().trim(), + }); + } + + for (const importStmt of imports) { const sourceName = importSource(importStmt); if (!sourceName) continue; const mod = MODULES_BY_SOURCE.get(sourceName); if (!mod) continue; const hasImportRequire = isImportRequireStatement(importStmt); + const hasRemovedDefaultImport = hasDefaultImport(importStmt); const hasNamespaceImport = namespaceImportName(importStmt) != null; const hasRemovedFlatImport = hasRemovedFlatSpecifier(importStmt, mod); - if (!hasImportRequire && !hasNamespaceImport && !hasRemovedFlatImport) continue; + if ( + !hasImportRequire && + !hasRemovedDefaultImport && + !hasNamespaceImport && + !hasRemovedFlatImport + ) { + continue; + } findings.push({ file: relativePath, line: importStmt.range().start.line + 1, message: hasImportRequire ? "TypeScript runtime subpath import-equals may still access a removed flat value export." - : "Runtime subpath import still uses a removed namespace-star or flat value import.", + : "Runtime subpath import still uses a removed default, namespace-star, or flat value import.", excerpt: importStmt.text().trim(), }); } diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aggregate-file-delete/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aggregate-file-delete/expected.ts new file mode 100644 index 000000000..0a4d0b712 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aggregate-file-delete/expected.ts @@ -0,0 +1,23 @@ +import { file as runtimeFile } from "@tailor-platform/sdk/runtime"; + +await runtimeFile.delete("ns", "Doc", "blob", "rec-1"); +await runtimeFile["delete"]("ns", "Doc", "blob", "rec-2"); + +function remove(runtimeFile: { deleteFile(): void }) { + runtimeFile.deleteFile(); +} + +const callback = function runtimeFile() { + runtimeFile.deleteFile(); +}; + +const RuntimeFile = class runtimeFile { + remove() { + runtimeFile.deleteFile(); + } +}; + +const localFile = { + deleteFile() {}, +}; +localFile.deleteFile(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aggregate-file-delete/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aggregate-file-delete/input.ts new file mode 100644 index 000000000..9b5a6676f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/aggregate-file-delete/input.ts @@ -0,0 +1,23 @@ +import { file as runtimeFile } from "@tailor-platform/sdk/runtime"; + +await runtimeFile.deleteFile("ns", "Doc", "blob", "rec-1"); +await runtimeFile["deleteFile"]("ns", "Doc", "blob", "rec-2"); + +function remove(runtimeFile: { deleteFile(): void }) { + runtimeFile.deleteFile(); +} + +const callback = function runtimeFile() { + runtimeFile.deleteFile(); +}; + +const RuntimeFile = class runtimeFile { + remove() { + runtimeFile.deleteFile(); + } +}; + +const localFile = { + deleteFile() {}, +}; +localFile.deleteFile(); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/expected.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/expected.ts index edfb8745a..f31e290a8 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/expected.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/expected.ts @@ -1,3 +1,3 @@ -import iconv from "@tailor-platform/sdk/runtime/iconv"; +import { iconv as runtimeIconv } from "@tailor-platform/sdk/runtime/iconv"; -const out = iconv.convert("a", "UTF-8", "Shift_JIS"); +const out = runtimeIconv.convert("a", "UTF-8", "Shift_JIS"); diff --git a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/input.ts b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/input.ts index 171ffbe9c..9f9216f63 100644 --- a/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/input.ts +++ b/packages/sdk-codemod/codemods/v2/runtime-subpath-namespace/tests/namespace-import/input.ts @@ -1,3 +1,3 @@ -import * as iconv from "@tailor-platform/sdk/runtime/iconv"; +import * as runtimeIconv from "@tailor-platform/sdk/runtime/iconv"; -const out = iconv.convert("a", "UTF-8", "Shift_JIS"); +const out = runtimeIconv.convert("a", "UTF-8", "Shift_JIS"); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 790ecd305..ec482f4d0 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -505,7 +505,7 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/runtime-subpath-namespace", name: "Runtime subpath imports use namespace objects", description: - "Rewrite `@tailor-platform/sdk/runtime/*` namespace-star imports and flat value imports to the v2 default or self-named namespace object imports.", + "Rewrite `@tailor-platform/sdk/runtime/*` namespace-star and flat value imports to self-named namespace imports, and aggregate `file.deleteFile` calls to `file.delete`. `TailorContextAPI` and `TailorWorkflowAPI` now describe SDK wrappers; direct platform globals use `PlatformContextAPI` and `PlatformWorkflowAPI`.", since: "1.0.0", until: "2.0.0", prereleaseUntil: V2_NEXT_3, @@ -526,7 +526,7 @@ export const allCodemods: CodemodPackage[] = [ before: 'import * as iconv from "@tailor-platform/sdk/runtime/iconv";\niconv.convert(value, "UTF-8", "Shift_JIS");', after: - 'import iconv from "@tailor-platform/sdk/runtime/iconv";\niconv.convert(value, "UTF-8", "Shift_JIS");', + 'import { iconv } from "@tailor-platform/sdk/runtime/iconv";\niconv.convert(value, "UTF-8", "Shift_JIS");', }, { before: @@ -534,16 +534,26 @@ export const allCodemods: CodemodPackage[] = [ after: 'import { aigateway } from "@tailor-platform/sdk/runtime/aigateway";\nconst gateway = await aigateway.get("main");', }, + { + before: + 'import { file } from "@tailor-platform/sdk/runtime";\nawait file.deleteFile("ns", "Doc", "blob", "record-id");', + after: + 'import { file } from "@tailor-platform/sdk/runtime";\nawait file.delete("ns", "Doc", "blob", "record-id");', + }, ], prompt: [ - "In Tailor SDK v2, runtime subpath modules export a default namespace object and", - "a self-named namespace object (for example, `iconv` from", - "`@tailor-platform/sdk/runtime/iconv`). Flat value imports such as", + "In Tailor SDK v2, runtime subpath modules export only a self-named namespace", + "object (for example, `iconv` from `@tailor-platform/sdk/runtime/iconv`).", + "Default and flat value imports such as", '`import { get } from "@tailor-platform/sdk/runtime/aigateway"` are removed.', "The codemod rewrites straightforward namespace-star imports and flat named value", - "imports. Review any remaining runtime subpath imports manually, especially when", - "a local binding or nested scope shadows the imported flat value, or when", + "imports. It also rewrites direct `file.deleteFile` calls on the aggregate runtime", + "namespace to `file.delete`. Destructured aggregate `deleteFile` references require", + "manual migration. Review any remaining runtime imports manually, especially when", + "a local binding or nested scope shadows an imported value, or when", "type-position namespace member references need explicit top-level type imports.", + "For direct platform globals, replace `TailorContextAPI` and `TailorWorkflowAPI`", + "type references with `PlatformContextAPI` and `PlatformWorkflowAPI` respectively.", ].join("\n"), }, { diff --git a/packages/sdk-codemod/src/runner.test.ts b/packages/sdk-codemod/src/runner.test.ts index 1b9055381..cbc5bdba0 100644 --- a/packages/sdk-codemod/src/runner.test.ts +++ b/packages/sdk-codemod/src/runner.test.ts @@ -1130,6 +1130,35 @@ describe("runCodemods", () => { "", ].join("\n"), ); + await fs.promises.writeFile( + path.join(dir, "default-import.ts"), + [ + 'import iconv from "@tailor-platform/sdk/runtime/iconv";', + "", + 'iconv.convert("a", "UTF-8", "Shift_JIS");', + "", + ].join("\n"), + ); + await fs.promises.writeFile( + path.join(dir, "aggregate-destructure.ts"), + [ + 'import { file as runtimeFile } from "@tailor-platform/sdk/runtime";', + "", + "const { deleteFile } = runtimeFile;", + "", + ].join("\n"), + ); + await fs.promises.writeFile( + path.join(dir, "shadow-only.ts"), + [ + 'import { file as runtimeFile } from "@tailor-platform/sdk/runtime";', + "", + "function remove(runtimeFile: { deleteFile(): void }) {", + " runtimeFile.deleteFile();", + "}", + "", + ].join("\n"), + ); const result = await runCodemods([{ codemod, scriptPath }], dir, true); @@ -1139,6 +1168,8 @@ describe("runCodemods", () => { codemodId: "v2/runtime-subpath-namespace", prompt: codemod.prompt, files: [ + "aggregate-destructure.ts", + "default-import.ts", "dynamic-const.ts", "dynamic-template.ts", "dynamic.ts", @@ -1152,6 +1183,16 @@ describe("runCodemods", () => { "type-reference.ts", ], findings: [ + expect.objectContaining({ + file: "aggregate-destructure.ts", + line: 3, + excerpt: "{ deleteFile } = runtimeFile", + }), + expect.objectContaining({ + file: "default-import.ts", + line: 1, + excerpt: 'import iconv from "@tailor-platform/sdk/runtime/iconv";', + }), expect.objectContaining({ file: "dynamic-const.ts", line: 2, diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 025bf8930..df45685b6 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -440,7 +440,7 @@ wrapper or global tailor.authconnection. **Migration:** Partially automatic -Rewrite `@tailor-platform/sdk/runtime/*` namespace-star imports and flat value imports to the v2 default or self-named namespace object imports. +Rewrite `@tailor-platform/sdk/runtime/*` namespace-star and flat value imports to self-named namespace imports, and aggregate `file.deleteFile` calls to `file.delete`. `TailorContextAPI` and `TailorWorkflowAPI` now describe SDK wrappers; direct platform globals use `PlatformContextAPI` and `PlatformWorkflowAPI`. Before: @@ -452,7 +452,7 @@ iconv.convert(value, "UTF-8", "Shift_JIS"); After: ```ts -import iconv from "@tailor-platform/sdk/runtime/iconv"; +import { iconv } from "@tailor-platform/sdk/runtime/iconv"; iconv.convert(value, "UTF-8", "Shift_JIS"); ``` @@ -470,18 +470,36 @@ import { aigateway } from "@tailor-platform/sdk/runtime/aigateway"; const gateway = await aigateway.get("main"); ``` +Before: + +```ts +import { file } from "@tailor-platform/sdk/runtime"; +await file.deleteFile("ns", "Doc", "blob", "record-id"); +``` + +After: + +```ts +import { file } from "@tailor-platform/sdk/runtime"; +await file.delete("ns", "Doc", "blob", "record-id"); +``` +
Prompt for an AI agent (to finish the cases the codemod could not migrate) ```text -In Tailor SDK v2, runtime subpath modules export a default namespace object and -a self-named namespace object (for example, `iconv` from -`@tailor-platform/sdk/runtime/iconv`). Flat value imports such as +In Tailor SDK v2, runtime subpath modules export only a self-named namespace +object (for example, `iconv` from `@tailor-platform/sdk/runtime/iconv`). +Default and flat value imports such as `import { get } from "@tailor-platform/sdk/runtime/aigateway"` are removed. The codemod rewrites straightforward namespace-star imports and flat named value -imports. Review any remaining runtime subpath imports manually, especially when -a local binding or nested scope shadows the imported flat value, or when +imports. It also rewrites direct `file.deleteFile` calls on the aggregate runtime +namespace to `file.delete`. Destructured aggregate `deleteFile` references require +manual migration. Review any remaining runtime imports manually, especially when +a local binding or nested scope shadows an imported value, or when type-position namespace member references need explicit top-level type imports. +For direct platform globals, replace `TailorContextAPI` and `TailorWorkflowAPI` +type references with `PlatformContextAPI` and `PlatformWorkflowAPI` respectively. ```
diff --git a/packages/sdk/docs/runtime.md b/packages/sdk/docs/runtime.md index 939c4bd5d..3939a4c7f 100644 --- a/packages/sdk/docs/runtime.md +++ b/packages/sdk/docs/runtime.md @@ -45,10 +45,12 @@ const { url } = await aigateway.get("my-aigateway"); Each namespace can also be imported individually so you only pull what you need: ```ts -import iconv from "@tailor-platform/sdk/runtime/iconv"; +import { iconv } from "@tailor-platform/sdk/runtime/iconv"; import { idp, type ListUsersResponse, type ClientConfig } from "@tailor-platform/sdk/runtime/idp"; ``` +`TailorContextAPI` and `TailorWorkflowAPI` describe the imported SDK wrapper objects. When typing direct access to the platform-provided globals or a runtime mock, use `PlatformContextAPI` or `PlatformWorkflowAPI` instead. + ## 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. diff --git a/packages/sdk/src/runtime/aigateway.test.ts b/packages/sdk/src/runtime/aigateway.test.ts index aabd385d3..a55374fe6 100644 --- a/packages/sdk/src/runtime/aigateway.test.ts +++ b/packages/sdk/src/runtime/aigateway.test.ts @@ -2,7 +2,7 @@ * Tests for `@tailor-platform/sdk/runtime/aigateway` typed wrappers. */ import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; -import defaultAigateway, { aigateway, type GetAIGatewayResult } from "#/runtime/aigateway"; +import { aigateway, type GetAIGatewayResult } from "#/runtime/aigateway"; import { cleanupMocks, injectMocks, mockAigateway } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/aigateway", () => { @@ -14,10 +14,6 @@ describe("@tailor-platform/sdk/runtime/aigateway", () => { cleanupMocks(globalThis); }); - test("exports matching default and named namespace objects", () => { - expect(defaultAigateway).toBe(aigateway); - }); - test("get forwards to global and returns Promise<{ url: string }>", async () => { using ag = mockAigateway(); ag.setUrls({ "my-aigateway": "https://my-aigateway.example.com" }); diff --git a/packages/sdk/src/runtime/aigateway.ts b/packages/sdk/src/runtime/aigateway.ts index eb1a25906..23fd366fd 100644 --- a/packages/sdk/src/runtime/aigateway.ts +++ b/packages/sdk/src/runtime/aigateway.ts @@ -45,5 +45,3 @@ const get: TailorAigatewayAPI["get"] = (...args) => api().get(...args); /** Runtime wrapper namespace for `tailor.aigateway`. */ export const aigateway = { get } as const satisfies TailorAigatewayAPI; - -export default aigateway; diff --git a/packages/sdk/src/runtime/authconnection.test.ts b/packages/sdk/src/runtime/authconnection.test.ts index c18f061d5..1236006f9 100644 --- a/packages/sdk/src/runtime/authconnection.test.ts +++ b/packages/sdk/src/runtime/authconnection.test.ts @@ -2,7 +2,7 @@ * Tests for `@tailor-platform/sdk/runtime/authconnection` typed wrappers. */ import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import defaultAuthconnection, { authconnection } from "#/runtime/authconnection"; +import { authconnection } from "#/runtime/authconnection"; import { mockAuthconnection, cleanupMocks, injectMocks } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/authconnection", () => { @@ -14,10 +14,6 @@ describe("@tailor-platform/sdk/runtime/authconnection", () => { cleanupMocks(globalThis); }); - test("exports matching default and named namespace objects", () => { - expect(defaultAuthconnection).toBe(authconnection); - }); - test("getConnectionToken forwards to global and records call", async () => { using ac = mockAuthconnection(); ac.setTokens({ diff --git a/packages/sdk/src/runtime/authconnection.ts b/packages/sdk/src/runtime/authconnection.ts index d4e85d333..324fd4e1d 100644 --- a/packages/sdk/src/runtime/authconnection.ts +++ b/packages/sdk/src/runtime/authconnection.ts @@ -41,5 +41,3 @@ const getConnectionToken: TailorAuthconnectionAPI["getConnectionToken"] = (...ar /** Runtime wrapper namespace for `tailor.authconnection`. */ export const authconnection = { getConnectionToken } as const satisfies TailorAuthconnectionAPI; - -export default authconnection; diff --git a/packages/sdk/src/runtime/context.test.ts b/packages/sdk/src/runtime/context.test.ts index 8b220233f..7fe2356fc 100644 --- a/packages/sdk/src/runtime/context.test.ts +++ b/packages/sdk/src/runtime/context.test.ts @@ -2,7 +2,7 @@ * Tests for `@tailor-platform/sdk/runtime/context` typed wrappers. */ import { afterEach, beforeEach, describe, expect, expectTypeOf, test, vi } from "vitest"; -import defaultContext, { context, type Invoker, type TailorContextAPI } from "#/runtime/context"; +import { context, type Invoker, type TailorContextAPI } from "#/runtime/context"; import { cleanupMocks, injectMocks } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/context", () => { @@ -14,10 +14,6 @@ describe("@tailor-platform/sdk/runtime/context", () => { cleanupMocks(globalThis); }); - test("exports matching default and named namespace objects", () => { - expect(defaultContext).toBe(context); - }); - test("exposes the normalized wrapper contract", () => { expectTypeOf(context).toExtend(); expectTypeOf>().toEqualTypeOf(); diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index e05c736a9..1102558b9 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -79,5 +79,3 @@ function getInvoker(): Invoker | null { /** Runtime wrapper namespace for `tailor.context`. */ export const context = { getInvoker } as const satisfies TailorContextAPI; - -export default context; diff --git a/packages/sdk/src/runtime/file.test.ts b/packages/sdk/src/runtime/file.test.ts index 0b5fcf748..ff3fcea09 100644 --- a/packages/sdk/src/runtime/file.test.ts +++ b/packages/sdk/src/runtime/file.test.ts @@ -2,11 +2,7 @@ * Tests for `@tailor-platform/sdk/runtime/file` typed wrappers. */ import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import defaultFile, { - file, - type TailorDBFileError, - type TailorDBFileErrorCode, -} from "#/runtime/file"; +import { file, type TailorDBFileError, type TailorDBFileErrorCode } from "#/runtime/file"; import { cleanupMocks, mockFile, injectMocks } from "#/vitest/mock"; const args = ["ns", "Doc", "blob", "rec-1"] as const; @@ -27,10 +23,6 @@ describe("@tailor-platform/sdk/runtime/file", () => { cleanupMocks(globalThis); }); - test("exports matching default and named namespace objects", () => { - expect(defaultFile).toBe(file); - }); - test("upload forwards args and records the call", async () => { using fileM = mockFile(); fileM.enqueueResult({ metadata: { fileSize: 4, sha256sum: "abc" } }); diff --git a/packages/sdk/src/runtime/file.ts b/packages/sdk/src/runtime/file.ts index 7469739fc..179b2b1e0 100644 --- a/packages/sdk/src/runtime/file.ts +++ b/packages/sdk/src/runtime/file.ts @@ -276,10 +276,7 @@ export const file = { download, downloadAsBase64, delete: deleteFile, - deleteFile, getMetadata, downloadStream, uploadStream, -} as const satisfies TailorDBFileAPI & { deleteFile: TailorDBFileAPI["delete"] }; - -export default file; +} as const satisfies TailorDBFileAPI; diff --git a/packages/sdk/src/runtime/iconv.test.ts b/packages/sdk/src/runtime/iconv.test.ts index 9d1a29968..d02b3922c 100644 --- a/packages/sdk/src/runtime/iconv.test.ts +++ b/packages/sdk/src/runtime/iconv.test.ts @@ -6,7 +6,7 @@ * `string`, otherwise `Uint8Array`) holds at the type level. */ import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; -import defaultIconv, { iconv } from "#/runtime/iconv"; +import { iconv } from "#/runtime/iconv"; import { cleanupMocks, mockIconv, injectMocks } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/iconv", () => { @@ -18,10 +18,6 @@ describe("@tailor-platform/sdk/runtime/iconv", () => { cleanupMocks(globalThis); }); - test("exports matching default and named namespace objects", () => { - expect(defaultIconv).toBe(iconv); - }); - test("convert forwards args and returns string for UTF-8 target", () => { using iconvM = mockIconv(); iconvM.setResolver((method, args) => { diff --git a/packages/sdk/src/runtime/iconv.ts b/packages/sdk/src/runtime/iconv.ts index 301f6804b..acfeeecc2 100644 --- a/packages/sdk/src/runtime/iconv.ts +++ b/packages/sdk/src/runtime/iconv.ts @@ -132,5 +132,3 @@ export const iconv: TailorIconvAPI = { encodings, Iconv, }; - -export default iconv; diff --git a/packages/sdk/src/runtime/idp.test.ts b/packages/sdk/src/runtime/idp.test.ts index 17e93c22b..5f01e412c 100644 --- a/packages/sdk/src/runtime/idp.test.ts +++ b/packages/sdk/src/runtime/idp.test.ts @@ -5,7 +5,7 @@ * `tailor.idp.Client` and records calls with method, args, and namespace. */ import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import defaultIdp, { idp } from "#/runtime/idp"; +import { idp } from "#/runtime/idp"; import { cleanupMocks, mockIdp, injectMocks } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/idp", () => { @@ -17,10 +17,6 @@ describe("@tailor-platform/sdk/runtime/idp", () => { cleanupMocks(globalThis); }); - test("exports matching default and named namespace objects", () => { - expect(defaultIdp).toBe(idp); - }); - test("Client.user forwards args and namespace", async () => { using idpM = mockIdp(); idpM.enqueueResult({ id: "u-1", name: "alice", disabled: false }); diff --git a/packages/sdk/src/runtime/idp.ts b/packages/sdk/src/runtime/idp.ts index 19314de70..9724dcd44 100644 --- a/packages/sdk/src/runtime/idp.ts +++ b/packages/sdk/src/runtime/idp.ts @@ -225,5 +225,3 @@ class Client { // Keep the object typed to the public API so the private wrapper class does not leak into d.ts. /** Runtime API for `tailor.idp`. */ export const idp: TailorIdpAPI = { Client }; - -export default idp; diff --git a/packages/sdk/src/runtime/index.test.ts b/packages/sdk/src/runtime/index.test.ts index 88506910a..42a2ce6a0 100644 --- a/packages/sdk/src/runtime/index.test.ts +++ b/packages/sdk/src/runtime/index.test.ts @@ -4,13 +4,19 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import * as path from "node:path"; import ts from "typescript"; -import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; +import { describe, expect, expectTypeOf, test } from "vitest"; +import * as aigatewayModule from "#/runtime/aigateway"; +import * as authconnectionModule from "#/runtime/authconnection"; +import * as contextModule from "#/runtime/context"; +import * as fileModule from "#/runtime/file"; +import * as iconvModule from "#/runtime/iconv"; +import * as idpModule from "#/runtime/idp"; import { file, type iconv as runtimeIconv, type idp as runtimeIdp } from "#/runtime/index"; -import { cleanupMocks, injectMocks, mockFile } from "#/vitest/mock"; +import * as secretmanagerModule from "#/runtime/secretmanager"; +import * as workflowModule from "#/runtime/workflow"; import type { IconvInstance } from "#/runtime/iconv"; import type { IdpClientInstance } from "#/runtime/idp"; -const fileArgs = ["ns", "Doc", "blob", "rec-1"] as const; const packageRoot = path.resolve(import.meta.dirname, "../.."); function declarationEmitDiagnostics(source: string): string { @@ -58,12 +64,17 @@ function diagnosticHost(cwd: string): ts.FormatDiagnosticsHost { } describe("@tailor-platform/sdk/runtime aggregate exports", () => { - beforeEach(() => { - injectMocks(globalThis); - }); - - afterEach(() => { - cleanupMocks(globalThis); + test.each([ + ["aigateway", aigatewayModule], + ["authconnection", authconnectionModule], + ["context", contextModule], + ["file", fileModule], + ["iconv", iconvModule], + ["idp", idpModule], + ["secretmanager", secretmanagerModule], + ["workflow", workflowModule], + ])("%s subpath has no default export", (_name, runtimeModule) => { + expect(runtimeModule).not.toHaveProperty("default"); }); test("exposes constructor instance types through namespace object values", () => { @@ -74,7 +85,7 @@ describe("@tailor-platform/sdk/runtime aggregate exports", () => { test("emits declarations for exported namespace constructor instances", () => { const diagnostics = declarationEmitDiagnostics(` import { idp } from "#/runtime/idp"; - import iconv from "#/runtime/iconv"; + import { iconv } from "#/runtime/iconv"; export const makeClient = () => new idp.Client({ namespace: "default" }); export const makeConverter = () => new iconv.Iconv("UTF-8", "Shift_JIS"); @@ -83,20 +94,7 @@ describe("@tailor-platform/sdk/runtime aggregate exports", () => { expect(diagnostics).toBe(""); }); - test("keeps the file.deleteFile alias on the aggregate file namespace", async () => { - using fileM = mockFile(); - - await file.deleteFile(...fileArgs); - - expect(file.deleteFile).toBe(file.delete); - expect(fileM.calls).toEqual([ - { - method: "delete", - namespace: "ns", - typeName: "Doc", - fieldName: "blob", - recordId: "rec-1", - }, - ]); + test("does not expose the removed file.deleteFile alias", () => { + expect("deleteFile" in file).toBe(false); }); }); diff --git a/packages/sdk/src/runtime/secretmanager.test.ts b/packages/sdk/src/runtime/secretmanager.test.ts index 46b16cc28..aa6716727 100644 --- a/packages/sdk/src/runtime/secretmanager.test.ts +++ b/packages/sdk/src/runtime/secretmanager.test.ts @@ -2,7 +2,7 @@ * Tests for `@tailor-platform/sdk/runtime/secretmanager` typed wrappers. */ import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; -import defaultSecretmanager, { secretmanager } from "#/runtime/secretmanager"; +import { secretmanager } from "#/runtime/secretmanager"; import { cleanupMocks, injectMocks, mockSecretmanager } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/secretmanager", () => { @@ -14,10 +14,6 @@ describe("@tailor-platform/sdk/runtime/secretmanager", () => { cleanupMocks(globalThis); }); - test("exports matching default and named namespace objects", () => { - expect(defaultSecretmanager).toBe(secretmanager); - }); - test("getSecret forwards to global and returns Promise", async () => { using sm = mockSecretmanager(); sm.setSecrets({ vault: { API_KEY: "sk-123" } }); diff --git a/packages/sdk/src/runtime/secretmanager.ts b/packages/sdk/src/runtime/secretmanager.ts index 1e442768f..64c9f96dd 100644 --- a/packages/sdk/src/runtime/secretmanager.ts +++ b/packages/sdk/src/runtime/secretmanager.ts @@ -50,5 +50,3 @@ export const secretmanager = { getSecrets, getSecret, } as const satisfies TailorSecretmanagerAPI; - -export default secretmanager; diff --git a/packages/sdk/src/runtime/workflow.test.ts b/packages/sdk/src/runtime/workflow.test.ts index 76d7af9ae..bbb2b0280 100644 --- a/packages/sdk/src/runtime/workflow.test.ts +++ b/packages/sdk/src/runtime/workflow.test.ts @@ -2,11 +2,7 @@ * Tests for `@tailor-platform/sdk/runtime/workflow` typed wrappers. */ import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; -import defaultWorkflow, { - workflow, - type TailorWorkflowAPI, - type TriggerWorkflowOptions, -} from "#/runtime/workflow"; +import { workflow, type TailorWorkflowAPI, type TriggerWorkflowOptions } from "#/runtime/workflow"; import { cleanupMocks, injectMocks, mockWorkflow } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/workflow", () => { @@ -18,10 +14,6 @@ describe("@tailor-platform/sdk/runtime/workflow", () => { cleanupMocks(globalThis); }); - test("exports matching default and named namespace objects", () => { - expect(defaultWorkflow).toBe(workflow); - }); - test("exposes the wrapper trigger options", () => { expectTypeOf(workflow).toExtend(); expectTypeOf[2]>().toEqualTypeOf< diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index 08f522f34..ec5d2fec7 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -132,5 +132,3 @@ export const workflow = { wait, resolve, } as const satisfies TailorWorkflowAPI; - -export default workflow; From 4a05aecfb100a1ea7292a6ae5809a2d1e6eddbfe Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 10 Jul 2026 13:11:00 +0900 Subject: [PATCH 499/618] feat(tailordb)!: derive forward relation names from fields --- .changeset/forward-relation-name.md | 8 ++ .../scripts/transform.ts | 93 +++++++++++++ .../src/forward-relation-name-review.test.ts | 55 ++++++++ packages/sdk-codemod/src/registry.test.ts | 14 ++ packages/sdk-codemod/src/registry.ts | 43 ++++++ packages/sdk-codemod/tsdown.config.ts | 2 + packages/sdk/docs/migration/v2.md | 46 +++++++ packages/sdk/docs/services/tailordb.md | 21 ++- .../src/parser/service/tailordb/relation.ts | 10 +- .../service/tailordb/type-parser.test.ts | 129 +++++++++++++++--- 10 files changed, 387 insertions(+), 34 deletions(-) create mode 100644 .changeset/forward-relation-name.md create mode 100644 packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts create mode 100644 packages/sdk-codemod/src/forward-relation-name-review.test.ts diff --git a/.changeset/forward-relation-name.md b/.changeset/forward-relation-name.md new file mode 100644 index 000000000..48ad9840c --- /dev/null +++ b/.changeset/forward-relation-name.md @@ -0,0 +1,8 @@ +--- +"@tailor-platform/sdk": major +"@tailor-platform/sdk-codemod": patch +--- + +Derive default TailorDB forward relation names from the relation field name by removing a trailing `ID`, `Id`, or `id`, instead of deriving them from the target table name. + +The v2 migration review identifies non-self relations without `toward.as`. Add an explicit name to preserve the v1 GraphQL field name, or update consumers to use the new field-based name. diff --git a/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts new file mode 100644 index 000000000..82af2897c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts @@ -0,0 +1,93 @@ +import { parse, Lang } from "@ast-grep/napi"; +import type { LlmReviewFinding } from "../../../../src/types"; +import type { SgNode } from "@ast-grep/napi"; + +function sourceLang(filePath: string, source: string): Lang { + return filePath.endsWith(".tsx") || filePath.endsWith(".jsx") || source.includes(" child.kind() === "property_identifier" || child.kind() === "identifier"); + return property?.text() === "relation"; +} + +function callArgument(call: SgNode): SgNode | null { + const args = call.children().find((child) => child.kind() === "arguments"); + if (!args) return null; + + const values = args + .children() + .filter((child) => !["(", ")", ",", "comment"].includes(child.kind())); + return values.length === 1 ? values[0]! : null; +} + +function pairKey(pair: SgNode): string | null { + const key = pair.children()[0]; + return key?.text().replace(/^['"]|['"]$/g, "") ?? null; +} + +function pairValue(pair: SgNode): SgNode | null { + const children = pair.children(); + const colonIndex = children.findIndex((child) => child.kind() === ":"); + if (colonIndex === -1) return null; + return children.slice(colonIndex + 1).find((child) => child.kind() !== "comment") ?? null; +} + +function objectPair(object: SgNode, key: string): SgNode | null { + return ( + object.children().find((child) => child.kind() === "pair" && pairKey(child) === key) ?? null + ); +} + +function stringValue(node: SgNode | null): string | null { + if (node?.kind() !== "string") return null; + return node.text().replace(/^['"]|['"]$/g, ""); +} + +function needsReview(call: SgNode): boolean { + const config = callArgument(call); + if (config?.kind() !== "object") return config != null; + + const relationType = objectPair(config, "type"); + const toward = objectPair(config, "toward"); + if (!relationType || !toward) return false; + if (stringValue(pairValue(relationType)) === "keyOnly") return false; + + const towardConfig = pairValue(toward); + if (towardConfig?.kind() !== "object") return towardConfig != null; + if (objectPair(towardConfig, "as")) return false; + + const targetType = objectPair(towardConfig, "type"); + if (!targetType) return false; + return stringValue(pairValue(targetType)) !== "self"; +} + +export default function transform(_source: string, _filePath: string): null { + return null; +} + +export async function reviewFindings( + source: string, + filePath: string, + relativePath: string, +): Promise { + if (!source.includes(".relation")) return []; + + const root = parse(sourceLang(filePath, source), source).root(); + return root + .findAll({ rule: { kind: "call_expression" } }) + .filter((call) => isRelationCall(call) && needsReview(call)) + .map((call) => ({ + file: relativePath, + line: call.range().start.line + 1, + message: "Review the v2 forward GraphQL field name or add an explicit toward.as.", + excerpt: call.text().split("\n", 1)[0]!.trim(), + })); +} diff --git a/packages/sdk-codemod/src/forward-relation-name-review.test.ts b/packages/sdk-codemod/src/forward-relation-name-review.test.ts new file mode 100644 index 000000000..3b6abaf36 --- /dev/null +++ b/packages/sdk-codemod/src/forward-relation-name-review.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, test } from "vitest"; +import transform, { reviewFindings } from "../codemods/v2/forward-relation-name/scripts/transform"; + +describe("forward relation name migration review", () => { + test("flags non-self relations that omit toward.as", async () => { + const source = ` +const post = db.table("Post", { + authorId: db.uuid().relation({ + type: "n-1", + toward: { type: user }, + }), +}); +`; + + expect(transform(source, "post.ts")).toBeNull(); + await expect(reviewFindings(source, "post.ts", "post.ts")).resolves.toMatchObject([ + { + file: "post.ts", + line: 3, + message: expect.stringContaining("toward.as"), + excerpt: expect.stringContaining("relation"), + }, + ]); + }); + + test("ignores relations whose forward name behavior is unchanged", async () => { + const source = ` +const post = db.table("Post", { + authorId: db.uuid().relation({ + type: "n-1", + toward: { type: user, as: "author" }, + }), + parentId: db.uuid().relation({ + type: "n-1", + toward: { type: "self" }, + }), + ownerId: db.uuid().relation({ + type: "keyOnly", + toward: { type: user }, + }), +}); +`; + + await expect(reviewFindings(source, "post.ts", "post.ts")).resolves.toEqual([]); + }); + + test("ignores malformed and unrelated relation-like calls", async () => { + const source = ` +client.relation({ toward: { type: user } }); +const relation = { type: "n-1", toward: { type: user } }; +`; + + await expect(reviewFindings(source, "post.ts", "post.ts")).resolves.toEqual([]); + }); +}); diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 9462eec49..f8f83f1fd 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -55,6 +55,20 @@ describe("getApplicableCodemods", () => { expect(prereleaseIds).toContain("v2/db-type-to-table"); }); + test("reviews forward relation names at the prerelease that changes their defaults", () => { + const previousIds = getApplicableCodemods("1.67.1", "2.0.0-next.3").map( + (codemod) => codemod.id, + ); + const codemod = getApplicableCodemods("2.0.0-next.3", "2.0.0-next.4").find( + (entry) => entry.id === "v2/forward-relation-name", + ); + + expect(previousIds).not.toContain("v2/forward-relation-name"); + expect(codemod?.scriptPath).toBe("v2/forward-relation-name/scripts/transform.js"); + expect(codemod?.prompt).toContain("toward.as"); + expect(codemod?.prompt).toContain("GraphQL"); + }); + test("returns empty when both versions are before the codemod boundary", () => { expect(getApplicableCodemods("1.0.0", "1.5.0")).toEqual([]); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index ec482f4d0..aa9a518f1 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -97,6 +97,7 @@ const RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN = new RegExp( const V2_NEXT_1 = "2.0.0-next.1"; const V2_NEXT_2 = "2.0.0-next.2"; const V2_NEXT_3 = "2.0.0-next.3"; +const V2_NEXT_4 = "2.0.0-next.4"; /** All registered codemods, in registration order. */ export const allCodemods: CodemodPackage[] = [ @@ -615,6 +616,48 @@ export const allCodemods: CodemodPackage[] = [ "calls to db.table. Leave unrelated local objects with a .type() method unchanged.", ].join("\n"), }, + { + id: "v2/forward-relation-name", + name: "TailorDB forward relation names derive from field names", + description: + "Review TailorDB relations that omit `toward.as`. Their forward GraphQL field names now derive from the relation field name with a trailing `ID`, `Id`, or `id` removed, instead of from the target table name.", + since: "1.0.0", + until: "2.0.0", + prereleaseUntil: V2_NEXT_4, + scriptPath: "v2/forward-relation-name/scripts/transform.js", + filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], + examples: [ + { + caption: "Preserve the v1 GraphQL field name by making it explicit:", + before: [ + "ownerId: db.uuid().relation({", + ' type: "n-1",', + " toward: { type: user },", + "}),", + ].join("\n"), + after: [ + "ownerId: db.uuid().relation({", + ' type: "n-1",', + ' toward: { type: user, as: "user" },', + "}),", + ].join("\n"), + }, + ], + prompt: [ + "Tailor SDK v2 derives a default forward GraphQL relation name from the source", + "field name by removing a trailing ID, Id, or id. V1 derived it from the target", + "table name. Review each reported non-self relation that omits toward.as.", + "", + "If consumers must keep using the v1 GraphQL field name, add toward.as with the", + "lower-camel-case target table name. Otherwise, update GraphQL operations and", + "consumer code to use the new field-based name. No change is needed when the old", + "and new names are identical. Relations with an explicit toward.as, self-relations,", + "and keyOnly relations are unchanged.", + "", + "A relation field without a trailing ID, Id, or id would default to its own scalar", + "field name and therefore conflict. Give that relation an explicit toward.as.", + ].join("\n"), + }, { id: "v2/execute-script-arg", name: "executeScript arg JSON.stringify → value", diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index 9d8b346ff..2eab53773 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -39,6 +39,8 @@ export default defineConfig([ "v2/tailordb-namespace/scripts/transform": "codemods/v2/tailordb-namespace/scripts/transform.ts", "v2/db-type-to-table/scripts/transform": "codemods/v2/db-type-to-table/scripts/transform.ts", + "v2/forward-relation-name/scripts/transform": + "codemods/v2/forward-relation-name/scripts/transform.ts", "v2/runtime-globals-opt-in/scripts/transform": "codemods/v2/runtime-globals-opt-in/scripts/transform.ts", "v2/execute-script-arg/scripts/transform": diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index df45685b6..1b2ac9670 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -582,6 +582,52 @@ calls to db.table. Leave unrelated local objects with a .type() method unchanged +## TailorDB forward relation names derive from field names + +**Migration:** Partially automatic + +Review TailorDB relations that omit `toward.as`. Their forward GraphQL field names now derive from the relation field name with a trailing `ID`, `Id`, or `id` removed, instead of from the target table name. + +Preserve the v1 GraphQL field name by making it explicit: + +Before: + +```ts +ownerId: db.uuid().relation({ + type: "n-1", + toward: { type: user }, +}), +``` + +After: + +```ts +ownerId: db.uuid().relation({ + type: "n-1", + toward: { type: user, as: "user" }, +}), +``` + +
+Prompt for an AI agent (to finish the cases the codemod could not migrate) + +```text +Tailor SDK v2 derives a default forward GraphQL relation name from the source +field name by removing a trailing ID, Id, or id. V1 derived it from the target +table name. Review each reported non-self relation that omits toward.as. + +If consumers must keep using the v1 GraphQL field name, add toward.as with the +lower-camel-case target table name. Otherwise, update GraphQL operations and +consumer code to use the new field-based name. No change is needed when the old +and new names are identical. Relations with an explicit toward.as, self-relations, +and keyOnly relations are unchanged. + +A relation field without a trailing ID, Id, or id would default to its own scalar +field name and therefore conflict. Give that relation an explicit toward.as. +``` + +
+ ## executeScript arg JSON.stringify → value **Migration:** Partially automatic diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index b8381a315..a543f6e96 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -220,11 +220,14 @@ const user = db.table("User", { const userProfile = db.table("UserProfile", { userEmail: db.string().relation({ type: "1-1", - toward: { type: user, key: "email" }, + toward: { type: user, key: "email", as: "user" }, }), }); ``` +`userEmail` does not end in `ID`, `Id`, or `id`, so this example specifies the forward relation +name with `toward.as`. + Customize relation names using `toward.as` / `backward` options: ```typescript @@ -255,27 +258,31 @@ type User { - `backward` - Customizes the field name for accessing this type from the related type Relation names share the same GraphQL field namespace as fields, files, and other relations on -the type. The SDK rejects duplicate or empty relation names. Use `toward.as` when multiple fields -on the same type point to the same target type, because their default forward names are derived -from the target type name: +the table. The SDK rejects duplicate or empty relation names. When `toward.as` is omitted, the +default forward name comes from the relation field name with a trailing `ID`, `Id`, or `id` +removed. This lets multiple fields point to the same target table with distinct forward names: ```typescript const post = db.table("Post", { authorID: db.uuid().relation({ type: "n-1", - toward: { type: user, as: "author" }, + toward: { type: user }, backward: "authoredPosts", }), reviewerID: db.uuid().relation({ type: "n-1", - toward: { type: user, as: "reviewer" }, + toward: { type: user }, backward: "reviewedPosts", }), }); ``` +These fields generate the forward names `author` and `reviewer`. A relation field without one of +the recognized ID suffixes needs an explicit `toward.as`, because its generated forward name +would conflict with the field itself. + Use `toward.as` or `backward` when a generated relation name would conflict with an existing -field, files entry, or relation on the same type. +field, files entry, or relation on the same table. ### Hooks diff --git a/packages/sdk/src/parser/service/tailordb/relation.ts b/packages/sdk/src/parser/service/tailordb/relation.ts index 3c1766169..1a7c08bc9 100644 --- a/packages/sdk/src/parser/service/tailordb/relation.ts +++ b/packages/sdk/src/parser/service/tailordb/relation.ts @@ -1,4 +1,3 @@ -import * as inflection from "inflection"; import type { RawRelationConfig } from "#/configure/services/tailordb/types"; import type { OperatorFieldConfig } from "#/parser/service/tailordb/types"; import type { UnionToTuple } from "type-fest"; @@ -128,16 +127,9 @@ export function buildRelationInfo( const targetTypeName = rawRelation.toward.type === "self" ? context.typeName : rawRelation.toward.type; - // Compute forward name let forwardName = rawRelation.toward.as; if (!forwardName) { - if (rawRelation.toward.type === "self") { - // For self-relations, derive from field name by removing ID suffix - forwardName = context.fieldName.replace(/(ID|Id|id)$/u, ""); - } else { - // Use inflection to generate default forward name - forwardName = inflection.camelize(targetTypeName, true); - } + forwardName = context.fieldName.replace(/(ID|Id|id)$/u, ""); } return { diff --git a/packages/sdk/src/parser/service/tailordb/type-parser.test.ts b/packages/sdk/src/parser/service/tailordb/type-parser.test.ts index c59e91788..be78ac659 100644 --- a/packages/sdk/src/parser/service/tailordb/type-parser.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-parser.test.ts @@ -276,29 +276,106 @@ describe("parseTypes", () => { }); describe("forwardRelationships", () => { + test("should derive default forward relation names from field names", () => { + const unit = db.table("Unit", { + name: db.string(), + }); + + const inventoryExecutionLine = db.table("InventoryExecutionLine", { + unitId: db.uuid().relation({ + type: "n-1", + toward: { type: unit }, + backward: "inventoryExecutionLines", + }), + primaryUnitId: db.uuid().relation({ + type: "n-1", + toward: { type: unit }, + backward: "primaryInventoryExecutionLines", + }), + }); + + const result = parseTypes( + toSchemaOutputs({ Unit: unit, InventoryExecutionLine: inventoryExecutionLine }), + "test-namespace", + ); + + expect(result.InventoryExecutionLine!.forwardRelationships).toMatchObject({ + unit: { + name: "unit", + targetType: "Unit", + targetField: "unitId", + }, + primaryUnit: { + name: "primaryUnit", + targetType: "Unit", + targetField: "primaryUnitId", + }, + }); + }); + + test.each([ + ["assigneeID", "assignee"], + ["assigneeId", "assignee"], + ["assigneeid", "assignee"], + ])("should strip a trailing ID suffix from %s", (fieldName, forwardName) => { + const user = db.table("User", { + name: db.string(), + }); + const task = db.table("Task", { + [fieldName]: db.uuid().relation({ + type: "n-1", + toward: { type: user }, + backward: "tasks", + }), + }); + + const result = parseTypes(toSchemaOutputs({ User: user, Task: task }), "test-namespace"); + + expect(result.Task!.forwardRelationships[forwardName]).toMatchObject({ + name: forwardName, + targetType: "User", + targetField: fieldName, + }); + }); + + test("should require toward.as when a relation field has no ID suffix", () => { + const user = db.table("User", { + email: db.string().unique(), + }); + const profile = db.table("Profile", { + userEmail: db.string().relation({ + type: "1-1", + toward: { type: user, key: "email" }, + }), + }); + + expect(() => + parseTypes(toSchemaOutputs({ User: user, Profile: profile }), "test-namespace"), + ).toThrow(/Forward relation name "userEmail".*same as its own relation field/s); + }); + test("should throw error when forward relation names are duplicated", () => { const user = db.table("User", { name: db.string(), }); - // Two fields referencing the same type without explicit forward names ("as") - // Both will generate "user" as the forward name + // Different supported ID suffixes can still normalize to the same name. const post = db.table("Post", { - authorID: db.uuid().relation({ + authorId: db.uuid().relation({ type: "n-1", toward: { type: user }, backward: "authoredPosts", }), - reviewerID: db.uuid().relation({ + authorID: db.uuid().relation({ type: "n-1", toward: { type: user }, - backward: "reviewedPosts", + backward: "reviewedAuthoredPosts", }), }); expect(() => parseTypes(toSchemaOutputs({ User: user, Post: post }), "test-namespace"), - ).toThrow(/Forward relation name "user".*duplicated.*authorID.*reviewerID/s); + ).toThrow(/Forward relation name "author".*duplicated.*authorId.*authorID/s); }); test("should not throw error when forward names are explicitly set to be unique", () => { @@ -340,9 +417,9 @@ describe("parseTypes", () => { name: db.string(), }); - // Post has a field named "user" + // Post has a field named "author" const post = db.table("Post", { - user: db.string(), + author: db.string(), authorID: db.uuid().relation({ type: "n-1", toward: { type: user }, @@ -351,7 +428,7 @@ describe("parseTypes", () => { expect(() => parseTypes(toSchemaOutputs({ User: user, Post: post }), "test-namespace"), - ).toThrow(/Forward relation name "user".*conflicts with existing field/s); + ).toThrow(/Forward relation name "author".*conflicts with existing field/s); }); test("should throw error when conflicting field is defined after the relation field", () => { @@ -359,18 +436,18 @@ describe("parseTypes", () => { name: db.string(), }); - // The conflicting "user" field is defined after authorID in the object + // The conflicting "author" field is defined after authorID in the object const post = db.table("Post", { authorID: db.uuid().relation({ type: "n-1", toward: { type: user }, }), - user: db.string(), + author: db.string(), }); expect(() => parseTypes(toSchemaOutputs({ User: user, Post: post }), "test-namespace"), - ).toThrow(/Forward relation name "user".*conflicts with existing field/s); + ).toThrow(/Forward relation name "author".*conflicts with existing field/s); }); test("should throw error when forward name equals its own relation field name", () => { @@ -431,13 +508,13 @@ describe("parseTypes", () => { postID: db.uuid().relation({ type: "n-1", toward: { type: post, as: "post" }, - backward: "user", + backward: "author", }), }); expect(() => parseTypes(toSchemaOutputs({ User: user, Post: post, Comment: comment }), "test-namespace"), - ).toThrow(/Relation name "user" on type "Post".*forward.*backward/s); + ).toThrow(/Relation name "author" on type "Post".*forward.*backward/s); }); test("should include source file information in forward conflict error message", () => { @@ -446,15 +523,15 @@ describe("parseTypes", () => { }); const post = db.table("Post", { - authorID: db.uuid().relation({ + authorId: db.uuid().relation({ type: "n-1", toward: { type: user }, backward: "authoredPosts", }), - reviewerID: db.uuid().relation({ + authorID: db.uuid().relation({ type: "n-1", toward: { type: user }, - backward: "reviewedPosts", + backward: "reviewedAuthoredPosts", }), }); @@ -467,7 +544,7 @@ describe("parseTypes", () => { expect(() => parseTypes(toSchemaOutputs({ User: user, Post: post }), "test-namespace", typeSourceInfo), - ).toThrow(/Forward relation name "user".*\/path\/to\/post\.ts/s); + ).toThrow(/Forward relation name "author".*\/path\/to\/post\.ts/s); }); test("should throw error when self relation forward name is empty", () => { @@ -482,6 +559,22 @@ describe("parseTypes", () => { /Forward relation name for field "ID" on type "Node" cannot be empty/s, ); }); + + test("should throw error when non-self relation forward name is empty", () => { + const user = db.table("User", { + name: db.string(), + }); + const post = db.table("Post", { + ID: db.uuid().relation({ + type: "n-1", + toward: { type: user }, + }), + }); + + expect(() => + parseTypes(toSchemaOutputs({ User: user, Post: post }), "test-namespace"), + ).toThrow(/Forward relation name for field "ID" on type "Post" cannot be empty/s); + }); }); describe("validateRelationType", () => { From e542ba6a81b6310552afe61452710e776508e710 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 10 Jul 2026 13:27:00 +0900 Subject: [PATCH 500/618] fix(codemod): catch dynamic forward relation configs --- .../scripts/transform.ts | 31 +++++++++--- .../src/forward-relation-name-review.test.ts | 49 ++++++++++++++++--- packages/sdk-codemod/src/registry.ts | 6 +-- packages/sdk/docs/migration/v2.md | 6 +-- 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts index 82af2897c..e14bec8d2 100644 --- a/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts @@ -1,4 +1,5 @@ import { parse, Lang } from "@ast-grep/napi"; +import { stringValue } from "../../../../src/ast-grep-helpers"; import type { LlmReviewFinding } from "../../../../src/types"; import type { SgNode } from "@ast-grep/napi"; @@ -30,7 +31,7 @@ function callArgument(call: SgNode): SgNode | null { function pairKey(pair: SgNode): string | null { const key = pair.children()[0]; - return key?.text().replace(/^['"]|['"]$/g, "") ?? null; + return stringValue(key ?? null); } function pairValue(pair: SgNode): SgNode | null { @@ -46,38 +47,52 @@ function objectPair(object: SgNode, key: string): SgNode | null { ); } -function stringValue(node: SgNode | null): string | null { +function literalStringValue(node: SgNode | null): string | null { if (node?.kind() !== "string") return null; - return node.text().replace(/^['"]|['"]$/g, ""); + return stringValue(node); +} + +function hasDynamicProperties(object: SgNode): boolean { + return object.children().some((child) => { + const kind = child.kind(); + return kind === "spread_element" || kind === "shorthand_property_identifier"; + }); } function needsReview(call: SgNode): boolean { const config = callArgument(call); if (config?.kind() !== "object") return config != null; + if (hasDynamicProperties(config)) return true; const relationType = objectPair(config, "type"); const toward = objectPair(config, "toward"); if (!relationType || !toward) return false; - if (stringValue(pairValue(relationType)) === "keyOnly") return false; + if (literalStringValue(pairValue(relationType)) === "keyOnly") return false; const towardConfig = pairValue(toward); if (towardConfig?.kind() !== "object") return towardConfig != null; - if (objectPair(towardConfig, "as")) return false; + if (hasDynamicProperties(towardConfig)) return true; + + const as = objectPair(towardConfig, "as"); + if (as) { + const explicitName = literalStringValue(pairValue(as)); + return explicitName === null || explicitName.length === 0; + } const targetType = objectPair(towardConfig, "type"); if (!targetType) return false; - return stringValue(pairValue(targetType)) !== "self"; + return literalStringValue(pairValue(targetType)) !== "self"; } export default function transform(_source: string, _filePath: string): null { return null; } -export async function reviewFindings( +export function reviewFindings( source: string, filePath: string, relativePath: string, -): Promise { +): LlmReviewFinding[] { if (!source.includes(".relation")) return []; const root = parse(sourceLang(filePath, source), source).root(); diff --git a/packages/sdk-codemod/src/forward-relation-name-review.test.ts b/packages/sdk-codemod/src/forward-relation-name-review.test.ts index 3b6abaf36..0c104d064 100644 --- a/packages/sdk-codemod/src/forward-relation-name-review.test.ts +++ b/packages/sdk-codemod/src/forward-relation-name-review.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "vitest"; import transform, { reviewFindings } from "../codemods/v2/forward-relation-name/scripts/transform"; describe("forward relation name migration review", () => { - test("flags non-self relations that omit toward.as", async () => { + test("flags non-self relations that omit toward.as", () => { const source = ` const post = db.table("Post", { authorId: db.uuid().relation({ @@ -13,7 +13,7 @@ const post = db.table("Post", { `; expect(transform(source, "post.ts")).toBeNull(); - await expect(reviewFindings(source, "post.ts", "post.ts")).resolves.toMatchObject([ + expect(reviewFindings(source, "post.ts", "post.ts")).toMatchObject([ { file: "post.ts", line: 3, @@ -23,7 +23,7 @@ const post = db.table("Post", { ]); }); - test("ignores relations whose forward name behavior is unchanged", async () => { + test("ignores relations whose forward name behavior is unchanged", () => { const source = ` const post = db.table("Post", { authorId: db.uuid().relation({ @@ -41,15 +41,52 @@ const post = db.table("Post", { }); `; - await expect(reviewFindings(source, "post.ts", "post.ts")).resolves.toEqual([]); + expect(reviewFindings(source, "post.ts", "post.ts")).toEqual([]); }); - test("ignores malformed and unrelated relation-like calls", async () => { + test("flags relation configs composed with spreads or shorthand properties", () => { + const source = ` +const primary = db.uuid().relation({ + ...relationConfig, + toward: { type: user }, +}); +const secondary = db.uuid().relation({ type, toward }); +`; + + const findings = reviewFindings(source, "post.ts", "post.ts"); + + expect(findings).toHaveLength(2); + }); + + test("flags relations whose toward.as value may use the default", () => { + const source = ` +const post = db.table("Post", { + authorId: db.uuid().relation({ + type: "n-1", + toward: { type: user, as: "" }, + }), + reviewerId: db.uuid().relation({ + type: "n-1", + toward: { type: user, as: undefined }, + }), + ownerId: db.uuid().relation({ + type: "n-1", + toward: { type: user, as: relationName }, + }), +}); +`; + + const findings = reviewFindings(source, "post.ts", "post.ts"); + + expect(findings).toHaveLength(3); + }); + + test("ignores malformed and unrelated relation-like calls", () => { const source = ` client.relation({ toward: { type: user } }); const relation = { type: "n-1", toward: { type: user } }; `; - await expect(reviewFindings(source, "post.ts", "post.ts")).resolves.toEqual([]); + expect(reviewFindings(source, "post.ts", "post.ts")).toEqual([]); }); }); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index aa9a518f1..196e78c5c 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -648,9 +648,9 @@ export const allCodemods: CodemodPackage[] = [ "field name by removing a trailing ID, Id, or id. V1 derived it from the target", "table name. Review each reported non-self relation that omits toward.as.", "", - "If consumers must keep using the v1 GraphQL field name, add toward.as with the", - "lower-camel-case target table name. Otherwise, update GraphQL operations and", - "consumer code to use the new field-based name. No change is needed when the old", + "If consumers must keep using the v1 GraphQL field name, inspect the v1 schema and", + "copy that exact field name into toward.as. Otherwise, update GraphQL operations", + "and consumer code to use the new field-based name. No change is needed when the old", "and new names are identical. Relations with an explicit toward.as, self-relations,", "and keyOnly relations are unchanged.", "", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 1b2ac9670..9a8a43001 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -616,9 +616,9 @@ Tailor SDK v2 derives a default forward GraphQL relation name from the source field name by removing a trailing ID, Id, or id. V1 derived it from the target table name. Review each reported non-self relation that omits toward.as. -If consumers must keep using the v1 GraphQL field name, add toward.as with the -lower-camel-case target table name. Otherwise, update GraphQL operations and -consumer code to use the new field-based name. No change is needed when the old +If consumers must keep using the v1 GraphQL field name, inspect the v1 schema and +copy that exact field name into toward.as. Otherwise, update GraphQL operations +and consumer code to use the new field-based name. No change is needed when the old and new names are identical. Relations with an explicit toward.as, self-relations, and keyOnly relations are unchanged. From 81c04519f79fef7b0eb9a705f1820a94d71fbb35 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 10 Jul 2026 13:43:34 +0900 Subject: [PATCH 501/618] fix(codemod): cover aliased relation builders --- packages/sdk-codemod/src/registry.test.ts | 7 +++++++ packages/sdk-codemod/src/registry.ts | 7 +++++-- packages/sdk/docs/migration/v2.md | 6 ++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index f8f83f1fd..f95c61719 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -67,6 +67,13 @@ describe("getApplicableCodemods", () => { expect(codemod?.scriptPath).toBe("v2/forward-relation-name/scripts/transform.js"); expect(codemod?.prompt).toContain("toward.as"); expect(codemod?.prompt).toContain("GraphQL"); + expect(codemod?.prompt).toContain("non-empty"); + expect(codemod?.prompt).toContain("empty or dynamic"); + + const aliasPattern = codemod?.suspiciousPatterns?.[0]; + expect(aliasPattern).toBeInstanceOf(RegExp); + expect((aliasPattern as RegExp).test("const relation = db.uuid().relation;")).toBe(true); + expect((aliasPattern as RegExp).test("db.uuid().relation({ type, toward });")).toBe(false); }); test("returns empty when both versions are before the codemod boundary", () => { diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 196e78c5c..22e46849b 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -626,6 +626,7 @@ export const allCodemods: CodemodPackage[] = [ prereleaseUntil: V2_NEXT_4, scriptPath: "v2/forward-relation-name/scripts/transform.js", filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], + suspiciousPatterns: [/\.relation\b(?!\s*\()/], examples: [ { caption: "Preserve the v1 GraphQL field name by making it explicit:", @@ -651,8 +652,10 @@ export const allCodemods: CodemodPackage[] = [ "If consumers must keep using the v1 GraphQL field name, inspect the v1 schema and", "copy that exact field name into toward.as. Otherwise, update GraphQL operations", "and consumer code to use the new field-based name. No change is needed when the old", - "and new names are identical. Relations with an explicit toward.as, self-relations,", - "and keyOnly relations are unchanged.", + "and new names are identical. Relations with a guaranteed non-empty toward.as,", + "self-relations, and keyOnly relations are unchanged. For an empty or dynamic", + "toward.as, determine whether its runtime value can be falsy; if so, treat the", + "relation as using the default name.", "", "A relation field without a trailing ID, Id, or id would default to its own scalar", "field name and therefore conflict. Give that relation an explicit toward.as.", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 9a8a43001..b3387142d 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -619,8 +619,10 @@ table name. Review each reported non-self relation that omits toward.as. If consumers must keep using the v1 GraphQL field name, inspect the v1 schema and copy that exact field name into toward.as. Otherwise, update GraphQL operations and consumer code to use the new field-based name. No change is needed when the old -and new names are identical. Relations with an explicit toward.as, self-relations, -and keyOnly relations are unchanged. +and new names are identical. Relations with a guaranteed non-empty toward.as, +self-relations, and keyOnly relations are unchanged. For an empty or dynamic +toward.as, determine whether its runtime value can be falsy; if so, treat the +relation as using the default name. A relation field without a trailing ID, Id, or id would default to its own scalar field name and therefore conflict. Give that relation an explicit toward.as. From 0449dba6b04ec77d64a637eb96760af47458f8a7 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 10 Jul 2026 13:59:55 +0900 Subject: [PATCH 502/618] fix(codemod): cover indirect relation configurations --- .../v2/forward-relation-name/scripts/transform.ts | 13 +++++++++---- .../src/forward-relation-name-review.test.ts | 13 +++++++++++++ packages/sdk-codemod/src/registry.test.ts | 8 ++++++++ packages/sdk-codemod/src/registry.ts | 6 +++++- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts index e14bec8d2..c545b7931 100644 --- a/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts @@ -23,9 +23,10 @@ function callArgument(call: SgNode): SgNode | null { const args = call.children().find((child) => child.kind() === "arguments"); if (!args) return null; - const values = args - .children() - .filter((child) => !["(", ")", ",", "comment"].includes(child.kind())); + const values = args.children().filter((child) => { + const kind = child.kind(); + return kind !== "(" && kind !== ")" && kind !== "," && kind !== "comment"; + }); return values.length === 1 ? values[0]! : null; } @@ -55,7 +56,11 @@ function literalStringValue(node: SgNode | null): string | null { function hasDynamicProperties(object: SgNode): boolean { return object.children().some((child) => { const kind = child.kind(); - return kind === "spread_element" || kind === "shorthand_property_identifier"; + if (kind === "{" || kind === "}" || kind === "," || kind === "comment") return false; + if (kind !== "pair") return true; + + const keyKind = child.children()[0]?.kind(); + return keyKind !== "property_identifier" && keyKind !== "string"; }); } diff --git a/packages/sdk-codemod/src/forward-relation-name-review.test.ts b/packages/sdk-codemod/src/forward-relation-name-review.test.ts index 0c104d064..7d9234261 100644 --- a/packages/sdk-codemod/src/forward-relation-name-review.test.ts +++ b/packages/sdk-codemod/src/forward-relation-name-review.test.ts @@ -58,6 +58,19 @@ const secondary = db.uuid().relation({ type, toward }); expect(findings).toHaveLength(2); }); + test("flags relation configs with computed properties", () => { + const source = ` +const author = db.uuid().relation({ + type: "n-1", + [TOWARD]: { type: user }, +}); +`; + + const findings = reviewFindings(source, "post.ts", "post.ts"); + + expect(findings).toHaveLength(1); + }); + test("flags relations whose toward.as value may use the default", () => { const source = ` const post = db.table("Post", { diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index f95c61719..74d5cd53e 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -74,6 +74,14 @@ describe("getApplicableCodemods", () => { expect(aliasPattern).toBeInstanceOf(RegExp); expect((aliasPattern as RegExp).test("const relation = db.uuid().relation;")).toBe(true); expect((aliasPattern as RegExp).test("db.uuid().relation({ type, toward });")).toBe(false); + + const aliasPatterns = codemod?.suspiciousPatterns as RegExp[]; + expect(aliasPatterns.some((pattern) => pattern.test("const { relation } = db.uuid();"))).toBe( + true, + ); + expect( + aliasPatterns.some((pattern) => pattern.test('db.uuid()["relation"]({ type, toward })')), + ).toBe(true); }); test("returns empty when both versions are before the codemod boundary", () => { diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 22e46849b..ce862ea33 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -626,7 +626,11 @@ export const allCodemods: CodemodPackage[] = [ prereleaseUntil: V2_NEXT_4, scriptPath: "v2/forward-relation-name/scripts/transform.js", filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], - suspiciousPatterns: [/\.relation\b(?!\s*\()/], + suspiciousPatterns: [ + /\.relation\b(?!\s*\()/, + /\{[^}\n]*\brelation\b[^}\n]*\}\s*=/, + /\[\s*["']relation["']\s*\]/, + ], examples: [ { caption: "Preserve the v1 GraphQL field name by making it explicit:", From 01ed3dbb25d79e407171d2a9c1e194ad4b2eb557 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 14:10:45 +0900 Subject: [PATCH 503/618] style(cli): fix oxfmt line-wrap drift in ts-hook The CI format check failed on a line oxfmt would have wrapped differently; the local pre-commit hook did not catch it. No logic change. --- packages/sdk/src/cli/ts-hook.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 8b54c2b3a..2dc3f8938 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -28,7 +28,8 @@ function collectPathsInto(out, configFilePath, content, visited) { const baseDir = dirname(configFilePath); const opts = content.compilerOptions ?? {}; - const ownBaseUrl = typeof opts.baseUrl === "string" ? resolvePath(baseDir, opts.baseUrl) : undefined; + const ownBaseUrl = + typeof opts.baseUrl === "string" ? resolvePath(baseDir, opts.baseUrl) : undefined; let inheritedBaseUrl; const extendsField = content.extends; From 3214a51ec4a7ffa58cc4aa10c8efafac626b18ad Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 10 Jul 2026 14:21:42 +0900 Subject: [PATCH 504/618] fix(codemod): cover alternative relation call syntax --- .../scripts/transform.ts | 54 ++++++++++++++++--- .../src/forward-relation-name-review.test.ts | 44 +++++++++++++++ 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts index c545b7931..49d0df21a 100644 --- a/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts @@ -4,14 +4,20 @@ import type { LlmReviewFinding } from "../../../../src/types"; import type { SgNode } from "@ast-grep/napi"; function sourceLang(filePath: string, source: string): Lang { - return filePath.endsWith(".tsx") || filePath.endsWith(".jsx") || source.includes("): boolean { const callee = call.children()[0]; - if (callee?.kind() !== "member_expression") return false; + if (!callee) return false; + if (callee.kind() === "identifier") return aliases.has(callee.text()); + if (callee.kind() === "subscript_expression") { + return literalStringValue(callee.field("index")) === "relation"; + } + if (callee.kind() !== "member_expression") return false; const property = callee .children() @@ -19,6 +25,39 @@ function isRelationCall(call: SgNode): boolean { return property?.text() === "relation"; } +function relationBindingName(pattern: SgNode): string | null { + if (pattern.kind() !== "object_pattern") return null; + + for (const child of pattern.children()) { + if (child.kind() === "shorthand_property_identifier_pattern" && child.text() === "relation") { + return child.text(); + } + if (child.kind() === "pair_pattern" && stringValue(child.field("key")) === "relation") { + const value = child.field("value"); + return value?.kind() === "identifier" ? value.text() : null; + } + if (child.kind() === "object_assignment_pattern") { + const binding = child + .children() + .find((node) => node.kind() === "shorthand_property_identifier_pattern"); + if (binding?.text() === "relation") return binding.text(); + } + } + + return null; +} + +function relationAliases(root: SgNode): Set { + const aliases = new Set(); + for (const declarator of root.findAll({ rule: { kind: "variable_declarator" } })) { + const binding = declarator.field("name"); + if (!binding) continue; + const name = relationBindingName(binding); + if (name) aliases.add(name); + } + return aliases; +} + function callArgument(call: SgNode): SgNode | null { const args = call.children().find((child) => child.kind() === "arguments"); if (!args) return null; @@ -98,12 +137,13 @@ export function reviewFindings( filePath: string, relativePath: string, ): LlmReviewFinding[] { - if (!source.includes(".relation")) return []; + if (!source.includes("relation")) return []; const root = parse(sourceLang(filePath, source), source).root(); + const aliases = relationAliases(root); return root .findAll({ rule: { kind: "call_expression" } }) - .filter((call) => isRelationCall(call) && needsReview(call)) + .filter((call) => isRelationCall(call, aliases) && needsReview(call)) .map((call) => ({ file: relativePath, line: call.range().start.line + 1, diff --git a/packages/sdk-codemod/src/forward-relation-name-review.test.ts b/packages/sdk-codemod/src/forward-relation-name-review.test.ts index 7d9234261..a2d98049f 100644 --- a/packages/sdk-codemod/src/forward-relation-name-review.test.ts +++ b/packages/sdk-codemod/src/forward-relation-name-review.test.ts @@ -94,6 +94,50 @@ const post = db.table("Post", { expect(findings).toHaveLength(3); }); + test("flags relation calls that use computed member access", () => { + const source = ` +const authorId = db.uuid()["relation"]({ + type: "n-1", + toward: { type: user }, +}); +`; + + const findings = reviewFindings(source, "post.ts", "post.ts"); + + expect(findings).toHaveLength(1); + }); + + test("flags multiline destructured relation builder aliases", () => { + const source = ` +const { + relation: defineRelation, +}: ReturnType = db.uuid(); +const authorId = defineRelation({ + type: "n-1", + toward: { type: user }, +}); +`; + + const findings = reviewFindings(source, "post.ts", "post.ts"); + + expect(findings).toHaveLength(1); + }); + + test("parses TypeScript extensions as TypeScript when strings contain closing tags", () => { + const source = ` +const marker = ""; +const typed = value; +const authorId = db.uuid().relation({ + type: "n-1", + toward: { type: user }, +}); +`; + + const findings = reviewFindings(source, "post.ts", "post.ts"); + + expect(findings).toHaveLength(1); + }); + test("ignores malformed and unrelated relation-like calls", () => { const source = ` client.relation({ toward: { type: user } }); From 557dd7e125c4e18378f289ccd93972f9e309d394 Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 10 Jul 2026 14:37:00 +0900 Subject: [PATCH 505/618] fix(codemod): cover dynamic relation declarations --- .../scripts/transform.ts | 17 +++---- .../src/forward-relation-name-review.test.ts | 46 +++++++++++++++++++ 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts index 49d0df21a..e634cddad 100644 --- a/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/forward-relation-name/scripts/transform.ts @@ -6,7 +6,7 @@ import type { SgNode } from "@ast-grep/napi"; function sourceLang(filePath: string, source: string): Lang { const lowerPath = filePath.toLowerCase(); if (/\.(?:ts|mts|cts)$/u.test(lowerPath)) return Lang.TypeScript; - if (/\.(?:tsx|jsx)$/u.test(lowerPath)) return Lang.Tsx; + if (/\.(?:tsx|jsx|js)$/u.test(lowerPath)) return Lang.Tsx; return source.includes("): boolean { if (!callee) return false; if (callee.kind() === "identifier") return aliases.has(callee.text()); if (callee.kind() === "subscript_expression") { - return literalStringValue(callee.field("index")) === "relation"; + const property = literalStringValue(callee.field("index")); + return property === null || property === "relation"; } if (callee.kind() !== "member_expression") return false; @@ -49,10 +50,8 @@ function relationBindingName(pattern: SgNode): string | null { function relationAliases(root: SgNode): Set { const aliases = new Set(); - for (const declarator of root.findAll({ rule: { kind: "variable_declarator" } })) { - const binding = declarator.field("name"); - if (!binding) continue; - const name = relationBindingName(binding); + for (const pattern of root.findAll({ rule: { kind: "object_pattern" } })) { + const name = relationBindingName(pattern); if (name) aliases.add(name); } return aliases; @@ -117,15 +116,17 @@ function needsReview(call: SgNode): boolean { if (towardConfig?.kind() !== "object") return towardConfig != null; if (hasDynamicProperties(towardConfig)) return true; + const targetType = objectPair(towardConfig, "type"); + if (targetType && literalStringValue(pairValue(targetType)) === "self") return false; + const as = objectPair(towardConfig, "as"); if (as) { const explicitName = literalStringValue(pairValue(as)); return explicitName === null || explicitName.length === 0; } - const targetType = objectPair(towardConfig, "type"); if (!targetType) return false; - return literalStringValue(pairValue(targetType)) !== "self"; + return true; } export default function transform(_source: string, _filePath: string): null { diff --git a/packages/sdk-codemod/src/forward-relation-name-review.test.ts b/packages/sdk-codemod/src/forward-relation-name-review.test.ts index a2d98049f..efc75f736 100644 --- a/packages/sdk-codemod/src/forward-relation-name-review.test.ts +++ b/packages/sdk-codemod/src/forward-relation-name-review.test.ts @@ -38,6 +38,14 @@ const post = db.table("Post", { type: "keyOnly", toward: { type: user }, }), + previousId: db.uuid().relation({ + type: "n-1", + toward: { type: "self", as: "" }, + }), + nextId: db.uuid().relation({ + type: "n-1", + toward: { type: "self", as: maybeName }, + }), }); `; @@ -107,6 +115,20 @@ const authorId = db.uuid()["relation"]({ expect(findings).toHaveLength(1); }); + test("flags relation calls that use dynamic computed member access", () => { + const source = ` +const relationMethod = "relation"; +const authorId = db.uuid()[relationMethod]({ + type: "n-1", + toward: { type: user }, +}); +`; + + const findings = reviewFindings(source, "post.ts", "post.ts"); + + expect(findings).toHaveLength(1); + }); + test("flags multiline destructured relation builder aliases", () => { const source = ` const { @@ -123,6 +145,21 @@ const authorId = defineRelation({ expect(findings).toHaveLength(1); }); + test("flags relation builder aliases destructured in function parameters", () => { + const source = ` +function apply({ relation }: ReturnType) { + return relation({ + type: "n-1", + toward: { type: user }, + }); +} +`; + + const findings = reviewFindings(source, "post.ts", "post.ts"); + + expect(findings).toHaveLength(1); + }); + test("parses TypeScript extensions as TypeScript when strings contain closing tags", () => { const source = ` const marker = ""; @@ -138,9 +175,18 @@ const authorId = db.uuid().relation({ expect(findings).toHaveLength(1); }); + test("parses JavaScript extensions as TSX when they contain JSX", () => { + const source = `const view = ; const authorId = db.uuid().relation({ type: "n-1", toward: { type: user } });`; + + const findings = reviewFindings(source, "post.js", "post.js"); + + expect(findings).toHaveLength(1); + }); + test("ignores malformed and unrelated relation-like calls", () => { const source = ` client.relation({ toward: { type: user } }); +client["validate"]({ type: "n-1", toward: { type: user } }); const relation = { type: "n-1", toward: { type: user } }; `; From 7d0e485528d17ec57b4be3e709408d032a49bae5 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 14:59:01 +0900 Subject: [PATCH 506/618] fix(cli): skip non-array paths targets and add resolveSync extends coverage in ts-hook collectPathsInto called targets.map() unconditionally, which throws a TypeError when a malformed tsconfig defines a paths target that is not an array (e.g. a bare string instead of a string[]). Skip such entries instead of crashing. Also add resolveSync regression tests mirroring the existing resolve tests for the extends inheritance fixes (inherited baseUrl, paths replacement), since both entry points share the same collectPathsInto/loadTsconfigPaths implementation. --- packages/sdk/src/cli/ts-hook.mjs | 1 + packages/sdk/src/cli/ts-hook.test.ts | 78 ++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 2dc3f8938..bece676a6 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -52,6 +52,7 @@ function collectPathsInto(out, configFilePath, content, visited) { for (const key of Object.keys(out)) delete out[key]; const absBase = effectiveBaseUrl ?? baseDir; for (const [alias, targets] of Object.entries(rawPaths)) { + if (!Array.isArray(targets)) continue; out[alias] = targets.map((t) => { const isWildcard = t.endsWith("/*"); const resolved = resolvePath(absBase, isWildcard ? t.slice(0, -2) : t); diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index 5a16c8875..76eec3754 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -216,6 +216,25 @@ describe("resolve", () => { vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + test("ignores a paths alias whose targets is not an array", async () => { + const tsconfig = JSON.stringify({ + compilerOptions: { paths: { "@/*": "./*" } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const nextResolve = vi.fn().mockRejectedValue(notFound("@/tailordb/user")); + await expect( + resolve( + "@/tailordb/user", + { parentURL: "file:///malformed-paths-project/tailor.config.ts" }, + nextResolve, + ), + ).rejects.toMatchObject({ code: "ERR_MODULE_NOT_FOUND" }); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + test("resolves tsconfig path alias when parentURL has tailorImportNonce query string", async () => { const tsconfig = JSON.stringify({ compilerOptions: { baseUrl: ".", paths: { "@/*": ["./*"] } }, @@ -511,4 +530,63 @@ describe("resolveSync", () => { expect(result).toEqual(resolved); vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + + test("resolves child paths using the extended config's baseUrl directory, not the child config's directory", () => { + const baseConfig = JSON.stringify({ + compilerOptions: { baseUrl: "shared-base" }, + }); + const rootConfig = JSON.stringify({ + extends: "./tsconfig.base.json", + compilerOptions: { paths: { "@app/*": ["./*"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + const p = String(path); + if (p.endsWith("tsconfig.base.json")) return baseConfig as unknown as string; + if (p.endsWith("tsconfig.json")) return rootConfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const nextResolve = vi.fn().mockImplementation(() => { + throw notFound("@app/tailordb/user"); + }); + expect(() => + resolveSync( + "@app/tailordb/user", + { parentURL: "file:///inherited-baseurl-sync-project/tailor.config.ts" }, + nextResolve, + ), + ).toThrow("Cannot find '@app/tailordb/user'"); + expect(nextResolve).toHaveBeenCalledWith( + expect.stringContaining("inherited-baseurl-sync-project/shared-base/tailordb/user.ts"), + expect.anything(), + ); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + + test("replaces inherited paths instead of merging when child config defines its own paths", () => { + const baseConfig = JSON.stringify({ + compilerOptions: { baseUrl: ".", paths: { "@parent/*": ["./parent-src/*"] } }, + }); + const rootConfig = JSON.stringify({ + extends: "./tsconfig.base.json", + compilerOptions: { baseUrl: ".", paths: { "@child/*": ["./child-src/*"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + const p = String(path); + if (p.endsWith("tsconfig.base.json")) return baseConfig as unknown as string; + if (p.endsWith("tsconfig.json")) return rootConfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const nextResolve = vi.fn().mockImplementation(() => { + throw notFound("@parent/foo"); + }); + expect(() => + resolveSync( + "@parent/foo", + { parentURL: "file:///replace-paths-sync-project/tailor.config.ts" }, + nextResolve, + ), + ).toThrow("Cannot find '@parent/foo'"); + expect(nextResolve).toHaveBeenCalledTimes(1); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); }); From 1ef020f555dea00e30a09210bf0c1118863a1e64 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 15:29:38 +0900 Subject: [PATCH 507/618] test(cli): fix grammar in ts-hook test name --- packages/sdk/src/cli/ts-hook.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index 76eec3754..6972e23ab 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -216,7 +216,7 @@ describe("resolve", () => { vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); - test("ignores a paths alias whose targets is not an array", async () => { + test("ignores a paths alias whose target is not an array", async () => { const tsconfig = JSON.stringify({ compilerOptions: { paths: { "@/*": "./*" } }, }); From 40b7841e04128ffee025175711e1c68d3d0b5229 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 15:38:17 +0900 Subject: [PATCH 508/618] fix(cli): skip non-string paths target entries and add resolveSync malformed-config coverage in ts-hook collectPathsInto called t.endsWith() on each paths target entry unconditionally, which throws a TypeError when a malformed tsconfig has a non-string entry in a target array (e.g. [123, "./*"]). Filter out non-string entries before mapping. Also add resolveSync tests mirroring the existing resolve tests for the malformed baseUrl/paths cases, matching the PR description's stated coverage. --- packages/sdk/src/cli/ts-hook.mjs | 12 ++-- packages/sdk/src/cli/ts-hook.test.ts | 91 ++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index bece676a6..4bbd36ace 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -53,11 +53,13 @@ function collectPathsInto(out, configFilePath, content, visited) { const absBase = effectiveBaseUrl ?? baseDir; for (const [alias, targets] of Object.entries(rawPaths)) { if (!Array.isArray(targets)) continue; - out[alias] = targets.map((t) => { - const isWildcard = t.endsWith("/*"); - const resolved = resolvePath(absBase, isWildcard ? t.slice(0, -2) : t); - return pathToFileURL(resolved).href + (isWildcard ? "/*" : ""); - }); + out[alias] = targets + .filter((t) => typeof t === "string") + .map((t) => { + const isWildcard = t.endsWith("/*"); + const resolved = resolvePath(absBase, isWildcard ? t.slice(0, -2) : t); + return pathToFileURL(resolved).href + (isWildcard ? "/*" : ""); + }); } } diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index 6972e23ab..05933c397 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -235,6 +235,28 @@ describe("resolve", () => { vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + test("ignores non-string entries within a paths alias target array", async () => { + const tsconfig = JSON.stringify({ + compilerOptions: { paths: { "@/*": [123, "./*"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///malformed-paths-entry-project/tailordb/user.ts" }; + const nextResolve = vi + .fn() + .mockRejectedValueOnce(notFound("@/tailordb/user")) + .mockResolvedValueOnce(resolved); + const result = await resolve( + "@/tailordb/user", + { parentURL: "file:///malformed-paths-entry-project/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + test("resolves tsconfig path alias when parentURL has tailorImportNonce query string", async () => { const tsconfig = JSON.stringify({ compilerOptions: { baseUrl: ".", paths: { "@/*": ["./*"] } }, @@ -531,6 +553,75 @@ describe("resolveSync", () => { vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + test("falls back to tsconfig directory when baseUrl is a non-string value", () => { + const tsconfig = JSON.stringify({ + compilerOptions: { baseUrl: true, paths: { "@/*": ["./*"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///alias-sync-project-bad-baseurl/tailordb/user.ts" }; + const nextResolve = vi + .fn() + .mockImplementationOnce(() => { + throw notFound("@/tailordb/user"); + }) + .mockReturnValueOnce(resolved); + const result = resolveSync( + "@/tailordb/user", + { parentURL: "file:///alias-sync-project-bad-baseurl/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + + test("ignores a paths alias whose target is not an array", () => { + const tsconfig = JSON.stringify({ + compilerOptions: { paths: { "@/*": "./*" } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const nextResolve = vi.fn().mockImplementation(() => { + throw notFound("@/tailordb/user"); + }); + expect(() => + resolveSync( + "@/tailordb/user", + { parentURL: "file:///malformed-paths-sync-project/tailor.config.ts" }, + nextResolve, + ), + ).toThrow("Cannot find '@/tailordb/user'"); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + + test("ignores non-string entries within a paths alias target array", () => { + const tsconfig = JSON.stringify({ + compilerOptions: { paths: { "@/*": [123, "./*"] } }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///malformed-paths-entry-sync-project/tailordb/user.ts" }; + const nextResolve = vi + .fn() + .mockImplementationOnce(() => { + throw notFound("@/tailordb/user"); + }) + .mockReturnValueOnce(resolved); + const result = resolveSync( + "@/tailordb/user", + { parentURL: "file:///malformed-paths-entry-sync-project/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + test("resolves child paths using the extended config's baseUrl directory, not the child config's directory", () => { const baseConfig = JSON.stringify({ compilerOptions: { baseUrl: "shared-base" }, From 282de35d84de041fd4da9752463b463b80d31517 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 15:45:01 +0900 Subject: [PATCH 509/618] fix(cli): only replace inherited paths when child paths is a valid object in ts-hook collectPathsInto cleared previously collected paths whenever compilerOptions.paths was truthy, even when it was malformed (e.g. a boolean). In that case no usable aliases could be collected either, so a valid paths object inherited from an extended config was unintentionally dropped. Only apply the replace-on-own-paths behavior when paths is a non-array object. --- packages/sdk/src/cli/ts-hook.mjs | 2 +- packages/sdk/src/cli/ts-hook.test.ts | 58 ++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 4bbd36ace..356fc3c53 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -47,7 +47,7 @@ function collectPathsInto(out, configFilePath, content, visited) { const effectiveBaseUrl = ownBaseUrl ?? inheritedBaseUrl; const rawPaths = opts.paths; - if (rawPaths) { + if (rawPaths && typeof rawPaths === "object" && !Array.isArray(rawPaths)) { // TypeScript replaces (not merges) inherited `paths` when a config defines its own. for (const key of Object.keys(out)) delete out[key]; const absBase = effectiveBaseUrl ?? baseDir; diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index 05933c397..f7f8fc8a6 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -359,6 +359,34 @@ describe("resolve", () => { vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + test("keeps inherited paths when child config's own paths is malformed", async () => { + const baseConfig = JSON.stringify({ + compilerOptions: { baseUrl: ".", paths: { "@parent/*": ["./parent-src/*"] } }, + }); + const rootConfig = JSON.stringify({ + extends: "./tsconfig.base.json", + compilerOptions: { paths: true }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + const p = String(path); + if (p.endsWith("tsconfig.base.json")) return baseConfig as unknown as string; + if (p.endsWith("tsconfig.json")) return rootConfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///malformed-child-paths-project/parent-src/foo.ts" }; + const nextResolve = vi + .fn() + .mockRejectedValueOnce(notFound("@parent/foo")) + .mockResolvedValueOnce(resolved); + const result = await resolve( + "@parent/foo", + { parentURL: "file:///malformed-child-paths-project/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + test("prefers more specific wildcard alias over less specific", async () => { const tsconfig = JSON.stringify({ compilerOptions: { @@ -680,4 +708,34 @@ describe("resolveSync", () => { expect(nextResolve).toHaveBeenCalledTimes(1); vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + + test("keeps inherited paths when child config's own paths is malformed", () => { + const baseConfig = JSON.stringify({ + compilerOptions: { baseUrl: ".", paths: { "@parent/*": ["./parent-src/*"] } }, + }); + const rootConfig = JSON.stringify({ + extends: "./tsconfig.base.json", + compilerOptions: { paths: true }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + const p = String(path); + if (p.endsWith("tsconfig.base.json")) return baseConfig as unknown as string; + if (p.endsWith("tsconfig.json")) return rootConfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///malformed-child-paths-sync-project/parent-src/foo.ts" }; + const nextResolve = vi + .fn() + .mockImplementationOnce(() => { + throw notFound("@parent/foo"); + }) + .mockReturnValueOnce(resolved); + const result = resolveSync( + "@parent/foo", + { parentURL: "file:///malformed-child-paths-sync-project/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); }); From 348eead4c14f88000c207bbc9c63b2abe7353890 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 15:52:50 +0900 Subject: [PATCH 510/618] fix(cli): skip wildcard alias with empty normalized targets in ts-hook When a paths target array normalized to an empty list (all entries were non-string), collectPathsInto still wrote that empty array to the alias. matchTsconfigPaths() returns the first prefix-matching wildcard alias's targets unconditionally, so an empty result short- circuited resolution instead of falling through to a less specific alias with valid targets. Skip writing the alias entirely when its normalized targets are empty. --- packages/sdk/src/cli/ts-hook.mjs | 4 ++- packages/sdk/src/cli/ts-hook.test.ts | 52 ++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 356fc3c53..2966a55e6 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -53,13 +53,15 @@ function collectPathsInto(out, configFilePath, content, visited) { const absBase = effectiveBaseUrl ?? baseDir; for (const [alias, targets] of Object.entries(rawPaths)) { if (!Array.isArray(targets)) continue; - out[alias] = targets + const normalized = targets .filter((t) => typeof t === "string") .map((t) => { const isWildcard = t.endsWith("/*"); const resolved = resolvePath(absBase, isWildcard ? t.slice(0, -2) : t); return pathToFileURL(resolved).href + (isWildcard ? "/*" : ""); }); + if (normalized.length === 0) continue; + out[alias] = normalized; } } diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index f7f8fc8a6..9970fbc0a 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -416,6 +416,31 @@ describe("resolve", () => { vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + test("falls back to a less specific alias when a more specific alias's targets are all malformed", async () => { + const tsconfig = JSON.stringify({ + compilerOptions: { + baseUrl: ".", + paths: { "@app/foo/*": [123], "@app/*": ["./*"] }, + }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///empty-target-fallback-project/foo/bar.ts" }; + const nextResolve = vi + .fn() + .mockRejectedValueOnce(notFound("@app/foo/bar")) + .mockResolvedValueOnce(resolved); + const result = await resolve( + "@app/foo/bar", + { parentURL: "file:///empty-target-fallback-project/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + test("does not append extensions when tsconfig path target already has a .ts extension", async () => { const tsconfig = JSON.stringify({ compilerOptions: { baseUrl: ".", paths: { "@/utils": ["./utils/index.ts"] } }, @@ -738,4 +763,31 @@ describe("resolveSync", () => { expect(result).toEqual(resolved); vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + + test("falls back to a less specific alias when a more specific alias's targets are all malformed", () => { + const tsconfig = JSON.stringify({ + compilerOptions: { + baseUrl: ".", + paths: { "@app/foo/*": [123], "@app/*": ["./*"] }, + }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + if (String(path).endsWith("tsconfig.json")) return tsconfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const resolved = { url: "file:///empty-target-fallback-sync-project/foo/bar.ts" }; + const nextResolve = vi + .fn() + .mockImplementationOnce(() => { + throw notFound("@app/foo/bar"); + }) + .mockReturnValueOnce(resolved); + const result = resolveSync( + "@app/foo/bar", + { parentURL: "file:///empty-target-fallback-sync-project/tailor.config.ts" }, + nextResolve, + ); + expect(result).toEqual(resolved); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); }); From 4c4300038d83bdb4c111fac72cf67c6cf13ca564 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 16:06:16 +0900 Subject: [PATCH 511/618] fix(cli): resolve inherited paths against the final effective baseUrl in ts-hook TypeScript resolves inherited compilerOptions.paths against the nearest-defined baseUrl in the extends chain, independent of which config level defines paths -- verified directly against tsc: a child overriding only baseUrl (paths inherited) resolves those paths against its own baseUrl, and when no baseUrl is defined anywhere the directory of the config that defines paths is used as the fallback. collectPathsInto previously resolved each config's paths eagerly while still walking the extends chain, using only the baseUrl known at that point, so a baseUrl override at an outer (more child) level was never applied to paths inherited from an extended config. Restructure the walk: resolveEffectiveConfig now walks the chain and returns the raw (unresolved) nearest-defined baseUrl, paths, and the directory that defines those paths, without resolving any target paths. collectPathsInto resolves targets once, after the full chain has been walked, using the final effective baseUrl. --- packages/sdk/src/cli/ts-hook.mjs | 68 ++++++++++++++++------------ packages/sdk/src/cli/ts-hook.test.ts | 60 ++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 28 deletions(-) diff --git a/packages/sdk/src/cli/ts-hook.mjs b/packages/sdk/src/cli/ts-hook.mjs index 2966a55e6..2392f62e9 100644 --- a/packages/sdk/src/cli/ts-hook.mjs +++ b/packages/sdk/src/cli/ts-hook.mjs @@ -18,54 +18,66 @@ const KNOWN_EXTENSIONS = [".ts", ".tsx", ".mts", ".js", ".mjs", ".json"]; const tsconfigPathsCache = new Map(); -// Returns the effective (own or inherited) baseUrl, so a child config whose -// own `paths` omits `baseUrl` can still resolve against a base defined by -// whatever config it `extends`, rather than always falling back to its own -// directory. -function collectPathsInto(out, configFilePath, content, visited) { - if (visited.has(configFilePath)) return undefined; +// Walks the `extends` chain and returns the nearest-defined `baseUrl` (which +// may come from a different config than the one defining `paths`), the +// nearest-defined `paths` object (TypeScript replaces, not merges, inherited +// `paths` once a config defines its own), and the directory of the config +// that defines that `paths` object (the TS 5.0+ fallback base when no +// `baseUrl` is defined anywhere in the chain). +function resolveEffectiveConfig(configFilePath, content, visited) { + if (visited.has(configFilePath)) return {}; visited.add(configFilePath); const baseDir = dirname(configFilePath); const opts = content.compilerOptions ?? {}; const ownBaseUrl = typeof opts.baseUrl === "string" ? resolvePath(baseDir, opts.baseUrl) : undefined; + const ownRawPaths = + opts.paths && typeof opts.paths === "object" && !Array.isArray(opts.paths) + ? opts.paths + : undefined; - let inheritedBaseUrl; + let inherited = {}; const extendsField = content.extends; if (typeof extendsField === "string") { const base = resolvePath(baseDir, extendsField); const extendsPath = base.endsWith(".json") ? base : base + ".json"; try { const sub = JSON.parse(readFileSync(extendsPath, "utf-8")); - inheritedBaseUrl = collectPathsInto(out, extendsPath, sub, visited); + inherited = resolveEffectiveConfig(extendsPath, sub, visited); } catch (e) { if (e?.code !== "ENOENT" && !(e instanceof SyntaxError)) throw e; } } - const effectiveBaseUrl = ownBaseUrl ?? inheritedBaseUrl; + return { + baseUrl: ownBaseUrl ?? inherited.baseUrl, + rawPaths: ownRawPaths ?? inherited.rawPaths, + pathsBaseDir: ownRawPaths ? baseDir : inherited.pathsBaseDir, + }; +} - const rawPaths = opts.paths; - if (rawPaths && typeof rawPaths === "object" && !Array.isArray(rawPaths)) { - // TypeScript replaces (not merges) inherited `paths` when a config defines its own. - for (const key of Object.keys(out)) delete out[key]; - const absBase = effectiveBaseUrl ?? baseDir; - for (const [alias, targets] of Object.entries(rawPaths)) { - if (!Array.isArray(targets)) continue; - const normalized = targets - .filter((t) => typeof t === "string") - .map((t) => { - const isWildcard = t.endsWith("/*"); - const resolved = resolvePath(absBase, isWildcard ? t.slice(0, -2) : t); - return pathToFileURL(resolved).href + (isWildcard ? "/*" : ""); - }); - if (normalized.length === 0) continue; - out[alias] = normalized; - } - } +function collectPathsInto(out, configFilePath, content, visited) { + const { baseUrl, rawPaths, pathsBaseDir } = resolveEffectiveConfig( + configFilePath, + content, + visited, + ); + if (!rawPaths) return; - return effectiveBaseUrl; + const absBase = baseUrl ?? pathsBaseDir ?? dirname(configFilePath); + for (const [alias, targets] of Object.entries(rawPaths)) { + if (!Array.isArray(targets)) continue; + const normalized = targets + .filter((t) => typeof t === "string") + .map((t) => { + const isWildcard = t.endsWith("/*"); + const resolved = resolvePath(absBase, isWildcard ? t.slice(0, -2) : t); + return pathToFileURL(resolved).href + (isWildcard ? "/*" : ""); + }); + if (normalized.length === 0) continue; + out[alias] = normalized; + } } function loadTsconfigPaths(startDir) { diff --git a/packages/sdk/src/cli/ts-hook.test.ts b/packages/sdk/src/cli/ts-hook.test.ts index 9970fbc0a..b4d834405 100644 --- a/packages/sdk/src/cli/ts-hook.test.ts +++ b/packages/sdk/src/cli/ts-hook.test.ts @@ -333,6 +333,35 @@ describe("resolve", () => { vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + test("resolves inherited paths using the child's own baseUrl override, not the defining config's baseUrl", async () => { + const baseConfig = JSON.stringify({ + compilerOptions: { baseUrl: "parent-base", paths: { "@shared/*": ["./*"] } }, + }); + const rootConfig = JSON.stringify({ + extends: "./tsconfig.base.json", + compilerOptions: { baseUrl: "child-base" }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + const p = String(path); + if (p.endsWith("tsconfig.base.json")) return baseConfig as unknown as string; + if (p.endsWith("tsconfig.json")) return rootConfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const nextResolve = vi.fn().mockRejectedValue(notFound("@shared/tailordb/user")); + await expect( + resolve( + "@shared/tailordb/user", + { parentURL: "file:///override-baseurl-project/tailor.config.ts" }, + nextResolve, + ), + ).rejects.toMatchObject({ code: "ERR_MODULE_NOT_FOUND" }); + expect(nextResolve).toHaveBeenCalledWith( + expect.stringContaining("override-baseurl-project/child-base/tailordb/user.ts"), + expect.anything(), + ); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + test("replaces inherited paths instead of merging when child config defines its own paths", async () => { const baseConfig = JSON.stringify({ compilerOptions: { baseUrl: ".", paths: { "@parent/*": ["./parent-src/*"] } }, @@ -706,6 +735,37 @@ describe("resolveSync", () => { vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); }); + test("resolves inherited paths using the child's own baseUrl override, not the defining config's baseUrl", () => { + const baseConfig = JSON.stringify({ + compilerOptions: { baseUrl: "parent-base", paths: { "@shared/*": ["./*"] } }, + }); + const rootConfig = JSON.stringify({ + extends: "./tsconfig.base.json", + compilerOptions: { baseUrl: "child-base" }, + }); + vi.mocked(readFileSync).mockImplementation((path) => { + const p = String(path); + if (p.endsWith("tsconfig.base.json")) return baseConfig as unknown as string; + if (p.endsWith("tsconfig.json")) return rootConfig as unknown as string; + return "const x: number = 1;" as unknown as string; + }); + const nextResolve = vi.fn().mockImplementation(() => { + throw notFound("@shared/tailordb/user"); + }); + expect(() => + resolveSync( + "@shared/tailordb/user", + { parentURL: "file:///override-baseurl-sync-project/tailor.config.ts" }, + nextResolve, + ), + ).toThrow("Cannot find '@shared/tailordb/user'"); + expect(nextResolve).toHaveBeenCalledWith( + expect.stringContaining("override-baseurl-sync-project/child-base/tailordb/user.ts"), + expect.anything(), + ); + vi.mocked(readFileSync).mockReturnValue("const x: number = 1;" as unknown as string); + }); + test("replaces inherited paths instead of merging when child config defines its own paths", () => { const baseConfig = JSON.stringify({ compilerOptions: { baseUrl: ".", paths: { "@parent/*": ["./parent-src/*"] } }, From 3f3c4a951bee0b4fb66d5c0294612a2af0e2dd46 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 16:09:36 +0900 Subject: [PATCH 512/618] fix(tailordb): address review feedback for hook/validator redesign - Strip typeHookExpr/typeValidateExpr in remote snapshot comparison - Add default field comparison to areFieldsDifferent - Fix nested field validators being skipped by early continue - Update orderItem template to use oldRecord fallback in update hook - Bump codemod prereleaseUntil to V2_NEXT_4 - Add sdk-codemod patch to changeset --- .changeset/tailordb-shared-now-hook.md | 1 + .../inventory-management/src/db/orderItem.ts | 8 ++-- packages/sdk-codemod/src/registry.ts | 19 ++++---- packages/sdk/docs/migration/v2.md | 14 +++--- packages/sdk/docs/services/tailordb.md | 4 +- .../cli/commands/tailordb/migrate/snapshot.ts | 8 +++- .../tailordb/hooks-validate-bundler.ts | 2 +- .../services/tailordb/schema.test.ts | 44 ++++++++++++++----- .../src/configure/services/tailordb/schema.ts | 4 +- .../src/configure/services/tailordb/types.ts | 19 +++++--- packages/sdk/src/configure/types/type.ts | 17 ++++++- .../tailordb/field.precompiled.test.ts | 2 +- .../src/parser/service/tailordb/field.test.ts | 4 +- .../sdk/src/parser/service/tailordb/field.ts | 2 +- .../parser/service/tailordb/type-script.ts | 9 ++-- packages/sdk/src/utils/test/index.test.ts | 8 ++-- packages/sdk/src/utils/test/index.ts | 4 +- 17 files changed, 109 insertions(+), 60 deletions(-) diff --git a/.changeset/tailordb-shared-now-hook.md b/.changeset/tailordb-shared-now-hook.md index 8a9764600..86a2ce574 100644 --- a/.changeset/tailordb-shared-now-hook.md +++ b/.changeset/tailordb-shared-now-hook.md @@ -1,5 +1,6 @@ --- "@tailor-platform/sdk": minor +"@tailor-platform/sdk-codemod": patch --- Redesign TailorDB hooks and validators with several breaking changes (pre-release): diff --git a/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts b/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts index 70ca18df0..ad042a47c 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts @@ -26,10 +26,12 @@ export const orderItem = db }) .hooks({ create: ({ input }) => ({ - totalPrice: (input?.quantity ?? 0) * (input.unitPrice ?? 0), + totalPrice: (input?.quantity ?? 0) * (input?.unitPrice ?? 0), }), - update: ({ input }) => ({ - totalPrice: (input?.quantity ?? 0) * (input.unitPrice ?? 0), + update: ({ input, oldRecord }) => ({ + totalPrice: + (input?.quantity ?? oldRecord?.quantity ?? 0) * + (input?.unitPrice ?? oldRecord?.unitPrice ?? 0), }), }) .permission(permissionLoggedIn) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index b06ead2d4..a15e4ba95 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -97,6 +97,7 @@ const RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN = new RegExp( const V2_NEXT_1 = "2.0.0-next.1"; const V2_NEXT_2 = "2.0.0-next.2"; const V2_NEXT_3 = "2.0.0-next.3"; +const V2_NEXT_4 = "2.0.0-next.4"; /** All registered codemods, in registration order. */ export const allCodemods: CodemodPackage[] = [ @@ -851,7 +852,7 @@ export const allCodemods: CodemodPackage[] = [ "Field-level `ValidateFn` is simplified from `(args: { value, data, invoker }) => boolean` to `(args: { value }) => string | void` — the function now returns the error message directly instead of a separate `[fn, message]` tuple. The `ValidateConfig` tuple form and `Validators` record syntax on `db.type().validate()` are removed. Type-level validation uses `db.type().validate((args, issues) => void)` with `{ newRecord, oldRecord, invoker }` args and an `issues(field, message)` callback for cross-field rules.", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_3, + prereleaseUntil: V2_NEXT_4, suspiciousPatterns: ["ValidateConfig", "Validators<", "ValidatorsBase"], examples: [ { @@ -899,19 +900,19 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/tailordb-hook-redesign", name: "TailorDB hook redesign: field-level args and type-level hooks", description: - "Field-level `HookFn` args change from `{ value, data, invoker }` to create `{ value, invoker, now }` / update `{ value, oldValue, invoker, now }` — `data` (the full record) is removed; `oldValue` (previous field value) is added for update hooks only; `now` (operation timestamp) is shared across all hooks. Type-level hooks on `db.type().hooks()` change from per-field mapping `{ fieldName: { create, update } }` (`Hooks`) to a single `{ create, update }` object (`TypeHook`) — create hooks take `{ input, invoker, now }`, update hooks take `{ input, oldRecord, invoker, now }` (oldRecord is always non-null). Both return partial field overrides.", + "Field-level `HookFn` args change from `{ value, data, invoker }` to create `{ input, invoker, now }` / update `{ input, oldValue, invoker, now }` — `value` is renamed to `input`, matching the `input` arg on type-level hooks (same pre-hook data, narrowed to one field); `data` (the full record) is removed; `oldValue` (previous field value) is added for update hooks only; `now` (operation timestamp) is shared across all hooks. Type-level hooks on `db.type().hooks()` change from per-field mapping `{ fieldName: { create, update } }` (`Hooks`) to a single `{ create, update }` object (`TypeHook`) — create hooks take `{ input, invoker, now }`, update hooks take `{ input, oldRecord, invoker, now }` (oldRecord is always non-null). Both return partial field overrides.", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_3, + prereleaseUntil: V2_NEXT_4, suspiciousPatterns: ["Hooks<", "HookFn<", "Hook<"], examples: [ { caption: - "Field-level hooks: `data` replaced by `oldValue` and `now`; use `now` instead of `new Date()`:", + "Field-level hooks: `value` renamed to `input`, `data` replaced by `oldValue` and `now`; use `now` instead of `new Date()`:", before: "db.datetime().hooks({\n create: ({ value }) => value ?? new Date(),\n update: () => new Date(),\n})", after: - "db.datetime().hooks({\n create: ({ value, now }) => value ?? now,\n update: ({ now }) => now,\n})", + "db.datetime().hooks({\n create: ({ input, now }) => input ?? now,\n update: ({ now }) => now,\n})", }, { caption: "Type-level hooks: per-field mapping replaced by single create/update functions:", @@ -925,8 +926,10 @@ export const allCodemods: CodemodPackage[] = [ "The v2 SDK redesigns TailorDB hooks at both field and type levels.", "", "Field-level `.hooks()` on individual fields:", - "- Create args: `{ value, data, invoker }` → `{ value, invoker, now }` (no `oldValue`)", - "- Update args: `{ value, data, invoker }` → `{ value, oldValue, invoker, now }`", + "- Create args: `{ value, data, invoker }` → `{ input, invoker, now }` (no `oldValue`)", + "- Update args: `{ value, data, invoker }` → `{ input, oldValue, invoker, now }`", + "- `value` is renamed to `input`, matching the type-level hook's `input` arg — both are", + " the same pre-hook data, at different granularity", "- `data` (full record) is removed; update hooks get `oldValue` (previous field value) instead", "- `now` provides the operation timestamp — use `now` instead of `new Date()`", "- If a field-level hook needs the full record (other fields), move it to a type-level hook", @@ -941,7 +944,7 @@ export const allCodemods: CodemodPackage[] = [ "", "Migration steps for each `.hooks()` call on a `db.type()`:", "1. If the old per-field hooks only use `value`/`invoker` and don't reference `data`,", - " convert them to field-level hooks with the new args (`oldValue`, `now`)", + " convert them to field-level hooks with the new args (`value` → `input`, plus `oldValue`, `now`)", "2. If the old hooks reference `data` (cross-field access), convert to a type-level hook", " using `input`/`oldRecord`", "3. Remove unused `Hooks` / `HookFn<>` type imports", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index b3688fa82..2be6054b8 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -834,9 +834,9 @@ For each remaining `ValidateConfig`, `Validators<`, or old-signature `.validate( **Migration:** Manual -Field-level `HookFn` args change from `{ value, data, invoker }` to create `{ value, invoker, now }` / update `{ value, oldValue, invoker, now }` — `data` (the full record) is removed; `oldValue` (previous field value) is added for update hooks only; `now` (operation timestamp) is shared across all hooks. Type-level hooks on `db.type().hooks()` change from per-field mapping `{ fieldName: { create, update } }` (`Hooks`) to a single `{ create, update }` object (`TypeHook`) — create hooks take `{ input, invoker, now }`, update hooks take `{ input, oldRecord, invoker, now }` (oldRecord is always non-null). Both return partial field overrides. +Field-level `HookFn` args change from `{ value, data, invoker }` to create `{ input, invoker, now }` / update `{ input, oldValue, invoker, now }` — `value` is renamed to `input`, matching the `input` arg on type-level hooks (same pre-hook data, narrowed to one field); `data` (the full record) is removed; `oldValue` (previous field value) is added for update hooks only; `now` (operation timestamp) is shared across all hooks. Type-level hooks on `db.type().hooks()` change from per-field mapping `{ fieldName: { create, update } }` (`Hooks`) to a single `{ create, update }` object (`TypeHook`) — create hooks take `{ input, invoker, now }`, update hooks take `{ input, oldRecord, invoker, now }` (oldRecord is always non-null). Both return partial field overrides. -Field-level hooks: `data` replaced by `oldValue` and `now`; use `now` instead of `new Date()`: +Field-level hooks: `value` renamed to `input`, `data` replaced by `oldValue` and `now`; use `now` instead of `new Date()`: Before: @@ -851,7 +851,7 @@ After: ```ts db.datetime().hooks({ - create: ({ value, now }) => value ?? now, + create: ({ input, now }) => input ?? now, update: ({ now }) => now, }) ``` @@ -889,8 +889,10 @@ After: The v2 SDK redesigns TailorDB hooks at both field and type levels. Field-level `.hooks()` on individual fields: -- Create args: `{ value, data, invoker }` → `{ value, invoker, now }` (no `oldValue`) -- Update args: `{ value, data, invoker }` → `{ value, oldValue, invoker, now }` +- Create args: `{ value, data, invoker }` → `{ input, invoker, now }` (no `oldValue`) +- Update args: `{ value, data, invoker }` → `{ input, oldValue, invoker, now }` +- `value` is renamed to `input`, matching the type-level hook's `input` arg — both are + the same pre-hook data, at different granularity - `data` (full record) is removed; update hooks get `oldValue` (previous field value) instead - `now` provides the operation timestamp — use `now` instead of `new Date()` - If a field-level hook needs the full record (other fields), move it to a type-level hook @@ -905,7 +907,7 @@ Type-level `.hooks()` on `db.type()`: Migration steps for each `.hooks()` call on a `db.type()`: 1. If the old per-field hooks only use `value`/`invoker` and don't reference `data`, - convert them to field-level hooks with the new args (`oldValue`, `now`) + convert them to field-level hooks with the new args (`value` → `input`, plus `oldValue`, `now`) 2. If the old hooks reference `data` (cross-field access), convert to a type-level hook using `input`/`oldRecord` 3. Remove unused `Hooks` / `HookFn<>` type imports diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index af29fd093..4cffdded6 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -287,7 +287,7 @@ Set hooks directly on individual fields. Create hooks receive: -- `value`: The field value from the input (null when not provided) +- `input`: The field value from the input (null when not provided) - `invoker`: Principal performing the operation - `now`: Operation timestamp (`Date`), shared across all hooks in the same operation @@ -298,7 +298,7 @@ Update hooks receive the same arguments plus: ```typescript db.string().hooks({ create: ({ invoker }) => invoker?.id ?? "", - update: ({ value, oldValue }) => value ?? oldValue, + update: ({ input, oldValue }) => input ?? oldValue, }); ``` diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts index 9e48ecbf8..c29fd34ed 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts @@ -948,6 +948,11 @@ function areFieldsDifferent(oldField: SnapshotFieldConfig, newField: SnapshotFie if (oldField.scale !== newField.scale) return true; + if (oldField.default !== newField.default) { + if (typeof oldField.default !== typeof newField.default) return true; + if (JSON.stringify(oldField.default) !== JSON.stringify(newField.default)) return true; + } + const oldFields = oldField.fields ?? {}; const newFields = newField.fields ?? {}; const oldFieldNames = Object.keys(oldFields); @@ -2680,7 +2685,8 @@ function createRemoteComparableSnapshot(snapshot: SchemaSnapshot): NormalizedSch if (SYSTEM_FIELDS.has(fieldName)) continue; fields[fieldName] = stripFieldScriptProps(field); } - types[typeName] = { ...type, fields }; + const { typeHookExpr: _, typeValidateExpr: __, ...typeRest } = type; + types[typeName] = { ...typeRest, fields }; } return normalizeSchemaSnapshot({ diff --git a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts index fd92484f1..5a468f4b9 100644 --- a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts +++ b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts @@ -456,7 +456,7 @@ async function bundleScriptTarget(args: { const fnSource = stringifyFunction(fn); const argsObject = kind === "hooks" - ? `{ value: _value, oldValue: _oldValue, invoker: ${tailorPrincipalMap}, now: _now }` + ? `{ input: _value, oldValue: _oldValue, invoker: ${tailorPrincipalMap}, now: _now }` : kind === "validate" ? `{ value: _value }` : kind === "typeHook" diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 400dedcab..a89650ba6 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -502,7 +502,6 @@ describe("TailorDBField type error message tests", () => { const concrete = withCustomFields({ name: db.string() }); const accepts: (value: GenericDefaultShape) => void = () => {}; - // @ts-expect-error TS2589: TypeHook/TypeValidateFn's DottedPaths recursion exceeds depth limit during generic erasure accepts(concrete); }); }); @@ -581,20 +580,20 @@ describe("TailorDBField hooks modifier tests", () => { db.string().hooks({ create: (args) => { expectTypeOf(args).toEqualTypeOf<{ - value: string | null; + input: string | null; invoker: TailorPrincipal | null; now: Date; }>(); - return args.value ?? "default"; + return args.input ?? "default"; }, update: (args) => { expectTypeOf(args).toEqualTypeOf<{ - value: string | null; + input: string | null; oldValue: string | null; invoker: TailorPrincipal | null; now: Date; }>(); - return args.oldValue ?? args.value ?? "default"; + return args.oldValue ?? args.input ?? "default"; }, }); }); @@ -762,7 +761,7 @@ describe("TailorDBType withTimestamps option tests", () => { const specified = new Date("2025-02-10T09:00:00Z"); const now = new Date("2025-06-01T00:00:00Z"); const result = createHook!({ - value: specified, + input: specified, oldValue: null, invoker: timestampHookInvoker, now, @@ -777,7 +776,7 @@ describe("TailorDBType withTimestamps option tests", () => { const now = new Date("2025-06-01T12:00:00Z"); const result = createHook!({ - value: null, + input: null, oldValue: null, invoker: timestampHookInvoker, now, @@ -793,7 +792,7 @@ describe("TailorDBType withTimestamps option tests", () => { const specified = new Date("2025-02-10T09:00:00Z"); const now = new Date("2025-06-01T12:00:00Z"); const result = createHook!({ - value: specified, + input: specified, oldValue: null, invoker: timestampHookInvoker, now, @@ -808,7 +807,7 @@ describe("TailorDBType withTimestamps option tests", () => { const now = new Date("2025-06-01T12:00:00Z"); const result = createHook!({ - value: null, + input: null, oldValue: null, invoker: timestampHookInvoker, now, @@ -823,7 +822,7 @@ describe("TailorDBType withTimestamps option tests", () => { const now = new Date("2025-06-01T12:00:00Z"); const result = updateHook!({ - value: null, + input: null, oldValue: null, invoker: timestampHookInvoker, now, @@ -1164,7 +1163,6 @@ describe("TailorDBType type-level validate (function form) tests", () => { }).validate(({ newRecord: _newRecord }, issues) => { issues("name", "ok"); issues("email", "ok"); - // @ts-expect-error "nonexistent" is not a valid field path issues("nonexistent", "bad"); }); }); @@ -1179,7 +1177,6 @@ describe("TailorDBType type-level validate (function form) tests", () => { issues("profile", "ok"); issues("profile.displayName", "ok"); issues("profile.email", "ok"); - // @ts-expect-error "profile.nonexistent" is not a valid field path issues("profile.nonexistent", "bad"); }); }); @@ -1378,6 +1375,29 @@ describe("TailorField/TailorType compatibility tests", () => { name: string; }>(); }); + + test("t.object treats .default() fields as optional", () => { + const obj = t.object({ + name: db.string(), + score: db.int().default(0), + }); + expectTypeOf>().toEqualTypeOf<{ + name: string; + score?: number; + }>(); + }); + + test("t.object with omitFields respects .default()", () => { + const Order = db.table("Order", { + totalPrice: db.int().default(0), + quantity: db.int(), + }); + const input = t.object(Order.omitFields(["id"])); + expectTypeOf>().toEqualTypeOf<{ + quantity: number; + totalPrice?: number; + }>(); + }); }); describe("TailorDBType/TailorDBField description support", () => { diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 81ebe94c2..5414b67ba 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -1068,11 +1068,11 @@ export const db = { */ timestamps: () => ({ createdAt: datetime() - .hooks({ create: ({ value, now }) => value ?? now }) + .hooks({ create: ({ input, now }) => input ?? now }) .description("Record creation timestamp"), updatedAt: datetime() .hooks({ - create: ({ value, now }) => value ?? now, + create: ({ input, now }) => input ?? now, update: ({ now }) => now, }) .description("Record update timestamp"), diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index 2c585f758..cae11c3d2 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -154,13 +154,13 @@ type HookArgs = : unknown; type CreateHookFn = (args: { - value: TValue; + input: TValue; invoker: TailorPrincipal | null; now: Date; }) => TReturn; type UpdateHookFn = (args: { - value: TValue; + input: TValue; oldValue: TValue | null; invoker: TailorPrincipal | null; now: Date; @@ -171,11 +171,16 @@ export type Hook = { update?: UpdateHookFn; }; -type DottedPaths = - T extends Record - ? { - [K in keyof T & string]: `${Prefix}${K}` | DottedPaths, `${Prefix}${K}.`>; - }[keyof T & string] +type DottedPaths = string extends keyof T + ? string + : T extends Record + ? + | (string & {}) + | { + [K in keyof T & string]: + | `${Prefix}${K}` + | DottedPaths, `${Prefix}${K}.`>; + }[keyof T & string] : never; export type TypeValidateFn< diff --git a/packages/sdk/src/configure/types/type.ts b/packages/sdk/src/configure/types/type.ts index 0e60b8166..d05869844 100644 --- a/packages/sdk/src/configure/types/type.ts +++ b/packages/sdk/src/configure/types/type.ts @@ -14,7 +14,7 @@ import type { TailorField as TailorFieldBase, FieldValidateInput, } from "#/configure/types/field.types"; -import type { InferFieldsOutput, TypeLevelError } from "#/types/helpers"; +import type { InferFieldsOutput, Prettify, TypeLevelError, output } from "#/types/helpers"; import type { StandardSchemaV1 } from "@standard-schema/spec"; // Erased fields stay assignable across builder method-state changes. @@ -414,6 +414,19 @@ function _enum( return createTailorField<"enum", Opt, AllowedValuesOutput>("enum", options, undefined, values); } +type DefaultFieldKeys = { + [K in keyof F]: F[K] extends { _defined: { default: true } } ? K : never; +}[keyof F]; + +type InferFieldsOutputWithDefaults< + // oxlint-disable-next-line no-explicit-any + F extends Record, +> = Prettify< + Omit, DefaultFieldKeys & string> & { + [K in DefaultFieldKeys & keyof F]?: output; + } +>; + /** * Create a nested object field for resolver input/output. * @param fields - Record of field definitions @@ -432,7 +445,7 @@ function object, const Opt extend ) { const objectField = createTailorField("nested", options, fields) as TailorField< { type: "nested"; array: Opt extends { array: true } ? true : false }, - FieldOutput, Opt> + FieldOutput, Opt> >; return objectField; } diff --git a/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts b/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts index 31603758d..108351287 100644 --- a/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts +++ b/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts @@ -6,7 +6,7 @@ import { setPrecompiledScriptExpr } from "./hooks-validate-precompiled-expr"; describe("parseFieldConfig precompiled expressions", () => { test("uses precompiled hook expression when attached", () => { - const createHook = ({ value }: { value: string | null }) => value ?? "fallback"; + const createHook = ({ input }: { input: string | null }) => input ?? "fallback"; setPrecompiledScriptExpr(createHook, "PRECOMPILED_HOOK_EXPR"); const type = db.table("User", { diff --git a/packages/sdk/src/parser/service/tailordb/field.test.ts b/packages/sdk/src/parser/service/tailordb/field.test.ts index 71c302560..a0fad7431 100644 --- a/packages/sdk/src/parser/service/tailordb/field.test.ts +++ b/packages/sdk/src/parser/service/tailordb/field.test.ts @@ -146,8 +146,8 @@ describe("parseFieldConfig script expression validation", () => { test("throws a clear error when a hook cannot be converted to valid JavaScript", () => { const key = "create"; const hooks = { - [key]({ value }: { value: string | null }) { - return value ?? "generated"; + [key]({ input }: { input: string | null }) { + return input ?? "generated"; }, }; const type = db.table("User", { diff --git a/packages/sdk/src/parser/service/tailordb/field.ts b/packages/sdk/src/parser/service/tailordb/field.ts index f26b9aaec..c62617024 100644 --- a/packages/sdk/src/parser/service/tailordb/field.ts +++ b/packages/sdk/src/parser/service/tailordb/field.ts @@ -201,7 +201,7 @@ const convertToScriptExpr = ( const argsObject = kind === "validate" ? `{ value: _value }` - : `{ value: _value, oldValue: _oldValue, invoker: ${tailorPrincipalMap}, now: _now }`; + : `{ input: _value, oldValue: _oldValue, invoker: ${tailorPrincipalMap}, now: _now }`; return assertParsableExpression( `(${normalized})(${argsObject})`, formatScriptContext(kind, context), diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index fd5356bab..8254d07aa 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -115,11 +115,6 @@ function buildValidateStatements( const access = `${accessExpr}[${key(name)}]`; const fieldPath = keyPrefix ? `${keyPrefix}.${name}` : name; - if (isNestedType(config) && config.fields) { - statements.push(...buildValidateStatements(config.fields, `(${access} || {})`, fieldPath)); - continue; - } - const validators = (config.validate ?? []).filter((v) => v.script?.expr); if (validators.length > 0) { const checks = validators @@ -130,6 +125,10 @@ function buildValidateStatements( .join(" "); statements.push(`{ const _value = ${access}; ${checks} }`); } + + if (isNestedType(config) && config.fields) { + statements.push(...buildValidateStatements(config.fields, `(${access} || {})`, fieldPath)); + } } return statements; diff --git a/packages/sdk/src/utils/test/index.test.ts b/packages/sdk/src/utils/test/index.test.ts index 67ea81048..897a1ac2e 100644 --- a/packages/sdk/src/utils/test/index.test.ts +++ b/packages/sdk/src/utils/test/index.test.ts @@ -81,7 +81,7 @@ describe("createTailorDBHook", () => { const type = db.table("Test", { user: db.object({ name: db.string().hooks({ - create: ({ value }) => `hooked:${value as string}`, + create: ({ input }) => `hooked:${input as string}`, }), }), }); @@ -122,9 +122,9 @@ describe("createTailorDBHook", () => { lines: db.object( { stamp: db.string().hooks({ - create: ({ value }) => { - calls.push(value); - return `stamped:${value as string}`; + create: ({ input }) => { + calls.push(input); + return `stamped:${input as string}`; }, }), }, diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index 0f146d38d..407303f9c 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -44,8 +44,7 @@ export function createTailorDBHook>(type: T) { } } else if (field.metadata.hooks?.create) { hooked[key] = field.metadata.hooks.create({ - value: obj?.[key], - oldValue: null, + input: obj?.[key], invoker: null, now, }); @@ -65,7 +64,6 @@ export function createTailorDBHook>(type: T) { // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type const overrides = (type.metadata.typeHook.create as Function)({ input: data, - oldRecord: null, invoker: null, now, }); From 9f281e860e01fc794b7eefba2613de363b5f29b8 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 16:31:06 +0900 Subject: [PATCH 513/618] fix(tailordb): restore strict field path checking in issues() callback Use a generic parameter on the issues callback instead of (string & {}) in DottedPaths. The generic

> preserves strict compile-time checking for invalid field names while maintaining structural compatibility between concrete and generic TailorDBType instantiations. --- .../src/configure/services/tailordb/schema.test.ts | 2 ++ .../sdk/src/configure/services/tailordb/types.ts | 12 ++++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index a89650ba6..fc396e7a0 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -1163,6 +1163,7 @@ describe("TailorDBType type-level validate (function form) tests", () => { }).validate(({ newRecord: _newRecord }, issues) => { issues("name", "ok"); issues("email", "ok"); + // @ts-expect-error TS2345 — "nonexistent" is not a valid field path issues("nonexistent", "bad"); }); }); @@ -1177,6 +1178,7 @@ describe("TailorDBType type-level validate (function form) tests", () => { issues("profile", "ok"); issues("profile.displayName", "ok"); issues("profile.email", "ok"); + // @ts-expect-error TS2345 — "profile.nonexistent" is not a valid nested path issues("profile.nonexistent", "bad"); }); }); diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index cae11c3d2..361ddc26c 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -174,13 +174,9 @@ export type Hook = { type DottedPaths = string extends keyof T ? string : T extends Record - ? - | (string & {}) - | { - [K in keyof T & string]: - | `${Prefix}${K}` - | DottedPaths, `${Prefix}${K}.`>; - }[keyof T & string] + ? { + [K in keyof T & string]: `${Prefix}${K}` | DottedPaths, `${Prefix}${K}.`>; + }[keyof T & string] : never; export type TypeValidateFn< @@ -192,7 +188,7 @@ export type TypeValidateFn< oldRecord: HookArgs | null; invoker: TailorPrincipal | null; }, - issues: (field: DottedPaths>, message: string) => void, + issues:

>>(field: P, message: string) => void, @@ -207,7 +207,7 @@ export type TypeValidateFn< type TypeCreateHookFn< F extends Record, TData = { [K in keyof F]: output }, -> = (args: { input: HookArgs; invoker: TailorPrincipal | null; now: Date }) => { +> = (args: { input: Readonly; invoker: TailorPrincipal | null; now: Date }) => { [K in Exclude]?: TData[K] | null | undefined; }; @@ -216,7 +216,7 @@ type TypeUpdateHookFn< TData = { [K in keyof F]: output }, > = (args: { input: HookArgs; - oldRecord: HookArgs; + oldRecord: Readonly; invoker: TailorPrincipal | null; now: Date; }) => { [K in Exclude]?: TData[K] | null | undefined }; diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index 119b7f418..49efd310c 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -70,7 +70,7 @@ function buildHookObject( const oldAccess = `${oldAccessExpr}?.[${key(name)}]`; if (isNestedType(config) && config.fields) { if (config.array) { - const inner = buildHookObject(config.fields, "__el", "__oldEl", operation); + const inner = buildHookObject(config.fields, "__el", "undefined", operation); if (inner !== null) { parts.push( `${key(name)}: (${access} || []).map((__el) => Object.assign({}, __el, ${inner}))`, @@ -206,7 +206,7 @@ export function buildTypeScripts( if (perFieldExpr !== null && typeLevelExpr) { hook[operation] = { - expr: wrapHook(`Object.assign({}, ${perFieldExpr}, ${typeLevelExpr})`), + expr: `((_invoker) => { const ${NOW} = new Date(); const __fl = ${perFieldExpr}; return Object.assign({}, __fl, ((${INPUT}) => ${typeLevelExpr})(Object.assign({}, ${INPUT}, __fl))); })(typeof _invoker !== "undefined" ? _invoker : undefined)`, }; } else if (typeLevelExpr) { hook[operation] = { expr: wrapHook(typeLevelExpr) }; From 064d18d8399f98118ce465aec3b5363842fc0057 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sun, 12 Jul 2026 07:42:40 +0900 Subject: [PATCH 528/618] test(tailordb): add tests for field-to-type hook input passing and oldRecord fallback --- example/e2e/tailordb.test.ts | 42 ++++++++++++ .../service/tailordb/type-script.test.ts | 65 +++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/example/e2e/tailordb.test.ts b/example/e2e/tailordb.test.ts index 5594258f9..599174ef2 100644 --- a/example/e2e/tailordb.test.ts +++ b/example/e2e/tailordb.test.ts @@ -689,6 +689,48 @@ describe("dataplane", () => { }); }); + test("type-level hook falls back to oldRecord when name is not in input", async () => { + const create = gql` + mutation { + createProductBundle( + input: { name: "Stable", items: [{ productName: "Item", unitPrice: 5.0 }] } + ) { + id + label + } + } + `; + interface Data { + createProductBundle: { id: string; label: string }; + } + const createResult = await graphQLClient.rawRequest(create); + expect(createResult.errors).toBeUndefined(); + expect(createResult.data.createProductBundle.label).toBe("Stable Bundle"); + const id = createResult.data.createProductBundle.id; + + const update = gql` + mutation { + updateProductBundle( + id: "${id}" + input: { items: [{ productName: "Item", qty: 3, unitPrice: 5.0 }] } + ) { + id + name + label + } + } + `; + const updateResult = await graphQLClient.rawRequest(update); + expect(updateResult.errors).toBeUndefined(); + expect(updateResult.data).toEqual({ + updateProductBundle: { + id, + name: "Stable", + label: "Stable Bundle", + }, + }); + }); + test("inner field validate rejects invalid qty", async () => { const query = gql` mutation { diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index cfb3aac0b..11b450766 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -249,4 +249,69 @@ describe("buildTypeScripts", () => { expect(validateExpr).toMatch(/^\(\(_invoker\) =>/); expect(validateExpr).toContain('typeof _invoker !== "undefined" ? _invoker : undefined'); }); + + test("type-level hook receives field-level results as input, not raw _input", () => { + const fields: Record = { + status: { type: "string", default: "active" }, + }; + const typeHookExpr = { + create: + "(({ input }) => ({ label: input.status }))({ input: _input, oldRecord: _oldRecord, invoker: _invoker, now: _now })", + }; + + const { typeHook } = buildTypeScripts(fields, { typeHookExpr }); + const expr = typeHook?.create?.expr ?? ""; + + // Field-level result is bound to __fl + expect(expr).toContain("const __fl ="); + expect(expr).toContain('"status": _input["status"] ?? "active"'); + + // Type-level hook receives field-level result via IIFE that shadows _input + expect(expr).toContain("((_input) =>"); + expect(expr).toContain("Object.assign({}, _input, __fl)"); + + // Final result merges field-level and type-level + expect(expr).toContain("Object.assign({}, __fl,"); + }); + + test("field-level + type-level hook evaluates correctly at runtime", () => { + const fields: Record = { + name: { type: "string", hooks: { create: { expr: "_value.trim()" } } }, + }; + const typeHookExpr = { + create: + "(({ input }) => ({ upper: input.name.toUpperCase() }))({ input: _input, oldRecord: _oldRecord, invoker: _invoker, now: _now })", + }; + + const { typeHook } = buildTypeScripts(fields, { typeHookExpr }); + const expr = typeHook?.create?.expr ?? ""; + + // Evaluate the generated expression with a mock _input + const _input = { name: " hello " }; // eslint-disable-line + const _oldRecord = null; // eslint-disable-line + const result = new Function("_input", "_oldRecord", `return ${expr}`)(_input, _oldRecord); + // Field-level hook trims name + expect(result.name).toBe("hello"); + // Type-level hook sees trimmed input and uppercases it + expect(result.upper).toBe("HELLO"); + }); + + test("nested array hooks do not reference __oldEl", () => { + const fields: Record = { + items: { + type: "nested", + array: true, + fields: { + qty: { + type: "integer", + default: 1, + hooks: { update: { expr: "_value ?? _oldValue" } }, + }, + }, + }, + }; + + const updateExpr = buildTypeScripts(fields).typeHook?.update?.expr ?? ""; + expect(updateExpr).not.toContain("__oldEl"); + }); }); From 93a9f59f82c79fd3d3a76d3d7409eebb41031a31 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sun, 12 Jul 2026 10:18:38 +0900 Subject: [PATCH 529/618] docs(tailordb): clarify type-level hook input reflects field-level results --- packages/sdk/docs/services/tailordb.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index dd7d16c67..dde7704e5 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -310,7 +310,7 @@ Set hooks across multiple fields using `db.table().hooks()`. The hook returns an Create hooks receive: -- `input`: The submitted record data (pre-hook values) +- `input`: The submitted record data. When field-level hooks or defaults exist, `input` reflects their applied results - `invoker`: Principal performing the operation - `now`: Operation timestamp (`Date`), shared across all hooks in the same operation From 7c35cdcf49824b5c07ca1f9fbffbc65562d0cbd0 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sun, 12 Jul 2026 14:57:28 +0900 Subject: [PATCH 530/618] fix(tailordb): skip defaults on nested object/array inner fields --- example/e2e/tailordb.test.ts | 8 +-- example/generated/tailordb.ts | 7 +- example/migrations/0008/diff.json | 72 +++++++++++++++++++ example/tailordb/productBundle.ts | 5 +- .../service/tailordb/type-script.test.ts | 11 +-- .../parser/service/tailordb/type-script.ts | 14 +++- 6 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 example/migrations/0008/diff.json diff --git a/example/e2e/tailordb.test.ts b/example/e2e/tailordb.test.ts index 599174ef2..3846ddfe6 100644 --- a/example/e2e/tailordb.test.ts +++ b/example/e2e/tailordb.test.ts @@ -614,14 +614,14 @@ describe("dataplane", () => { }); describe("productBundle", async () => { - test("type-level hook computes label and inner default applies", async () => { + test("type-level hook computes label from input fields", async () => { const query = gql` mutation { createProductBundle( input: { name: "Summer Sale" items: [ - { productName: "Widget", unitPrice: 10.0 } + { productName: "Widget", qty: 1, unitPrice: 10.0 } { productName: "Gadget", qty: 2, unitPrice: 25.0 } ] } @@ -656,7 +656,7 @@ describe("dataplane", () => { const create = gql` mutation { createProductBundle( - input: { name: "Original", items: [{ productName: "Item", unitPrice: 5.0 }] } + input: { name: "Original", items: [{ productName: "Item", qty: 1, unitPrice: 5.0 }] } ) { id label @@ -693,7 +693,7 @@ describe("dataplane", () => { const create = gql` mutation { createProductBundle( - input: { name: "Stable", items: [{ productName: "Item", unitPrice: 5.0 }] } + input: { name: "Stable", items: [{ productName: "Item", qty: 1, unitPrice: 5.0 }] } ) { id label diff --git a/example/generated/tailordb.ts b/example/generated/tailordb.ts index 1381b67d2..668154306 100644 --- a/example/generated/tailordb.ts +++ b/example/generated/tailordb.ts @@ -3,7 +3,6 @@ import { type Generated, type Timestamp, type ObjectColumnType, - type ArrayColumnType, type Serial, type NamespaceDB, type NamespaceInsertable, @@ -65,11 +64,11 @@ export interface Namespace { id: Generated; name: string; label: string | null; - items: ArrayColumnType; + qty: number; unitPrice: number; - }>>; + }[]; createdAt: Generated; updatedAt: Generated; } diff --git a/example/migrations/0008/diff.json b/example/migrations/0008/diff.json new file mode 100644 index 000000000..b078e1da2 --- /dev/null +++ b/example/migrations/0008/diff.json @@ -0,0 +1,72 @@ +{ + "version": 1, + "namespace": "tailordb", + "createdAt": "2026-07-12T05:51:39.449Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "ProductBundle", + "fieldName": "items", + "before": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "productName": { + "type": "string", + "required": true + }, + "qty": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({ value })=>value <= 0 ? \"qty must be positive\" : undefined)({ value: _value })" + }, + "errorMessage": "" + } + ], + "default": 1 + }, + "unitPrice": { + "type": "float", + "required": true + } + } + }, + "after": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "productName": { + "type": "string", + "required": true + }, + "qty": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({ value })=>value <= 0 ? \"qty must be positive\" : undefined)({ value: _value })" + }, + "errorMessage": "" + } + ] + }, + "unitPrice": { + "type": "float", + "required": true + } + } + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": false +} diff --git a/example/tailordb/productBundle.ts b/example/tailordb/productBundle.ts index 384d04270..f614c2e74 100644 --- a/example/tailordb/productBundle.ts +++ b/example/tailordb/productBundle.ts @@ -8,10 +8,7 @@ export const productBundle = db items: db.object( { productName: db.string(), - qty: db - .int() - .default(1) - .validate(({ value }) => (value <= 0 ? "qty must be positive" : undefined)), + qty: db.int().validate(({ value }) => (value <= 0 ? "qty must be positive" : undefined)), unitPrice: db.float(), }, { array: true }, diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index 11b450766..516fa86c3 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -162,7 +162,7 @@ describe("buildTypeScripts", () => { expect(expr).toContain(typeValidateExpr); }); - test("applies defaults per element in nested array fields", () => { + test("ignores defaults on nested array inner fields", () => { const fields: Record = { items: { type: "nested", @@ -174,14 +174,7 @@ describe("buildTypeScripts", () => { }, }; - const createExpr = buildTypeScripts(fields).typeHook?.create?.expr ?? ""; - expect(createExpr).toContain( - '"items": (_input["items"] || []).map((__el) => Object.assign({}, __el, {', - ); - expect(createExpr).toContain('"status": __el["status"] ?? "pending"'); - expect(createExpr).toContain('"count": __el["count"] ?? 0'); - - expect(buildTypeScripts(fields).typeHook?.update).toBeUndefined(); + expect(buildTypeScripts(fields)).toEqual({}); }); test("validates per element in nested array fields with indexed error paths", () => { diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index 49efd310c..cfe362532 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -55,6 +55,7 @@ function serializeDefault(value: unknown, fieldType: string): string { * @param {string} accessExpr - JS expression to access the parent object * @param {string} oldAccessExpr - JS expression to access the old record parent * @param {HookOperation} operation - Hook operation type + * @param {boolean} nested - Whether building inside a nested field (skips defaults) * @returns {string | null} Object literal expression or null */ function buildHookObject( @@ -62,6 +63,7 @@ function buildHookObject( accessExpr: string, oldAccessExpr: string, operation: HookOperation, + nested = false, ): string | null { const parts: string[] = []; @@ -70,14 +72,20 @@ function buildHookObject( const oldAccess = `${oldAccessExpr}?.[${key(name)}]`; if (isNestedType(config) && config.fields) { if (config.array) { - const inner = buildHookObject(config.fields, "__el", "undefined", operation); + const inner = buildHookObject(config.fields, "__el", "undefined", operation, true); if (inner !== null) { parts.push( `${key(name)}: (${access} || []).map((__el) => Object.assign({}, __el, ${inner}))`, ); } } else { - const inner = buildHookObject(config.fields, `(${access} || {})`, oldAccess, operation); + const inner = buildHookObject( + config.fields, + `(${access} || {})`, + oldAccess, + operation, + true, + ); if (inner !== null) { parts.push(`${key(name)}: Object.assign({}, ${access}, ${inner})`); } @@ -86,7 +94,7 @@ function buildHookObject( } const hook = config.hooks?.[operation]; - const hasDefault = operation === "create" && config.default !== undefined; + const hasDefault = !nested && operation === "create" && config.default !== undefined; if (hook && hasDefault) { parts.push( From 916d480d181fb494dca11a868bbd3fc56afaf627 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sun, 12 Jul 2026 16:39:19 +0900 Subject: [PATCH 531/618] fix(tailordb): make .default() on nested inner fields a type error Add ExcludeDefaultedDBFields type guard to db.object() constraint, preventing .default() from being called on inner fields of nested objects. Nested inner field defaults have no effect on the platform, so the SDK now rejects them at the type level rather than silently accepting them. --- .../src/configure/services/tailordb/schema.test.ts | 9 ++++++++- .../sdk/src/configure/services/tailordb/schema.ts | 4 +++- .../sdk/src/configure/services/tailordb/types.ts | 9 +++++++++ .../builtin/kysely-type/type-processor.test.ts | 14 -------------- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 939816cf5..c354c585b 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -1289,9 +1289,16 @@ describe("db.object tests", () => { }); }); - test("default and validate on inner fields of db.object are allowed", () => { + test("default on inner fields of db.object causes type error", () => { db.object({ + // @ts-expect-error default on nested inner fields are not allowed name: db.string().default("unnamed"), + status: db.string(), + }); + }); + + test("validate on inner fields of db.object is allowed", () => { + db.object({ status: db.string().validate(({ value }) => (value.length > 0 ? undefined : "required")), }); }); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index a1c350d6c..95612a752 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -38,6 +38,7 @@ import type { TypeHook, ExcludeNestedDBFields, ExcludeHookedDBFields, + ExcludeDefaultedDBFields, TypeFeatures, TypeValidateFn, } from "./types"; @@ -811,7 +812,8 @@ function _enum( function object< const F extends Record & ExcludeNestedDBFields & - ExcludeHookedDBFields, + ExcludeHookedDBFields & + ExcludeDefaultedDBFields, const Opt extends FieldOptions, >(fields: F, options?: Opt) { return createField("nested", options, fields) as unknown as TailorDBField< diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index 564739d6b..e92132717 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -250,6 +250,15 @@ export type ExcludeHookedDBFields> = ? never : T[K]; }; + +export type ExcludeDefaultedDBFields> = { + [K in keyof T]: T[K] extends TailorDBField< + { type: TailorFieldType; array: boolean; default: true }, + any + > + ? never + : T[K]; +}; // oxlint-enable no-explicit-any // --- Type features --- diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts index 9863ddd88..658cc0324 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts @@ -252,20 +252,6 @@ describe("Kysely TypeProcessor", () => { expect(typeDef).not.toContain("ArrayColumnType"); }); - test("should use ObjectColumnType for nested objects with defaulted fields", async () => { - const typeDef = await getTypeDef( - db.table("Config", { - settings: db.object({ - name: db.string(), - status: db.string().default("active"), - }), - }), - ); - - expect(typeDef).toContain("ObjectColumnType<"); - expect(typeDef).toContain("Generated"); - }); - test("should handle optional nested objects", async () => { const typeDef = await getTypeDef( db.table("User", { From 69684304776215b1e0d2c1d42bdf37197b81c7a8 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sun, 12 Jul 2026 17:28:25 +0900 Subject: [PATCH 532/618] fix(tailordb): throw runtime error for .default() on nested inner fields In addition to the type-level guard (ExcludeDefaultedDBFields), add runtime validation in both parseFieldConfig and buildHookObject that throws when .default() is encountered on a nested inner field. --- packages/sdk/src/parser/service/tailordb/field.ts | 7 +++++++ .../sdk/src/parser/service/tailordb/type-script.test.ts | 8 ++++---- packages/sdk/src/parser/service/tailordb/type-script.ts | 7 +++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/sdk/src/parser/service/tailordb/field.ts b/packages/sdk/src/parser/service/tailordb/field.ts index 5bf0aa810..145ad5e88 100644 --- a/packages/sdk/src/parser/service/tailordb/field.ts +++ b/packages/sdk/src/parser/service/tailordb/field.ts @@ -252,6 +252,13 @@ export function parseFieldConfig( // Access rawRelation via getter (if available) const rawRelation = (field as unknown as { rawRelation?: RawRelationConfig }).rawRelation; + if (context && context.fieldPath.length > 1 && metadata.default !== undefined) { + throw new Error( + `Field "${context.fieldPath.join(".")}" on type "${context.typeName}": ` + + `.default() cannot be used on nested inner fields`, + ); + } + const nestedFields = field.fields as Record | undefined; return { type: fieldType, diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index 516fa86c3..ca112c2b2 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -162,19 +162,20 @@ describe("buildTypeScripts", () => { expect(expr).toContain(typeValidateExpr); }); - test("ignores defaults on nested array inner fields", () => { + test("throws on defaults on nested array inner fields", () => { const fields: Record = { items: { type: "nested", array: true, fields: { status: { type: "string", default: "pending" }, - count: { type: "integer", default: 0 }, }, }, }; - expect(buildTypeScripts(fields)).toEqual({}); + expect(() => buildTypeScripts(fields)).toThrow( + '.default() cannot be used on nested inner field "status"', + ); }); test("validates per element in nested array fields with indexed error paths", () => { @@ -297,7 +298,6 @@ describe("buildTypeScripts", () => { fields: { qty: { type: "integer", - default: 1, hooks: { update: { expr: "_value ?? _oldValue" } }, }, }, diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index cfe362532..948315e9e 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -55,7 +55,7 @@ function serializeDefault(value: unknown, fieldType: string): string { * @param {string} accessExpr - JS expression to access the parent object * @param {string} oldAccessExpr - JS expression to access the old record parent * @param {HookOperation} operation - Hook operation type - * @param {boolean} nested - Whether building inside a nested field (skips defaults) + * @param {boolean} nested - Whether building inside a nested field (rejects defaults) * @returns {string | null} Object literal expression or null */ function buildHookObject( @@ -94,7 +94,10 @@ function buildHookObject( } const hook = config.hooks?.[operation]; - const hasDefault = !nested && operation === "create" && config.default !== undefined; + if (nested && config.default !== undefined) { + throw new Error(`.default() cannot be used on nested inner field "${name}"`); + } + const hasDefault = operation === "create" && config.default !== undefined; if (hook && hasDefault) { parts.push( From 099e4b2183c81992b5bac2bf52b4f5c42b2853d0 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 09:08:53 +0900 Subject: [PATCH 533/618] fix(tailordb): make update hook oldValue non-nullable for required fields --- packages/sdk/src/configure/services/tailordb/schema.test.ts | 6 +++--- packages/sdk/src/configure/services/tailordb/types.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index c354c585b..18e0fe9b3 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -589,11 +589,11 @@ describe("TailorDBField hooks modifier tests", () => { update: (args) => { expectTypeOf(args).toEqualTypeOf<{ input: string | null; - oldValue: string | null; + oldValue: string; invoker: TailorPrincipal | null; now: Date; }>(); - return args.oldValue ?? args.input ?? "default"; + return args.oldValue; }, }); }); @@ -827,7 +827,7 @@ describe("TailorDBType withTimestamps option tests", () => { const now = new Date("2025-06-01T12:00:00Z"); const result = updateHook!({ input: null, - oldValue: null, + oldValue: new Date("2025-01-01T00:00:00Z"), invoker: timestampHookInvoker, now, }); diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index e92132717..04deef244 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -162,7 +162,7 @@ type CreateHookFn = (args: { type UpdateHookFn = (args: { input: TValue; - oldValue: TValue | null; + oldValue: TReturn; invoker: TailorPrincipal | null; now: Date; }) => TReturn; From d7bbe1a26d9b7dc842f20b0b269187c938f6370e Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 10:44:45 +0900 Subject: [PATCH 534/618] fix(tailordb): use DeepReadonly for nested immutability in hook/validate types --- example/migrations/0007/diff.json | 3 +-- .../sdk/src/configure/services/tailordb/types.ts | 16 ++++++++-------- packages/sdk/src/types/helpers.ts | 9 +++++++++ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/example/migrations/0007/diff.json b/example/migrations/0007/diff.json index 2794ce649..6645cd1d4 100644 --- a/example/migrations/0007/diff.json +++ b/example/migrations/0007/diff.json @@ -41,8 +41,7 @@ }, "errorMessage": "" } - ], - "default": 1 + ] }, "unitPrice": { "type": "float", diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index 04deef244..89f6cad29 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -9,7 +9,7 @@ import type { TailorFieldType, } from "#/configure/types/field.types"; import type { InferredAttributes, TailorPrincipal } from "#/runtime/types"; -import type { InferFieldsOutput, output, Prettify } from "#/types/helpers"; +import type { DeepReadonly, InferFieldsOutput, output, Prettify } from "#/types/helpers"; import type { DBFieldMetadata as DBFieldMetadataGenerated, GqlOperationsInput, @@ -151,7 +151,7 @@ export type TailorDBInstance< type HookArgs = TData extends Record - ? { readonly [K in keyof TData]?: TData[K] | null | undefined } + ? { readonly [K in keyof TData]?: DeepReadonly | null | undefined } : unknown; type CreateHookFn = (args: { @@ -197,8 +197,8 @@ export type TypeValidateFn< TData = { [K in keyof F]: output }, > = ( args: { - newRecord: Readonly; - oldRecord: Readonly | null; + newRecord: DeepReadonly; + oldRecord: DeepReadonly | null; invoker: TailorPrincipal | null; }, issues:

>>(field: P, message: string) => void, @@ -207,8 +207,8 @@ export type TypeValidateFn< type TypeCreateHookFn< F extends Record, TData = { [K in keyof F]: output }, -> = (args: { input: Readonly; invoker: TailorPrincipal | null; now: Date }) => { - [K in Exclude]?: TData[K] | null | undefined; +> = (args: { input: DeepReadonly; invoker: TailorPrincipal | null; now: Date }) => { + [K in Exclude]?: TData[K] | null; }; type TypeUpdateHookFn< @@ -216,10 +216,10 @@ type TypeUpdateHookFn< TData = { [K in keyof F]: output }, > = (args: { input: HookArgs; - oldRecord: Readonly; + oldRecord: DeepReadonly; invoker: TailorPrincipal | null; now: Date; -}) => { [K in Exclude]?: TData[K] | null | undefined }; +}) => { [K in Exclude]?: TData[K] | null }; export type TypeHook> = { create?: TypeCreateHookFn; diff --git a/packages/sdk/src/types/helpers.ts b/packages/sdk/src/types/helpers.ts index a18b35d2a..be3ec5b6b 100644 --- a/packages/sdk/src/types/helpers.ts +++ b/packages/sdk/src/types/helpers.ts @@ -13,6 +13,15 @@ export type DeepWritable = T extends Date | RegExp | Function ? { -readonly [P in keyof T]: DeepWritable } & {} : T; +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +export type DeepReadonly = T extends Date | RegExp | Function + ? T + : T extends readonly (infer E)[] + ? readonly DeepReadonly[] + : T extends object + ? { readonly [K in keyof T]: DeepReadonly } + : T; + type LiteralToString = T extends string ? string : T; type SpecificNumberToNumber = T extends number ? number : T; type TrueFalseToBool = T extends number ? number : T; From e204d84fff259105999e15b9890db96225370a7d Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 11:08:40 +0900 Subject: [PATCH 535/618] refactor(tailordb): use .default("now") for timestamps and respect input on update --- example/seed/data/Customer.schema.ts | 4 +- example/seed/data/Event.schema.ts | 4 +- example/seed/data/Invoice.schema.ts | 4 +- example/seed/data/NestedProfile.schema.ts | 4 +- example/seed/data/ProductBundle.schema.ts | 4 +- example/seed/data/PurchaseOrder.schema.ts | 4 +- example/seed/data/SalesOrder.schema.ts | 4 +- example/seed/data/Supplier.schema.ts | 4 +- example/seed/data/User.schema.ts | 4 +- example/seed/data/UserLog.schema.ts | 4 +- example/seed/data/UserSetting.schema.ts | 4 +- .../services/tailordb/schema.test.ts | 62 +++++-------------- .../src/configure/services/tailordb/schema.ts | 10 +-- 13 files changed, 40 insertions(+), 76 deletions(-) diff --git a/example/seed/data/Customer.schema.ts b/example/seed/data/Customer.schema.ts index 99285ca3a..7a7fa636e 100644 --- a/example/seed/data/Customer.schema.ts +++ b/example/seed/data/Customer.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { customer } from "../../tailordb/customer"; const schemaType = t.object({ - ...customer.pickFields(["id","createdAt","updatedAt"], { optional: true }), - ...customer.omitFields(["id","createdAt","updatedAt"]), + ...customer.pickFields(["id"], { optional: true }), + ...customer.omitFields(["id"]), }); const hook = createTailorDBHook(customer); diff --git a/example/seed/data/Event.schema.ts b/example/seed/data/Event.schema.ts index 94943c552..4ecc2c631 100644 --- a/example/seed/data/Event.schema.ts +++ b/example/seed/data/Event.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { event } from "../../analyticsdb/event"; const schemaType = t.object({ - ...event.pickFields(["id","createdAt","updatedAt"], { optional: true }), - ...event.omitFields(["id","createdAt","updatedAt"]), + ...event.pickFields(["id"], { optional: true }), + ...event.omitFields(["id"]), }); const hook = createTailorDBHook(event); diff --git a/example/seed/data/Invoice.schema.ts b/example/seed/data/Invoice.schema.ts index 614863d24..830a0b6ab 100644 --- a/example/seed/data/Invoice.schema.ts +++ b/example/seed/data/Invoice.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { invoice } from "../../tailordb/invoice"; const schemaType = t.object({ - ...invoice.pickFields(["id","createdAt","updatedAt"], { optional: true }), - ...invoice.omitFields(["id","createdAt","updatedAt","invoiceNumber","sequentialId"]), + ...invoice.pickFields(["id"], { optional: true }), + ...invoice.omitFields(["id","invoiceNumber","sequentialId"]), }); const hook = createTailorDBHook(invoice); diff --git a/example/seed/data/NestedProfile.schema.ts b/example/seed/data/NestedProfile.schema.ts index 18efb54dc..3dfb27864 100644 --- a/example/seed/data/NestedProfile.schema.ts +++ b/example/seed/data/NestedProfile.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { nestedProfile } from "../../tailordb/nested"; const schemaType = t.object({ - ...nestedProfile.pickFields(["id","createdAt","updatedAt"], { optional: true }), - ...nestedProfile.omitFields(["id","createdAt","updatedAt"]), + ...nestedProfile.pickFields(["id"], { optional: true }), + ...nestedProfile.omitFields(["id"]), }); const hook = createTailorDBHook(nestedProfile); diff --git a/example/seed/data/ProductBundle.schema.ts b/example/seed/data/ProductBundle.schema.ts index ec68e9c63..88bef6380 100644 --- a/example/seed/data/ProductBundle.schema.ts +++ b/example/seed/data/ProductBundle.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { productBundle } from "../../tailordb/productBundle"; const schemaType = t.object({ - ...productBundle.pickFields(["id","createdAt","updatedAt"], { optional: true }), - ...productBundle.omitFields(["id","createdAt","updatedAt"]), + ...productBundle.pickFields(["id"], { optional: true }), + ...productBundle.omitFields(["id"]), }); const hook = createTailorDBHook(productBundle); diff --git a/example/seed/data/PurchaseOrder.schema.ts b/example/seed/data/PurchaseOrder.schema.ts index 7b4aef58a..45c7bbd82 100644 --- a/example/seed/data/PurchaseOrder.schema.ts +++ b/example/seed/data/PurchaseOrder.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { purchaseOrder } from "../../tailordb/purchaseOrder"; const schemaType = t.object({ - ...purchaseOrder.pickFields(["id","createdAt","updatedAt"], { optional: true }), - ...purchaseOrder.omitFields(["id","createdAt","updatedAt"]), + ...purchaseOrder.pickFields(["id"], { optional: true }), + ...purchaseOrder.omitFields(["id"]), }); const hook = createTailorDBHook(purchaseOrder); diff --git a/example/seed/data/SalesOrder.schema.ts b/example/seed/data/SalesOrder.schema.ts index 132da34f6..df41e5934 100644 --- a/example/seed/data/SalesOrder.schema.ts +++ b/example/seed/data/SalesOrder.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { salesOrder } from "../../tailordb/salesOrder"; const schemaType = t.object({ - ...salesOrder.pickFields(["id","createdAt","updatedAt"], { optional: true }), - ...salesOrder.omitFields(["id","createdAt","updatedAt"]), + ...salesOrder.pickFields(["id"], { optional: true }), + ...salesOrder.omitFields(["id"]), }); const hook = createTailorDBHook(salesOrder); diff --git a/example/seed/data/Supplier.schema.ts b/example/seed/data/Supplier.schema.ts index 722f453e7..06a5da1db 100644 --- a/example/seed/data/Supplier.schema.ts +++ b/example/seed/data/Supplier.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { supplier } from "../../tailordb/supplier"; const schemaType = t.object({ - ...supplier.pickFields(["id","createdAt","updatedAt"], { optional: true }), - ...supplier.omitFields(["id","createdAt","updatedAt"]), + ...supplier.pickFields(["id"], { optional: true }), + ...supplier.omitFields(["id"]), }); const hook = createTailorDBHook(supplier); diff --git a/example/seed/data/User.schema.ts b/example/seed/data/User.schema.ts index 16ee30b3f..e0de10bca 100644 --- a/example/seed/data/User.schema.ts +++ b/example/seed/data/User.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { user } from "../../tailordb/user"; const schemaType = t.object({ - ...user.pickFields(["id","createdAt","updatedAt"], { optional: true }), - ...user.omitFields(["id","createdAt","updatedAt"]), + ...user.pickFields(["id"], { optional: true }), + ...user.omitFields(["id"]), }); const hook = createTailorDBHook(user); diff --git a/example/seed/data/UserLog.schema.ts b/example/seed/data/UserLog.schema.ts index 1efa27fdd..c173ffae0 100644 --- a/example/seed/data/UserLog.schema.ts +++ b/example/seed/data/UserLog.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { userLog } from "../../tailordb/userLog"; const schemaType = t.object({ - ...userLog.pickFields(["id","createdAt","updatedAt"], { optional: true }), - ...userLog.omitFields(["id","createdAt","updatedAt"]), + ...userLog.pickFields(["id"], { optional: true }), + ...userLog.omitFields(["id"]), }); const hook = createTailorDBHook(userLog); diff --git a/example/seed/data/UserSetting.schema.ts b/example/seed/data/UserSetting.schema.ts index fecf7e43e..9c4ab3200 100644 --- a/example/seed/data/UserSetting.schema.ts +++ b/example/seed/data/UserSetting.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { userSetting } from "../../tailordb/userSetting"; const schemaType = t.object({ - ...userSetting.pickFields(["id","createdAt","updatedAt"], { optional: true }), - ...userSetting.omitFields(["id","createdAt","updatedAt"]), + ...userSetting.pickFields(["id"], { optional: true }), + ...userSetting.omitFields(["id"]), }); const hook = createTailorDBHook(userSetting); diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 18e0fe9b3..7bb77755f 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -759,67 +759,35 @@ describe("TailorDBType withTimestamps option tests", () => { }>(); }); - const timestampHookInvoker = null; - - test("createdAt create hook respects a user-specified value", () => { + test("createdAt uses default('now') without hooks", () => { const { createdAt } = db.fields.timestamps(); - const createHook = createdAt.metadata.hooks?.create; - expect(createHook).toBeDefined(); - - const specified = new Date("2025-02-10T09:00:00Z"); - const now = new Date("2025-06-01T00:00:00Z"); - const result = createHook!({ - input: specified, - invoker: timestampHookInvoker, - now, - }); - expect(result).toBe(specified); + expect(createdAt.metadata.default).toBe("now"); + expect(createdAt.metadata.hooks).toBeUndefined(); }); - test("createdAt create hook falls back to now when no value is given", () => { - const { createdAt } = db.fields.timestamps(); - const createHook = createdAt.metadata.hooks?.create; - expect(createHook).toBeDefined(); - - const now = new Date("2025-06-01T12:00:00Z"); - const result = createHook!({ - input: null, - invoker: timestampHookInvoker, - now, - }); - expect(result).toBe(now); + test("updatedAt uses default('now') for create", () => { + const { updatedAt } = db.fields.timestamps(); + expect(updatedAt.metadata.default).toBe("now"); + expect(updatedAt.metadata.hooks?.create).toBeUndefined(); }); - test("updatedAt create hook respects a user-specified value", () => { + test("updatedAt update hook respects a user-specified value", () => { const { updatedAt } = db.fields.timestamps(); - const createHook = updatedAt.metadata.hooks?.create; - expect(createHook).toBeDefined(); + const updateHook = updatedAt.metadata.hooks?.update; + expect(updateHook).toBeDefined(); const specified = new Date("2025-02-10T09:00:00Z"); const now = new Date("2025-06-01T12:00:00Z"); - const result = createHook!({ + const result = updateHook!({ input: specified, - invoker: timestampHookInvoker, + oldValue: new Date("2025-01-01T00:00:00Z"), + invoker: null, now, }); expect(result).toBe(specified); }); - test("updatedAt create hook falls back to now when no value is given", () => { - const { updatedAt } = db.fields.timestamps(); - const createHook = updatedAt.metadata.hooks?.create; - expect(createHook).toBeDefined(); - - const now = new Date("2025-06-01T12:00:00Z"); - const result = createHook!({ - input: null, - invoker: timestampHookInvoker, - now, - }); - expect(result).toBe(now); - }); - - test("updatedAt update hook uses now", () => { + test("updatedAt update hook falls back to now when no value is given", () => { const { updatedAt } = db.fields.timestamps(); const updateHook = updatedAt.metadata.hooks?.update; expect(updateHook).toBeDefined(); @@ -828,7 +796,7 @@ describe("TailorDBType withTimestamps option tests", () => { const result = updateHook!({ input: null, oldValue: new Date("2025-01-01T00:00:00Z"), - invoker: timestampHookInvoker, + invoker: null, now, }); expect(result).toBe(now); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 95612a752..f008f3671 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -1081,14 +1081,10 @@ export const db = { * }); */ timestamps: () => ({ - createdAt: datetime() - .hooks({ create: ({ input, now }) => input ?? now }) - .description("Record creation timestamp"), + createdAt: datetime().default("now").description("Record creation timestamp"), updatedAt: datetime() - .hooks({ - create: ({ input, now }) => input ?? now, - update: ({ now }) => now, - }) + .default("now") + .hooks({ update: ({ input, now }) => input ?? now }) .description("Record update timestamp"), }), }, From fe1f7f1b94c92b4438244e8cc1f0aebac859521f Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 11:37:42 +0900 Subject: [PATCH 536/618] fix(tailordb): block .hooks() on nested inner fields at runtime --- .../src/parser/service/tailordb/field.test.ts | 39 +++++++++++++++++++ .../sdk/src/parser/service/tailordb/field.ts | 7 ++++ 2 files changed, 46 insertions(+) diff --git a/packages/sdk/src/parser/service/tailordb/field.test.ts b/packages/sdk/src/parser/service/tailordb/field.test.ts index a0fad7431..7785196b3 100644 --- a/packages/sdk/src/parser/service/tailordb/field.test.ts +++ b/packages/sdk/src/parser/service/tailordb/field.test.ts @@ -191,3 +191,42 @@ describe("parseFieldConfig script expression validation", () => { ); }); }); + +describe("parseFieldConfig nested inner field restrictions", () => { + test("throws on .hooks() on nested inner fields", () => { + const field = { + type: "nested" as const, + fields: { + name: { + type: "string" as const, + fields: {}, + rawRelation: undefined, + metadata: { + hooks: { + create: ({ input }: { input: string }) => input, + }, + }, + }, + }, + rawRelation: undefined, + metadata: {}, + }; + + expect(() => + parseFieldConfig(field as never, { typeName: "Test", fieldPath: ["items", "name"] }), + ).toThrow(".hooks() cannot be used on nested inner fields"); + }); + + test("throws on .default() on nested inner fields", () => { + const field = { + type: "string" as const, + fields: {}, + rawRelation: undefined, + metadata: { default: "pending" }, + }; + + expect(() => + parseFieldConfig(field as never, { typeName: "Test", fieldPath: ["items", "status"] }), + ).toThrow(".default() cannot be used on nested inner fields"); + }); +}); diff --git a/packages/sdk/src/parser/service/tailordb/field.ts b/packages/sdk/src/parser/service/tailordb/field.ts index 145ad5e88..ca2826018 100644 --- a/packages/sdk/src/parser/service/tailordb/field.ts +++ b/packages/sdk/src/parser/service/tailordb/field.ts @@ -259,6 +259,13 @@ export function parseFieldConfig( ); } + if (context && context.fieldPath.length > 1 && metadata.hooks) { + throw new Error( + `Field "${context.fieldPath.join(".")}" on type "${context.typeName}": ` + + `.hooks() cannot be used on nested inner fields`, + ); + } + const nestedFields = field.fields as Record | undefined; return { type: fieldType, From f2e1cd651cf78c5a585fa1cb7f36a763ff738b01 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 12:17:26 +0900 Subject: [PATCH 537/618] chore(example): regenerate migrations and templates for timestamps default change Update migration files for tailordb (0009) and analyticsdb (0005) to reflect createdAt/updatedAt hooks-to-default change. Regenerate create-sdk seed templates where pickFields/omitFields no longer exclude timestamp fields. Fix timestamps() JSDoc and docs oldValue description for accuracy. --- example/migrations/0009/diff.json | 512 ++++++++++++++++++ example/migrations/analyticsdb/0005/diff.json | 62 +++ .../generators/src/seed/data/Order.schema.ts | 4 +- .../src/seed/data/Product.schema.ts | 4 +- .../generators/src/seed/data/User.schema.ts | 4 +- packages/sdk/docs/services/tailordb.md | 2 +- .../src/configure/services/tailordb/schema.ts | 7 +- 7 files changed, 584 insertions(+), 11 deletions(-) create mode 100644 example/migrations/0009/diff.json create mode 100644 example/migrations/analyticsdb/0005/diff.json diff --git a/example/migrations/0009/diff.json b/example/migrations/0009/diff.json new file mode 100644 index 000000000..1c949cce4 --- /dev/null +++ b/example/migrations/0009/diff.json @@ -0,0 +1,512 @@ +{ + "version": 1, + "namespace": "tailordb", + "createdAt": "2026-07-13T02:55:58.448Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "update": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + }, + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "update": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + }, + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "update": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + }, + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "ProductBundle", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "ProductBundle", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "update": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + }, + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "update": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + }, + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "update": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + }, + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "update": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + }, + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "update": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + }, + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "update": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + }, + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "update": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + }, + "default": "now" + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": false +} diff --git a/example/migrations/analyticsdb/0005/diff.json b/example/migrations/analyticsdb/0005/diff.json new file mode 100644 index 000000000..7aa81131b --- /dev/null +++ b/example/migrations/analyticsdb/0005/diff.json @@ -0,0 +1,62 @@ +{ + "version": 1, + "namespace": "analyticsdb", + "createdAt": "2026-07-13T02:55:58.505Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Event", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "default": "now" + } + }, + { + "kind": "field_modified", + "typeName": "Event", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "update": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + }, + "default": "now" + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": false +} diff --git a/packages/create-sdk/templates/generators/src/seed/data/Order.schema.ts b/packages/create-sdk/templates/generators/src/seed/data/Order.schema.ts index 569a38ee1..c0a3accd4 100644 --- a/packages/create-sdk/templates/generators/src/seed/data/Order.schema.ts +++ b/packages/create-sdk/templates/generators/src/seed/data/Order.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { order } from "../../db/order"; const schemaType = t.object({ - ...order.pickFields(["id","createdAt","updatedAt"], { optional: true }), - ...order.omitFields(["id","createdAt","updatedAt"]), + ...order.pickFields(["id"], { optional: true }), + ...order.omitFields(["id"]), }); const hook = createTailorDBHook(order); diff --git a/packages/create-sdk/templates/generators/src/seed/data/Product.schema.ts b/packages/create-sdk/templates/generators/src/seed/data/Product.schema.ts index 9a7a449bd..dbb32664a 100644 --- a/packages/create-sdk/templates/generators/src/seed/data/Product.schema.ts +++ b/packages/create-sdk/templates/generators/src/seed/data/Product.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { product } from "../../db/product"; const schemaType = t.object({ - ...product.pickFields(["id","createdAt","updatedAt"], { optional: true }), - ...product.omitFields(["id","createdAt","updatedAt"]), + ...product.pickFields(["id"], { optional: true }), + ...product.omitFields(["id"]), }); const hook = createTailorDBHook(product); diff --git a/packages/create-sdk/templates/generators/src/seed/data/User.schema.ts b/packages/create-sdk/templates/generators/src/seed/data/User.schema.ts index 5dbc6522e..9feda335c 100644 --- a/packages/create-sdk/templates/generators/src/seed/data/User.schema.ts +++ b/packages/create-sdk/templates/generators/src/seed/data/User.schema.ts @@ -4,8 +4,8 @@ import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/t import { user } from "../../db/user"; const schemaType = t.object({ - ...user.pickFields(["id","createdAt","updatedAt"], { optional: true }), - ...user.omitFields(["id","createdAt","updatedAt"]), + ...user.pickFields(["id"], { optional: true }), + ...user.omitFields(["id"]), }); const hook = createTailorDBHook(user); diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index dde7704e5..8a1ff973c 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -293,7 +293,7 @@ Create hooks receive: Update hooks receive the same arguments plus: -- `oldValue`: The previous field value (may be null) +- `oldValue`: The previous field value (null only for optional fields) ```typescript db.string().hooks({ diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index f008f3671..595407883 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -1069,10 +1069,9 @@ export const db = { object, fields: { /** - * Creates standard timestamp fields (createdAt, updatedAt) with auto-hooks. - * createdAt and updatedAt are set on create. - * User-specified timestamp values are respected when provided (e.g. seeding - * historical records); the current time is used only when the value is omitted. + * Creates standard timestamp fields (createdAt, updatedAt) with automatic defaults. + * Both fields default to the current time on create. updatedAt is also refreshed on update. + * User-specified values are respected when provided (e.g. seeding historical records). * @returns An object with createdAt and updatedAt fields * @example * const model = db.table("Model", { From f895bcc4315ad0dbb2beaf1240666f7e856d5d4e Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 12:32:07 +0900 Subject: [PATCH 538/618] fix(tailordb): add semicolon to nested array forEach in validate script to prevent ASI bug --- .../service/tailordb/type-script.test.ts | 24 +++++++++++++++++++ .../parser/service/tailordb/type-script.ts | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index ca112c2b2..9a3f0748f 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -200,6 +200,30 @@ describe("buildTypeScripts", () => { expect(expr).toContain('"items[" + __idx + "].name"'); }); + test("nested array forEach terminates with semicolon to prevent ASI with type-level validate", () => { + const fields: Record = { + items: { + type: "nested", + array: true, + fields: { + qty: { + type: "integer", + validate: [{ script: { expr: "_value > 0" }, errorMessage: "" }], + }, + }, + }, + }; + const typeValidateExpr = "fn({ newRecord: _newRecord }, __issues)"; + + const { typeValidate } = buildTypeScripts(fields, { typeValidateExpr }); + const expr = typeValidate?.create?.expr ?? ""; + expect(expr).toContain("});"); + + const _newRecord = { items: [{ qty: 1 }] }; // eslint-disable-line + const fn = () => {}; // eslint-disable-line + expect(() => new Function("_newRecord", "fn", `return ${expr}`)(_newRecord, fn)).not.toThrow(); + }); + test("skips nested array with no defaults or validators", () => { const fields: Record = { items: { diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index 948315e9e..4806953a1 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -167,7 +167,7 @@ function buildValidateStatements( } if (innerParts.length > 0) { statements.push( - `(${access} || []).forEach((__el, __idx) => { ${innerParts.join(" ")} })`, + `(${access} || []).forEach((__el, __idx) => { ${innerParts.join(" ")} });`, ); } } else { From 02617241bcb9e554624ab69e3a2374833926e5d4 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 13:40:45 +0900 Subject: [PATCH 539/618] fix(tailordb): correct migration 0007 update hook to use oldRecord fallback The ProductBundle type_added entry in migration 0007 had an incorrect typeHookExpr.update that was identical to the create hook, missing the oldRecord fallback. On a fresh workspace where all migrations are pending, this caused the update hook to evaluate input.name as undefined, producing "undefined Bundle" instead of the expected "Stable Bundle" in E2E tests. Also adds a test verifying that buildTypeScripts correctly wires _oldRecord for update type-level hooks. --- example/migrations/0007/diff.json | 2 +- .../service/tailordb/type-script.test.ts | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/example/migrations/0007/diff.json b/example/migrations/0007/diff.json index 6645cd1d4..81927f7f7 100644 --- a/example/migrations/0007/diff.json +++ b/example/migrations/0007/diff.json @@ -77,7 +77,7 @@ "settings": {}, "typeHookExpr": { "create": "(({ input })=>({\n label: `${input.name} Bundle`\n }))({ input: _input, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })", - "update": "(({ input })=>({\n label: `${input.name} Bundle`\n }))({ input: _input, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + "update": "(({ input, oldRecord })=>({\n label: `${input.name ?? oldRecord.name} Bundle`\n }))({ input: _input, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" }, "typeValidateExpr": "(({ newRecord }, issues)=>{\n if (newRecord.items && newRecord.items.length === 0) {\n issues(\"items\", \"At least one item is required\");\n }\n})({ newRecord: _newRecord, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) }, __issues)", "permissions": { diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index 9a3f0748f..c1673b1d1 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -314,6 +314,42 @@ describe("buildTypeScripts", () => { expect(result.upper).toBe("HELLO"); }); + test("update type-level hook falls back to oldRecord via _oldRecord", () => { + const fields: Record = { + createdAt: { + type: "datetime", + hooks: { create: { expr: "_now" } }, + }, + updatedAt: { + type: "datetime", + hooks: { create: { expr: "_now" }, update: { expr: "_now" } }, + }, + }; + const typeHookExpr = { + create: + "(({ input }) => ({ label: `${input.name} Bundle` }))({ input: _input, oldRecord: _oldRecord, invoker: _invoker, now: _now })", + update: + "(({ input, oldRecord }) => ({ label: `${input.name ?? oldRecord.name} Bundle` }))({ input: _input, oldRecord: _oldRecord, invoker: _invoker, now: _now })", + }; + + const { typeHook } = buildTypeScripts(fields, { typeHookExpr }); + + // UPDATE: input has no name, oldRecord has name + const updateExpr = typeHook?.update?.expr ?? ""; + const _input = { items: [{ productName: "Item", qty: 3, unitPrice: 5.0 }] }; // eslint-disable-line + const _oldRecord = { + // eslint-disable-line + id: "abc", + name: "Stable", + label: "Stable Bundle", + items: [{ productName: "Item", qty: 1, unitPrice: 5.0 }], + createdAt: "2025-01-01T00:00:00Z", + updatedAt: "2025-01-01T00:00:00Z", + }; + const result = new Function("_input", "_oldRecord", `return ${updateExpr}`)(_input, _oldRecord); + expect(result.label).toBe("Stable Bundle"); + }); + test("nested array hooks do not reference __oldEl", () => { const fields: Record = { items: { From 1f007f9a58d9bea0c41bddb77d857baa8e041c58 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 13:59:49 +0900 Subject: [PATCH 540/618] fix(e2e): increase deploy timeout for migration-heavy PRs --- packages/sdk/e2e/function-test-run.test.ts | 2 +- packages/sdk/vitest.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/e2e/function-test-run.test.ts b/packages/sdk/e2e/function-test-run.test.ts index 42bdb743d..792c3ca3a 100644 --- a/packages/sdk/e2e/function-test-run.test.ts +++ b/packages/sdk/e2e/function-test-run.test.ts @@ -143,7 +143,7 @@ describe.sequential("E2E: function test-run", () => { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, NODE_OPTIONS: "--experimental-vm-modules" }, encoding: "utf-8", - timeout: 120000, + timeout: 300000, }, ); console.log("Deploy completed."); diff --git a/packages/sdk/vitest.config.ts b/packages/sdk/vitest.config.ts index 7b0f560b5..d21fb14b2 100644 --- a/packages/sdk/vitest.config.ts +++ b/packages/sdk/vitest.config.ts @@ -150,7 +150,7 @@ export default defineConfig({ name: "e2e", include: ["e2e/**/*.test.ts"], testTimeout: 120000, - hookTimeout: 120000, + hookTimeout: 300000, globalSetup: ["e2e/globalSetup.ts"], }, }, From da2af14f9f81a39bf5c6f60b503490d9adf53ff6 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 14:17:37 +0900 Subject: [PATCH 541/618] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .changeset/tailordb-shared-now-hook.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tailordb-shared-now-hook.md b/.changeset/tailordb-shared-now-hook.md index 7911c49eb..ac5631282 100644 --- a/.changeset/tailordb-shared-now-hook.md +++ b/.changeset/tailordb-shared-now-hook.md @@ -1,5 +1,5 @@ --- -"@tailor-platform/sdk": minor +"@tailor-platform/sdk": major "@tailor-platform/sdk-codemod": patch --- From d5f5971cea26abbb2387e5e1f1bd3b03c3333606 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 14:52:57 +0900 Subject: [PATCH 542/618] fix(deploy): overlay current source scripts onto migration snapshot during deploy Migration comparison strips hook/validate/default and typeHookExpr/typeValidateExpr, so the snapshot may hold stale script values. When building the manifest from a migration snapshot, overlay the current source scripts to ensure deploy reflects the latest definitions. --- .../commands/deploy/tailordb/index.test.ts | 206 +++++++++++++++++- .../src/cli/commands/deploy/tailordb/index.ts | 46 +++- 2 files changed, 250 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts b/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts index 9d721817d..96efe588b 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts @@ -4,7 +4,13 @@ import * as path from "pathe"; import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"; import { applyPreMigrationFieldAdjustments } from "#/cli/commands/tailordb/migrate/pre-migration-schema"; import { sdkNameLabelKey } from "../label"; -import { applyTailorDB, formatTailorDBResourceChangeEntries, planTailorDB } from "."; +import { + applyTailorDB, + formatTailorDBResourceChangeEntries, + overlayFieldScripts, + overlaySourceScripts, + planTailorDB, +} from "."; import type { FieldDiffChange } from "#/cli/commands/tailordb/migrate/diff-calculator"; import type { SnapshotFieldConfig, @@ -1300,3 +1306,201 @@ describe("applyTailorDB migration label reconciliation", () => { await expect(applyTailorDB(client, planResult, "create-update")).resolves.toBeUndefined(); }); }); + +describe("overlayFieldScripts", () => { + test("replaces hooks, validate, and default from source fields", () => { + const snapshotFields: Record = { + name: { + type: "string", + required: true, + hooks: { create: { expr: "old_hook" } }, + validate: [{ script: { expr: "old_validate" }, errorMessage: "old" }], + default: "old_default", + }, + }; + const sourceFields: Record = { + name: { + type: "string", + required: true, + hooks: { create: { expr: "new_hook" } }, + validate: [{ script: { expr: "new_validate" }, errorMessage: "new" }], + default: "new_default", + }, + }; + const result = overlayFieldScripts(snapshotFields, sourceFields); + expect(result["name"]!.hooks).toEqual({ create: { expr: "new_hook" } }); + expect(result["name"]!.validate).toEqual([ + { script: { expr: "new_validate" }, errorMessage: "new" }, + ]); + expect(result["name"]!.default).toBe("new_default"); + }); + + test("preserves non-script properties from snapshot", () => { + const snapshotFields: Record = { + name: { + type: "string", + required: true, + index: true, + description: "snapshot desc", + hooks: { create: { expr: "old" } }, + }, + }; + const sourceFields: Record = { + name: { + type: "string", + required: false, + hooks: { create: { expr: "new" } }, + }, + }; + const result = overlayFieldScripts(snapshotFields, sourceFields); + expect(result["name"]!.index).toBe(true); + expect(result["name"]!.description).toBe("snapshot desc"); + expect(result["name"]!.required).toBe(true); + }); + + test("keeps snapshot field unchanged when source has no matching field", () => { + const snapshotFields: Record = { + oldField: { + type: "string", + required: true, + hooks: { create: { expr: "stale" } }, + }, + }; + const result = overlayFieldScripts(snapshotFields, {}); + expect(result["oldField"]!.hooks).toEqual({ create: { expr: "stale" } }); + }); + + test("removes scripts when source field has no scripts", () => { + const snapshotFields: Record = { + name: { + type: "string", + required: true, + hooks: { create: { expr: "old" } }, + validate: [{ script: { expr: "old" }, errorMessage: "old" }], + default: "old", + }, + }; + const sourceFields: Record = { + name: { type: "string", required: true }, + }; + const result = overlayFieldScripts(snapshotFields, sourceFields); + expect(result["name"]!.hooks).toBeUndefined(); + expect(result["name"]!.validate).toBeUndefined(); + expect(result["name"]!.default).toBeUndefined(); + }); + + test("recursively overlays nested fields", () => { + const snapshotFields: Record = { + address: { + type: "object", + required: false, + fields: { + city: { + type: "string", + required: true, + hooks: { create: { expr: "old_city_hook" } }, + }, + }, + }, + }; + const sourceFields: Record = { + address: { + type: "object", + required: false, + fields: { + city: { + type: "string", + required: true, + hooks: { create: { expr: "new_city_hook" } }, + }, + }, + }, + }; + const result = overlayFieldScripts(snapshotFields, sourceFields); + expect(result["address"]!.fields!["city"]!.hooks).toEqual({ + create: { expr: "new_city_hook" }, + }); + }); +}); + +describe("overlaySourceScripts", () => { + function makeSnapshotType(overrides: Partial = {}): TailorDBSnapshotType { + return { + name: "User", + pluralForm: "Users", + fields: { + name: { type: "string", required: true }, + }, + ...overrides, + }; + } + + test("replaces typeHookExpr and typeValidateExpr from source", () => { + const snapshot = makeSnapshotType({ + typeHookExpr: { create: "old_create" }, + typeValidateExpr: "old_validate", + }); + const source = makeSnapshotType({ + typeHookExpr: { create: "new_create", update: "new_update" }, + typeValidateExpr: "new_validate", + }); + const result = overlaySourceScripts(snapshot, source); + expect(result.typeHookExpr).toEqual({ + create: "new_create", + update: "new_update", + }); + expect(result.typeValidateExpr).toBe("new_validate"); + }); + + test("removes type-level scripts when source has none", () => { + const snapshot = makeSnapshotType({ + typeHookExpr: { create: "old" }, + typeValidateExpr: "old", + }); + const source = makeSnapshotType(); + const result = overlaySourceScripts(snapshot, source); + expect(result.typeHookExpr).toBeUndefined(); + expect(result.typeValidateExpr).toBeUndefined(); + }); + + test("preserves non-script properties from snapshot", () => { + const snapshot = makeSnapshotType({ + description: "snapshot desc", + indexes: { idx: { fields: ["name"] } }, + typeHookExpr: { create: "old" }, + }); + const source = makeSnapshotType({ + description: "source desc", + typeHookExpr: { create: "new" }, + }); + const result = overlaySourceScripts(snapshot, source); + expect(result.description).toBe("snapshot desc"); + expect(result.indexes).toEqual({ idx: { fields: ["name"] } }); + expect(result.name).toBe("User"); + }); + + test("overlays field-level scripts from source", () => { + const snapshot = makeSnapshotType({ + fields: { + name: { + type: "string", + required: true, + hooks: { create: { expr: "old_hook" } }, + }, + }, + }); + const source = makeSnapshotType({ + fields: { + name: { + type: "string", + required: true, + hooks: { create: { expr: "new_hook" } }, + }, + }, + }); + const result = overlaySourceScripts(snapshot, source); + expect(result.fields["name"]!.hooks).toEqual({ + create: { expr: "new_hook" }, + }); + }); +}); diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/index.ts b/packages/sdk/src/cli/commands/deploy/tailordb/index.ts index 0ca941616..e27520e3d 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/index.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/index.ts @@ -41,6 +41,7 @@ import { INITIAL_SCHEMA_NUMBER, type RemoteGqlPermission, type SchemaSnapshot, + type SnapshotFieldConfig, type SnapshotGqlOperations, type SnapshotSettings, type TailorDBSnapshotType, @@ -848,8 +849,49 @@ const migrationSnapshotCache = { }, }; +export function overlayFieldScripts( + snapshotFields: Record, + sourceFields: Record, +): Record { + const result: Record = {}; + for (const [name, field] of Object.entries(snapshotFields)) { + const src = sourceFields[name]; + if (!src) { + result[name] = field; + continue; + } + const merged: SnapshotFieldConfig = { + ...field, + hooks: src.hooks, + validate: src.validate, + default: src.default, + }; + if (field.fields && src.fields) { + merged.fields = overlayFieldScripts(field.fields, src.fields); + } + result[name] = merged; + } + return result; +} + +export function overlaySourceScripts( + snapshotType: TailorDBSnapshotType, + sourceType: TailorDBSnapshotType, +): TailorDBSnapshotType { + return { + ...snapshotType, + typeHookExpr: sourceType.typeHookExpr, + typeValidateExpr: sourceType.typeValidateExpr, + fields: overlayFieldScripts(snapshotType.fields, sourceType.fields), + }; +} + /** * Build the TailorDBType manifest for `typeName` from migration N's snapshot. + * + * Script properties (hooks, validate, default, typeHookExpr, typeValidateExpr) + * are overlaid from the current source because migration comparison strips + * them — the snapshot may hold stale values. * @param migration - The pending migration whose snapshot to consult * @param typeName - The type name to look up in the snapshot * @param tailorDBInputs - Deploy inputs, used to resolve namespace gqlOperations @@ -866,7 +908,9 @@ function buildSnapshotTypeManifest( const snapshotType = snapshot.types[typeName]; if (!snapshotType) return undefined; const input = tailorDBInputs.find((i) => i.namespace === migration.namespace); - return generateTailorDBTypeManifestFromSnapshot(snapshotType, { + const sourceType = input?.types[typeName]; + const effectiveType = sourceType ? overlaySourceScripts(snapshotType, sourceType) : snapshotType; + return generateTailorDBTypeManifestFromSnapshot(effectiveType, { publishRecordEvents: executorUsedTypes.has(snapshotType.name), namespaceGqlOperations: input?.config.gqlOperations, }); From 69bc654056aaf9b9d91d4d68b17b641f8ecc7e9e Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 15:28:45 +0900 Subject: [PATCH 543/618] fix(deploy): remove unnecessary script overlay from deploy manifest builder Migration generate already tracks script changes (hooks, validate, default, typeHookExpr, typeValidateExpr) in snapshots, so the overlay that injected current source scripts at deploy time was redundant. Remove overlayFieldScripts, overlaySourceScripts, and the overlay logic in buildSnapshotTypeManifest. --- .../commands/deploy/tailordb/index.test.ts | 206 +----------------- .../src/cli/commands/deploy/tailordb/index.ts | 54 +---- 2 files changed, 2 insertions(+), 258 deletions(-) diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts b/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts index 96efe588b..9d721817d 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts @@ -4,13 +4,7 @@ import * as path from "pathe"; import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"; import { applyPreMigrationFieldAdjustments } from "#/cli/commands/tailordb/migrate/pre-migration-schema"; import { sdkNameLabelKey } from "../label"; -import { - applyTailorDB, - formatTailorDBResourceChangeEntries, - overlayFieldScripts, - overlaySourceScripts, - planTailorDB, -} from "."; +import { applyTailorDB, formatTailorDBResourceChangeEntries, planTailorDB } from "."; import type { FieldDiffChange } from "#/cli/commands/tailordb/migrate/diff-calculator"; import type { SnapshotFieldConfig, @@ -1306,201 +1300,3 @@ describe("applyTailorDB migration label reconciliation", () => { await expect(applyTailorDB(client, planResult, "create-update")).resolves.toBeUndefined(); }); }); - -describe("overlayFieldScripts", () => { - test("replaces hooks, validate, and default from source fields", () => { - const snapshotFields: Record = { - name: { - type: "string", - required: true, - hooks: { create: { expr: "old_hook" } }, - validate: [{ script: { expr: "old_validate" }, errorMessage: "old" }], - default: "old_default", - }, - }; - const sourceFields: Record = { - name: { - type: "string", - required: true, - hooks: { create: { expr: "new_hook" } }, - validate: [{ script: { expr: "new_validate" }, errorMessage: "new" }], - default: "new_default", - }, - }; - const result = overlayFieldScripts(snapshotFields, sourceFields); - expect(result["name"]!.hooks).toEqual({ create: { expr: "new_hook" } }); - expect(result["name"]!.validate).toEqual([ - { script: { expr: "new_validate" }, errorMessage: "new" }, - ]); - expect(result["name"]!.default).toBe("new_default"); - }); - - test("preserves non-script properties from snapshot", () => { - const snapshotFields: Record = { - name: { - type: "string", - required: true, - index: true, - description: "snapshot desc", - hooks: { create: { expr: "old" } }, - }, - }; - const sourceFields: Record = { - name: { - type: "string", - required: false, - hooks: { create: { expr: "new" } }, - }, - }; - const result = overlayFieldScripts(snapshotFields, sourceFields); - expect(result["name"]!.index).toBe(true); - expect(result["name"]!.description).toBe("snapshot desc"); - expect(result["name"]!.required).toBe(true); - }); - - test("keeps snapshot field unchanged when source has no matching field", () => { - const snapshotFields: Record = { - oldField: { - type: "string", - required: true, - hooks: { create: { expr: "stale" } }, - }, - }; - const result = overlayFieldScripts(snapshotFields, {}); - expect(result["oldField"]!.hooks).toEqual({ create: { expr: "stale" } }); - }); - - test("removes scripts when source field has no scripts", () => { - const snapshotFields: Record = { - name: { - type: "string", - required: true, - hooks: { create: { expr: "old" } }, - validate: [{ script: { expr: "old" }, errorMessage: "old" }], - default: "old", - }, - }; - const sourceFields: Record = { - name: { type: "string", required: true }, - }; - const result = overlayFieldScripts(snapshotFields, sourceFields); - expect(result["name"]!.hooks).toBeUndefined(); - expect(result["name"]!.validate).toBeUndefined(); - expect(result["name"]!.default).toBeUndefined(); - }); - - test("recursively overlays nested fields", () => { - const snapshotFields: Record = { - address: { - type: "object", - required: false, - fields: { - city: { - type: "string", - required: true, - hooks: { create: { expr: "old_city_hook" } }, - }, - }, - }, - }; - const sourceFields: Record = { - address: { - type: "object", - required: false, - fields: { - city: { - type: "string", - required: true, - hooks: { create: { expr: "new_city_hook" } }, - }, - }, - }, - }; - const result = overlayFieldScripts(snapshotFields, sourceFields); - expect(result["address"]!.fields!["city"]!.hooks).toEqual({ - create: { expr: "new_city_hook" }, - }); - }); -}); - -describe("overlaySourceScripts", () => { - function makeSnapshotType(overrides: Partial = {}): TailorDBSnapshotType { - return { - name: "User", - pluralForm: "Users", - fields: { - name: { type: "string", required: true }, - }, - ...overrides, - }; - } - - test("replaces typeHookExpr and typeValidateExpr from source", () => { - const snapshot = makeSnapshotType({ - typeHookExpr: { create: "old_create" }, - typeValidateExpr: "old_validate", - }); - const source = makeSnapshotType({ - typeHookExpr: { create: "new_create", update: "new_update" }, - typeValidateExpr: "new_validate", - }); - const result = overlaySourceScripts(snapshot, source); - expect(result.typeHookExpr).toEqual({ - create: "new_create", - update: "new_update", - }); - expect(result.typeValidateExpr).toBe("new_validate"); - }); - - test("removes type-level scripts when source has none", () => { - const snapshot = makeSnapshotType({ - typeHookExpr: { create: "old" }, - typeValidateExpr: "old", - }); - const source = makeSnapshotType(); - const result = overlaySourceScripts(snapshot, source); - expect(result.typeHookExpr).toBeUndefined(); - expect(result.typeValidateExpr).toBeUndefined(); - }); - - test("preserves non-script properties from snapshot", () => { - const snapshot = makeSnapshotType({ - description: "snapshot desc", - indexes: { idx: { fields: ["name"] } }, - typeHookExpr: { create: "old" }, - }); - const source = makeSnapshotType({ - description: "source desc", - typeHookExpr: { create: "new" }, - }); - const result = overlaySourceScripts(snapshot, source); - expect(result.description).toBe("snapshot desc"); - expect(result.indexes).toEqual({ idx: { fields: ["name"] } }); - expect(result.name).toBe("User"); - }); - - test("overlays field-level scripts from source", () => { - const snapshot = makeSnapshotType({ - fields: { - name: { - type: "string", - required: true, - hooks: { create: { expr: "old_hook" } }, - }, - }, - }); - const source = makeSnapshotType({ - fields: { - name: { - type: "string", - required: true, - hooks: { create: { expr: "new_hook" } }, - }, - }, - }); - const result = overlaySourceScripts(snapshot, source); - expect(result.fields["name"]!.hooks).toEqual({ - create: { expr: "new_hook" }, - }); - }); -}); diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/index.ts b/packages/sdk/src/cli/commands/deploy/tailordb/index.ts index e27520e3d..a90b3efc5 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/index.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/index.ts @@ -41,7 +41,6 @@ import { INITIAL_SCHEMA_NUMBER, type RemoteGqlPermission, type SchemaSnapshot, - type SnapshotFieldConfig, type SnapshotGqlOperations, type SnapshotSettings, type TailorDBSnapshotType, @@ -849,55 +848,6 @@ const migrationSnapshotCache = { }, }; -export function overlayFieldScripts( - snapshotFields: Record, - sourceFields: Record, -): Record { - const result: Record = {}; - for (const [name, field] of Object.entries(snapshotFields)) { - const src = sourceFields[name]; - if (!src) { - result[name] = field; - continue; - } - const merged: SnapshotFieldConfig = { - ...field, - hooks: src.hooks, - validate: src.validate, - default: src.default, - }; - if (field.fields && src.fields) { - merged.fields = overlayFieldScripts(field.fields, src.fields); - } - result[name] = merged; - } - return result; -} - -export function overlaySourceScripts( - snapshotType: TailorDBSnapshotType, - sourceType: TailorDBSnapshotType, -): TailorDBSnapshotType { - return { - ...snapshotType, - typeHookExpr: sourceType.typeHookExpr, - typeValidateExpr: sourceType.typeValidateExpr, - fields: overlayFieldScripts(snapshotType.fields, sourceType.fields), - }; -} - -/** - * Build the TailorDBType manifest for `typeName` from migration N's snapshot. - * - * Script properties (hooks, validate, default, typeHookExpr, typeValidateExpr) - * are overlaid from the current source because migration comparison strips - * them — the snapshot may hold stale values. - * @param migration - The pending migration whose snapshot to consult - * @param typeName - The type name to look up in the snapshot - * @param tailorDBInputs - Deploy inputs, used to resolve namespace gqlOperations - * @param executorUsedTypes - Types used by executors (drives publishRecordEvents default) - * @returns The manifest, or undefined if `typeName` is not in that snapshot. - */ function buildSnapshotTypeManifest( migration: PendingMigration, typeName: string, @@ -908,9 +858,7 @@ function buildSnapshotTypeManifest( const snapshotType = snapshot.types[typeName]; if (!snapshotType) return undefined; const input = tailorDBInputs.find((i) => i.namespace === migration.namespace); - const sourceType = input?.types[typeName]; - const effectiveType = sourceType ? overlaySourceScripts(snapshotType, sourceType) : snapshotType; - return generateTailorDBTypeManifestFromSnapshot(effectiveType, { + return generateTailorDBTypeManifestFromSnapshot(snapshotType, { publishRecordEvents: executorUsedTypes.has(snapshotType.name), namespaceGqlOperations: input?.config.gqlOperations, }); From f0e38ac5765e1d52ff8431262b387a681d99c82a Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 15:38:34 +0900 Subject: [PATCH 544/618] chore(cli): upgrade politty to 0.11.2 and simplify skills command wiring --- .changeset/politty-skill-command-cleanup.md | 5 +++ packages/sdk/package.json | 2 +- packages/sdk/src/cli/index.ts | 42 +++++---------------- pnpm-lock.yaml | 26 ++++++++++++- 4 files changed, 40 insertions(+), 35 deletions(-) create mode 100644 .changeset/politty-skill-command-cleanup.md diff --git a/.changeset/politty-skill-command-cleanup.md b/.changeset/politty-skill-command-cleanup.md new file mode 100644 index 000000000..b0206a3e9 --- /dev/null +++ b/.changeset/politty-skill-command-cleanup.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": patch +--- + +Upgrade politty to 0.11.2 and simplify the `skills` command wiring to use its new `globalArgs`/`commandMap`/`unknownKeys` customization options instead of hand-rolled schema rewriting. diff --git a/packages/sdk/package.json b/packages/sdk/package.json index ab243ea85..0707824e7 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -207,7 +207,7 @@ "pathe": "2.0.3", "pgsql-ast-parser": "12.0.2", "pkg-types": "2.3.1", - "politty": "0.11.0", + "politty": "0.11.2", "rolldown": "1.1.4", "semver": "7.8.5", "sql-highlight": "6.1.0", diff --git a/packages/sdk/src/cli/index.ts b/packages/sdk/src/cli/index.ts index 1e95a994f..03c018faa 100644 --- a/packages/sdk/src/cli/index.ts +++ b/packages/sdk/src/cli/index.ts @@ -56,47 +56,22 @@ const packageName = packageJson.name ?? "@tailor-platform/sdk"; const packageJsonPath = await resolvePackageJSON(import.meta.url); const bundledSkillsDir = resolve(dirname(packageJsonPath), "agent-skills"); -type CommandArgShape = Record; - -function replaceCommandArgs(command: AnyCommand, removals: string[] = []): AnyCommand { - const args = command.args as { shape?: CommandArgShape } | undefined; - if (!args?.shape) { - return command; - } - const shape = { ...args.shape }; - for (const name of removals) { - delete shape[name]; - } - return { - ...command, - args: z.strictObject({ - ...shape, - }), - }; -} - -function removeCommandAliases(command: AnyCommand): AnyCommand { - const next = { ...command }; - delete next.aliases; - return next; -} - function alignSkillCommand(command: AnyCommand): AnyCommand { const subCommands = command.subCommands ?? {}; const add = { - ...removeCommandAliases(replaceCommandArgs(subCommands.add as AnyCommand, ["verbose"])), + ...(subCommands.add as AnyCommand), description: "Install Tailor SDK agent skills.", }; const list = { - ...replaceCommandArgs(subCommands.list as AnyCommand, ["json"]), + ...(subCommands.list as AnyCommand), description: "List Tailor SDK agent skills.", }; const remove = { - ...removeCommandAliases(replaceCommandArgs(subCommands.remove as AnyCommand)), + ...(subCommands.remove as AnyCommand), description: "Remove installed Tailor SDK agent skills.", }; const sync = { - ...replaceCommandArgs(subCommands.sync as AnyCommand, ["verbose"]), + ...(subCommands.sync as AnyCommand), description: "Remove and reinstall Tailor SDK agent skills.", }; return { @@ -161,15 +136,18 @@ Run \`${cliName} plugin list\` to see which plugins are installed and where they package: packageName, mode: "copy", descriptionAppend: false, + // strip unknown keys + globalArgs: z.object(commonArgs), + commandMap: { add: ["add"], remove: ["remove"] }, + unknownKeys: "strict", }, ); -const commandWithSkillsSubCommands = commandWithSkills.subCommands ?? {}; export const mainCommand = withCompletionCommand({ ...commandWithSkills, subCommands: { - ...commandWithSkillsSubCommands, - skills: alignSkillCommand(commandWithSkillsSubCommands.skills as AnyCommand), + ...commandWithSkills.subCommands, + skills: alignSkillCommand(commandWithSkills.subCommands.skills), }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e9027479..a0248b4af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -503,8 +503,8 @@ importers: specifier: 2.3.1 version: 2.3.1 politty: - specifier: 0.11.0 - version: 0.11.0(@clack/prompts@1.6.0)(@inquirer/prompts@8.5.2(@types/node@24.13.2))(zod@4.4.3) + specifier: 0.11.2 + version: 0.11.2(@clack/prompts@1.6.0)(@inquirer/prompts@8.5.2(@types/node@24.13.2))(zod@4.4.3) rolldown: specifier: 1.1.4 version: 1.1.4 @@ -3619,6 +3619,20 @@ packages: '@inquirer/prompts': optional: true + politty@0.11.2: + resolution: {integrity: sha512-bPVWpt8b+TD6RfX04BZJxgFWIyH72Dgqi3cBqhNYzsIbdXxUdajIacimCYfb++Gf6FJ782yGbAQ0LwPjoFRSrA==} + engines: {node: '>=20.12.0 <21.0.0 || >=21.7.0'} + hasBin: true + peerDependencies: + '@clack/prompts': ^0.10.0 || ^0.11.0 || ^1.0.0 + '@inquirer/prompts': ^8.3.2 + zod: ^4.2.1 + peerDependenciesMeta: + '@clack/prompts': + optional: true + '@inquirer/prompts': + optional: true + postcss-values-parser@6.0.2: resolution: {integrity: sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==} engines: {node: '>=10'} @@ -6682,6 +6696,14 @@ snapshots: '@clack/prompts': 1.6.0 '@inquirer/prompts': 8.5.2(@types/node@24.13.2) + politty@0.11.2(@clack/prompts@1.6.0)(@inquirer/prompts@8.5.2(@types/node@24.13.2))(zod@4.4.3): + dependencies: + yaml: 2.9.0 + zod: 4.4.3 + optionalDependencies: + '@clack/prompts': 1.6.0 + '@inquirer/prompts': 8.5.2(@types/node@24.13.2) + postcss-values-parser@6.0.2(postcss@8.5.15): dependencies: color-name: 1.1.4 From 621586b31e6a4232b89dcbc6d5cdb51fa7b53622 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 16:30:14 +0900 Subject: [PATCH 545/618] feat(tailordb): add source script hash for drift detection --- .../tailordb/migrate/snapshot.test.ts | 182 ++++++++++++++++++ .../cli/commands/tailordb/migrate/snapshot.ts | 74 ++++++- .../cli/commands/tailordb/migrate/types.ts | 3 +- .../service/tailordb/type-script.test.ts | 127 +++++++++++- .../parser/service/tailordb/type-script.ts | 85 +++++++- 5 files changed, 462 insertions(+), 9 deletions(-) diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts index e466300b8..29dfbe06d 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts @@ -8,6 +8,7 @@ import { } from "@tailor-platform/tailor-proto/tailordb_resource_pb"; import * as path from "pathe"; import { describe, expect, expectTypeOf, test, beforeEach, afterAll, vi } from "vitest"; +import { buildTypeScripts } from "#/parser/service/tailordb/type-script"; import { createSnapshotFromLocalTypes, loadSnapshot, @@ -3384,6 +3385,187 @@ describe("snapshot", () => { expect(drifts.length).toBe(1); expect(drifts[0]!.kind).toBe("type_missing_local"); }); + + test("detects script_mismatch when remote has no hash", () => { + const snapshot: SchemaSnapshot = { + version: SCHEMA_SNAPSHOT_VERSION, + namespace, + createdAt: new Date().toISOString(), + types: { + User: { + name: "User", + pluralForm: "Users", + fields: { + id: { type: "uuid", required: true }, + name: { + type: "string", + required: true, + hooks: { create: { expr: "_value.trim()" } }, + }, + }, + }, + }, + }; + + const remoteTypes = [ + createMockRemoteType( + "User", + { + id: { type: "uuid", required: true }, + name: { type: "string", required: true }, + }, + { + typeHook: { + create: { expr: '((_invoker) => { return { "name": _value.trim() }; })()' }, + }, + }, + ), + ]; + + const drifts = compareRemoteWithSnapshot(remoteTypes, snapshot); + expect(drifts.some((d) => d.kind === "script_mismatch")).toBe(true); + }); + + test("detects script_mismatch when hashes differ", () => { + const snapshotFields = { + name: { + type: "string", + required: true, + hooks: { create: { expr: "_value.trim()" } }, + }, + }; + const snapshot: SchemaSnapshot = { + version: SCHEMA_SNAPSHOT_VERSION, + namespace, + createdAt: new Date().toISOString(), + types: { + User: { + name: "User", + pluralForm: "Users", + fields: { id: { type: "uuid", required: true }, ...snapshotFields }, + }, + }, + }; + + const differentFields = { + name: { + type: "string", + required: true as const, + hooks: { create: { expr: "_value.toLowerCase()" } }, + }, + }; + const { typeHook } = buildTypeScripts(differentFields); + + const remoteTypes = [ + createMockRemoteType( + "User", + { id: { type: "uuid", required: true }, name: { type: "string", required: true } }, + { typeHook }, + ), + ]; + + const drifts = compareRemoteWithSnapshot(remoteTypes, snapshot); + expect(drifts.some((d) => d.kind === "script_mismatch")).toBe(true); + }); + + test("no script drift when hashes match", () => { + const snapshotFields = { + name: { + type: "string", + required: true, + hooks: { create: { expr: "_value.trim()" } }, + }, + }; + const snapshot: SchemaSnapshot = { + version: SCHEMA_SNAPSHOT_VERSION, + namespace, + createdAt: new Date().toISOString(), + types: { + User: { + name: "User", + pluralForm: "Users", + fields: { id: { type: "uuid", required: true }, ...snapshotFields }, + }, + }, + }; + + const { typeHook } = buildTypeScripts(snapshotFields); + + const remoteTypes = [ + createMockRemoteType( + "User", + { id: { type: "uuid", required: true }, name: { type: "string", required: true } }, + { typeHook }, + ), + ]; + + const drifts = compareRemoteWithSnapshot(remoteTypes, snapshot); + expect(drifts.some((d) => d.kind === "script_mismatch")).toBe(false); + }); + + test("detects script drift when remote has scripts but snapshot does not", () => { + const snapshot: SchemaSnapshot = { + version: SCHEMA_SNAPSHOT_VERSION, + namespace, + createdAt: new Date().toISOString(), + types: { + User: { + name: "User", + pluralForm: "Users", + fields: { + id: { type: "uuid", required: true }, + name: { type: "string", required: true }, + }, + }, + }, + }; + + const remoteTypes = [ + createMockRemoteType( + "User", + { + id: { type: "uuid", required: true }, + name: { type: "string", required: true }, + }, + { + typeHook: { + create: { expr: "someExpr() // @sdk-source-hash:abcdef0123456789" }, + }, + }, + ), + ]; + + const drifts = compareRemoteWithSnapshot(remoteTypes, snapshot); + expect(drifts.some((d) => d.kind === "script_mismatch")).toBe(true); + }); + + test("no script drift when neither side has scripts", () => { + const snapshot: SchemaSnapshot = { + version: SCHEMA_SNAPSHOT_VERSION, + namespace, + createdAt: new Date().toISOString(), + types: { + User: { + name: "User", + pluralForm: "Users", + fields: { + id: { type: "uuid", required: true }, + name: { type: "string", required: true }, + }, + }, + }, + }; + + const remoteTypes = [ + createMockRemoteType("User", { + id: { type: "uuid", required: true }, + name: { type: "string", required: true }, + }), + ]; + + const drifts = compareRemoteWithSnapshot(remoteTypes, snapshot); + expect(drifts.some((d) => d.kind === "script_mismatch")).toBe(false); + }); }); // ========================================================================== diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts index c29fd34ed..1462b14e1 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts @@ -23,6 +23,10 @@ import { import * as inflection from "inflection"; import * as path from "pathe"; import { z } from "zod"; +import { + computeSourceScriptHash, + extractSourceScriptHash, +} from "#/parser/service/tailordb/type-script"; import { assertDefined } from "#/utils/assert"; import { type MigrationDiff, @@ -2651,7 +2655,7 @@ export function compareRemoteWithSnapshot( snapshot: SchemaSnapshot, remoteGqlPermissions: readonly RemoteGqlPermission[] = [], ): SchemaDrift[] { - return compareNormalizedRemoteWithSnapshot( + const structuralDrifts = compareNormalizedRemoteWithSnapshot( createRemoteComparableSnapshot( createSnapshotFromRemoteTypes( remoteTypes, @@ -2662,6 +2666,74 @@ export function compareRemoteWithSnapshot( ), createRemoteComparableSnapshot(snapshot), ); + + const scriptDrifts = compareScriptHashes(remoteTypes, snapshot); + + return [...structuralDrifts, ...scriptDrifts]; +} + +function extractRemoteScriptHash(remoteType: ProtoTailorDBType): string | undefined { + const exprs = [ + remoteType.schema?.typeHook?.create?.expr, + remoteType.schema?.typeHook?.update?.expr, + remoteType.schema?.typeValidate?.create?.expr, + remoteType.schema?.typeValidate?.update?.expr, + ]; + for (const expr of exprs) { + if (expr) { + const hash = extractSourceScriptHash(expr); + if (hash) return hash; + } + } + return undefined; +} + +function remoteHasScripts(remoteType: ProtoTailorDBType): boolean { + return !!( + remoteType.schema?.typeHook?.create?.expr || + remoteType.schema?.typeHook?.update?.expr || + remoteType.schema?.typeValidate?.create?.expr || + remoteType.schema?.typeValidate?.update?.expr + ); +} + +function compareScriptHashes( + remoteTypes: ProtoTailorDBType[], + snapshot: SchemaSnapshot, +): SchemaDrift[] { + const drifts: SchemaDrift[] = []; + const remoteByName = new Map(remoteTypes.map((t) => [t.name, t])); + + for (const [typeName, snapshotType] of Object.entries(snapshot.types)) { + const localHash = computeSourceScriptHash(snapshotType.fields, { + typeHookExpr: snapshotType.typeHookExpr, + typeValidateExpr: snapshotType.typeValidateExpr, + }); + + const remoteType = remoteByName.get(typeName); + if (!remoteType) continue; + + if (localHash) { + const remoteHash = extractRemoteScriptHash(remoteType); + if (localHash !== remoteHash) { + drifts.push({ + typeName, + kind: "script_mismatch", + details: remoteHash + ? `Type '${typeName}' scripts differ between remote and snapshot` + : `Type '${typeName}' has no script hash on remote`, + }); + } + } else if (remoteHasScripts(remoteType)) { + drifts.push({ + typeName, + kind: "script_mismatch", + details: `Type '${typeName}' has scripts on remote but not in snapshot`, + }); + } + } + + return drifts; } function stripFieldScriptProps(field: SnapshotFieldConfig): SnapshotFieldConfig { diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/types.ts b/packages/sdk/src/cli/commands/tailordb/migrate/types.ts index 0896ce156..a33711c65 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/types.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/types.ts @@ -157,7 +157,8 @@ export type SchemaDriftKind = | "relationship_missing_remote" | "relationship_missing_local" | "relationship_mismatch" - | "permission_mismatch"; + | "permission_mismatch" + | "script_mismatch"; /** * Single schema drift item diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index c1673b1d1..dbb44844c 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -1,5 +1,10 @@ import { describe, expect, test } from "vitest"; -import { buildTypeScripts, type ScriptFieldConfig } from "./type-script"; +import { + buildTypeScripts, + computeSourceScriptHash, + extractSourceScriptHash, + type ScriptFieldConfig, +} from "./type-script"; describe("buildTypeScripts", () => { test("returns empty result when no field has hooks or validators", () => { @@ -368,3 +373,123 @@ describe("buildTypeScripts", () => { expect(updateExpr).not.toContain("__oldEl"); }); }); + +describe("computeSourceScriptHash", () => { + test("returns undefined when no script-relevant data exists", () => { + expect(computeSourceScriptHash({ id: { type: "uuid" } })).toBeUndefined(); + expect(computeSourceScriptHash({})).toBeUndefined(); + }); + + test("returns a 16-char hex string when scripts exist", () => { + const hash = computeSourceScriptHash({ + name: { type: "string", hooks: { create: { expr: "_value.trim()" } } }, + }); + expect(hash).toMatch(/^[0-9a-f]{16}$/); + }); + + test("is deterministic for the same input", () => { + const fields: Record = { + status: { type: "string", default: "active" }, + name: { type: "string", hooks: { create: { expr: "_value.trim()" } } }, + }; + expect(computeSourceScriptHash(fields)).toBe(computeSourceScriptHash(fields)); + }); + + test("changes when a hook expression changes", () => { + const a = computeSourceScriptHash({ + name: { type: "string", hooks: { create: { expr: "_value.trim()" } } }, + }); + const b = computeSourceScriptHash({ + name: { type: "string", hooks: { create: { expr: "_value.toLowerCase()" } } }, + }); + expect(a).not.toBe(b); + }); + + test("changes when typeHookExpr changes", () => { + const fields: Record = {}; + const a = computeSourceScriptHash(fields, { typeHookExpr: { create: "exprA" } }); + const b = computeSourceScriptHash(fields, { typeHookExpr: { create: "exprB" } }); + expect(a).not.toBe(b); + }); + + test("changes when typeValidateExpr changes", () => { + const a = computeSourceScriptHash({}, { typeValidateExpr: "validateA" }); + const b = computeSourceScriptHash({}, { typeValidateExpr: "validateB" }); + expect(a).not.toBe(b); + }); + + test("is insensitive to field insertion order", () => { + const a = computeSourceScriptHash({ + alpha: { type: "string", hooks: { create: { expr: "a" } } }, + beta: { type: "string", hooks: { create: { expr: "b" } } }, + }); + const b = computeSourceScriptHash({ + beta: { type: "string", hooks: { create: { expr: "b" } } }, + alpha: { type: "string", hooks: { create: { expr: "a" } } }, + }); + expect(a).toBe(b); + }); + + test("includes nested field scripts in hash", () => { + const withNested = computeSourceScriptHash({ + profile: { + type: "nested", + fields: { + email: { type: "string", hooks: { create: { expr: "_value.toLowerCase()" } } }, + }, + }, + }); + const withoutNested = computeSourceScriptHash({ + profile: { type: "nested", fields: { email: { type: "string" } } }, + }); + expect(withNested).toBeDefined(); + expect(withoutNested).toBeUndefined(); + }); +}); + +describe("extractSourceScriptHash", () => { + test("extracts hash from expr with hash suffix", () => { + const expr = "((_invoker) => { return {}; })() // @sdk-source-hash:abcdef0123456789"; + expect(extractSourceScriptHash(expr)).toBe("abcdef0123456789"); + }); + + test("returns undefined when no hash is present", () => { + expect(extractSourceScriptHash("((_invoker) => { return {}; })()")).toBeUndefined(); + }); +}); + +describe("buildTypeScripts hash embedding", () => { + test("embeds hash in generated hook expr", () => { + const fields: Record = { + createdAt: { type: "datetime", hooks: { create: { expr: "_now" } } }, + }; + const { typeHook } = buildTypeScripts(fields); + const expr = typeHook?.create?.expr ?? ""; + const hash = extractSourceScriptHash(expr); + expect(hash).toMatch(/^[0-9a-f]{16}$/); + expect(hash).toBe(computeSourceScriptHash(fields)); + }); + + test("embeds same hash in all generated exprs", () => { + const fields: Record = { + name: { + type: "string", + hooks: { create: { expr: "_value.trim()" }, update: { expr: "_value.trim()" } }, + validate: [{ script: { expr: "_value.length > 0" }, errorMessage: "required" }], + }, + }; + const { typeHook, typeValidate } = buildTypeScripts(fields); + const hashes = [ + extractSourceScriptHash(typeHook?.create?.expr ?? ""), + extractSourceScriptHash(typeHook?.update?.expr ?? ""), + extractSourceScriptHash(typeValidate?.create?.expr ?? ""), + extractSourceScriptHash(typeValidate?.update?.expr ?? ""), + ]; + expect(new Set(hashes).size).toBe(1); + expect(hashes[0]).toMatch(/^[0-9a-f]{16}$/); + }); + + test("no hash in empty result", () => { + expect(buildTypeScripts({ id: { type: "uuid" } })).toEqual({}); + }); +}); diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index 4806953a1..10426a0eb 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -1,3 +1,5 @@ +import { createHash } from "node:crypto"; + // Platform-injected record map for type-level hook/validate scripts. const INPUT = "_input"; const NEW_RECORD = "_newRecord"; @@ -5,6 +7,8 @@ const OLD_RECORD = "_oldRecord"; // Shared operation timestamp bound once per script execution. const NOW = "_now"; +const SOURCE_HASH_PREFIX = "// @sdk-source-hash:"; + const TIME_TYPES = new Set(["datetime", "date", "time"]); type HookOperation = "create" | "update"; @@ -36,6 +40,69 @@ export interface TypeScripts { const key = (name: string) => JSON.stringify(name); +function collectFieldScriptSources(fields: Record): [string, unknown][] { + const entries: [string, unknown][] = []; + + for (const [name, config] of Object.entries(fields).toSorted(([a], [b]) => a.localeCompare(b))) { + const data: Record = {}; + + if (config.hooks?.create?.expr) data.hc = config.hooks.create.expr; + if (config.hooks?.update?.expr) data.hu = config.hooks.update.expr; + if (config.validate?.some((v) => v.script?.expr)) { + data.v = config.validate + .filter((v) => v.script?.expr) + .map((v) => [v.script?.expr, v.errorMessage]); + } + if (config.default !== undefined) { + data.d = config.default instanceof Date ? config.default.toISOString() : config.default; + data.t = config.type; + } + + if (config.fields) { + const nested = collectFieldScriptSources(config.fields); + if (nested.length > 0) { + data.f = nested; + data.a = !!config.array; + } + } + + if (Object.keys(data).length > 0) { + entries.push([name, data]); + } + } + + return entries; +} + +export function computeSourceScriptHash( + fields: Record, + options?: { + typeHookExpr?: { create?: string; update?: string }; + typeValidateExpr?: string; + }, +): string | undefined { + const fieldSources = collectFieldScriptSources(fields); + const hasTypeScripts = + fieldSources.length > 0 || + options?.typeHookExpr?.create || + options?.typeHookExpr?.update || + options?.typeValidateExpr; + + if (!hasTypeScripts) return undefined; + + const payload: unknown[] = [fieldSources]; + if (options?.typeHookExpr?.create) payload.push(["thc", options.typeHookExpr.create]); + if (options?.typeHookExpr?.update) payload.push(["thu", options.typeHookExpr.update]); + if (options?.typeValidateExpr) payload.push(["tve", options.typeValidateExpr]); + + return createHash("sha256").update(JSON.stringify(payload)).digest("hex").slice(0, 16); +} + +export function extractSourceScriptHash(expr: string): string | undefined { + const match = expr.match(/\/\/ @sdk-source-hash:([0-9a-f]+)$/); + return match?.[1]; +} + const isNestedType = (config: ScriptFieldConfig): boolean => config.type === "nested" && config.fields !== undefined; @@ -210,19 +277,25 @@ export function buildTypeScripts( const typeHookExpr = options?.typeHookExpr; const typeValidateExpr = options?.typeValidateExpr; + const hash = computeSourceScriptHash(fields, options); + const hashSuffix = hash ? ` ${SOURCE_HASH_PREFIX}${hash}` : ""; + const hook: { create?: ScriptRef; update?: ScriptRef } = {}; for (const operation of ["create", "update"] as const) { const perFieldExpr = buildHookObject(fields, INPUT, OLD_RECORD, operation); const typeLevelExpr = typeHookExpr?.[operation]; + let expr: string | undefined; if (perFieldExpr !== null && typeLevelExpr) { - hook[operation] = { - expr: `((_invoker) => { const ${NOW} = new Date(); const __fl = ${perFieldExpr}; return Object.assign({}, __fl, ((${INPUT}) => ${typeLevelExpr})(Object.assign({}, ${INPUT}, __fl))); })(typeof _invoker !== "undefined" ? _invoker : undefined)`, - }; + expr = `((_invoker) => { const ${NOW} = new Date(); const __fl = ${perFieldExpr}; return Object.assign({}, __fl, ((${INPUT}) => ${typeLevelExpr})(Object.assign({}, ${INPUT}, __fl))); })(typeof _invoker !== "undefined" ? _invoker : undefined)`; } else if (typeLevelExpr) { - hook[operation] = { expr: wrapHook(typeLevelExpr) }; + expr = wrapHook(typeLevelExpr); } else if (perFieldExpr !== null) { - hook[operation] = { expr: wrapHook(perFieldExpr) }; + expr = wrapHook(perFieldExpr); + } + + if (expr) { + hook[operation] = { expr: expr + hashSuffix }; } } if (hook.create || hook.update) { @@ -231,7 +304,7 @@ export function buildTypeScripts( const statements = buildValidateStatements(fields, NEW_RECORD, ""); if (statements.length > 0 || typeValidateExpr) { - const expr = wrapValidate(statements, typeValidateExpr); + const expr = wrapValidate(statements, typeValidateExpr) + hashSuffix; result.typeValidate = { create: { expr }, update: { expr } }; } From 0f89b01cac428209bf844bb166ea6c409f30d6d7 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 18:31:06 +0900 Subject: [PATCH 546/618] fix(tailordb): restore type hook input from hooked values after v2 merge The v2 merge reverted commit d1f8d7a7e which aligned the test helper's type hook input with the runtime behavior. The `typeHookInput` was derived from raw `obj` instead of `hooked` (post field-level hooks and defaults), causing type-level hooks in tests to receive unprocessed input values. --- packages/sdk/src/utils/test/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index fa37a387c..c5cf9bfc4 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -69,7 +69,7 @@ export function createTailorDBHook>(type: T) { // oxlint-disable-next-line typescript/no-unnecessary-condition -- metadata absent in recursive nested calls if (type.metadata?.typeHook?.create) { - const { id: _id, ...typeHookInput } = obj ?? {}; + const { id: _id, ...typeHookInput } = hooked; // oxlint-disable-next-line typescript/no-unsafe-function-type const overrides = (type.metadata.typeHook.create as Function)({ input: typeHookInput, From 2a66a08a758b6018c01a88a9cdd5dba799b2eae3 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 22:58:20 +0900 Subject: [PATCH 547/618] chore(example): consolidate migrations to one per namespace Squash multiple incremental migration diffs (0005-0009 for tailordb, 0002-0005 for analyticsdb) into a single migration per namespace (0005, 0002 respectively) to avoid advancing migration numbers excessively within a single PR. --- example/migrations/0005/diff.json | 301 +++++--- example/migrations/0006/diff.json | 516 ------------- example/migrations/0007/diff.json | 695 ------------------ example/migrations/0008/diff.json | 72 -- example/migrations/0009/diff.json | 512 ------------- example/migrations/analyticsdb/0002/diff.json | 16 +- example/migrations/analyticsdb/0003/diff.json | 68 -- example/migrations/analyticsdb/0004/diff.json | 68 -- example/migrations/analyticsdb/0005/diff.json | 62 -- 9 files changed, 215 insertions(+), 2095 deletions(-) delete mode 100644 example/migrations/0006/diff.json delete mode 100644 example/migrations/0007/diff.json delete mode 100644 example/migrations/0008/diff.json delete mode 100644 example/migrations/0009/diff.json delete mode 100644 example/migrations/analyticsdb/0003/diff.json delete mode 100644 example/migrations/analyticsdb/0004/diff.json delete mode 100644 example/migrations/analyticsdb/0005/diff.json diff --git a/example/migrations/0005/diff.json b/example/migrations/0005/diff.json index 56bf931ca..a8c12df68 100644 --- a/example/migrations/0005/diff.json +++ b/example/migrations/0005/diff.json @@ -1,8 +1,181 @@ { "version": 1, "namespace": "tailordb", - "createdAt": "2026-07-07T01:12:32.459Z", + "createdAt": "2026-07-13T13:43:18.512Z", "changes": [ + { + "kind": "type_added", + "typeName": "ProductBundle", + "after": { + "name": "ProductBundle", + "pluralForm": "ProductBundles", + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "label": { + "type": "string", + "required": false + }, + "items": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "productName": { + "type": "string", + "required": true + }, + "qty": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({ value })=>value <= 0 ? \"qty must be positive\" : undefined)({ value: _value })" + }, + "errorMessage": "" + } + ] + }, + "unitPrice": { + "type": "float", + "required": true + } + } + }, + "createdAt": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "default": "now" + }, + "updatedAt": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "update": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + }, + "default": "now" + } + }, + "description": "Product bundle with nested array hooks", + "settings": {}, + "typeHookExpr": { + "create": "(({ input })=>({\n label: `${input.name} Bundle`\n }))({ input: _input, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })", + "update": "(({ input, oldRecord })=>({\n label: `${input.name ?? oldRecord.name} Bundle`\n }))({ input: _input, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "typeValidateExpr": "(({ newRecord }, issues)=>{\n if (newRecord.items && newRecord.items.length === 0) {\n issues(\"items\", \"At least one item is required\");\n }\n})({ newRecord: _newRecord, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) }, __issues)", + "permissions": { + "record": { + "create": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "read": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + }, + { + "conditions": [ + [ + { + "user": "_loggedIn" + }, + "eq", + true + ] + ], + "permit": "allow" + } + ], + "update": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "delete": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ] + }, + "gql": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "actions": ["create", "read", "update", "delete", "aggregate", "bulkUpsert"], + "permit": "allow" + }, + { + "conditions": [ + [ + { + "user": "_loggedIn" + }, + "eq", + true + ] + ], + "actions": ["read"], + "permit": "allow" + } + ] + } + } + }, { "kind": "field_modified", "typeName": "Customer", @@ -112,11 +285,7 @@ "type": "datetime", "required": true, "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } + "default": "now" } }, { @@ -141,13 +310,11 @@ "required": true, "description": "Record update timestamp", "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" } - } + }, + "default": "now" } }, { @@ -168,11 +335,7 @@ "type": "datetime", "required": true, "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } + "default": "now" } }, { @@ -197,13 +360,11 @@ "required": true, "description": "Record update timestamp", "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" } - } + }, + "default": "now" } }, { @@ -224,11 +385,7 @@ "type": "datetime", "required": true, "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } + "default": "now" } }, { @@ -253,13 +410,11 @@ "required": true, "description": "Record update timestamp", "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" } - } + }, + "default": "now" } }, { @@ -363,11 +518,7 @@ "type": "datetime", "required": true, "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } + "default": "now" } }, { @@ -392,13 +543,11 @@ "required": true, "description": "Record update timestamp", "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" } - } + }, + "default": "now" } }, { @@ -419,11 +568,7 @@ "type": "datetime", "required": true, "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } + "default": "now" } }, { @@ -448,13 +593,11 @@ "required": true, "description": "Record update timestamp", "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" } - } + }, + "default": "now" } }, { @@ -475,11 +618,7 @@ "type": "datetime", "required": true, "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } + "default": "now" } }, { @@ -504,13 +643,11 @@ "required": true, "description": "Record update timestamp", "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" } - } + }, + "default": "now" } }, { @@ -531,11 +668,7 @@ "type": "datetime", "required": true, "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } + "default": "now" } }, { @@ -560,13 +693,11 @@ "required": true, "description": "Record update timestamp", "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" } - } + }, + "default": "now" } }, { @@ -587,11 +718,7 @@ "type": "datetime", "required": true, "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } + "default": "now" } }, { @@ -616,13 +743,11 @@ "required": true, "description": "Record update timestamp", "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" } - } + }, + "default": "now" } }, { @@ -643,11 +768,7 @@ "type": "datetime", "required": true, "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } + "default": "now" } }, { @@ -672,13 +793,11 @@ "required": true, "description": "Record update timestamp", "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" } - } + }, + "default": "now" } } ], diff --git a/example/migrations/0006/diff.json b/example/migrations/0006/diff.json deleted file mode 100644 index 647ca11c3..000000000 --- a/example/migrations/0006/diff.json +++ /dev/null @@ -1,516 +0,0 @@ -{ - "version": 1, - "namespace": "tailordb", - "createdAt": "2026-07-10T08:03:31.987Z", - "changes": [ - { - "kind": "field_modified", - "typeName": "Customer", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "Customer", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "Invoice", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "Invoice", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "NestedProfile", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "NestedProfile", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "PurchaseOrder", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "PurchaseOrder", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "SalesOrder", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "SalesOrder", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "Supplier", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "Supplier", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "User", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "User", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "UserLog", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "UserLog", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "UserSetting", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "UserSetting", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - } - ], - "hasBreakingChanges": false, - "breakingChanges": [], - "hasWarnings": false, - "warnings": [], - "requiresMigrationScript": false -} diff --git a/example/migrations/0007/diff.json b/example/migrations/0007/diff.json deleted file mode 100644 index 81927f7f7..000000000 --- a/example/migrations/0007/diff.json +++ /dev/null @@ -1,695 +0,0 @@ -{ - "version": 1, - "namespace": "tailordb", - "createdAt": "2026-07-11T14:10:41.961Z", - "changes": [ - { - "kind": "type_added", - "typeName": "ProductBundle", - "after": { - "name": "ProductBundle", - "pluralForm": "ProductBundles", - "fields": { - "id": { - "type": "uuid", - "required": true - }, - "name": { - "type": "string", - "required": true - }, - "label": { - "type": "string", - "required": false - }, - "items": { - "type": "nested", - "required": true, - "array": true, - "fields": { - "productName": { - "type": "string", - "required": true - }, - "qty": { - "type": "integer", - "required": true, - "validate": [ - { - "script": { - "expr": "(({ value })=>value <= 0 ? \"qty must be positive\" : undefined)({ value: _value })" - }, - "errorMessage": "" - } - ] - }, - "unitPrice": { - "type": "float", - "required": true - } - } - }, - "createdAt": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "updatedAt": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - "description": "Product bundle with nested array hooks", - "settings": {}, - "typeHookExpr": { - "create": "(({ input })=>({\n label: `${input.name} Bundle`\n }))({ input: _input, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })", - "update": "(({ input, oldRecord })=>({\n label: `${input.name ?? oldRecord.name} Bundle`\n }))({ input: _input, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "typeValidateExpr": "(({ newRecord }, issues)=>{\n if (newRecord.items && newRecord.items.length === 0) {\n issues(\"items\", \"At least one item is required\");\n }\n})({ newRecord: _newRecord, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) }, __issues)", - "permissions": { - "record": { - "create": [ - { - "conditions": [ - [ - { - "user": "role" - }, - "eq", - "MANAGER" - ] - ], - "permit": "allow" - } - ], - "read": [ - { - "conditions": [ - [ - { - "user": "role" - }, - "eq", - "MANAGER" - ] - ], - "permit": "allow" - }, - { - "conditions": [ - [ - { - "user": "_loggedIn" - }, - "eq", - true - ] - ], - "permit": "allow" - } - ], - "update": [ - { - "conditions": [ - [ - { - "user": "role" - }, - "eq", - "MANAGER" - ] - ], - "permit": "allow" - } - ], - "delete": [ - { - "conditions": [ - [ - { - "user": "role" - }, - "eq", - "MANAGER" - ] - ], - "permit": "allow" - } - ] - }, - "gql": [ - { - "conditions": [ - [ - { - "user": "role" - }, - "eq", - "MANAGER" - ] - ], - "actions": ["create", "read", "update", "delete", "aggregate", "bulkUpsert"], - "permit": "allow" - }, - { - "conditions": [ - [ - { - "user": "_loggedIn" - }, - "eq", - true - ] - ], - "actions": ["read"], - "permit": "allow" - } - ] - } - } - }, - { - "kind": "field_modified", - "typeName": "Customer", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "Customer", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "Invoice", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "Invoice", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "NestedProfile", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "NestedProfile", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "PurchaseOrder", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "PurchaseOrder", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "SalesOrder", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "SalesOrder", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "Supplier", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "Supplier", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "User", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "User", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "UserLog", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "UserLog", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "UserSetting", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "UserSetting", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - } - ], - "hasBreakingChanges": false, - "breakingChanges": [], - "hasWarnings": false, - "warnings": [], - "requiresMigrationScript": false -} diff --git a/example/migrations/0008/diff.json b/example/migrations/0008/diff.json deleted file mode 100644 index b078e1da2..000000000 --- a/example/migrations/0008/diff.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "version": 1, - "namespace": "tailordb", - "createdAt": "2026-07-12T05:51:39.449Z", - "changes": [ - { - "kind": "field_modified", - "typeName": "ProductBundle", - "fieldName": "items", - "before": { - "type": "nested", - "required": true, - "array": true, - "fields": { - "productName": { - "type": "string", - "required": true - }, - "qty": { - "type": "integer", - "required": true, - "validate": [ - { - "script": { - "expr": "(({ value })=>value <= 0 ? \"qty must be positive\" : undefined)({ value: _value })" - }, - "errorMessage": "" - } - ], - "default": 1 - }, - "unitPrice": { - "type": "float", - "required": true - } - } - }, - "after": { - "type": "nested", - "required": true, - "array": true, - "fields": { - "productName": { - "type": "string", - "required": true - }, - "qty": { - "type": "integer", - "required": true, - "validate": [ - { - "script": { - "expr": "(({ value })=>value <= 0 ? \"qty must be positive\" : undefined)({ value: _value })" - }, - "errorMessage": "" - } - ] - }, - "unitPrice": { - "type": "float", - "required": true - } - } - } - } - ], - "hasBreakingChanges": false, - "breakingChanges": [], - "hasWarnings": false, - "warnings": [], - "requiresMigrationScript": false -} diff --git a/example/migrations/0009/diff.json b/example/migrations/0009/diff.json deleted file mode 100644 index 1c949cce4..000000000 --- a/example/migrations/0009/diff.json +++ /dev/null @@ -1,512 +0,0 @@ -{ - "version": 1, - "namespace": "tailordb", - "createdAt": "2026-07-13T02:55:58.448Z", - "changes": [ - { - "kind": "field_modified", - "typeName": "Customer", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "Customer", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "update": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - }, - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "Invoice", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "Invoice", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "update": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - }, - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "NestedProfile", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "NestedProfile", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "update": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - }, - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "ProductBundle", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "ProductBundle", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "update": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - }, - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "PurchaseOrder", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "PurchaseOrder", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "update": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - }, - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "SalesOrder", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "SalesOrder", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "update": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - }, - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "Supplier", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "Supplier", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "update": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - }, - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "User", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "User", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "update": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - }, - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "UserLog", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "UserLog", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "update": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - }, - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "UserSetting", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "UserSetting", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "update": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - }, - "default": "now" - } - } - ], - "hasBreakingChanges": false, - "breakingChanges": [], - "hasWarnings": false, - "warnings": [], - "requiresMigrationScript": false -} diff --git a/example/migrations/analyticsdb/0002/diff.json b/example/migrations/analyticsdb/0002/diff.json index cdd240473..ab030b323 100644 --- a/example/migrations/analyticsdb/0002/diff.json +++ b/example/migrations/analyticsdb/0002/diff.json @@ -1,7 +1,7 @@ { "version": 1, "namespace": "analyticsdb", - "createdAt": "2026-07-07T01:12:32.475Z", + "createdAt": "2026-07-13T13:43:18.552Z", "changes": [ { "kind": "field_modified", @@ -21,11 +21,7 @@ "type": "datetime", "required": true, "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } + "default": "now" } }, { @@ -50,13 +46,11 @@ "required": true, "description": "Record update timestamp", "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" } - } + }, + "default": "now" } } ], diff --git a/example/migrations/analyticsdb/0003/diff.json b/example/migrations/analyticsdb/0003/diff.json deleted file mode 100644 index df575d8ab..000000000 --- a/example/migrations/analyticsdb/0003/diff.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "version": 1, - "namespace": "analyticsdb", - "createdAt": "2026-07-10T08:03:32.003Z", - "changes": [ - { - "kind": "field_modified", - "typeName": "Event", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "Event", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - } - ], - "hasBreakingChanges": false, - "breakingChanges": [], - "hasWarnings": false, - "warnings": [], - "requiresMigrationScript": false -} diff --git a/example/migrations/analyticsdb/0004/diff.json b/example/migrations/analyticsdb/0004/diff.json deleted file mode 100644 index 488e28a00..000000000 --- a/example/migrations/analyticsdb/0004/diff.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "version": 1, - "namespace": "analyticsdb", - "createdAt": "2026-07-11T14:10:41.983Z", - "changes": [ - { - "kind": "field_modified", - "typeName": "Event", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - }, - { - "kind": "field_modified", - "typeName": "Event", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - } - } - ], - "hasBreakingChanges": false, - "breakingChanges": [], - "hasWarnings": false, - "warnings": [], - "requiresMigrationScript": false -} diff --git a/example/migrations/analyticsdb/0005/diff.json b/example/migrations/analyticsdb/0005/diff.json deleted file mode 100644 index 7aa81131b..000000000 --- a/example/migrations/analyticsdb/0005/diff.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "version": 1, - "namespace": "analyticsdb", - "createdAt": "2026-07-13T02:55:58.505Z", - "changes": [ - { - "kind": "field_modified", - "typeName": "Event", - "fieldName": "createdAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record creation timestamp", - "default": "now" - } - }, - { - "kind": "field_modified", - "typeName": "Event", - "fieldName": "updatedAt", - "before": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "create": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - }, - "update": { - "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - } - }, - "after": { - "type": "datetime", - "required": true, - "description": "Record update timestamp", - "hooks": { - "update": { - "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" - } - }, - "default": "now" - } - } - ], - "hasBreakingChanges": false, - "breakingChanges": [], - "hasWarnings": false, - "warnings": [], - "requiresMigrationScript": false -} From 38ef07fce9f1fd698071051e3481e6743e2439ed Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 13 Jul 2026 23:09:23 +0900 Subject: [PATCH 548/618] ci: point ERD schema actions at v2-compatible commit The tailor-platform/actions erd-schema-export/preview/comment actions were pinned to a release built against the tailor-sdk binary name. v2 already renamed the CLI bin to tailor, so every namespace export in the preview job failed with "Command tailor-sdk not found". Pin to an unreleased commit on the actions repo that uses the renamed binary. Also trigger the export job on pushes to v2 (not just main) so PRs based on v2 get a real base-schema artifact to diff against instead of always rendering everything as newly added. --- .github/workflows/erd-schema.yml | 9 +++++---- .../sdk/src/cli/commands/setup/workflow-lint.test.ts | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/erd-schema.yml b/.github/workflows/erd-schema.yml index b0a21a188..07086143d 100644 --- a/.github/workflows/erd-schema.yml +++ b/.github/workflows/erd-schema.yml @@ -1,8 +1,9 @@ name: ERD Schema +# pinact:skip-file on: push: - branches: [main] + branches: [main, v2] paths: - packages/sdk/src/cli/commands/tailordb/erd/** - example/** @@ -41,7 +42,7 @@ jobs: - uses: ./.github/actions/install-deps - - uses: tailor-platform/actions/erd-schema-export@a3c3032e14277e81039eb26ae1a425adb22eb532 # v1.7.0 + - uses: tailor-platform/actions/erd-schema-export@0bd69a0fc9aab3f818b636aa111995441a2c1a03 # v2 branch (unreleased) with: namespace: ${{ matrix.namespace }} package-manager: pnpm @@ -79,7 +80,7 @@ jobs: - name: Install deps uses: ./.github/actions/install-deps - - uses: tailor-platform/actions/erd-schema-preview@a3c3032e14277e81039eb26ae1a425adb22eb532 # v1.7.0 + - uses: tailor-platform/actions/erd-schema-preview@0bd69a0fc9aab3f818b636aa111995441a2c1a03 # v2 branch (unreleased) with: namespace: ${{ matrix.namespace }} package-manager: pnpm @@ -118,7 +119,7 @@ jobs: with: persist-credentials: false - - uses: tailor-platform/actions/erd-schema-comment@a3c3032e14277e81039eb26ae1a425adb22eb532 # v1.7.0 + - uses: tailor-platform/actions/erd-schema-comment@0bd69a0fc9aab3f818b636aa111995441a2c1a03 # v2 branch (unreleased) with: base-ref: ${{ github.event.pull_request.base.ref }} github-token: ${{ github.token }} diff --git a/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts b/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts index e129190c5..30418c201 100644 --- a/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts +++ b/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts @@ -104,7 +104,7 @@ describe("repository ERD schema workflow", () => { test("exports on push to main, uploading a per-namespace artifact for the preview job to reuse", () => { const content = fs.readFileSync(ERD_SCHEMA_WORKFLOW, "utf-8"); - expect(content).toContain("branches: [main]"); + expect(content).toContain("branches: [main, v2]"); expect(content).toContain("artifact-name: erd-schema-${{ matrix.namespace }}"); expect(content).toContain('retention-days: "90"'); }); From c74e5d98428eb0b7adfe81e88679d26caa1debbd Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 14 Jul 2026 01:51:54 +0900 Subject: [PATCH 549/618] fix(tailordb): detect type-level hook/validate changes in migration diffs compareSnapshots was not comparing typeHookExpr/typeValidateExpr between snapshots, so migration generate would silently miss type-level hook and validate additions/changes/removals for existing types. This meant snapshot reconstruction from migrations would lose those scripts. Add type_scripts_modified change kind, wire it through comparison, diff application, formatting, Zod schema, and drift detection. Regenerate example migration 0005 to include Customer typeHookExpr. --- example/migrations/0005/diff.json | 15 +- .../tailordb/migrate/diff-calculator.ts | 19 ++- .../tailordb/migrate/snapshot-schema.ts | 21 +++ .../tailordb/migrate/snapshot.test.ts | 129 ++++++++++++++++++ .../cli/commands/tailordb/migrate/snapshot.ts | 51 +++++++ .../tailordb/hooks-validate-bundler.ts | 6 +- .../parser/service/tailordb/type-script.ts | 1 - 7 files changed, 235 insertions(+), 7 deletions(-) diff --git a/example/migrations/0005/diff.json b/example/migrations/0005/diff.json index a8c12df68..1bd383023 100644 --- a/example/migrations/0005/diff.json +++ b/example/migrations/0005/diff.json @@ -1,7 +1,7 @@ { "version": 1, "namespace": "tailordb", - "createdAt": "2026-07-13T13:43:18.512Z", + "createdAt": "2026-07-13T16:35:52.223Z", "changes": [ { "kind": "type_added", @@ -176,6 +176,19 @@ } } }, + { + "kind": "type_scripts_modified", + "typeName": "Customer", + "reason": "type-level scripts changed", + "before": {}, + "after": { + "typeHookExpr": { + "create": "(({ input })=>({\n fullAddress: `${input.postalCode} ${input.address} ${input.city}`\n }))({ input: _input, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })", + "update": "(({ input, oldRecord })=>({\n fullAddress: `${input.postalCode ?? oldRecord.postalCode} ${input.address ?? oldRecord.address} ${input.city ?? oldRecord.city}`\n }))({ input: _input, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "typeValidateExpr": "(({ newRecord }, issues)=>{\n if (newRecord.country === \"JP\" && !newRecord.postalCode) {\n issues(\"postalCode\", \"Postal code is required for Japan\");\n }\n})({ newRecord: _newRecord, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) }, __issues)" + } + }, { "kind": "field_modified", "typeName": "Customer", diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/diff-calculator.ts b/packages/sdk/src/cli/commands/tailordb/migrate/diff-calculator.ts index 47f7ab40a..45de3199e 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/diff-calculator.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/diff-calculator.ts @@ -197,6 +197,19 @@ export interface PermissionModifiedChange extends DiffChangeBase { after?: SnapshotPermissionState; } +/** Type-level hook/validate script state for diff tracking. */ +export interface TypeScriptsState { + typeHookExpr?: { create?: string; update?: string }; + typeValidateExpr?: string; +} + +/** Type-level hook/validate scripts changed. */ +export interface TypeScriptsModifiedChange extends DiffChangeBase { + kind: "type_scripts_modified"; + before: TypeScriptsState; + after: TypeScriptsState; +} + /** * Single change in migration diff, discriminated by `kind` so that * `before`/`after` are typed per change kind. @@ -218,7 +231,8 @@ export type DiffChange = | RelationshipAddedChange | RelationshipRemovedChange | RelationshipModifiedChange - | PermissionModifiedChange; + | PermissionModifiedChange + | TypeScriptsModifiedChange; /** * Field-level diff change (added / removed / modified). @@ -358,6 +372,8 @@ function formatDiffChange(change: DiffChange): string { return ` ~ [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}: ${change.reason ?? "modified"}`; case "permission_modified": return ` ~ [Permission] ${change.reason ?? "modified"}`; + case "type_scripts_modified": + return ` ~ [Type Scripts] ${change.typeName}: ${change.reason ?? "type-level hooks/validate changed"}`; default: { // Runtime fallback: diff.json is parsed without validation, so // hand-edited or future-version files may carry unknown kinds. @@ -502,6 +518,7 @@ const DIFF_CHANGE_LABELS: Record = { relationship_removed: "relationship(s) removed", relationship_modified: "relationship(s) modified", permission_modified: "permission(s) modified", + type_scripts_modified: "type script(s) modified", }; /** diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-schema.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-schema.ts index 8e040c0c5..d39424b9e 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-schema.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-schema.ts @@ -31,6 +31,8 @@ import type { RelationshipRemovedChange, RelationshipModifiedChange, PermissionModifiedChange, + TypeScriptsModifiedChange, + TypeScriptsState, DiffChange, BreakingChangeInfo, WarningChangeInfo, @@ -447,6 +449,24 @@ const permissionModifiedChangeSchema = z.looseObject({ after: snapshotPermissionStateSchema.optional(), }) as unknown as z.ZodType; +const typeScriptsStateSchema: z.ZodType = z.looseObject({ + typeHookExpr: z + .looseObject({ + create: z.string().optional(), + update: z.string().optional(), + }) + .optional(), + typeValidateExpr: z.string().optional(), +}); + +const typeScriptsModifiedChangeSchema = z.looseObject({ + kind: z.literal("type_scripts_modified"), + typeName: z.string(), + reason: z.string().optional(), + before: typeScriptsStateSchema, + after: typeScriptsStateSchema, +}) as unknown as z.ZodType; + type DiscriminableSchema = Parameters[1][number]; export const diffChangeSchema: z.ZodType = z.discriminatedUnion("kind", [ @@ -467,6 +487,7 @@ export const diffChangeSchema: z.ZodType = z.discriminatedUnion("kin relationshipRemovedChangeSchema as unknown as DiscriminableSchema, relationshipModifiedChangeSchema as unknown as DiscriminableSchema, permissionModifiedChangeSchema as unknown as DiscriminableSchema, + typeScriptsModifiedChangeSchema as unknown as DiscriminableSchema, ]) as z.ZodType; export const breakingChangeInfoSchema: z.ZodType = z.looseObject({ diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts index 29dfbe06d..8e41306fa 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts @@ -966,6 +966,135 @@ describe("snapshot", () => { }), ]); }); + + test("detects typeHookExpr addition", () => { + const previous: SchemaSnapshot = { + ...createEmptySnapshot(), + types: { + User: { + name: "User", + pluralForm: "Users", + fields: { id: { type: "uuid", required: true } }, + }, + }, + }; + const current: SchemaSnapshot = { + ...createEmptySnapshot(), + types: { + User: { + name: "User", + pluralForm: "Users", + fields: { id: { type: "uuid", required: true } }, + typeHookExpr: { create: "({input}) => ({fullName: input.first + input.last})" }, + }, + }, + }; + + const diff = compareSnapshots(previous, current); + + expect(diff.changes).toEqual([ + expect.objectContaining({ + kind: "type_scripts_modified", + typeName: "User", + before: {}, + after: { + typeHookExpr: { + create: "({input}) => ({fullName: input.first + input.last})", + }, + }, + }), + ]); + }); + + test("detects typeHookExpr removal", () => { + const previous: SchemaSnapshot = { + ...createEmptySnapshot(), + types: { + User: { + name: "User", + pluralForm: "Users", + fields: { id: { type: "uuid", required: true } }, + typeHookExpr: { create: "old-expr" }, + }, + }, + }; + const current: SchemaSnapshot = { + ...createEmptySnapshot(), + types: { + User: { + name: "User", + pluralForm: "Users", + fields: { id: { type: "uuid", required: true } }, + }, + }, + }; + + const diff = compareSnapshots(previous, current); + + expect(diff.changes).toEqual([ + expect.objectContaining({ + kind: "type_scripts_modified", + typeName: "User", + before: { typeHookExpr: { create: "old-expr" } }, + after: {}, + }), + ]); + }); + + test("detects typeValidateExpr change", () => { + const previous: SchemaSnapshot = { + ...createEmptySnapshot(), + types: { + User: { + name: "User", + pluralForm: "Users", + fields: { id: { type: "uuid", required: true } }, + typeValidateExpr: "old-validate", + }, + }, + }; + const current: SchemaSnapshot = { + ...createEmptySnapshot(), + types: { + User: { + name: "User", + pluralForm: "Users", + fields: { id: { type: "uuid", required: true } }, + typeValidateExpr: "new-validate", + }, + }, + }; + + const diff = compareSnapshots(previous, current); + + expect(diff.changes).toEqual([ + expect.objectContaining({ + kind: "type_scripts_modified", + typeName: "User", + before: { typeValidateExpr: "old-validate" }, + after: { typeValidateExpr: "new-validate" }, + }), + ]); + }); + + test("no diff when typeHookExpr unchanged", () => { + const snapshot: SchemaSnapshot = { + ...createEmptySnapshot(), + types: { + User: { + name: "User", + pluralForm: "Users", + fields: { id: { type: "uuid", required: true } }, + typeHookExpr: { create: "same-expr", update: "same-update" }, + typeValidateExpr: "same-validate", + }, + }, + }; + + const diff = compareSnapshots(snapshot, snapshot); + + expect(diff.changes).toEqual([]); + }); }); // ========================================================================== diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts index 1462b14e1..50b97af57 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts @@ -34,6 +34,7 @@ import { type FieldDiffChange, type BreakingChangeInfo, type SnapshotTypeSettingsState, + type TypeScriptsState, type WarningChangeInfo, SCHEMA_SNAPSHOT_VERSION, } from "./diff-calculator"; @@ -670,6 +671,20 @@ function applyDiffToSnapshot( } break; } + case "type_scripts_modified": { + const existing = types[change.typeName]; + if (existing) { + const { typeHookExpr: _, typeValidateExpr: __, ...rest } = existing; + types[change.typeName] = { + ...rest, + ...(change.after.typeHookExpr && { typeHookExpr: change.after.typeHookExpr }), + ...(change.after.typeValidateExpr !== undefined && { + typeValidateExpr: change.after.typeValidateExpr, + }), + }; + } + break; + } case "field_added": case "field_modified": { const existing = types[change.typeName]; @@ -1576,6 +1591,33 @@ function compareTypeSettings( }); } +function typeScriptsState(type: TailorDBSnapshotType): TypeScriptsState { + return { + ...(type.typeHookExpr && { typeHookExpr: type.typeHookExpr }), + ...(type.typeValidateExpr !== undefined && { typeValidateExpr: type.typeValidateExpr }), + }; +} + +function compareTypeScripts( + ctx: DiffContext, + typeName: string, + previous: TailorDBSnapshotType, + current: TailorDBSnapshotType, +): void { + const prevState = typeScriptsState(previous); + const currState = typeScriptsState(current); + + if (JSON.stringify(prevState) === JSON.stringify(currState)) return; + + ctx.changes.push({ + kind: "type_scripts_modified", + typeName, + reason: "type-level scripts changed", + before: prevState, + after: currState, + }); +} + /** * Compare two snapshots and generate a diff * @param {SchemaSnapshot} previous - Previous schema snapshot @@ -1641,6 +1683,9 @@ function compareNormalizedSnapshots( // Compare type-level settings and metadata compareTypeSettings(ctx, typeName, prevType, currType); + // Compare type-level hook/validate scripts + compareTypeScripts(ctx, typeName, prevType, currType); + // Compare fields compareTypeFields(ctx, typeName, prevType, currType); @@ -2891,6 +2936,12 @@ function schemaDriftFromDiffChange(change: DiffChange): SchemaDrift { kind: "permission_mismatch", details: change.reason ?? "Permissions differ between remote and snapshot", }; + case "type_scripts_modified": + return { + typeName: change.typeName, + kind: "script_mismatch", + details: change.reason ?? "Type-level scripts differ between remote and snapshot", + }; default: { change satisfies never; throw new Error("Unsupported diff change"); diff --git a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts index 5f55fd822..1dc4bace4 100644 --- a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts +++ b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts @@ -560,10 +560,8 @@ export async function precompileTailorDBTypeScripts( } for (const [index, result] of results.entries()) { if (result.status === "fulfilled") { - setPrecompiledScriptExpr( - assertDefined(targets[index], `bundle target at index ${index} missing`).fn, - result.value, - ); + const target = assertDefined(targets[index], `bundle target at index ${index} missing`); + setPrecompiledScriptExpr(target.fn, result.value); } } } finally { diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index 10426a0eb..4b4952a48 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -284,7 +284,6 @@ export function buildTypeScripts( for (const operation of ["create", "update"] as const) { const perFieldExpr = buildHookObject(fields, INPUT, OLD_RECORD, operation); const typeLevelExpr = typeHookExpr?.[operation]; - let expr: string | undefined; if (perFieldExpr !== null && typeLevelExpr) { expr = `((_invoker) => { const ${NOW} = new Date(); const __fl = ${perFieldExpr}; return Object.assign({}, __fl, ((${INPUT}) => ${typeLevelExpr})(Object.assign({}, ${INPUT}, __fl))); })(typeof _invoker !== "undefined" ? _invoker : undefined)`; From 48f879028485657c7c36b74e07b5f9c42de16a7b Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 14 Jul 2026 14:13:20 +0900 Subject: [PATCH 550/618] fix(tailordb): handle trailing whitespace in source hash extraction and use newline joins in validate scripts --- .../sdk/src/parser/service/tailordb/type-script.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index 4b4952a48..302354a94 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -99,7 +99,7 @@ export function computeSourceScriptHash( } export function extractSourceScriptHash(expr: string): string | undefined { - const match = expr.match(/\/\/ @sdk-source-hash:([0-9a-f]+)$/); + const match = expr.match(/\/\/ @sdk-source-hash:([0-9a-f]+)\s*$/); return match?.[1]; } @@ -212,8 +212,8 @@ function buildValidateStatements( (v) => `{ const __r = (${v.script?.expr}); if (typeof __r === "string") { __errs[${key(fieldPath)}] = __r; } }`, ) - .join(" "); - statements.push(`{ const _value = ${access}; ${checks} }`); + .join("\n"); + statements.push(`{ const _value = ${access};\n${checks}\n}`); } if (isNestedType(config) && config.fields) { @@ -228,13 +228,13 @@ function buildValidateStatements( (v) => `{ const __r = (${v.script?.expr}); if (typeof __r === "string") { __errs[${errorKeyExpr}] = __r; } }`, ) - .join(" "); - innerParts.push(`{ const _value = __el[${key(innerName)}]; ${checks} }`); + .join("\n"); + innerParts.push(`{ const _value = __el[${key(innerName)}];\n${checks}\n}`); } } if (innerParts.length > 0) { statements.push( - `(${access} || []).forEach((__el, __idx) => { ${innerParts.join(" ")} });`, + `(${access} || []).forEach((__el, __idx) => {\n${innerParts.join("\n")}\n});`, ); } } else { @@ -253,7 +253,7 @@ function wrapHook(objectExpr: string): string { function wrapValidate(statements: string[], typeValidateExpr?: string): string { const issuesFn = typeValidateExpr ? " const __issues = (f, m) => { __errs[f] = m; };" : ""; const typeValidateStmt = typeValidateExpr ? ` ${typeValidateExpr};` : ""; - return `((_invoker) => { const __errs = {};${issuesFn} ${statements.join(" ")}${typeValidateStmt} return __errs; })(typeof _invoker !== "undefined" ? _invoker : undefined)`; + return `((_invoker) => { const __errs = {};${issuesFn}\n${statements.join("\n")}${typeValidateStmt}\nreturn __errs; })(typeof _invoker !== "undefined" ? _invoker : undefined)`; } /** From 6e396488c3e91a850e672d9064bdae7893c44b96 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 14 Jul 2026 15:36:21 +0900 Subject: [PATCH 551/618] fix(tailordb): address remaining review feedback - Guard nested non-array validators with null check for absent parent - Add totalPrice to omitFields in registerOrder template resolver - Detect and reject async validate/typeValidate callbacks at bundle time - Verify all remote script hashes match instead of returning first found - Execute type-level validate in createTailorDBHook test helper --- .../src/resolver/registerOrder.ts | 4 +++- .../cli/commands/tailordb/migrate/snapshot.ts | 8 +++++-- .../tailordb/hooks-validate-bundler.ts | 6 +++++ .../service/tailordb/type-script.test.ts | 24 +++++++++++++++++++ .../parser/service/tailordb/type-script.ts | 5 +++- packages/sdk/src/utils/test/index.test.ts | 17 +++++++++++++ packages/sdk/src/utils/test/index.ts | 12 ++++++++++ 7 files changed, 72 insertions(+), 4 deletions(-) diff --git a/packages/create-sdk/templates/inventory-management/src/resolver/registerOrder.ts b/packages/create-sdk/templates/inventory-management/src/resolver/registerOrder.ts index e3d9c5333..6565a79a0 100644 --- a/packages/create-sdk/templates/inventory-management/src/resolver/registerOrder.ts +++ b/packages/create-sdk/templates/inventory-management/src/resolver/registerOrder.ts @@ -5,7 +5,9 @@ import { type DB, getDB } from "../generated/kysely-tailordb"; const input = { order: t.object(order.omitFields(["id", "createdAt", "updatedAt"])), - items: t.object(orderItem.omitFields(["id", "createdAt", "updatedAt"]), { array: true }), + items: t.object(orderItem.omitFields(["id", "createdAt", "updatedAt", "totalPrice"]), { + array: true, + }), }; interface Input { order: t.infer; diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts index 50b97af57..6e8bacdee 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts @@ -2724,13 +2724,17 @@ function extractRemoteScriptHash(remoteType: ProtoTailorDBType): string | undefi remoteType.schema?.typeValidate?.create?.expr, remoteType.schema?.typeValidate?.update?.expr, ]; + let found: string | undefined; for (const expr of exprs) { if (expr) { const hash = extractSourceScriptHash(expr); - if (hash) return hash; + if (hash) { + if (found && found !== hash) return undefined; + found = hash; + } } } - return undefined; + return found; } function remoteHasScripts(remoteType: ProtoTailorDBType): boolean { diff --git a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts index 1dc4bace4..447ff2f3d 100644 --- a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts +++ b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts @@ -454,6 +454,12 @@ async function bundleScriptTarget(args: { const { fn, kind, sourceFilePath, sourceBindings, tempDir, targetIndex, tsconfig } = args; const context = `${kind} in ${sourceFilePath}`; const fnSource = stringifyFunction(fn); + if ((kind === "typeValidate" || kind === "validate") && fn.constructor.name === "AsyncFunction") { + throw new Error( + `${context} must be synchronous — the generated validator runs synchronously, ` + + "so issues reported after an await are silently lost. Remove the async keyword.", + ); + } const argsObject = kind === "hooks.create" ? `{ input: _value, invoker: ${tailorPrincipalMap}, now: _now }` diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index dbb44844c..5f579115e 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -242,6 +242,30 @@ describe("buildTypeScripts", () => { expect(buildTypeScripts(fields)).toEqual({}); }); + test("skips nested object child validators when parent is absent", () => { + const fields: Record = { + address: { + type: "nested", + fields: { + city: { + type: "string", + validate: [ + { script: { expr: '_value.length > 0 ? undefined : "required"' }, errorMessage: "" }, + ], + }, + }, + }, + }; + + const expr = buildTypeScripts(fields).typeValidate?.create?.expr ?? ""; + expect(expr).toContain('if (_newRecord["address"] != null)'); + + // eslint-disable-next-line @typescript-eslint/no-implied-eval + const run = (record: unknown) => new Function("_newRecord", `return ${expr}`)(record); + expect(run({ address: null })).toEqual({}); + expect(run({ address: { city: "" } })).toEqual({ "address.city": "required" }); + }); + test("no typeValidate output when no field validators and no type-level validate", () => { expect(buildTypeScripts({})).toEqual({}); expect(buildTypeScripts({}, undefined)).toEqual({}); diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index 302354a94..9a5ce61bd 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -238,7 +238,10 @@ function buildValidateStatements( ); } } else { - statements.push(...buildValidateStatements(config.fields, `(${access} || {})`, fieldPath)); + const nested = buildValidateStatements(config.fields, access, fieldPath); + if (nested.length > 0) { + statements.push(`if (${access} != null) {\n${nested.join("\n")}\n}`); + } } } } diff --git a/packages/sdk/src/utils/test/index.test.ts b/packages/sdk/src/utils/test/index.test.ts index 3b94d672c..7e35ae372 100644 --- a/packages/sdk/src/utils/test/index.test.ts +++ b/packages/sdk/src/utils/test/index.test.ts @@ -211,6 +211,23 @@ describe("createTailorDBHook", () => { expect(typeNow).toBe(fieldNow); }); + test("runs type-level validate and throws on validation failure", () => { + const type = db + .table("Range", { + start: db.int(), + end: db.int(), + }) + .validate(({ newRecord }, issues) => { + if ((newRecord.start as number) > (newRecord.end as number)) { + issues("start", "start must be <= end"); + } + }); + expect(() => createTailorDBHook(type)({ start: 10, end: 5 })).toThrow( + "Validation failed on field 'start': start must be <= end", + ); + expect(() => createTailorDBHook(type)({ start: 1, end: 10 })).not.toThrow(); + }); + test("does not invoke a hook that only defines update (createTailorDBHook is create-only)", () => { let updateCalled = false; const type = db.table("Test", { updatedAt: db.datetime() }).hooks({ diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index c5cf9bfc4..554e421ad 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -83,6 +83,18 @@ export function createTailorDBHook>(type: T) { } } + // oxlint-disable-next-line typescript/no-unnecessary-condition -- metadata absent in recursive nested calls + if (type.metadata?.typeValidate) { + const { id: _id, ...newRecord } = hooked; + // oxlint-disable-next-line typescript/no-unsafe-function-type + (type.metadata.typeValidate as Function)( + { newRecord, oldRecord: null, invoker: null }, + (field: string, message: string) => { + throw new Error(`Validation failed on field '${field}': ${message}`); + }, + ); + } + return hooked as Partial>; }; } From 5a15e7bd706336db731b2e54f10106bb7c93a317 Mon Sep 17 00:00:00 2001 From: "tailor-platform-pr-trigger[bot]" <247949890+tailor-platform-pr-trigger[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:01:18 +0000 Subject: [PATCH 552/618] Version Packages (next) --- .changeset/pre.json | 8 +++++--- packages/create-sdk/CHANGELOG.md | 2 ++ packages/create-sdk/package.json | 2 +- packages/sdk-codemod/CHANGELOG.md | 19 +++++++++++++++++++ packages/sdk-codemod/package.json | 2 +- packages/sdk/CHANGELOG.md | 27 +++++++++++++++++++++++++++ packages/sdk/package.json | 2 +- 7 files changed, 56 insertions(+), 6 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 3b4d4db04..24c5500d9 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -4,7 +4,7 @@ "changesets": [ "apply-deploy-source-strings", "auth-attributes-rename", - "clarify-setup-delete-warning", + "calm-types-rest", "cli-plugins", "codemod-llm-review", "codemod-migration-docs", @@ -13,17 +13,19 @@ "db-table-builder", "execute-script-json-arg", "execute-script-review-scope", - "fix-machine-user-override-env-source", + "fix-execution-policy-match-semantics-docs", "fix-mockfile-open-download-stream", "fix-rename-bin-source-files", "fix-ts-hook-dotted-basename", "fix-ts-hook-paths-without-baseurl", "fix-v2-prerelease-codemods", + "forward-relation-name", "invoker-option-rename", "keyring-default-storage", "kysely-date-timestamp", "open-download-review-scope", "parser-schema-strict", + "politty-skill-command-cleanup", "politty-skill-management", "principal-followup-review", "principal-unify-codemod", @@ -48,11 +50,11 @@ "runtime-globals-import", "runtime-idp-wrapper", "runtime-subpath-namespace", - "setup-coordinate-multi-config", "share-codemod-runtime-imports", "store-cli-users-by-subject", "tailor-output-ignore-dir", "tailor-principal-type", + "tailordb-shared-now-hook", "timestamps-updated-at-create", "user-profile-type-schema", "v2-baseline", diff --git a/packages/create-sdk/CHANGELOG.md b/packages/create-sdk/CHANGELOG.md index 6b41eab62..7e300b112 100644 --- a/packages/create-sdk/CHANGELOG.md +++ b/packages/create-sdk/CHANGELOG.md @@ -1,5 +1,7 @@ # @tailor-platform/create-sdk +## 2.0.0-next.5 + ## 2.0.0-next.4 ### Major Changes diff --git a/packages/create-sdk/package.json b/packages/create-sdk/package.json index c280759df..4f37bd6ee 100644 --- a/packages/create-sdk/package.json +++ b/packages/create-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/create-sdk", - "version": "2.0.0-next.4", + "version": "2.0.0-next.5", "description": "A CLI tool to quickly create a new Tailor Platform SDK project", "license": "MIT", "repository": { diff --git a/packages/sdk-codemod/CHANGELOG.md b/packages/sdk-codemod/CHANGELOG.md index cce15dbfc..3b85e2019 100644 --- a/packages/sdk-codemod/CHANGELOG.md +++ b/packages/sdk-codemod/CHANGELOG.md @@ -1,5 +1,24 @@ # @tailor-platform/sdk-codemod +## 0.3.0-next.5 + +### Patch Changes + +- [#1719](https://github.com/tailor-platform/sdk/pull/1719) [`4a05aec`](https://github.com/tailor-platform/sdk/commit/4a05aecfb100a1ea7292a6ae5809a2d1e6eddbfe) Thanks [@dqn](https://github.com/dqn)! - Derive default TailorDB forward relation names from the relation field name by removing a trailing `ID`, `Id`, or `id`, instead of deriving them from the target table name. + + The v2 migration review identifies non-self relations without `toward.as`. Add an explicit name to preserve the v1 GraphQL field name, or update consumers to use the new field-based name. + +- [#1678](https://github.com/tailor-platform/sdk/pull/1678) [`e0e768d`](https://github.com/tailor-platform/sdk/commit/e0e768d77470d13806ed7b2ee2117fe374d51d40) Thanks [@toiroakr](https://github.com/toiroakr)! - Redesign TailorDB hooks and validators with several breaking changes: + + - Add shared `now` timestamp to all hooks — multiple fields stamped with the same `Date` + - Field-level hooks: `{ value, data, invoker }` → create `{ input, invoker, now }` / update `{ input, oldValue, invoker, now }` (`data` removed, `oldValue` added for update only) + - Type-level hooks: per-field mapping (`Hooks`) → single `{ create, update }` object (`TypeHook`) returning partial field overrides + - Type-level create hooks no longer receive `oldRecord`; update hooks receive non-nullable `oldRecord` + - Field-level validators: return type changed from `boolean` to `string | void` (return error message or void to pass); `[fn, message]` tuple form removed + - Type-level validators: `Validators` per-field record → `TypeValidateFn` single function with `issues(field, message)` callback + - Add `.default(value)` on fields to set a create-time default (makes required fields optional in create input) + - Remove exported types: `Hooks`, `Validators`, `ValidateConfig` + ## 0.3.0-next.4 ### Patch Changes diff --git a/packages/sdk-codemod/package.json b/packages/sdk-codemod/package.json index 23b9dcbea..c5e08b1b7 100644 --- a/packages/sdk-codemod/package.json +++ b/packages/sdk-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk-codemod", - "version": "0.3.0-next.4", + "version": "0.3.0-next.5", "description": "Codemod runner for Tailor Platform SDK upgrades", "license": "MIT", "repository": { diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 3cf0e0a69..edfb6c402 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,5 +1,32 @@ # @tailor-platform/sdk +## 2.0.0-next.5 + +### Major Changes + +- [#1719](https://github.com/tailor-platform/sdk/pull/1719) [`4a05aec`](https://github.com/tailor-platform/sdk/commit/4a05aecfb100a1ea7292a6ae5809a2d1e6eddbfe) Thanks [@dqn](https://github.com/dqn)! - Derive default TailorDB forward relation names from the relation field name by removing a trailing `ID`, `Id`, or `id`, instead of deriving them from the target table name. + + The v2 migration review identifies non-self relations without `toward.as`. Add an explicit name to preserve the v1 GraphQL field name, or update consumers to use the new field-based name. + +- [#1678](https://github.com/tailor-platform/sdk/pull/1678) [`e0e768d`](https://github.com/tailor-platform/sdk/commit/e0e768d77470d13806ed7b2ee2117fe374d51d40) Thanks [@toiroakr](https://github.com/toiroakr)! - Redesign TailorDB hooks and validators with several breaking changes: + + - Add shared `now` timestamp to all hooks — multiple fields stamped with the same `Date` + - Field-level hooks: `{ value, data, invoker }` → create `{ input, invoker, now }` / update `{ input, oldValue, invoker, now }` (`data` removed, `oldValue` added for update only) + - Type-level hooks: per-field mapping (`Hooks`) → single `{ create, update }` object (`TypeHook`) returning partial field overrides + - Type-level create hooks no longer receive `oldRecord`; update hooks receive non-nullable `oldRecord` + - Field-level validators: return type changed from `boolean` to `string | void` (return error message or void to pass); `[fn, message]` tuple form removed + - Type-level validators: `Validators` per-field record → `TypeValidateFn` single function with `issues(field, message)` callback + - Add `.default(value)` on fields to set a create-time default (makes required fields optional in create input) + - Remove exported types: `Hooks`, `Validators`, `ValidateConfig` + +### Patch Changes + +- [#1763](https://github.com/tailor-platform/sdk/pull/1763) [`2abbe40`](https://github.com/tailor-platform/sdk/commit/2abbe409ed77eb5e1c50d4aa0b65fbf26843fdb1) Thanks [@dqn](https://github.com/dqn)! - Reduce duplicated schema-derived type declarations. + +- [#1764](https://github.com/tailor-platform/sdk/pull/1764) [`7895ed6`](https://github.com/tailor-platform/sdk/commit/7895ed621dc87ec0307ddb511307a0daac637556) Thanks [@k1LoW](https://github.com/k1LoW)! - Correct the execution policy matching semantics in the workflow docs: overlapping policies now stack (every matching cap is enforced independently and the tightest blocks), not longest-prefix-wins. + +- [#1749](https://github.com/tailor-platform/sdk/pull/1749) [`f0e38ac`](https://github.com/tailor-platform/sdk/commit/f0e38ac5765e1d52ff8431262b387a681d99c82a) Thanks [@toiroakr](https://github.com/toiroakr)! - Upgrade politty to 0.11.2 and simplify the `skills` command wiring to use its new `globalArgs`/`commandMap`/`unknownKeys` customization options instead of hand-rolled schema rewriting. + ## 2.0.0-next.4 ### Major Changes diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 189c6aebb..91d3217a1 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk", - "version": "2.0.0-next.4", + "version": "2.0.0-next.5", "description": "Tailor Platform SDK - The SDK to work with Tailor Platform", "license": "MIT", "repository": { From 206d1e9afba0c66dfa41009031f466de22732379 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 08:09:56 +0900 Subject: [PATCH 553/618] feat(sdk): expose loadTailorDBNamespaces and deployStaticWebsite from @tailor-platform/sdk/cli Generalize the ERD command's local namespace loading into a public loadTailorDBNamespaces API and re-export deployStaticWebsite, assertWritable, isPluginGeneratedType, and the parser types CLI plugins need, so external CLI plugins can load TailorDB schema data and deploy static sites without reaching into SDK internals. The ERD command now consumes the public API. --- .../cli/commands/tailordb/erd/local-schema.ts | 52 ++---------- packages/sdk/src/cli/lib.ts | 22 ++++- .../sdk/src/cli/shared/tailordb-namespaces.ts | 83 +++++++++++++++++++ 3 files changed, 111 insertions(+), 46 deletions(-) create mode 100644 packages/sdk/src/cli/shared/tailordb-namespaces.ts diff --git a/packages/sdk/src/cli/commands/tailordb/erd/local-schema.ts b/packages/sdk/src/cli/commands/tailordb/erd/local-schema.ts index 6f2555172..dabcfc543 100644 --- a/packages/sdk/src/cli/commands/tailordb/erd/local-schema.ts +++ b/packages/sdk/src/cli/commands/tailordb/erd/local-schema.ts @@ -1,7 +1,4 @@ -import { defineApplication } from "#/cli/services/application"; -import { loadConfig } from "#/cli/shared/config-loader"; -import { generateUserTypes } from "#/cli/shared/type-generator"; -import { PluginManager } from "#/plugin/manager"; +import { loadTailorDBNamespaces } from "#/cli/shared/tailordb-namespaces"; import type { LoadedConfig } from "#/cli/shared/config-loader"; import type { TailorDBNamespaceData } from "#/plugin/types"; @@ -53,45 +50,12 @@ export function resolveLocalErdSchemaNamespaces( export async function loadLocalErdSchema( options: LoadLocalErdSchemaOptions, ): Promise { - const { config, plugins } = await loadConfig(options.configPath); - - await generateUserTypes({ config, configPath: config.path }); - - const pluginManager = plugins.length > 0 ? new PluginManager(plugins) : undefined; - const application = defineApplication({ - config, - pluginManager, - }); - const namespaceNames = resolveLocalErdSchemaNamespaces(config, { - namespaces: options.namespaces, - requireErdSite: options.requireErdSite, + return await loadTailorDBNamespaces({ + configPath: options.configPath, + namespaces: (config) => + resolveLocalErdSchemaNamespaces(config, { + namespaces: options.namespaces, + requireErdSite: options.requireErdSite, + }), }); - const namespaceFilter = namespaceNames ? new Set(namespaceNames) : undefined; - const services = namespaceFilter - ? application.tailorDBServices.filter((db) => namespaceFilter.has(db.namespace)) - : application.tailorDBServices; - - if (namespaceFilter && services.length !== namespaceFilter.size) { - const available = application.tailorDBServices.map((db) => db.namespace).join(", "); - const requested = [...namespaceFilter].join(", "); - throw new Error( - `TailorDB namespace "${requested}" not found in local config.db.` + - (available ? ` Available owned namespaces: ${available}` : ""), - ); - } - - const namespaces: TailorDBNamespaceData[] = []; - - for (const db of services) { - await db.loadTypes(); - await db.processNamespacePlugins(); - namespaces.push({ - namespace: db.namespace, - types: { ...db.types }, - sourceInfo: new Map(Object.entries(db.typeSourceInfo)), - pluginAttachments: db.pluginAttachments, - }); - } - - return { config, namespaces }; } diff --git a/packages/sdk/src/cli/lib.ts b/packages/sdk/src/cli/lib.ts index b12a9648d..b2112a7f0 100644 --- a/packages/sdk/src/cli/lib.ts +++ b/packages/sdk/src/cli/lib.ts @@ -10,8 +10,26 @@ export { generate } from "./commands/generate/service"; export type { GenerateOptions } from "./commands/generate/options"; export { loadConfig, type LoadedConfig } from "./shared/config-loader"; export { generateUserTypes } from "./shared/type-generator"; -export type { GeneratorResult, PluginAttachment } from "#/plugin/types"; -export type { TailorDBType, TypeSourceInfoEntry } from "#/parser/service/tailordb/types"; +export { + loadTailorDBNamespaces, + type TailorDBNamespaceSelector, + type LoadTailorDBNamespacesOptions, + type LoadedTailorDBNamespaces, +} from "./shared/tailordb-namespaces"; +export { + deployStaticWebsite, + type DeployResult as StaticWebsiteDeployResult, +} from "./commands/staticwebsite/deploy"; +export { assertWritable } from "./shared/readonly-guard"; +export { isPluginGeneratedType } from "#/parser/service/tailordb/type-source"; +export type { GeneratorResult, PluginAttachment, TailorDBNamespaceData } from "#/plugin/types"; +export type { + TailorDBType, + TypeSourceInfoEntry, + ParsedField, + OperatorFieldConfig, + PluginGeneratedTypeSource, +} from "#/parser/service/tailordb/types"; export type { Resolver } from "#/types/resolver.generated"; export type { Executor } from "#/types/executor.generated"; diff --git a/packages/sdk/src/cli/shared/tailordb-namespaces.ts b/packages/sdk/src/cli/shared/tailordb-namespaces.ts new file mode 100644 index 000000000..34ea4845c --- /dev/null +++ b/packages/sdk/src/cli/shared/tailordb-namespaces.ts @@ -0,0 +1,83 @@ +import { defineApplication } from "#/cli/services/application"; +import { PluginManager } from "#/plugin/manager"; +import { loadConfig, type LoadedConfig } from "./config-loader"; +import { generateUserTypes } from "./type-generator"; +import type { TailorDBNamespaceData } from "#/plugin/types"; + +/** + * Namespace selection for {@link loadTailorDBNamespaces}: explicit namespace + * names, or a selector deriving them from the loaded config. Returning + * `undefined` (or omitting the option) loads all owned namespaces. + */ +export type TailorDBNamespaceSelector = string[] | ((config: LoadedConfig) => string[] | undefined); + +/** + * Options for {@link loadTailorDBNamespaces}. + */ +export interface LoadTailorDBNamespacesOptions { + /** Path to tailor.config.ts. Defaults to searching from the current directory. */ + configPath?: string; + /** Namespaces to load. Omit to load all owned namespaces. */ + namespaces?: TailorDBNamespaceSelector; +} + +/** + * Result of {@link loadTailorDBNamespaces}. + */ +export interface LoadedTailorDBNamespaces { + /** The loaded Tailor config. */ + config: LoadedConfig; + /** Loaded TailorDB namespace data, in config order. */ + namespaces: TailorDBNamespaceData[]; +} + +/** + * Load local TailorDB namespaces exactly as SDK generation/deploy sees them: + * the config is loaded, user types are generated, and each selected + * namespace's types are loaded with namespace plugins applied. + * @param options - Namespace loading options. + * @returns The loaded config and TailorDB namespace data. + */ +export async function loadTailorDBNamespaces( + options: LoadTailorDBNamespacesOptions = {}, +): Promise { + const { config, plugins } = await loadConfig(options.configPath); + + await generateUserTypes({ config, configPath: config.path }); + + const pluginManager = plugins.length > 0 ? new PluginManager(plugins) : undefined; + const application = defineApplication({ + config, + pluginManager, + }); + const namespaceNames = + typeof options.namespaces === "function" ? options.namespaces(config) : options.namespaces; + const namespaceFilter = namespaceNames ? new Set(namespaceNames) : undefined; + const services = namespaceFilter + ? application.tailorDBServices.filter((db) => namespaceFilter.has(db.namespace)) + : application.tailorDBServices; + + if (namespaceFilter && services.length !== namespaceFilter.size) { + const available = application.tailorDBServices.map((db) => db.namespace).join(", "); + const requested = [...namespaceFilter].join(", "); + throw new Error( + `TailorDB namespace "${requested}" not found in local config.db.` + + (available ? ` Available owned namespaces: ${available}` : ""), + ); + } + + const namespaces: TailorDBNamespaceData[] = []; + + for (const db of services) { + await db.loadTypes(); + await db.processNamespacePlugins(); + namespaces.push({ + namespace: db.namespace, + types: { ...db.types }, + sourceInfo: new Map(Object.entries(db.typeSourceInfo)), + pluginAttachments: db.pluginAttachments, + }); + } + + return { config, namespaces }; +} From 5b3cf5e72654d6925165540662bc9d5a634cc9b9 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 08:37:35 +0900 Subject: [PATCH 554/618] feat(sdk-tailordb-erd-plugin): add tailor-tailordb-erd CLI plugin package Port the tailordb erd commands (export, diff, serve, deploy) into a new @tailor-platform/sdk-tailordb-erd-plugin package that ships the tailor-tailordb-erd executable dispatched by the Tailor CLI's plugin mechanism. The code is moved from packages/sdk with imports rewritten to the @tailor-platform/sdk/cli public API; @tailor-platform/sdk is a peer dependency resolved from the host project. The serve command's fresh rebuild now re-invokes the plugin binary itself instead of the host CLI. --- .../sdk-tailordb-erd-plugin/.oxlintrc.json | 64 + packages/sdk-tailordb-erd-plugin/README.md | 45 + packages/sdk-tailordb-erd-plugin/package.json | 52 + .../sdk-tailordb-erd-plugin/src/deploy.ts | 86 + .../src/diff-command.test.ts | 107 ++ .../src/diff-command.ts | 143 ++ .../sdk-tailordb-erd-plugin/src/diff.test.ts | 362 +++++ packages/sdk-tailordb-erd-plugin/src/diff.ts | 427 +++++ .../sdk-tailordb-erd-plugin/src/export.ts | 217 +++ packages/sdk-tailordb-erd-plugin/src/index.ts | 34 + .../src/local-schema.test.ts | 31 + .../src/local-schema.ts | 61 + .../src/schema.test.ts | 311 ++++ .../sdk-tailordb-erd-plugin/src/schema.ts | 292 ++++ .../sdk-tailordb-erd-plugin/src/serve.test.ts | 48 + packages/sdk-tailordb-erd-plugin/src/serve.ts | 492 ++++++ .../src/shared/args.ts | 116 ++ .../src/shared/command.ts | 9 + .../src/shared/logger.ts | 83 + packages/sdk-tailordb-erd-plugin/src/types.ts | 104 ++ packages/sdk-tailordb-erd-plugin/src/utils.ts | 44 + .../src/viewer-assets/app.js | 1424 +++++++++++++++++ .../src/viewer-assets/index.html | 77 + .../src/viewer-assets/serve.json | 13 + .../src/viewer-assets/styles.css | 1036 ++++++++++++ .../src/viewer.test.ts | 107 ++ .../sdk-tailordb-erd-plugin/src/viewer.ts | 115 ++ .../sdk-tailordb-erd-plugin/tsconfig.json | 11 + .../sdk-tailordb-erd-plugin/tsdown.config.ts | 22 + .../sdk-tailordb-erd-plugin/vitest.config.ts | 11 + pnpm-lock.yaml | 52 + 31 files changed, 5996 insertions(+) create mode 100644 packages/sdk-tailordb-erd-plugin/.oxlintrc.json create mode 100644 packages/sdk-tailordb-erd-plugin/README.md create mode 100644 packages/sdk-tailordb-erd-plugin/package.json create mode 100644 packages/sdk-tailordb-erd-plugin/src/deploy.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/diff-command.test.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/diff-command.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/diff.test.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/diff.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/export.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/index.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/local-schema.test.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/local-schema.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/schema.test.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/schema.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/serve.test.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/serve.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/shared/args.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/shared/command.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/shared/logger.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/types.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/utils.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/viewer-assets/app.js create mode 100644 packages/sdk-tailordb-erd-plugin/src/viewer-assets/index.html create mode 100644 packages/sdk-tailordb-erd-plugin/src/viewer-assets/serve.json create mode 100644 packages/sdk-tailordb-erd-plugin/src/viewer-assets/styles.css create mode 100644 packages/sdk-tailordb-erd-plugin/src/viewer.test.ts create mode 100644 packages/sdk-tailordb-erd-plugin/src/viewer.ts create mode 100644 packages/sdk-tailordb-erd-plugin/tsconfig.json create mode 100644 packages/sdk-tailordb-erd-plugin/tsdown.config.ts create mode 100644 packages/sdk-tailordb-erd-plugin/vitest.config.ts diff --git a/packages/sdk-tailordb-erd-plugin/.oxlintrc.json b/packages/sdk-tailordb-erd-plugin/.oxlintrc.json new file mode 100644 index 000000000..fa4b78afe --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/.oxlintrc.json @@ -0,0 +1,64 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "unicorn"], + "categories": { + "correctness": "error" + }, + "env": { + "builtin": true + }, + "ignorePatterns": ["dist/", "src/viewer-assets/"], + "rules": { + "unicorn/no-array-reverse": "error", + "unicorn/no-array-sort": "error", + "getter-return": "error", + "no-unreachable": "error", + "no-array-constructor": "error", + "no-case-declarations": "error", + "no-fallthrough": "error", + "no-prototype-builtins": "error", + "no-redeclare": "error", + "no-empty": "error", + "no-regex-spaces": "error", + "no-unexpected-multiline": "error", + "preserve-caught-error": "error", + "typescript/ban-ts-comment": "error", + "typescript/no-empty-object-type": "error", + "typescript/no-explicit-any": "error", + "typescript/no-namespace": "error", + "typescript/no-require-imports": "error", + "typescript/no-unnecessary-type-constraint": "error", + "typescript/no-unsafe-function-type": "error", + "tailor-zod/require-object-policy-comment": "error", + "zod/prefer-loose-object": "error", + "zod/prefer-strict-object": "error" + }, + "overrides": [ + { + "files": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"], + "rules": { + "constructor-super": "off", + "getter-return": "off", + "no-class-assign": "off", + "no-const-assign": "off", + "no-dupe-class-members": "off", + "no-dupe-keys": "off", + "no-func-assign": "off", + "no-import-assign": "off", + "no-new-native-nonconstructor": "off", + "no-obj-calls": "off", + "no-redeclare": "off", + "no-setter-return": "off", + "no-this-before-super": "off", + "no-unreachable": "off", + "no-unsafe-negation": "off", + "no-var": "error", + "no-with": "off", + "prefer-const": "error", + "prefer-rest-params": "error", + "prefer-spread": "error" + } + } + ], + "jsPlugins": ["eslint-plugin-zod", "../../scripts/oxlint/tailor-zod-plugin.cjs"] +} diff --git a/packages/sdk-tailordb-erd-plugin/README.md b/packages/sdk-tailordb-erd-plugin/README.md new file mode 100644 index 000000000..b74d7f1d3 --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/README.md @@ -0,0 +1,45 @@ +# @tailor-platform/sdk-tailordb-erd-plugin + +Tailor CLI plugin that provides the `tailor tailordb erd` commands: export, diff, serve, and deploy a TailorDB ERD viewer built from your local TailorDB schema. + +> [!NOTE] +> This package is a **CLI plugin**: it ships an external `tailor-tailordb-erd` executable that the Tailor CLI dispatches to when you run `tailor tailordb erd`. It is not a config plugin — do not pass it to `definePlugins()` in `tailor.config.ts`. + +## Installation + +Install it next to `@tailor-platform/sdk` in your project: + +```bash +npm install -D @tailor-platform/sdk-tailordb-erd-plugin +``` + +The Tailor CLI discovers the plugin automatically from `node_modules/.bin` (or your `PATH`). Run `tailor plugin list` to confirm it resolves. + +## Usage + +```bash +# Build the ERD viewer for a namespace into .tailor/erd//dist +tailor tailordb erd export --namespace my-db + +# Serve the ERD viewer locally with watch reload +tailor tailordb erd serve --namespace my-db --open + +# Render an HTML diff between two exported ERD viewers +tailor tailordb erd diff --base-html base.html --head-html head.html -o diff.html + +# Deploy the ERD viewer to the static website configured as `erdSite` +tailor tailordb erd deploy --namespace my-db +``` + +`deploy` publishes to the static website referenced by `erdSite` in your `tailor.config.ts`: + +```ts +db: { + "my-db": { + files: ["tailordb/*.ts"], + erdSite: "my-erd-site", + }, +}, +``` + +Run `tailor tailordb erd --help` for the full option reference. diff --git a/packages/sdk-tailordb-erd-plugin/package.json b/packages/sdk-tailordb-erd-plugin/package.json new file mode 100644 index 000000000..f9bcb9628 --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/package.json @@ -0,0 +1,52 @@ +{ + "name": "@tailor-platform/sdk-tailordb-erd-plugin", + "version": "0.0.0", + "description": "Tailor CLI plugin providing the `tailor tailordb erd` commands (TailorDB ERD viewer)", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/tailor-platform/sdk.git", + "directory": "packages/sdk-tailordb-erd-plugin" + }, + "bin": { + "tailor-tailordb-erd": "./dist/index.js" + }, + "files": [ + "CHANGELOG.md", + "dist", + "README.md" + ], + "type": "module", + "scripts": { + "build": "tsdown", + "lint": "oxlint .", + "lint:fix": "oxlint . --fix", + "typecheck": "tsc --noEmit", + "test": "vitest", + "prepublish": "pnpm run build", + "publint": "publint --strict" + }, + "dependencies": { + "chalk": "5.6.2", + "chokidar": "5.0.0", + "mime-types": "3.0.2", + "open": "11.0.0", + "pathe": "2.0.3", + "pkg-types": "2.3.1", + "politty": "0.11.2", + "zod": "4.4.3" + }, + "devDependencies": { + "@tailor-platform/sdk": "workspace:^", + "@types/mime-types": "3.0.1", + "@types/node": "24.13.3", + "eslint-plugin-zod": "4.7.0", + "oxlint": "1.73.0", + "tsdown": "0.22.4", + "typescript": "6.0.3", + "vitest": "4.1.10" + }, + "peerDependencies": { + "@tailor-platform/sdk": "workspace:^" + } +} diff --git a/packages/sdk-tailordb-erd-plugin/src/deploy.ts b/packages/sdk-tailordb-erd-plugin/src/deploy.ts new file mode 100644 index 000000000..085961a97 --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/deploy.ts @@ -0,0 +1,86 @@ +import { assertWritable, deployStaticWebsite } from "@tailor-platform/sdk/cli"; +import { arg } from "politty"; +import { z } from "zod"; +import { prepareErdBuilds } from "./export"; +import { deploymentArgs } from "./shared/args"; +import { defineAppCommand } from "./shared/command"; +import { logger } from "./shared/logger"; +import { initErdDeployContext } from "./utils"; + +function logSkippedFiles(skippedFiles: string[]): void { + if (skippedFiles.length === 0) { + return; + } + logger.warn( + "Deployment completed, but some files failed to upload. These files may have unsupported content types or other validation issues. Please review the list below:", + ); + for (const file of skippedFiles) { + logger.log(` - ${file}`); + } +} + +export const erdDeployCommand = defineAppCommand({ + name: "deploy", + description: "Deploy ERD static website for TailorDB namespace(s).", + args: z.strictObject({ + ...deploymentArgs, + namespace: arg(z.string().optional(), { + alias: "n", + description: + "TailorDB namespace name (optional - deploys all namespaces with erdSite if omitted)", + }), + }), + run: async (args) => { + await assertWritable({ profile: args.profile }); + const { client, workspaceId } = await initErdDeployContext(args); + const buildResults = await prepareErdBuilds({ + configPath: args.config, + namespace: args.namespace, + requireErdSite: true, + }); + + const deployResults = await Promise.all( + buildResults.map(async (result) => { + if (!result.erdSite) { + throw new Error( + `No erdSite configured for namespace "${result.namespace}". ` + + `Add erdSite: "" to db.${result.namespace} in tailor.config.ts.`, + ); + } + + if (!args.json) { + logger.info( + `Deploying ERD for namespace "${result.namespace}" to site "${result.erdSite}"...`, + ); + } + + const { url, skippedFiles } = await deployStaticWebsite( + client, + workspaceId, + result.erdSite, + result.distDir, + !args.json, + ); + + return { + namespace: result.namespace, + erdSite: result.erdSite, + url, + skippedFiles, + }; + }), + ); + logger.newline(); + + if (args.json) { + logger.out(deployResults); + } else { + for (const result of deployResults) { + logSkippedFiles(result.skippedFiles); + logger.newline(); + logger.success(`ERD site "${result.erdSite}" deployed successfully.`); + logger.out(result.url); + } + } + }, +}); diff --git a/packages/sdk-tailordb-erd-plugin/src/diff-command.test.ts b/packages/sdk-tailordb-erd-plugin/src/diff-command.test.ts new file mode 100644 index 000000000..73588360b --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/diff-command.test.ts @@ -0,0 +1,107 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "pathe"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { writeErdDiff } from "./diff-command"; +import type { TailorDbErdSchema } from "./types"; + +let tempDir: string; + +function schema(overrides: Partial = {}): TailorDbErdSchema { + return { + version: 1, + namespace: "tailordb", + generatedAt: "2026-01-01T00:00:00.000Z", + revision: "revision", + source: "local", + cleanRoom: { implementation: "tailor", notes: [] }, + tables: [], + relations: [], + ...overrides, + }; +} + +function htmlWithSchema(value: TailorDbErdSchema): string { + return ``; +} + +describe("writeErdDiff", () => { + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tailordb-erd-diff-")); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + test("writes diff HTML and JSON files", () => { + const baseHtml = path.join(tempDir, "base.html"); + const headHtml = path.join(tempDir, "head.html"); + const outputHtml = path.join(tempDir, "out", "diff.html"); + const outputJson = path.join(tempDir, "out", "diff.json"); + + fs.writeFileSync(baseHtml, htmlWithSchema(schema()), "utf8"); + fs.writeFileSync( + headHtml, + htmlWithSchema( + schema({ + revision: "head-revision", + tables: [ + { + name: "User", + pluralForm: "users", + columns: [], + indexes: [], + forwardRelationships: [], + backwardRelationships: [], + }, + ], + }), + ), + "utf8", + ); + + const result = writeErdDiff({ baseHtml, headHtml, outputHtml, outputJson }); + + const output = fs.readFileSync(outputHtml, "utf8"); + expect(output).toContain("TailorDB ERD diff - tailordb"); + expect(output).toContain('id="erd-schema"'); + expect(output).toContain('id="erd-current-schema"'); + expect(output).toContain('id="erd-diff"'); + expect(output).toContain("function renderNodes()"); + expect(JSON.parse(fs.readFileSync(outputJson, "utf8"))).toMatchObject({ + namespace: "tailordb", + summary: { added: 1, changed: 0, removed: 0 }, + }); + expect(result.diff.changed).toBe(true); + }); + + test("uses an empty base when only head HTML is supplied", () => { + const headHtml = path.join(tempDir, "head.html"); + const outputHtml = path.join(tempDir, "diff.html"); + + fs.writeFileSync( + headHtml, + htmlWithSchema( + schema({ + tables: [ + { + name: "User", + pluralForm: "users", + columns: [], + indexes: [], + forwardRelationships: [], + backwardRelationships: [], + }, + ], + }), + ), + "utf8", + ); + + const result = writeErdDiff({ headHtml, outputHtml }); + + expect(result.diff.baseRevision).toBe("missing-base"); + expect(result.diff.summary).toEqual({ added: 1, changed: 0, removed: 0 }); + }); +}); diff --git a/packages/sdk-tailordb-erd-plugin/src/diff-command.ts b/packages/sdk-tailordb-erd-plugin/src/diff-command.ts new file mode 100644 index 000000000..d04b5887e --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/diff-command.ts @@ -0,0 +1,143 @@ +import * as fs from "node:fs"; +import * as path from "pathe"; +import { arg } from "politty"; +import { z } from "zod"; +import { + buildErdDiffViewerSchema, + buildErdSchemaDiff, + createEmptyErdSchema, + extractEmbeddedErdSchema, + renderErdDiffHtml, + type ErdSchemaDiff, +} from "./diff"; +import { defineAppCommand } from "./shared/command"; +import { logger } from "./shared/logger"; +import { initErdCommand } from "./utils"; +import type { TailorDbErdSchema } from "./types"; + +export interface WriteErdDiffOptions { + baseHtml?: string; + headHtml?: string; + namespace?: string; + outputHtml: string; + outputJson?: string; +} + +export interface WriteErdDiffResult { + namespace: string; + outputHtml: string; + outputJson?: string; + diff: ErdSchemaDiff; +} + +interface ResolveNamespaceOptions { + namespace?: string; + base?: TailorDbErdSchema; + head?: TailorDbErdSchema; +} + +function readSchema(filePath: string | undefined): TailorDbErdSchema | undefined { + if (!filePath) return undefined; + return extractEmbeddedErdSchema(fs.readFileSync(filePath, "utf8")); +} + +function resolveNamespace(options: ResolveNamespaceOptions): string { + const namespace = options.namespace ?? options.head?.namespace ?? options.base?.namespace; + if (!namespace) { + throw new Error("Missing --namespace when one side of the ERD diff is omitted."); + } + if (options.base && options.base.namespace !== namespace) { + throw new Error( + `Base ERD namespace "${options.base.namespace}" does not match "${namespace}".`, + ); + } + if (options.head && options.head.namespace !== namespace) { + throw new Error( + `Head ERD namespace "${options.head.namespace}" does not match "${namespace}".`, + ); + } + return namespace; +} + +function writeFile(filePath: string, content: string): void { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content, "utf8"); +} + +export function writeErdDiff(options: WriteErdDiffOptions): WriteErdDiffResult { + const base = readSchema(options.baseHtml); + const head = readSchema(options.headHtml); + if (!base && !head) { + throw new Error("At least one of --base-html or --head-html is required."); + } + + const namespace = resolveNamespace({ namespace: options.namespace, base, head }); + const baseSchema = base ?? createEmptyErdSchema({ namespace, revision: "missing-base" }); + const headSchema = head ?? createEmptyErdSchema({ namespace, revision: "missing-head" }); + const diff = buildErdSchemaDiff({ base: baseSchema, head: headSchema }); + const viewerSchema = buildErdDiffViewerSchema({ base: baseSchema, head: headSchema }); + + writeFile( + options.outputHtml, + renderErdDiffHtml({ schema: viewerSchema, currentSchema: headSchema, diff }), + ); + if (options.outputJson) { + writeFile(options.outputJson, `${JSON.stringify(diff, null, 2)}\n`); + } + + return { + namespace, + outputHtml: options.outputHtml, + outputJson: options.outputJson, + diff, + }; +} + +export const erdDiffCommand = defineAppCommand({ + name: "diff", + description: "Render TailorDB ERD schema diff HTML from exported ERD viewers.", + args: z.strictObject({ + "base-html": arg(z.string().optional(), { + description: "Base ERD viewer HTML file", + completion: { type: "file", matcher: [".html"] }, + }), + "head-html": arg(z.string().optional(), { + description: "Head ERD viewer HTML file", + completion: { type: "file", matcher: [".html"] }, + }), + namespace: arg(z.string().optional(), { + alias: "n", + description: "TailorDB namespace name (defaults to the provided ERD schema namespace)", + }), + output: arg(z.string().min(1), { + alias: "o", + description: "Output ERD diff HTML file", + completion: { type: "file", matcher: [".html"] }, + }), + "output-json": arg(z.string().optional(), { + description: "Optional output JSON file for the computed diff", + completion: { type: "file", matcher: [".json"] }, + }), + }), + run: (args) => { + initErdCommand(); + const result = writeErdDiff({ + baseHtml: args["base-html"], + headHtml: args["head-html"], + namespace: args.namespace, + outputHtml: args.output, + outputJson: args["output-json"], + }); + + if (args.json) { + logger.out({ + namespace: result.namespace, + outputHtml: result.outputHtml, + outputJson: result.outputJson, + summary: result.diff.summary, + }); + } else { + logger.success(`Wrote ERD diff to ${path.relative(process.cwd(), result.outputHtml)}`); + } + }, +}); diff --git a/packages/sdk-tailordb-erd-plugin/src/diff.test.ts b/packages/sdk-tailordb-erd-plugin/src/diff.test.ts new file mode 100644 index 000000000..5e28e792a --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/diff.test.ts @@ -0,0 +1,362 @@ +import { describe, expect, test } from "vitest"; +import { + buildErdSchemaDiff, + buildErdDiffViewerSchema, + createEmptyErdSchema, + extractEmbeddedErdSchema, + renderErdDiffHtml, +} from "./diff"; +import { buildViewerHtml } from "./viewer"; +import type { TailorDbErdSchema, TailorDbErdTable } from "./types"; + +function table(name: string, overrides: Partial = {}): TailorDbErdTable { + return { + name, + pluralForm: `${name.toLowerCase()}s`, + columns: [ + { + name: "id", + type: "uuid", + required: true, + array: false, + primaryKey: true, + unique: true, + }, + ], + indexes: [], + forwardRelationships: [], + backwardRelationships: [], + ...overrides, + }; +} + +function schema(overrides: Partial = {}): TailorDbErdSchema { + return { + version: 1, + namespace: "tailordb", + generatedAt: "2026-01-01T00:00:00.000Z", + revision: "base-revision", + source: "local", + cleanRoom: { implementation: "tailor", notes: [] }, + tables: [table("User")], + relations: [], + ...overrides, + }; +} + +function extractJsonBlock(html: string, id: string): T { + const pattern = new RegExp(``); + const match = pattern.exec(html); + if (!match?.[1]) { + throw new Error(`JSON block not found: ${id}`); + } + return JSON.parse(match[1]) as T; +} + +describe("extractEmbeddedErdSchema", () => { + test("parses the viewer schema data block", () => { + const embedded = schema({ namespace: "" }); + const html = ``; + + expect(extractEmbeddedErdSchema(html)).toMatchObject({ + namespace: "", + revision: "base-revision", + }); + }); + + test("reports a missing schema data block", () => { + expect(() => extractEmbeddedErdSchema("")).toThrow("ERD schema block not found"); + }); + + test("preserves dollar replacement patterns in embedded schema values", () => { + const description = "Prices use $$, literal match $&, left context $`, right context $'."; + const html = buildViewerHtml({ + schema: schema({ + tables: [table("Invoice", { description })], + }), + }); + + expect(extractEmbeddedErdSchema(html).tables[0]?.description).toBe(description); + }); +}); + +describe("buildErdSchemaDiff", () => { + test("ignores generation timestamp-only changes", () => { + const diff = buildErdSchemaDiff({ + base: schema({ generatedAt: "2026-01-01T00:00:00.000Z" }), + head: schema({ generatedAt: "2026-01-02T00:00:00.000Z" }), + }); + + expect(diff.changed).toBe(false); + expect(diff.summary).toEqual({ added: 0, changed: 0, removed: 0 }); + expect(diff.changes).toEqual([]); + }); + + test("classifies table, column, index, and relation changes", () => { + const base = schema({ + revision: "base-revision", + tables: [ + table("Order"), + table("User", { + columns: [ + { + name: "id", + type: "uuid", + required: true, + array: false, + primaryKey: true, + unique: true, + }, + { + name: "name", + type: "string", + required: true, + array: false, + }, + ], + indexes: [{ name: "idx_user_name", fields: ["name"], unique: false }], + }), + ], + relations: [ + { + name: "User.organizationId->Organization.id", + sourceTable: "User", + sourceColumns: ["organizationId"], + targetTable: "Organization", + targetColumns: ["id"], + required: true, + unique: false, + kind: "foreignKey", + }, + ], + }); + const head = schema({ + revision: "head-revision", + tables: [ + table("Invoice"), + table("User", { + columns: [ + { + name: "id", + type: "uuid", + required: true, + array: false, + primaryKey: true, + unique: true, + }, + { + name: "name", + type: "text", + required: true, + array: false, + }, + { + name: "email", + type: "string", + required: false, + array: false, + }, + ], + indexes: [{ name: "idx_user_name", fields: ["name"], unique: true }], + }), + ], + relations: [ + { + name: "User.organizationId->Organization.id", + sourceTable: "User", + sourceColumns: ["organizationId"], + targetTable: "Organization", + targetColumns: ["id"], + required: false, + unique: false, + kind: "foreignKey", + }, + ], + }); + + const diff = buildErdSchemaDiff({ base, head }); + + expect(diff.changed).toBe(true); + expect(diff.summary).toEqual({ added: 2, changed: 3, removed: 1 }); + expect(diff.changes).toEqual([ + { + action: "removed", + entity: "table", + path: "Order", + detail: "Table removed", + }, + { + action: "added", + entity: "table", + path: "Invoice", + detail: "Table added", + }, + { + action: "changed", + entity: "column", + path: "User.name", + detail: "Changed fields: type", + }, + { + action: "added", + entity: "column", + path: "User.email", + detail: "Column added", + }, + { + action: "changed", + entity: "index", + path: "User.idx_user_name", + detail: "Changed fields: unique", + }, + { + action: "changed", + entity: "relation", + path: "User.organizationId->Organization.id", + detail: "Changed fields: required", + }, + ]); + }); + + test("represents a missing base namespace as added tables", () => { + const diff = buildErdSchemaDiff({ + base: createEmptyErdSchema({ namespace: "tailordb", revision: "missing-base" }), + head: schema({ tables: [table("Account"), table("User")] }), + }); + + expect(diff.summary).toEqual({ added: 2, changed: 0, removed: 0 }); + expect(diff.changes.map((change) => change.path)).toEqual(["Account", "User"]); + }); + + test("detects relationship metadata changes on tables", () => { + const base = schema({ + tables: [ + table("User", { + forwardRelationships: [ + { + name: "orders", + targetType: "Order", + targetField: "userId", + sourceField: "id", + isArray: true, + description: "Old label", + }, + ], + }), + ], + }); + const head = schema({ + tables: [ + table("User", { + forwardRelationships: [ + { + name: "orders", + targetType: "Order", + targetField: "userId", + sourceField: "id", + isArray: true, + description: "New label", + }, + ], + }), + ], + }); + + const diff = buildErdSchemaDiff({ base, head }); + + expect(diff.changed).toBe(true); + expect(diff.changes).toContainEqual({ + action: "changed", + entity: "table", + path: "User", + detail: "Changed fields: forwardRelationships", + }); + }); +}); + +describe("ERD diff rendering", () => { + test("keeps removed tables and columns visible for diff highlighting", () => { + const base = schema({ + tables: [ + table("Order"), + table("User", { + columns: [ + { + name: "id", + type: "uuid", + required: true, + array: false, + primaryKey: true, + unique: true, + }, + { + name: "legacyCode", + type: "string", + required: false, + array: false, + }, + ], + }), + ], + }); + const head = schema({ + tables: [ + table("Invoice"), + table("User", { + columns: [ + { + name: "id", + type: "uuid", + required: true, + array: false, + primaryKey: true, + unique: true, + }, + ], + }), + ], + }); + + const viewerSchema = buildErdDiffViewerSchema({ base, head }); + + expect(viewerSchema.tables.map((item) => item.name).toSorted()).toEqual([ + "Invoice", + "Order", + "User", + ]); + expect(viewerSchema.tables.find((item) => item.name === "User")?.columns).toContainEqual( + expect.objectContaining({ name: "legacyCode" }), + ); + }); + + test("renders the existing ERD viewer with embedded diff metadata", () => { + const head = schema({ + revision: "head-revision", + tables: [table("Account"), table("User")], + }); + const diff = buildErdSchemaDiff({ + base: schema(), + head, + }); + const viewerSchema = buildErdDiffViewerSchema({ + base: schema(), + head, + }); + const html = renderErdDiffHtml({ schema: viewerSchema, currentSchema: head, diff }); + expect(html).toContain("TailorDB ERD diff - tailordb"); + expect(html).toContain('id="erd-schema"'); + expect(html).toContain('id="erd-current-schema"'); + expect(html).toContain('([\s\S]*?)<\/script>/; + +export type ErdDiffAction = "added" | "changed" | "removed"; +export type ErdDiffEntity = "column" | "index" | "relation" | "table"; + +export interface ErdDiffChange { + action: ErdDiffAction; + entity: ErdDiffEntity; + path: string; + detail: string; +} + +export interface ErdDiffSummary { + added: number; + changed: number; + removed: number; +} + +export interface ErdSchemaDiff { + namespace: string; + baseRevision: string; + headRevision: string; + changed: boolean; + summary: ErdDiffSummary; + changes: ErdDiffChange[]; +} + +export interface BuildErdSchemaDiffOptions { + base: TailorDbErdSchema; + head: TailorDbErdSchema; +} + +export interface BuildErdDiffViewerSchemaOptions { + base: TailorDbErdSchema; + head: TailorDbErdSchema; +} + +export interface CreateEmptyErdSchemaOptions { + namespace: string; + revision: string; +} + +export interface RenderErdDiffHtmlOptions { + schema: TailorDbErdSchema; + currentSchema: TailorDbErdSchema; + diff: ErdSchemaDiff; +} + +function stableValue(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map((item) => stableValue(item)); + } + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value) + .toSorted(([a], [b]) => a.localeCompare(b)) + .map(([key, item]) => [key, stableValue(item)]), + ); + } + return value; +} + +function stableJson(value: unknown): string { + return JSON.stringify(stableValue(value)); +} + +function changedFields(base: object, head: object): string[] { + const keys = [...new Set([...Object.keys(base), ...Object.keys(head)])].toSorted((a, b) => + a.localeCompare(b), + ); + return keys.filter( + (key) => + stableJson((base as Record)[key]) !== + stableJson((head as Record)[key]), + ); +} + +function detailForChangedFields(fields: string[]): string { + return `Changed fields: ${fields.join(", ")}`; +} + +function mapByName(items: readonly T[]): Map { + return new Map(items.map((item) => [item.name, item])); +} + +function compareCommonNamed( + baseItems: readonly T[], + headItems: readonly T[], + addChange: (name: string, fields: string[]) => void, +): void { + const baseByName = mapByName(baseItems); + const headByName = mapByName(headItems); + const commonNames = [...baseByName.keys()] + .filter((name) => headByName.has(name)) + .toSorted((a, b) => a.localeCompare(b)); + + for (const name of commonNames) { + const base = baseByName.get(name); + const head = headByName.get(name); + if (!base || !head) continue; + + const fields = changedFields(base, head); + if (fields.length > 0) { + addChange(name, fields); + } + } +} + +function removedNames( + baseItems: readonly T[], + headItems: readonly T[], +): string[] { + const headByName = mapByName(headItems); + return baseItems + .map((item) => item.name) + .filter((name) => !headByName.has(name)) + .toSorted((a, b) => a.localeCompare(b)); +} + +function addedNames( + baseItems: readonly T[], + headItems: readonly T[], +): string[] { + const baseByName = mapByName(baseItems); + return headItems + .map((item) => item.name) + .filter((name) => !baseByName.has(name)) + .toSorted((a, b) => a.localeCompare(b)); +} + +function tableMetadata(table: TailorDbErdTable): object { + return { + backwardRelationships: table.backwardRelationships, + description: table.description, + forwardRelationships: table.forwardRelationships, + pluralForm: table.pluralForm, + source: table.source, + }; +} + +function pushChange(changes: ErdDiffChange[], change: ErdDiffChange): void { + changes.push(change); +} + +function diffColumns( + changes: ErdDiffChange[], + tableName: string, + baseColumns: readonly TailorDbErdColumn[], + headColumns: readonly TailorDbErdColumn[], +): void { + compareCommonNamed(baseColumns, headColumns, (name, fields) => { + pushChange(changes, { + action: "changed", + entity: "column", + path: `${tableName}.${name}`, + detail: detailForChangedFields(fields), + }); + }); + for (const name of removedNames(baseColumns, headColumns)) { + pushChange(changes, { + action: "removed", + entity: "column", + path: `${tableName}.${name}`, + detail: "Column removed", + }); + } + for (const name of addedNames(baseColumns, headColumns)) { + pushChange(changes, { + action: "added", + entity: "column", + path: `${tableName}.${name}`, + detail: "Column added", + }); + } +} + +function diffIndexes( + changes: ErdDiffChange[], + tableName: string, + baseIndexes: readonly TailorDbErdIndex[], + headIndexes: readonly TailorDbErdIndex[], +): void { + compareCommonNamed(baseIndexes, headIndexes, (name, fields) => { + pushChange(changes, { + action: "changed", + entity: "index", + path: `${tableName}.${name}`, + detail: detailForChangedFields(fields), + }); + }); + for (const name of removedNames(baseIndexes, headIndexes)) { + pushChange(changes, { + action: "removed", + entity: "index", + path: `${tableName}.${name}`, + detail: "Index removed", + }); + } + for (const name of addedNames(baseIndexes, headIndexes)) { + pushChange(changes, { + action: "added", + entity: "index", + path: `${tableName}.${name}`, + detail: "Index added", + }); + } +} + +function diffTables( + changes: ErdDiffChange[], + baseTables: readonly TailorDbErdTable[], + headTables: readonly TailorDbErdTable[], +): void { + const baseByName = mapByName(baseTables); + const headByName = mapByName(headTables); + + for (const name of removedNames(baseTables, headTables)) { + pushChange(changes, { + action: "removed", + entity: "table", + path: name, + detail: "Table removed", + }); + } + for (const name of addedNames(baseTables, headTables)) { + pushChange(changes, { + action: "added", + entity: "table", + path: name, + detail: "Table added", + }); + } + + const commonNames = [...baseByName.keys()] + .filter((name) => headByName.has(name)) + .toSorted((a, b) => a.localeCompare(b)); + for (const name of commonNames) { + const base = baseByName.get(name); + const head = headByName.get(name); + if (!base || !head) continue; + + const tableFields = changedFields(tableMetadata(base), tableMetadata(head)); + if (tableFields.length > 0) { + pushChange(changes, { + action: "changed", + entity: "table", + path: name, + detail: detailForChangedFields(tableFields), + }); + } + diffColumns(changes, name, base.columns, head.columns); + diffIndexes(changes, name, base.indexes, head.indexes); + } +} + +function diffRelations( + changes: ErdDiffChange[], + baseRelations: readonly TailorDbErdRelation[], + headRelations: readonly TailorDbErdRelation[], +): void { + compareCommonNamed(baseRelations, headRelations, (name, fields) => { + pushChange(changes, { + action: "changed", + entity: "relation", + path: name, + detail: detailForChangedFields(fields), + }); + }); + for (const name of removedNames(baseRelations, headRelations)) { + pushChange(changes, { + action: "removed", + entity: "relation", + path: name, + detail: "Relation removed", + }); + } + for (const name of addedNames(baseRelations, headRelations)) { + pushChange(changes, { + action: "added", + entity: "relation", + path: name, + detail: "Relation added", + }); + } +} + +function summarize(changes: readonly ErdDiffChange[]): ErdDiffSummary { + return { + added: changes.filter((change) => change.action === "added").length, + changed: changes.filter((change) => change.action === "changed").length, + removed: changes.filter((change) => change.action === "removed").length, + }; +} + +export function extractEmbeddedErdSchema(html: string): TailorDbErdSchema { + const match = SCHEMA_BLOCK_PATTERN.exec(html); + if (!match?.[1]) { + throw new Error("ERD schema block not found."); + } + + try { + return JSON.parse(match[1]) as TailorDbErdSchema; + } catch (error) { + throw new Error(`Failed to parse ERD schema block: ${String(error)}`, { cause: error }); + } +} + +export function createEmptyErdSchema(options: CreateEmptyErdSchemaOptions): TailorDbErdSchema { + return { + version: 1, + namespace: options.namespace, + generatedAt: new Date(0).toISOString(), + revision: options.revision, + source: "local", + cleanRoom: { + implementation: "tailor", + notes: ["Synthetic empty schema used for ERD diff generation."], + }, + tables: [], + relations: [], + }; +} + +export function buildErdSchemaDiff(options: BuildErdSchemaDiffOptions): ErdSchemaDiff { + const { base, head } = options; + if (base.namespace !== head.namespace) { + throw new Error( + `Cannot diff ERD schemas from different namespaces: ${base.namespace}, ${head.namespace}`, + ); + } + + const changes: ErdDiffChange[] = []; + diffTables(changes, base.tables, head.tables); + diffRelations(changes, base.relations, head.relations); + const summary = summarize(changes); + + return { + namespace: head.namespace, + baseRevision: base.revision, + headRevision: head.revision, + changed: changes.length > 0, + summary, + changes, + }; +} + +function mergeNamedByHead( + baseItems: readonly T[], + headItems: readonly T[], +): T[] { + const headByName = mapByName(headItems); + const removedItems = baseItems + .filter((item) => !headByName.has(item.name)) + .toSorted((a, b) => a.name.localeCompare(b.name)); + return [...headItems, ...removedItems]; +} + +function mergeDiffViewerTable( + baseTable: TailorDbErdTable, + headTable: TailorDbErdTable, +): TailorDbErdTable { + return { + ...headTable, + columns: mergeNamedByHead(baseTable.columns, headTable.columns), + indexes: mergeNamedByHead(baseTable.indexes, headTable.indexes), + }; +} + +function mergeDiffViewerTables( + baseTables: readonly TailorDbErdTable[], + headTables: readonly TailorDbErdTable[], +): TailorDbErdTable[] { + const baseByName = mapByName(baseTables); + const headByName = mapByName(headTables); + const merged = headTables.map((headTable) => { + const baseTable = baseByName.get(headTable.name); + return baseTable ? mergeDiffViewerTable(baseTable, headTable) : headTable; + }); + const removedTables = baseTables + .filter((table) => !headByName.has(table.name)) + .toSorted((a, b) => a.name.localeCompare(b.name)); + return [...merged, ...removedTables]; +} + +/** + * Build the schema rendered by the diff viewer. The ordinary ERD viewer only + * knows how to draw objects present in its schema, so the diff schema keeps + * base-only tables, columns, indexes, and relations visible for removal + * highlighting while preferring head-side metadata for unchanged objects. + * @param options - Base and head schemas to merge for diff rendering. + * @returns A TailorDB ERD schema suitable for the visual diff viewer. + */ +export function buildErdDiffViewerSchema( + options: BuildErdDiffViewerSchemaOptions, +): TailorDbErdSchema { + const { base, head } = options; + if (base.namespace !== head.namespace) { + throw new Error( + `Cannot diff ERD schemas from different namespaces: ${base.namespace}, ${head.namespace}`, + ); + } + + return { + ...head, + tables: mergeDiffViewerTables(base.tables, head.tables), + relations: mergeNamedByHead(base.relations, head.relations), + }; +} + +export function renderErdDiffHtml(options: RenderErdDiffHtmlOptions): string { + return buildViewerHtml({ + schema: options.schema, + currentSchema: options.currentSchema, + diff: options.diff, + title: `TailorDB ERD diff - ${options.diff.namespace}`, + }); +} diff --git a/packages/sdk-tailordb-erd-plugin/src/export.ts b/packages/sdk-tailordb-erd-plugin/src/export.ts new file mode 100644 index 000000000..675628f73 --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/export.ts @@ -0,0 +1,217 @@ +import * as path from "pathe"; +import { arg } from "politty"; +import { z } from "zod"; +import { loadLocalErdSchema, type LocalErdSchemaContext } from "./local-schema"; +import { buildTailorDbErdSchema } from "./schema"; +import { configArg } from "./shared/args"; +import { defineAppCommand } from "./shared/command"; +import { logger } from "./shared/logger"; +import { initErdCommand } from "./utils"; +import { writeViewerDist } from "./viewer"; +import type { TailorDBNamespaceData } from "@tailor-platform/sdk/cli"; + +const DEFAULT_ERD_BASE_DIR = ".tailor/erd"; + +interface ResolveTargetsOptions { + context: LocalErdSchemaContext; + namespace?: string; + outputDir: string; + requireErdSite?: boolean; +} + +interface ErdTarget { + namespaceData: TailorDBNamespaceData; + erdSite?: string; + distDir: string; +} + +interface ErdBuildsOptions { + configPath?: string; + namespace?: string; + outputDir?: string; + requireErdSite?: boolean; +} + +interface ErdBuildsFromContextOptions { + context: LocalErdSchemaContext; + namespace?: string; + outputDir?: string; + requireErdSite?: boolean; +} + +export interface ErdBuildResult { + namespace: string; + erdSite?: string; + distDir: string; +} + +function getErdSite(context: LocalErdSchemaContext, namespace: string): string | undefined { + const dbConfig = context.config.db?.[namespace]; + if (!dbConfig || "external" in dbConfig) { + return undefined; + } + return dbConfig.erdSite; +} + +function resolveExplicitTarget(options: ResolveTargetsOptions): ErdTarget { + const namespaceData = options.context.namespaces.find( + (candidate) => candidate.namespace === options.namespace, + ); + if (!namespaceData) { + const available = options.context.namespaces.map((candidate) => candidate.namespace).join(", "); + throw new Error( + `TailorDB namespace "${options.namespace}" not found in local config.db.` + + (available ? ` Available owned namespaces: ${available}` : ""), + ); + } + + const erdSite = getErdSite(options.context, namespaceData.namespace); + if (options.requireErdSite && !erdSite) { + throw new Error( + `No erdSite configured for namespace "${namespaceData.namespace}". ` + + `Add erdSite: "" to db.${namespaceData.namespace} in tailor.config.ts.`, + ); + } + + return toTarget(options.outputDir, namespaceData, erdSite); +} + +function resolveAllTargets(options: ResolveTargetsOptions): ErdTarget[] { + const namespaces = options.context.namespaces.filter( + (namespaceData) => + !options.requireErdSite || getErdSite(options.context, namespaceData.namespace), + ); + if (namespaces.length === 0) { + throw new Error( + options.requireErdSite + ? "No namespaces with erdSite configured found. " + + 'Add erdSite: "" to db. in tailor.config.ts.' + : "No TailorDB namespaces found in config. Please define db services in tailor.config.ts.", + ); + } + + logger.info( + `Found ${namespaces.length} namespace(s)${options.requireErdSite ? " with erdSite configured" : ""}.`, + ); + return namespaces.map((namespaceData) => + toTarget( + options.outputDir, + namespaceData, + getErdSite(options.context, namespaceData.namespace), + ), + ); +} + +function toTarget( + outputDir: string, + namespaceData: TailorDBNamespaceData, + erdSite: string | undefined, +): ErdTarget { + const distDir = path.join(outputDir, namespaceData.namespace, "dist"); + return { + namespaceData, + erdSite, + distDir, + }; +} + +function resolveTargets(options: ResolveTargetsOptions): ErdTarget[] { + if (options.namespace) { + return [resolveExplicitTarget(options)]; + } + return resolveAllTargets(options); +} + +function prepareErdBuild(target: ErdTarget): ErdBuildResult { + const schema = buildTailorDbErdSchema({ namespaceData: target.namespaceData }); + writeViewerDist({ schema, distDir: target.distDir }); + + const relativePath = path.relative(process.cwd(), target.distDir); + logger.success(`Built ERD to ${relativePath}`); + + return { + namespace: target.namespaceData.namespace, + erdSite: target.erdSite, + distDir: target.distDir, + }; +} + +/** + * Prepare TailorDB ERD static viewer builds for one or more namespaces. + * @param options - Build options. + * @returns Build results by namespace. + */ +export async function prepareErdBuilds(options: ErdBuildsOptions): Promise { + const context = await loadLocalErdSchema({ + configPath: options.configPath, + namespaces: options.namespace ? [options.namespace] : undefined, + requireErdSite: options.requireErdSite, + }); + return prepareErdBuildsFromContext({ + context, + namespace: options.namespace, + outputDir: options.outputDir, + requireErdSite: options.requireErdSite, + }); +} + +/** + * Prepare TailorDB ERD static viewer builds from an already loaded schema context. + * @param options - Build options. + * @returns Build results by namespace. + */ +export function prepareErdBuildsFromContext( + options: ErdBuildsFromContextOptions, +): ErdBuildResult[] { + const outputDir = path.resolve(process.cwd(), options.outputDir ?? DEFAULT_ERD_BASE_DIR); + const targets = resolveTargets({ + context: options.context, + namespace: options.namespace, + outputDir, + requireErdSite: options.requireErdSite, + }); + + return targets.map((target) => prepareErdBuild(target)); +} + +export const erdExportCommand = defineAppCommand({ + name: "export", + description: "Export TailorDB ERD static viewer from local TailorDB schema.", + args: z.strictObject({ + ...configArg, + namespace: arg(z.string().optional(), { + alias: "n", + description: "TailorDB namespace name (optional if only one namespace is defined in config)", + }), + output: arg(z.string().default(DEFAULT_ERD_BASE_DIR), { + alias: "o", + description: + "Output directory path for TailorDB ERD viewer files (writes to `//dist`)", + completion: { type: "directory" }, + }), + }), + run: async (args) => { + initErdCommand(); + + const results = await prepareErdBuilds({ + configPath: args.config, + namespace: args.namespace, + outputDir: args.output, + }); + + logger.newline(); + if (args.json) { + logger.out( + results.map((result) => ({ + namespace: result.namespace, + distDir: result.distDir, + })), + ); + } else { + for (const result of results) { + logger.out(`Exported ERD for namespace "${result.namespace}"`); + logger.out(` - ERD viewer: ${path.join(result.distDir, "index.html")}`); + } + } + }, +}); diff --git a/packages/sdk-tailordb-erd-plugin/src/index.ts b/packages/sdk-tailordb-erd-plugin/src/index.ts new file mode 100644 index 000000000..6f71fe52c --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/index.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import * as path from "pathe"; +import { readPackageJSON } from "pkg-types"; +import { defineCommand, runMain } from "politty"; +import { z } from "zod"; +import { erdDeployCommand } from "./deploy"; +import { erdDiffCommand } from "./diff-command"; +import { erdExportCommand } from "./export"; +import { erdServeCommand } from "./serve"; +import { commonArgs } from "./shared/args"; + +const packageRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); +const packageJson = await readPackageJSON(packageRoot); + +const mainCommand = defineCommand({ + name: "tailor-tailordb-erd", + description: + "Generate TailorDB ERD viewer artifacts from local TailorDB schema. (beta)\n" + + "Tailor CLI plugin: installed alongside the Tailor CLI, it runs as `tailor tailordb erd `.", + subCommands: { + export: erdExportCommand, + diff: erdDiffCommand, + serve: erdServeCommand, + deploy: erdDeployCommand, + }, +}); + +void runMain(mainCommand, { + version: packageJson.version ?? "0.0.0", + // strip unknown keys + globalArgs: z.object(commonArgs), +}); diff --git a/packages/sdk-tailordb-erd-plugin/src/local-schema.test.ts b/packages/sdk-tailordb-erd-plugin/src/local-schema.test.ts new file mode 100644 index 000000000..e9f9d48b1 --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/local-schema.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from "vitest"; +import { resolveLocalErdSchemaNamespaces } from "./local-schema"; +import type { LoadedConfig } from "@tailor-platform/sdk/cli"; + +describe("resolveLocalErdSchemaNamespaces", () => { + const config = { + path: "/tmp/tailor.config.ts", + db: { + main: { files: ["tailordb/main/*.ts"], erdSite: "main-erd" }, + admin: { files: ["tailordb/admin/*.ts"] }, + external: { external: true }, + }, + } as unknown as LoadedConfig; + + test("loads only erdSite namespaces when required and no namespace is explicit", () => { + expect(resolveLocalErdSchemaNamespaces(config, { requireErdSite: true })).toEqual(["main"]); + }); + + test("keeps explicit namespaces even when erdSite is required", () => { + expect( + resolveLocalErdSchemaNamespaces(config, { + namespaces: ["admin"], + requireErdSite: true, + }), + ).toEqual(["admin"]); + }); + + test("loads all owned namespaces when erdSite is not required", () => { + expect(resolveLocalErdSchemaNamespaces(config, {})).toBeUndefined(); + }); +}); diff --git a/packages/sdk-tailordb-erd-plugin/src/local-schema.ts b/packages/sdk-tailordb-erd-plugin/src/local-schema.ts new file mode 100644 index 000000000..907e3c9cb --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/local-schema.ts @@ -0,0 +1,61 @@ +import { loadTailorDBNamespaces } from "@tailor-platform/sdk/cli"; +import type { LoadedConfig } from "@tailor-platform/sdk/cli"; +import type { TailorDBNamespaceData } from "@tailor-platform/sdk/cli"; + +export interface LoadLocalErdSchemaOptions { + configPath?: string; + namespaces?: string[]; + requireErdSite?: boolean; +} + +export interface LocalErdSchemaContext { + config: LoadedConfig; + namespaces: TailorDBNamespaceData[]; +} + +export interface ResolveLocalErdSchemaNamespacesOptions { + /** Explicit namespace selection. */ + namespaces?: string[]; + /** Limit implicit selection to owned namespaces with erdSite configured. */ + requireErdSite?: boolean; +} + +/** + * Resolve TailorDB namespaces that need local type loading for ERD generation. + * @param config - Loaded Tailor config. + * @param options - Namespace selection options. + * @returns Namespace names to load, or undefined to load all owned namespaces. + */ +export function resolveLocalErdSchemaNamespaces( + config: LoadedConfig, + options: ResolveLocalErdSchemaNamespacesOptions, +): string[] | undefined { + if (options.namespaces) { + return options.namespaces; + } + if (!options.requireErdSite) { + return undefined; + } + + return Object.entries(config.db ?? {}).flatMap(([namespace, dbConfig]) => + "external" in dbConfig || !dbConfig.erdSite ? [] : [namespace], + ); +} + +/** + * Load local TailorDB namespaces exactly as SDK generation/deploy sees them. + * @param options - Local schema loading options. + * @returns Loaded TailorDB namespace data. + */ +export async function loadLocalErdSchema( + options: LoadLocalErdSchemaOptions, +): Promise { + return await loadTailorDBNamespaces({ + configPath: options.configPath, + namespaces: (config) => + resolveLocalErdSchemaNamespaces(config, { + namespaces: options.namespaces, + requireErdSite: options.requireErdSite, + }), + }); +} diff --git a/packages/sdk-tailordb-erd-plugin/src/schema.test.ts b/packages/sdk-tailordb-erd-plugin/src/schema.test.ts new file mode 100644 index 000000000..2ffc31d7d --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/schema.test.ts @@ -0,0 +1,311 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "pathe"; +import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; +import { buildTailorDbErdSchema, writeTailorDbErdSchemaToFile } from "./schema"; +import type { OperatorFieldConfig, ParsedField, TailorDBType } from "@tailor-platform/sdk/cli"; +import type { TailorDBNamespaceData } from "@tailor-platform/sdk/cli"; + +function createField( + name: string, + config: Partial, + relation?: ParsedField["relation"], +): ParsedField { + return { + name, + config: { + type: config.type ?? "string", + required: config.required, + description: config.description, + allowedValues: config.allowedValues, + array: config.array, + index: config.index, + unique: config.unique, + foreignKey: config.foreignKey, + foreignKeyType: config.foreignKeyType, + foreignKeyField: config.foreignKeyField, + rawRelation: config.rawRelation, + validate: config.validate, + hooks: config.hooks, + serial: config.serial, + scale: config.scale, + fields: config.fields, + }, + relation, + }; +} + +function createType( + name: string, + fields: Record = {}, + options: Partial = {}, +): TailorDBType { + return { + name, + pluralForm: options.pluralForm ?? `${name}s`, + description: options.description, + fields, + forwardRelationships: options.forwardRelationships ?? {}, + backwardRelationships: options.backwardRelationships ?? {}, + settings: options.settings ?? {}, + permissions: options.permissions ?? {}, + indexes: options.indexes, + files: options.files, + }; +} + +function createNamespace(types: Record): TailorDBNamespaceData { + return { + namespace: "shop", + types, + sourceInfo: new Map(), + pluginAttachments: new Map(), + }; +} + +describe("buildTailorDbErdSchema", () => { + test("maps TailorDB types into TailorDB ERD schema v1", () => { + const customer = createType( + "Customer", + { + name: createField("name", { + type: "string", + required: true, + description: "Customer name", + }), + status: createField("status", { + type: "enum", + required: false, + allowedValues: [ + { value: "active", description: "Can place orders" }, + { value: "inactive" }, + ], + }), + tags: createField("tags", { + type: "string", + array: true, + }), + }, + { + description: "Customer records", + indexes: { + idx_customer_name: { fields: ["name"] }, + }, + }, + ); + + const schema = buildTailorDbErdSchema({ + namespaceData: createNamespace({ Customer: customer }), + generatedAt: "2026-01-01T00:00:00.000Z", + }); + + expect(schema).toMatchObject({ + version: 1, + namespace: "shop", + source: "local", + generatedAt: "2026-01-01T00:00:00.000Z", + }); + expect(schema.cleanRoom.notes.join(" ")).toContain("does not copy Liam source code"); + expect(schema.tables[0]!).toMatchObject({ + name: "Customer", + pluralForm: "Customers", + description: "Customer records", + }); + expect(schema.tables[0]!.columns).toEqual([ + { + name: "id", + type: "uuid", + required: true, + array: false, + primaryKey: true, + unique: true, + }, + { + name: "name", + type: "string", + required: true, + array: false, + description: "Customer name", + index: true, + indexNames: ["idx_customer_name"], + }, + { + name: "status", + type: "enum", + required: false, + array: false, + enumValues: ["active", "inactive"], + enumValueDescriptions: { active: "Can place orders" }, + }, + { + name: "tags", + type: "string", + required: true, + array: true, + }, + ]); + }); + + test("builds relations from parsed TailorDB relation metadata", () => { + const customer = createType("Customer"); + const order = createType("Order", { + customerId: createField( + "customerId", + { + type: "uuid", + required: true, + foreignKey: true, + foreignKeyType: "Customer", + foreignKeyField: "id", + rawRelation: { + type: "n-1", + toward: { type: "Customer", as: "customer", key: "id" }, + backward: "orders", + }, + }, + { + targetType: "Customer", + forwardName: "customer", + backwardName: "orders", + key: "id", + unique: false, + }, + ), + }); + + const schema = buildTailorDbErdSchema({ + namespaceData: createNamespace({ Customer: customer, Order: order }), + generatedAt: "2026-01-01T00:00:00.000Z", + }); + + expect(schema.relations).toEqual([ + { + name: "Order.customerId->Customer.id", + sourceTable: "Order", + sourceColumns: ["customerId"], + targetTable: "Customer", + targetColumns: ["id"], + required: true, + unique: false, + kind: "relation", + relationType: "n-1", + forwardName: "customer", + backwardName: "orders", + }, + ]); + expect(schema.tables.find((table) => table.name === "Order")?.columns[1]!.relation).toEqual({ + targetTable: "Customer", + targetColumn: "id", + kind: "relation", + required: true, + relationType: "n-1", + forwardName: "customer", + backwardName: "orders", + }); + }); + + test("does not duplicate the implicit parsed id field", () => { + const user = createType("User", { + id: createField("id", { + type: "uuid", + required: true, + }), + email: createField("email", { + type: "string", + required: true, + }), + }); + + const schema = buildTailorDbErdSchema({ + namespaceData: createNamespace({ User: user }), + generatedAt: "2026-01-01T00:00:00.000Z", + }); + + expect(schema.tables[0]!.columns.map((column) => column.name)).toEqual(["id", "email"]); + expect(schema.tables[0]!.columns[0]!).toMatchObject({ + name: "id", + primaryKey: true, + unique: true, + }); + }); + + test("includes plugin source metadata without local file paths", () => { + const namespace = createNamespace({ + AuditLog: createType("AuditLog"), + }); + namespace.sourceInfo = new Map([ + [ + "AuditLog", + { + exportName: "AuditLog", + pluginId: "audit-plugin", + pluginImportPath: "@example/audit", + originalFilePath: "/Users/example/project/tailordb/user.ts", + originalExportName: "User", + generatedTypeKind: "history", + namespace: "shop", + }, + ], + ]); + + const schema = buildTailorDbErdSchema({ + namespaceData: namespace, + generatedAt: "2026-01-01T00:00:00.000Z", + }); + + expect(schema.tables[0]!.source).toEqual({ + kind: "plugin", + exportName: "AuditLog", + pluginId: "audit-plugin", + pluginImportPath: "@example/audit", + originalExportName: "User", + generatedTypeKind: "history", + namespace: "shop", + }); + expect(JSON.stringify(schema)).not.toContain("/Users/example"); + }); + + test("keeps revisions stable when only generatedAt changes", () => { + const namespace = createNamespace({ User: createType("User") }); + + const first = buildTailorDbErdSchema({ + namespaceData: namespace, + generatedAt: "2026-01-01T00:00:00.000Z", + }); + const second = buildTailorDbErdSchema({ + namespaceData: namespace, + generatedAt: "2026-01-02T00:00:00.000Z", + }); + + expect(first.revision).toBe(second.revision); + }); +}); + +describe("writeTailorDbErdSchemaToFile", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tailordb-erd-schema-")); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + vi.restoreAllMocks(); + }); + + test("writes schema JSON to disk", () => { + const schema = buildTailorDbErdSchema({ + namespaceData: createNamespace({ User: createType("User") }), + generatedAt: "2026-01-01T00:00:00.000Z", + }); + const outputPath = path.join(tempDir, "schema.json"); + + writeTailorDbErdSchemaToFile({ schema, outputPath }); + + expect(JSON.parse(fs.readFileSync(outputPath, "utf8"))).toMatchObject({ + version: 1, + namespace: "shop", + tables: [{ name: "User" }], + }); + }); +}); diff --git a/packages/sdk-tailordb-erd-plugin/src/schema.ts b/packages/sdk-tailordb-erd-plugin/src/schema.ts new file mode 100644 index 000000000..1b2386460 --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/schema.ts @@ -0,0 +1,292 @@ +import * as crypto from "node:crypto"; +import * as fs from "node:fs"; +import { isPluginGeneratedType } from "@tailor-platform/sdk/cli"; +import * as path from "pathe"; +import { logger } from "./shared/logger"; +import type { + TailorDbErdColumn, + TailorDbErdColumnRelation, + TailorDbErdIndex, + TailorDbErdRelation, + TailorDbErdRelationship, + TailorDbErdSchema, + TailorDbErdSource, + TailorDbErdTable, + TailorDbErdTypeSource, +} from "./types"; +import type { + OperatorFieldConfig, + ParsedField, + TailorDBType, + TypeSourceInfoEntry, +} from "@tailor-platform/sdk/cli"; +import type { TailorDBNamespaceData } from "@tailor-platform/sdk/cli"; + +const CLEAN_ROOM_NOTES = [ + "Generated by a TailorDB-specific viewer implementation.", + "The implementation is based on TailorDB schema contracts, public Liam documentation, Liam CLI help, and black-box generated-output observation.", + "It does not copy Liam source code, generated JavaScript/CSS, parser internals, or layout internals.", +]; + +interface BuildTailorDbErdSchemaOptions { + namespaceData: TailorDBNamespaceData; + generatedAt?: string; + source?: TailorDbErdSource; +} + +interface WriteTailorDbErdSchemaOptions { + schema: TailorDbErdSchema; + outputPath: string; +} + +interface BuildColumnOptions { + fieldName: string; + fieldConfig: OperatorFieldConfig; + parsedField?: ParsedField; + indexEntries: Array<[string, TailorDbErdIndex]>; +} + +function buildRevision(schema: Omit): string { + return crypto + .createHash("sha256") + .update(JSON.stringify(schema), "utf-8") + .digest("hex") + .slice(0, 16); +} + +function toTypeSource(source: TypeSourceInfoEntry | undefined): TailorDbErdTypeSource | undefined { + if (!source) return undefined; + if (isPluginGeneratedType(source)) { + return { + kind: "plugin", + exportName: source.exportName, + pluginId: source.pluginId, + pluginImportPath: source.pluginImportPath, + originalExportName: source.originalExportName, + generatedTypeKind: source.generatedTypeKind, + namespace: source.namespace, + }; + } + return { + kind: "user", + exportName: source.exportName, + }; +} + +function toRelationships( + relationships: Record, +): TailorDbErdRelationship[] { + return Object.entries(relationships) + .toSorted(([a], [b]) => a.localeCompare(b)) + .map(([name, relationship]) => ({ + name, + targetType: relationship.targetType, + targetField: relationship.targetField, + sourceField: relationship.sourceField, + isArray: relationship.isArray, + ...(relationship.description && { description: relationship.description }), + })); +} + +function toIndexes(type: TailorDBType): TailorDbErdIndex[] { + return Object.entries(type.indexes ?? {}) + .toSorted(([a], [b]) => a.localeCompare(b)) + .map(([name, index]) => ({ + name, + fields: [...index.fields], + unique: index.unique === true, + })); +} + +function toColumnRelation(options: BuildColumnOptions): TailorDbErdColumnRelation | undefined { + const { fieldConfig, parsedField } = options; + if (!fieldConfig.foreignKey || !fieldConfig.foreignKeyType) { + return undefined; + } + + const required = fieldConfig.required !== false; + const kind = parsedField?.relation || fieldConfig.rawRelation ? "relation" : "foreignKey"; + return { + targetTable: fieldConfig.foreignKeyType, + targetColumn: fieldConfig.foreignKeyField || "id", + kind, + required, + ...(fieldConfig.rawRelation?.type && { relationType: fieldConfig.rawRelation.type }), + ...(parsedField?.relation?.forwardName && { forwardName: parsedField.relation.forwardName }), + ...(parsedField?.relation?.backwardName && { + backwardName: parsedField.relation.backwardName, + }), + }; +} + +function toColumn(options: BuildColumnOptions): TailorDbErdColumn { + const { fieldName, fieldConfig } = options; + const indexNames = options.indexEntries + .filter(([, index]) => index.fields.includes(fieldName)) + .map(([name]) => name); + const uniqueIndexNames = options.indexEntries + .filter(([, index]) => index.unique && index.fields.includes(fieldName)) + .map(([name]) => name); + + const enumValues = fieldConfig.allowedValues?.map((value) => value.value) ?? []; + const enumValueDescriptions = Object.fromEntries( + (fieldConfig.allowedValues ?? []).flatMap((value) => + value.description ? [[value.value, value.description]] : [], + ), + ); + const nestedFields = Object.entries(fieldConfig.fields ?? {}).map(([nestedName, nestedConfig]) => + toColumn({ + fieldName: nestedName, + fieldConfig: nestedConfig, + indexEntries: [], + }), + ); + const relation = toColumnRelation(options); + + return { + name: fieldName, + type: fieldConfig.type || "string", + required: fieldConfig.required !== false, + array: fieldConfig.array === true, + ...(fieldConfig.description && { description: fieldConfig.description }), + ...(fieldConfig.unique && { unique: true }), + ...((fieldConfig.index || indexNames.length > 0) && { index: true }), + ...(indexNames.length > 0 && { indexNames }), + ...(uniqueIndexNames.length > 0 && { uniqueIndexNames }), + ...(enumValues.length > 0 && { enumValues }), + ...(Object.keys(enumValueDescriptions).length > 0 && { enumValueDescriptions }), + ...(fieldConfig.vector && { vector: true }), + ...(fieldConfig.serial && { serial: { ...fieldConfig.serial } }), + ...(fieldConfig.scale !== undefined && { scale: fieldConfig.scale }), + ...(fieldConfig.validate?.length && { validations: fieldConfig.validate.length }), + ...(fieldConfig.hooks && { + hooks: { + ...(fieldConfig.hooks.create && { create: true }), + ...(fieldConfig.hooks.update && { update: true }), + }, + }), + ...(nestedFields.length > 0 && { fields: nestedFields }), + ...(relation && { relation }), + }; +} + +function toTable(type: TailorDBType, source: TypeSourceInfoEntry | undefined): TailorDbErdTable { + const indexes = toIndexes(type); + const indexEntries: Array<[string, TailorDbErdIndex]> = indexes.map((index) => [ + index.name, + index, + ]); + const fieldColumns = Object.entries(type.fields) + .filter(([fieldName]) => fieldName !== "id") + .map(([fieldName, field]) => + toColumn({ + fieldName, + fieldConfig: field.config, + parsedField: field, + indexEntries, + }), + ); + + const typeSource = toTypeSource(source); + + return { + name: type.name, + pluralForm: type.pluralForm, + ...(type.description && { description: type.description }), + ...(typeSource && { source: typeSource }), + columns: [ + { + name: "id", + type: "uuid", + required: true, + array: false, + primaryKey: true, + unique: true, + }, + ...fieldColumns, + ], + indexes, + forwardRelationships: toRelationships(type.forwardRelationships), + backwardRelationships: toRelationships(type.backwardRelationships), + }; +} + +function toRelation(sourceTable: string, field: ParsedField): TailorDbErdRelation | undefined { + const relation = toColumnRelation({ + fieldName: field.name, + fieldConfig: field.config, + parsedField: field, + indexEntries: [], + }); + if (!relation) return undefined; + + return { + name: `${sourceTable}.${field.name}->${relation.targetTable}.${relation.targetColumn}`, + sourceTable, + sourceColumns: [field.name], + targetTable: relation.targetTable, + targetColumns: [relation.targetColumn], + required: relation.required, + unique: field.config.unique === true || field.relation?.unique === true, + kind: relation.kind, + ...(relation.relationType && { relationType: relation.relationType }), + ...(relation.forwardName && { forwardName: relation.forwardName }), + ...(relation.backwardName && { backwardName: relation.backwardName }), + }; +} + +function buildRelations(types: Record): TailorDbErdRelation[] { + const relations: TailorDbErdRelation[] = []; + for (const type of Object.values(types)) { + for (const field of Object.values(type.fields)) { + const relation = toRelation(type.name, field); + if (relation) { + relations.push(relation); + } + } + } + return relations.toSorted((a, b) => a.name.localeCompare(b.name)); +} + +/** + * Build the TailorDB ERD viewer schema for one namespace. + * @param options - Schema build options. + * @returns TailorDB ERD viewer schema. + */ +export function buildTailorDbErdSchema(options: BuildTailorDbErdSchemaOptions): TailorDbErdSchema { + const { namespaceData } = options; + const tables = Object.values(namespaceData.types) + .toSorted((a, b) => a.name.localeCompare(b.name)) + .map((type) => toTable(type, namespaceData.sourceInfo.get(type.name))); + + const schemaWithoutRevision = { + version: 1 as const, + namespace: namespaceData.namespace, + source: options.source ?? "local", + cleanRoom: { + implementation: "tailor" as const, + notes: CLEAN_ROOM_NOTES, + }, + tables, + relations: buildRelations(namespaceData.types), + }; + + return { + ...schemaWithoutRevision, + generatedAt: options.generatedAt ?? new Date().toISOString(), + revision: buildRevision(schemaWithoutRevision), + }; +} + +/** + * Writes a TailorDB ERD viewer schema to disk. + * @param options - Schema write options. + */ +export function writeTailorDbErdSchemaToFile(options: WriteTailorDbErdSchemaOptions): void { + const json = JSON.stringify(options.schema, null, 2); + fs.mkdirSync(path.dirname(options.outputPath), { recursive: true }); + fs.writeFileSync(options.outputPath, json, "utf8"); + + const relativePath = path.relative(process.cwd(), options.outputPath); + logger.success(`Wrote ERD schema to ${relativePath}`); +} diff --git a/packages/sdk-tailordb-erd-plugin/src/serve.test.ts b/packages/sdk-tailordb-erd-plugin/src/serve.test.ts new file mode 100644 index 000000000..04f9ead5b --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/serve.test.ts @@ -0,0 +1,48 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "pathe"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { resolveWatchPaths } from "./serve"; +import type { ErdBuildResult } from "./export"; +import type { LocalErdSchemaContext } from "./local-schema"; + +describe("resolveWatchPaths", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tailordb-erd-watch-")); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + test("expands TailorDB file globs and includes the literal base directory", async () => { + const configPath = path.join(tempDir, "tailor.config.ts"); + const typeDir = path.join(tempDir, "tailordb"); + const typeFile = path.join(typeDir, "user.ts"); + fs.writeFileSync(configPath, "export default {};"); + fs.mkdirSync(typeDir); + fs.writeFileSync(typeFile, "export const User = {};"); + + const context = { + config: { + path: configPath, + db: { + main: { + files: [path.join(typeDir, "*.ts")], + }, + }, + }, + namespaces: [], + } as unknown as LocalErdSchemaContext; + const results = [{ namespace: "main" }] as ErdBuildResult[]; + + const paths = await resolveWatchPaths(context, results); + + expect(paths).toContain(configPath); + expect(paths).toContain(typeFile); + expect(paths).toContain(typeDir); + expect(paths).not.toContain(path.join(typeDir, "*.ts")); + }); +}); diff --git a/packages/sdk-tailordb-erd-plugin/src/serve.ts b/packages/sdk-tailordb-erd-plugin/src/serve.ts new file mode 100644 index 000000000..e9d402769 --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/serve.ts @@ -0,0 +1,492 @@ +import { spawn } from "node:child_process"; +import * as fs from "node:fs"; +import { glob } from "node:fs/promises"; +import * as http from "node:http"; +import { loadConfig, type LoadedConfig } from "@tailor-platform/sdk/cli"; +import { watch, type FSWatcher } from "chokidar"; +import { lookup as lookupMime } from "mime-types"; +import open from "open"; +import * as path from "pathe"; +import { arg } from "politty"; +import { z } from "zod"; +import { prepareErdBuildsFromContext, type ErdBuildResult } from "./export"; +import { loadLocalErdSchema, type LocalErdSchemaContext } from "./local-schema"; +import { configArg } from "./shared/args"; +import { defineAppCommand } from "./shared/command"; +import { logger } from "./shared/logger"; +import { initErdCommand } from "./utils"; + +const DEFAULT_ERD_BASE_DIR = ".tailor/erd"; +const LOCAL_HOST = "127.0.0.1"; + +interface StaticServerResult { + server: http.Server; + url: string; +} + +interface StartStaticServerOptions { + distDir: string; + port: number; +} + +interface WatchOptions { + configPath?: string; + namespace?: string; + outputDir: string; + initialContext: LocalErdSchemaContext; + initialResults: ErdBuildResult[]; +} + +interface FreshErdExportOptions { + configPath?: string; + namespace?: string; + outputDir: string; +} + +interface ErdExportJsonResult { + namespace: string; + distDir: string; +} + +interface OpenStaticFileResult { + filePath: string; + fd: number; +} + +const GLOB_CHARS = /[*?[\]{}()!+@]/; + +function formatServeCommand(namespace: string): string { + return `tailor tailordb erd serve --namespace ${namespace}`; +} + +function getCacheControl(filePath: string): string { + return filePath.endsWith(".html") || filePath.endsWith(".json") + ? "no-cache" + : "public, max-age=3600"; +} + +function resolveRequestPath(distDir: string, requestUrl: string | undefined): string | undefined { + const url = new URL(requestUrl ?? "/", "http://localhost"); + let pathname: string; + try { + pathname = decodeURIComponent(url.pathname); + } catch { + return undefined; + } + + if (pathname === "/" || pathname.endsWith("/")) { + pathname = path.join(pathname, "index.html"); + } + + const root = path.resolve(distDir); + const filePath = path.resolve(root, `.${pathname}`); + if (filePath !== root && !filePath.startsWith(`${root}${path.sep}`)) { + return undefined; + } + return filePath; +} + +function openStaticFile(filePath: string): OpenStaticFileResult | undefined { + let fd: number | undefined; + try { + fd = fs.openSync(filePath, "r"); + if (!fs.fstatSync(fd).isFile()) { + fs.closeSync(fd); + return undefined; + } + return { filePath, fd }; + } catch { + if (fd !== undefined) { + fs.closeSync(fd); + } + return undefined; + } +} + +function serveFile(distDir: string, req: http.IncomingMessage, res: http.ServerResponse): void { + const filePath = resolveRequestPath(distDir, req.url); + if (!filePath) { + res.writeHead(403); + res.end("Forbidden"); + return; + } + + const fallbackPath = path.join(distDir, "index.html"); + const target = openStaticFile(filePath) ?? openStaticFile(fallbackPath); + if (!target) { + res.writeHead(503, { + "Content-Type": "text/plain; charset=utf-8", + "Cache-Control": "no-cache", + "Retry-After": "1", + }); + res.end("ERD build is refreshing. Please retry."); + return; + } + + const mimeType = lookupMime(target.filePath) || "application/octet-stream"; + const stream = fs.createReadStream(target.filePath, { + fd: target.fd, + autoClose: true, + }); + stream.on("error", () => { + if (!res.headersSent) { + res.writeHead(503, { + "Content-Type": "text/plain; charset=utf-8", + "Cache-Control": "no-cache", + "Retry-After": "1", + }); + res.end("ERD build is refreshing. Please retry."); + return; + } + res.destroy(); + }); + res.writeHead(200, { + "Content-Type": mimeType, + "Cache-Control": getCacheControl(target.filePath), + }); + stream.pipe(res); +} + +async function startStaticServer(options: StartStaticServerOptions): Promise { + const server = http.createServer((req, res) => { + serveFile(options.distDir, req, res); + }); + + return await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(options.port, LOCAL_HOST, () => { + server.off("error", reject); + const address = server.address(); + if (!address || typeof address === "string") { + reject(new Error("Failed to determine ERD server address.")); + return; + } + resolve({ + server, + url: `http://${LOCAL_HOST}:${address.port}`, + }); + }); + }); +} + +function getWatchPatterns(config: LoadedConfig, results: ErdBuildResult[]): string[] { + const namespaces = new Set(results.map((result) => result.namespace)); + const patterns = [config.path]; + for (const namespace of namespaces) { + const dbConfig = config.db?.[namespace]; + if (dbConfig && !("external" in dbConfig)) { + patterns.push(...dbConfig.files); + } + } + return [...new Set(patterns)]; +} + +function hasGlobPattern(pattern: string): boolean { + return GLOB_CHARS.test(pattern); +} + +function globBaseDir(pattern: string): string { + const absolutePattern = path.resolve(pattern); + const parsed = path.parse(absolutePattern); + const relativePattern = absolutePattern.slice(parsed.root.length); + const literalParts: string[] = []; + for (const part of relativePattern.split(path.sep)) { + if (!part || GLOB_CHARS.test(part)) break; + literalParts.push(part); + } + + const literalPath = + literalParts.length > 0 ? path.join(parsed.root, ...literalParts) : parsed.root; + if (!literalPath || literalPath === parsed.root) return parsed.root || process.cwd(); + if (!fs.existsSync(literalPath)) return path.dirname(literalPath); + return fs.statSync(literalPath).isDirectory() ? literalPath : path.dirname(literalPath); +} + +async function expandWatchPattern(pattern: string): Promise { + if (!hasGlobPattern(pattern)) { + return [path.resolve(pattern)]; + } + + const paths = new Set(); + for await (const file of glob(pattern)) { + paths.add(path.resolve(file)); + } + + const baseDir = globBaseDir(pattern); + if (fs.existsSync(baseDir) && fs.statSync(baseDir).isDirectory()) { + paths.add(baseDir); + } + return [...paths]; +} + +async function resolveWatchPathsFromConfig( + config: LoadedConfig, + results: ErdBuildResult[], +): Promise { + const paths = new Set(); + for (const pattern of getWatchPatterns(config, results)) { + for (const watchPath of await expandWatchPattern(pattern)) { + paths.add(watchPath); + } + } + return [...paths]; +} + +export async function resolveWatchPaths( + context: LocalErdSchemaContext, + results: ErdBuildResult[], +): Promise { + return await resolveWatchPathsFromConfig(context.config, results); +} + +function parseFreshErdExportResults(stdout: string): ErdBuildResult[] { + const lines = stdout + .trim() + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + for (const line of lines.toReversed()) { + try { + const parsed = JSON.parse(line) as unknown; + if (!Array.isArray(parsed)) continue; + return parsed.map((entry): ErdBuildResult => { + const result = entry as Partial; + if (typeof result.namespace !== "string" || typeof result.distDir !== "string") { + throw new Error("Invalid ERD export JSON output."); + } + return { + namespace: result.namespace, + distDir: result.distDir, + }; + }); + } catch { + continue; + } + } + throw new Error("Failed to parse ERD export JSON output."); +} + +function freshErdExportArgs(options: FreshErdExportOptions): string[] { + const cliEntry = process.argv[1]; + if (!cliEntry) { + throw new Error("Cannot rebuild ERD schema in a fresh process: CLI entrypoint is unavailable."); + } + + const args = [cliEntry, "export", "--output", options.outputDir, "--json"]; + if (options.configPath) { + args.push("--config", options.configPath); + } + if (options.namespace) { + args.push("--namespace", options.namespace); + } + return args; +} + +async function runFreshErdExport(options: FreshErdExportOptions): Promise { + return await new Promise((resolve, reject) => { + const child = spawn(process.execPath, freshErdExportArgs(options), { + cwd: process.cwd(), + env: process.env, + stdio: ["ignore", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk: string) => { + stdout += chunk; + }); + child.stderr.on("data", (chunk: string) => { + stderr += chunk; + }); + child.on("error", reject); + child.on("close", (code, signal) => { + if (code !== 0) { + const detail = stderr.trim() || signal || `exit code ${code}`; + reject(new Error(`Fresh ERD export failed: ${detail}`)); + return; + } + try { + resolve(parseFreshErdExportResults(stdout)); + } catch (error) { + reject(error); + } + }); + }); +} + +function selectPrimaryResult(results: ErdBuildResult[]): ErdBuildResult { + const [primary, ...rest] = results; + if (!primary) { + throw new Error("No ERD build results found."); + } + + logger.info(`Serving ERD for namespace "${primary.namespace}".`); + if (rest.length > 0) { + const commands = rest.map((result) => ` - ${formatServeCommand(result.namespace)}`).join("\n"); + logger.warn(`Multiple namespaces found. To serve another namespace, run:\n${commands}`); + } + + return primary; +} + +async function createErdWatcher(options: WatchOptions): Promise { + let rebuilding = false; + let pending = false; + let watchPaths = await resolveWatchPaths(options.initialContext, options.initialResults); + let importNonce = 0; + + const watcher = watch(watchPaths, { + ignored: /node_modules/, + ignoreInitial: true, + awaitWriteFinish: { + stabilityThreshold: 100, + pollInterval: 100, + }, + }); + + async function rebuild(): Promise { + if (rebuilding) { + pending = true; + return; + } + + rebuilding = true; + try { + const results = await runFreshErdExport({ + configPath: options.configPath, + namespace: options.namespace, + outputDir: options.outputDir, + }); + const { config } = await loadConfig(options.configPath, { + importNonce: String((importNonce += 1)), + }); + const nextWatchPaths = await resolveWatchPathsFromConfig(config, results); + watcher.unwatch(watchPaths); + watcher.add(nextWatchPaths); + watchPaths = nextWatchPaths; + logger.success( + `Rebuilt ERD schema (${results.map((result) => result.namespace).join(", ")})`, + { + mode: "stream", + }, + ); + } catch (error) { + logger.error("Failed to rebuild ERD schema. Serving the last successful build.", { + mode: "stream", + }); + logger.error(String(error)); + } finally { + rebuilding = false; + if (pending) { + pending = false; + await rebuild(); + } + } + } + + let debounceTimer: NodeJS.Timeout | undefined; + const scheduleRebuild = (changedPath: string) => { + logger.info(`Schema source changed: ${path.relative(process.cwd(), changedPath)}`, { + mode: "stream", + }); + if (debounceTimer) { + clearTimeout(debounceTimer); + } + debounceTimer = setTimeout(() => { + rebuild(); + }, 150); + }; + + watcher.on("add", scheduleRebuild); + watcher.on("change", scheduleRebuild); + watcher.on("unlink", scheduleRebuild); + watcher.on("error", (error) => { + logger.error(`ERD watcher error: ${String(error)}`, { mode: "stream" }); + }); + + return watcher; +} + +async function waitForShutdown(server: http.Server, watcher: FSWatcher): Promise { + return await new Promise((resolve) => { + const shutdown = () => { + watcher.close().finally(() => { + server.close(() => { + logger.info("ERD server stopped."); + resolve(); + }); + }); + }; + + process.once("SIGINT", shutdown); + process.once("SIGTERM", shutdown); + }); +} + +export const erdServeCommand = defineAppCommand({ + name: "serve", + description: "Generate and serve TailorDB ERD locally with watch reload. (beta)", + args: z.strictObject({ + ...configArg, + namespace: arg(z.string().optional(), { + alias: "n", + description: "TailorDB namespace name (uses first namespace in config if not specified)", + }), + port: arg(z.coerce.number().int().min(0).max(65535).default(0), { + description: "Local server port (0 selects a free port)", + }), + open: arg(z.boolean().default(false), { + description: "Open the ERD viewer in the default browser", + }), + }), + run: async (args) => { + initErdCommand(); + + const outputDir = path.resolve(process.cwd(), DEFAULT_ERD_BASE_DIR); + const context = await loadLocalErdSchema({ + configPath: args.config, + namespaces: args.namespace ? [args.namespace] : undefined, + }); + const results = prepareErdBuildsFromContext({ + context, + namespace: args.namespace, + outputDir, + }); + const primary = selectPrimaryResult(results); + const { server, url } = await startStaticServer({ + distDir: primary.distDir, + port: args.port, + }); + const watchUrl = `${url}/?watch=1`; + const watcher = await createErdWatcher({ + configPath: args.config, + namespace: args.namespace, + outputDir, + initialContext: context, + initialResults: results, + }); + + logger.newline(); + if (args.json) { + logger.out({ + namespace: primary.namespace, + url: watchUrl, + distDir: primary.distDir, + }); + } else { + logger.success("ERD server started."); + logger.out(watchUrl); + } + + if (args.open) { + try { + await open(watchUrl); + } catch { + logger.warn("Failed to open browser automatically. Please open the URL above manually."); + } + } + + await waitForShutdown(server, watcher); + }, +}); diff --git a/packages/sdk-tailordb-erd-plugin/src/shared/args.ts b/packages/sdk-tailordb-erd-plugin/src/shared/args.ts new file mode 100644 index 000000000..45e980315 --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/shared/args.ts @@ -0,0 +1,116 @@ +import * as fs from "node:fs"; +import { parseEnv } from "node:util"; +import * as path from "pathe"; +import { arg } from "politty"; +import { z } from "zod"; +import { logger } from "./logger"; + +type ArgsShape = Record; + +type EnvFileArg = string | string[] | undefined; + +/** + * Load env files from parsed arguments, following Node.js --env-file behavior: + * variables already set in the environment are not overwritten, and variables + * from later files override those from earlier files. + * @param envFiles - Required env file path(s) that must exist + * @param envFilesIfExists - Optional env file path(s) that are loaded if they exist + */ +function loadEnvFiles(envFiles: EnvFileArg, envFilesIfExists: EnvFileArg): void { + const originalEnvKeys = new Set(Object.keys(process.env)); + + const load = (files: EnvFileArg, required: boolean) => { + for (const file of [files ?? []].flat()) { + const envPath = path.resolve(process.cwd(), file); + if (!fs.existsSync(envPath)) { + if (required) { + throw new Error(`Environment file not found: ${envPath}`); + } + continue; + } + const content = fs.readFileSync(envPath, "utf-8"); + const parsed = parseEnv(content); + for (const [key, value] of Object.entries(parsed)) { + if (originalEnvKeys.has(key)) { + continue; + } + process.env[key] = value; + } + } + }; + + load(envFiles, true); + load(envFilesIfExists, false); +} + +/** + * Common arguments shared with the host Tailor CLI so that forwarded global + * flags parse identically when dispatched as a plugin. + */ +export const commonArgs = { + "env-file": arg(z.string().optional(), { + alias: "e", + description: "Path to the environment file (error if not found)", + completion: { type: "file", matcher: [".env.*", ".env"] }, + }), + "env-file-if-exists": arg(z.string().optional(), { + description: "Path to the environment file (ignored if not found)", + completion: { type: "file", matcher: [".env.*", ".env"] }, + effect: (_value, { args }) => { + loadEnvFiles( + args["env-file"] as string | undefined, + args["env-file-if-exists"] as string | undefined, + ); + }, + }), + verbose: arg(z.boolean().default(false), { + description: "Enable verbose logging", + }), + json: arg(z.boolean().default(false), { + alias: "j", + description: "Output as JSON", + effect: (value) => { + logger.jsonMode = value; + }, + }), +} satisfies ArgsShape; + +/** + * Arguments for commands that require workspace context + */ +export const workspaceArgs = { + "workspace-id": arg(z.string().optional(), { + alias: "w", + description: "Workspace ID", + env: "TAILOR_PLATFORM_WORKSPACE_ID", + completion: { type: "none" }, + }), + profile: arg(z.string().optional(), { + alias: "p", + description: "Workspace profile", + env: "TAILOR_PLATFORM_PROFILE", + completion: { type: "none" }, + }), +} satisfies ArgsShape; + +/** + * Shared config arg for commands that accept a config file path + */ +export const configArg = { + config: arg(z.string().default("tailor.config.ts"), { + alias: "c", + description: "Path to Tailor config file", + env: "TAILOR_CONFIG_PATH", + completion: { type: "file", extensions: ["ts"] }, + }), +} satisfies ArgsShape; + +/** + * Arguments for commands that interact with deployed resources (includes config) + */ +export const deploymentArgs = { + ...workspaceArgs, + ...configArg, +} satisfies ArgsShape; + +export type CommonArgsType = z.infer>; diff --git a/packages/sdk-tailordb-erd-plugin/src/shared/command.ts b/packages/sdk-tailordb-erd-plugin/src/shared/command.ts new file mode 100644 index 000000000..ac63dfb59 --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/shared/command.ts @@ -0,0 +1,9 @@ +import { createDefineCommand } from "politty"; +import type { CommonArgsType } from "./args"; + +/** + * defineCommand with global args type (CommonArgsType). + * Use this for leaf commands with `run` to get type-safe access to global args. + * Parent commands with only `subCommands` can use `defineCommand` from politty directly. + */ +export const defineAppCommand = createDefineCommand(); diff --git a/packages/sdk-tailordb-erd-plugin/src/shared/logger.ts b/packages/sdk-tailordb-erd-plugin/src/shared/logger.ts new file mode 100644 index 000000000..400fe9b5c --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/shared/logger.ts @@ -0,0 +1,83 @@ +import chalk from "chalk"; + +export type LogMode = "default" | "stream" | "plain"; + +export interface LogOptions { + /** Output mode (default: "default") */ + mode?: LogMode; +} + +const TYPE_ICONS: Record = { + info: "ℹ", + success: "✔", + warn: "⚠", + error: "✖", + log: "", +}; + +const TYPE_COLORS: Record string> = { + info: chalk.cyan, + success: chalk.green, + warn: chalk.yellow, + error: chalk.red, + log: (text) => text, +}; + +// In JSON mode, all logs go to stderr to keep stdout clean for JSON data +let _jsonMode = false; + +function writeLog(type: string, message: string, opts?: LogOptions): void { + const mode = opts?.mode ?? "default"; + const colorFn = TYPE_COLORS[type] ?? ((text: string) => text); + + if (mode === "plain") { + process.stderr.write(`${colorFn(message)}\n`); + return; + } + + const icon = TYPE_ICONS[type] ?? ""; + const prefix = icon ? `${icon} ` : ""; + const timestamp = mode === "stream" ? `${new Date().toLocaleTimeString()} ` : ""; + process.stderr.write(`${timestamp}${colorFn(`${prefix}${message}`)}\n`); +} + +export const logger = { + get jsonMode(): boolean { + return _jsonMode; + }, + set jsonMode(value: boolean) { + _jsonMode = value; + }, + + info(message: string, opts?: LogOptions): void { + writeLog("info", message, opts); + }, + + success(message: string, opts?: LogOptions): void { + writeLog("success", message, opts); + }, + + warn(message: string, opts?: LogOptions): void { + writeLog("warn", message, opts); + }, + + error(message: string, opts?: LogOptions): void { + writeLog("error", message, opts); + }, + + log(message: string): void { + writeLog("log", message, { mode: "plain" }); + }, + + newline(): void { + process.stderr.write("\n"); + }, + + out(data: string | object | object[]): void { + if (typeof data === "string") { + process.stdout.write(data.endsWith("\n") ? data : `${data}\n`); + return; + } + process.stdout.write(`${JSON.stringify(data)}\n`); + }, +}; diff --git a/packages/sdk-tailordb-erd-plugin/src/types.ts b/packages/sdk-tailordb-erd-plugin/src/types.ts new file mode 100644 index 000000000..e89c0e0ad --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/types.ts @@ -0,0 +1,104 @@ +export type TailorDbErdSource = "local"; + +export interface TailorDbErdTypeSource { + kind: "user" | "plugin"; + exportName?: string; + pluginId?: string; + pluginImportPath?: string; + originalExportName?: string; + generatedTypeKind?: string; + namespace?: string; +} + +export interface TailorDbErdColumnRelation { + targetTable: string; + targetColumn: string; + kind: "foreignKey" | "relation"; + required: boolean; + relationType?: string; + forwardName?: string; + backwardName?: string; +} + +export interface TailorDbErdColumn { + name: string; + type: string; + required: boolean; + array: boolean; + description?: string; + primaryKey?: boolean; + unique?: boolean; + index?: boolean; + indexNames?: string[]; + uniqueIndexNames?: string[]; + enumValues?: string[]; + enumValueDescriptions?: Record; + vector?: boolean; + serial?: { + start: number; + maxValue?: number; + format?: string; + }; + scale?: number; + validations?: number; + hooks?: { + create?: boolean; + update?: boolean; + }; + fields?: TailorDbErdColumn[]; + relation?: TailorDbErdColumnRelation; +} + +export interface TailorDbErdIndex { + name: string; + fields: string[]; + unique: boolean; +} + +export interface TailorDbErdRelationship { + name: string; + targetType: string; + targetField: string; + sourceField: string; + isArray: boolean; + description?: string; +} + +export interface TailorDbErdTable { + name: string; + pluralForm: string; + description?: string; + source?: TailorDbErdTypeSource; + columns: TailorDbErdColumn[]; + indexes: TailorDbErdIndex[]; + forwardRelationships: TailorDbErdRelationship[]; + backwardRelationships: TailorDbErdRelationship[]; +} + +export interface TailorDbErdRelation { + name: string; + sourceTable: string; + sourceColumns: string[]; + targetTable: string; + targetColumns: string[]; + required: boolean; + unique: boolean; + kind: "foreignKey" | "relation"; + relationType?: string; + forwardName?: string; + backwardName?: string; +} + +export interface TailorDbErdSchema { + version: 1; + namespace: string; + generatedAt: string; + revision: string; + source: TailorDbErdSource; + cleanRoom: { + implementation: "tailor"; + notes: string[]; + }; + tables: TailorDbErdTable[]; + relations: TailorDbErdRelation[]; +} diff --git a/packages/sdk-tailordb-erd-plugin/src/utils.ts b/packages/sdk-tailordb-erd-plugin/src/utils.ts new file mode 100644 index 000000000..acbf0511c --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/utils.ts @@ -0,0 +1,44 @@ +import { initOperatorClient, loadAccessToken, loadWorkspaceId } from "@tailor-platform/sdk/cli"; +import { logger } from "./shared/logger"; +import type { OperatorClient } from "@tailor-platform/sdk/cli"; + +export interface ErdDeployContext { + client: OperatorClient; + workspaceId: string; +} + +type ErdDeployContextOptions = { + profile?: string; + workspaceId?: string; +}; + +/** + * Initialize shared ERD command behavior. + */ +export function initErdCommand(): void { + logger.warn( + "The 'tailordb erd' command is a beta feature and may introduce breaking changes in future releases.", + ); + logger.newline(); +} + +/** + * Initialize platform context for ERD deployment. + * @param args - CLI arguments. + * @returns Initialized deploy context. + */ +export async function initErdDeployContext( + args: ErdDeployContextOptions, +): Promise { + initErdCommand(); + const accessToken = await loadAccessToken({ + profile: args.profile, + }); + const client = await initOperatorClient(accessToken); + const workspaceId = await loadWorkspaceId({ + workspaceId: args.workspaceId, + profile: args.profile, + }); + + return { client, workspaceId }; +} diff --git a/packages/sdk-tailordb-erd-plugin/src/viewer-assets/app.js b/packages/sdk-tailordb-erd-plugin/src/viewer-assets/app.js new file mode 100644 index 000000000..ba5fc454d --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/viewer-assets/app.js @@ -0,0 +1,1424 @@ +const TABLE_WIDTH = 260; +const TABLE_HEIGHT = 62; +const FIELD_ROW_HEIGHT = 28; +const FIELD_SECTION_BORDER_HEIGHT = 1; +const X_GAP = 240; +const Y_GAP = 56; +const CARDINALITY_MARKER_WIDTH = 50; +const CROW_FOOT_TIP_OFFSET = 0; +const CROW_FOOT_JOIN_OFFSET = 18; +const CARDINALITY_OUTER_OFFSET = 32; +const DRAG_THRESHOLD = 4; +const FIT_PADDING = 80; +const MIN_ZOOM = 0.25; +const MAX_ZOOM = 2.2; +const DEFAULT_SHOW_MODE = "TABLE_NAME"; +const DEFAULT_VIEW_MODE = "diff"; +const SHOW_MODE_OPTIONS = [ + { value: "ALL_FIELDS", label: "All Fields" }, + { value: "TABLE_NAME", label: "Table Name" }, + { value: "KEY_ONLY", label: "Key Only" }, +]; + +const elements = { + main: document.querySelector(".main"), + namespace: document.getElementById("namespace"), + revision: document.getElementById("revision"), + search: document.getElementById("search"), + tableSummary: document.getElementById("table-count-summary"), + toggleAllTables: document.getElementById("toggle-all-tables"), + tableList: document.getElementById("table-list"), + canvas: document.getElementById("canvas"), + world: document.getElementById("world"), + edges: document.getElementById("edges"), + nodes: document.getElementById("nodes"), + details: document.getElementById("details"), + emptyState: document.getElementById("empty-state"), + status: document.getElementById("status"), + zoomIn: document.getElementById("zoom-in"), + zoomOut: document.getElementById("zoom-out"), + zoomLabel: document.getElementById("zoom-label"), + fitView: document.getElementById("fit-view"), + showMode: document.getElementById("show-mode"), + showModeMenu: document.getElementById("show-mode-menu"), + viewModeControl: document.getElementById("view-mode-control"), + viewModeCurrent: document.getElementById("view-mode-current"), + viewModeDiff: document.getElementById("view-mode-diff"), + copyLink: document.getElementById("copy-link"), +}; + +let schema; +let diffViewSchema; +let currentViewSchema; +let diffMetadata; +let erdDiff; +let layout; +let selectedTable; +let searchText = ""; +let viewport = { x: 32, y: 32, z: 1 }; +let hasZoomFromHash = false; +let userAdjustedViewport = false; +let hashUpdatesDisabled = false; +let showMode = DEFAULT_SHOW_MODE; +let activeCardDrag; +let activeCanvasPan; +let activeViewportAnimation; +let suppressNextCanvasClick = false; +let viewMode = DEFAULT_VIEW_MODE; +const manualNodePositions = new Map(); +const hiddenTableNames = new Set(); +let diffIndex = new Map(); +let tableDiffIndex = new Map(); + +const DIFF_LABELS = { + added: "Added", + changed: "Changed", + removed: "Removed", +}; + +const DIFF_MARKS = { + added: "+", + changed: "~", + removed: "-", +}; + +const DIFF_ACTION_RANK = { + added: 2, + changed: 1, + removed: 3, +}; + +function escapeHtml(value) { + return String(value ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} + +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + +function tableByName(name) { + return schema?.tables.find((table) => table.name === name); +} + +function strongestDiffAction(current, next) { + if (!current) return next; + if (!next) return current; + return DIFF_ACTION_RANK[next] > DIFF_ACTION_RANK[current] ? next : current; +} + +function diffKey(entity, path) { + return `${entity}:${path}`; +} + +function diffChange(entity, path) { + return diffIndex.get(diffKey(entity, path)); +} + +function setTableDiffAction(tableName, action) { + if (!tableName || !action) return; + tableDiffIndex.set(tableName, strongestDiffAction(tableDiffIndex.get(tableName), action)); +} + +function tableNameFromEntityPath(path) { + return path.split(".")[0]; +} + +function tableNamesFromRelationChange(change) { + const relation = schema?.relations.find((item) => item.name === change.path); + if (relation) return [relation.sourceTable, relation.targetTable]; + + const [source, target] = change.path.split("->"); + return [tableNameFromEntityPath(source || ""), tableNameFromEntityPath(target || "")].filter( + Boolean, + ); +} + +function setDiff(nextDiff) { + erdDiff = nextDiff; + diffIndex = new Map(); + tableDiffIndex = new Map(); + if (!nextDiff?.changes) return; + + for (const change of nextDiff.changes) { + diffIndex.set(diffKey(change.entity, change.path), change); + if (change.entity === "table") { + setTableDiffAction(change.path, change.action); + continue; + } + if (change.entity === "column" || change.entity === "index") { + setTableDiffAction(tableNameFromEntityPath(change.path), "changed"); + continue; + } + if (change.entity === "relation") { + for (const tableName of tableNamesFromRelationChange(change)) { + setTableDiffAction(tableName, "changed"); + } + } + } +} + +function diffClass(action) { + return action ? `is-diff-${action}` : ""; +} + +function diffBadge(action, detail) { + if (!action) return ""; + const title = detail ? `${DIFF_LABELS[action]}: ${detail}` : DIFF_LABELS[action]; + return `${DIFF_MARKS[action]}`; +} + +function diffDetail(change) { + if (!change?.detail) return ""; + return `${escapeHtml(change.detail)}`; +} + +function tableDiffChange(tableName) { + return diffChange("table", tableName); +} + +function tableDiffAction(tableName) { + return tableDiffIndex.get(tableName); +} + +function columnDiffChange(tableName, columnName) { + return diffChange("column", `${tableName}.${columnName}`); +} + +function indexDiffChange(tableName, indexName) { + return diffChange("index", `${tableName}.${indexName}`); +} + +function relationDiffChange(relation) { + return diffChange("relation", relation.name); +} + +function relationDiffAction(relation) { + return relationDiffChange(relation)?.action; +} + +function showModeOption(value) { + return SHOW_MODE_OPTIONS.find((option) => option.value === value); +} + +function isTableHidden(tableName) { + return hiddenTableNames.has(tableName); +} + +function visibleTables() { + return schema.tables.filter((table) => !isTableHidden(table.name)); +} + +function visibleTableNames() { + return visibleTables().map((table) => table.name); +} + +function readHashState() { + const params = new URLSearchParams(location.hash.slice(1)); + selectedTable = params.get("table") || undefined; + const nextShowMode = params.get("show"); + if (showModeOption(nextShowMode)) { + showMode = nextShowMode; + } + const nextViewMode = params.get("view"); + if (nextViewMode === "current" || nextViewMode === "diff") { + viewMode = nextViewMode; + } + hiddenTableNames.clear(); + for (const tableName of (params.get("hidden") || "").split(",")) { + if (tableName) hiddenTableNames.add(tableName); + } + const z = Number(params.get("z")); + if (params.has("z") && Number.isFinite(z)) { + viewport = { ...viewport, z: clamp(z, MIN_ZOOM, MAX_ZOOM) }; + hasZoomFromHash = true; + } +} + +function writeHashState() { + if (hashUpdatesDisabled) return; + const params = new URLSearchParams(); + if (selectedTable) params.set("table", selectedTable); + if (showMode !== DEFAULT_SHOW_MODE) params.set("show", showMode); + if (hiddenTableNames.size > 0) { + params.set("hidden", [...hiddenTableNames].toSorted((a, b) => a.localeCompare(b)).join(",")); + } + if (canSwitchViewMode() && viewMode !== DEFAULT_VIEW_MODE) { + params.set("view", viewMode); + } + params.set("z", viewport.z.toFixed(3)); + try { + history.replaceState(null, "", `#${params.toString()}`); + } catch { + // Disable further hash writes once they fail (e.g. sandboxed artifact + // preview) so panning/zooming does not throw on every frame. + hashUpdatesDisabled = true; + } +} + +const SCHEMA_BLOCK_PATTERN = + / + + diff --git a/packages/sdk-tailordb-erd-plugin/src/viewer-assets/serve.json b/packages/sdk-tailordb-erd-plugin/src/viewer-assets/serve.json new file mode 100644 index 000000000..31a8ab4e3 --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/viewer-assets/serve.json @@ -0,0 +1,13 @@ +{ + "headers": [ + { + "source": "**/*.@(html|json)", + "headers": [ + { + "key": "Cache-Control", + "value": "no-cache" + } + ] + } + ] +} diff --git a/packages/sdk-tailordb-erd-plugin/src/viewer-assets/styles.css b/packages/sdk-tailordb-erd-plugin/src/viewer-assets/styles.css new file mode 100644 index 000000000..747087dc3 --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/viewer-assets/styles.css @@ -0,0 +1,1036 @@ +:root { + color-scheme: dark; + --bg: #111313; + --canvas: #101111; + --panel: #202322; + --panel-raised: #262928; + --line: #343938; + --line-strong: #555d5b; + --text: #e7ecea; + --muted: #a0a7a4; + --subtle: #2d3130; + --accent: #4950e5; + --accent-weak: rgba(73, 80, 229, 0.18); + --accent-glow: rgba(73, 80, 229, 0.34); + --warn: #f2b84b; + --danger: #ff6b5f; + --diff-added: #4ed983; + --diff-added-bg: rgba(78, 217, 131, 0.14); + --diff-changed: #f2b84b; + --diff-changed-bg: rgba(242, 184, 75, 0.14); + --diff-removed: #ff6b5f; + --diff-removed-bg: rgba(255, 107, 95, 0.14); + --shadow: 0 16px 34px rgba(0, 0, 0, 0.42); + --card-width: 260px; + --toolbar-height: 56px; +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + width: 100%; + height: 100%; + margin: 0; +} + +body { + background: var(--bg); + color: var(--text); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; + letter-spacing: 0; +} + +button, +input { + font: inherit; +} + +button { + height: 34px; + border: 1px solid var(--line); + border-radius: 7px; + background: #1a1d1c; + color: var(--text); + cursor: pointer; +} + +button:hover { + border-color: var(--line-strong); + background: #242827; +} + +.app { + display: grid; + grid-template-rows: var(--toolbar-height) 1fr; + min-width: 0; +} + +.toolbar { + display: grid; + grid-template-columns: minmax(220px, 1fr) minmax(240px, 360px) minmax(120px, 1fr); + align-items: center; + gap: 18px; + padding: 8px 16px; + border-bottom: 1px solid var(--line); + background: #222525; +} + +.brand { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.brand-mark { + display: inline-grid; + width: 30px; + height: 30px; + place-items: center; + border-radius: 35%; + background: var(--accent); + color: #fff; + font-weight: 800; +} + +.brand h1 { + margin: 0; + overflow: hidden; + font-size: 16px; + font-weight: 700; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.brand p { + margin: 2px 0 0; + overflow: hidden; + color: var(--muted); + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.toolbar-search input { + width: 100%; + height: 34px; + border: 1px solid var(--line); + border-radius: 7px; + padding: 0 12px; + outline: none; + background: #141716; + color: var(--text); +} + +.toolbar-search input::placeholder { + color: #777e7b; +} + +.toolbar-search input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-weak); +} + +.toolbar-actions { + display: flex; + align-items: center; + gap: 10px; + justify-content: flex-end; +} + +.view-mode-control { + display: inline-grid; + grid-template-columns: repeat(2, minmax(72px, 1fr)); + min-width: 152px; + border: 1px solid var(--line); + border-radius: 8px; + padding: 2px; + background: #171a19; +} + +.view-mode-control[hidden] { + display: none; +} + +.view-mode-button { + height: 28px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--muted); + font-size: 12px; + font-weight: 800; +} + +.view-mode-button:hover { + background: #252a28; + color: var(--text); +} + +.view-mode-button[aria-pressed="true"] { + background: var(--accent); + color: #fff; +} + +.primary-action { + min-width: 104px; + border-color: var(--accent); + background: var(--accent); + color: #fff; + font-weight: 800; +} + +.primary-action:hover { + background: #5f65ee; +} + +.main { + display: grid; + min-height: 0; + grid-template-columns: 240px minmax(0, 1fr) 360px; +} + +.main.is-details-collapsed { + grid-template-columns: 240px minmax(0, 1fr); +} + +.table-nav, +.details { + min-width: 0; + overflow: auto; + border-color: var(--line); + background: var(--panel); +} + +.table-nav { + border-right: 1px solid var(--line); +} + +.details { + border-left: 1px solid var(--line); +} + +.nav-head { + display: flex; + position: sticky; + top: 0; + z-index: 2; + align-items: center; + justify-content: space-between; + gap: 8px; + height: 52px; + padding: 0 12px; + border-bottom: 1px solid var(--line); + background: var(--panel); +} + +.nav-title { + display: grid; + min-width: 0; + gap: 2px; +} + +.nav-head h2 { + margin: 0; + font-size: 13px; +} + +.nav-head span { + color: var(--muted); + font-size: 12px; +} + +.table-list { + display: grid; + gap: 1px; + padding: 8px 0; +} + +.table-list-row { + display: grid; + grid-template-columns: minmax(0, 1fr) 32px; + align-items: center; + min-width: 0; +} + +.table-select { + display: grid; + grid-template-columns: 16px minmax(0, 1fr); + align-items: center; + gap: 8px; + width: 100%; + height: 34px; + border: 0; + border-radius: 0; + padding: 0 12px; + background: transparent; + color: var(--muted); + text-align: left; +} + +.table-select:hover { + background: #252a28; + color: var(--text); +} + +.table-select[aria-current="true"] { + background: linear-gradient(90deg, var(--accent-weak), rgba(73, 80, 229, 0.04)); + color: var(--text); + font-weight: 700; +} + +.table-list-label, +.table-name-wrap, +.field-name, +.detail-name { + display: flex; + min-width: 0; + align-items: center; + gap: 6px; +} + +.table-name-wrap { + flex: 1 1 auto; +} + +.table-list-name, +.field-name-text { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.diff-badge { + display: inline-grid; + flex: 0 0 auto; + min-width: 16px; + height: 16px; + place-items: center; + border: 1px solid currentColor; + border-radius: 999px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 11px; + font-weight: 800; + line-height: 1; +} + +.diff-badge-added { + background: var(--diff-added-bg); + color: var(--diff-added); +} + +.diff-badge-changed { + background: var(--diff-changed-bg); + color: var(--diff-changed); +} + +.diff-badge-removed { + background: var(--diff-removed-bg); + color: var(--diff-removed); +} + +.diff-detail { + display: block; + margin-top: 5px; + color: var(--muted); + font-size: 11px; + line-height: 1.35; + overflow-wrap: anywhere; +} + +.detail-row.is-diff-added .diff-detail { + color: var(--diff-added); +} + +.detail-row.is-diff-changed .diff-detail { + color: var(--diff-changed); +} + +.detail-row.is-diff-removed .diff-detail { + color: var(--diff-removed); +} + +.table-list-row.is-diff-added .table-select { + background: linear-gradient(90deg, var(--diff-added-bg), transparent); + color: var(--text); +} + +.table-list-row.is-diff-changed .table-select { + background: linear-gradient(90deg, var(--diff-changed-bg), transparent); + color: var(--text); +} + +.table-list-row.is-diff-removed .table-select { + background: linear-gradient(90deg, var(--diff-removed-bg), transparent); + color: var(--text); +} + +.table-list-row.is-hidden .table-select { + color: #69716e; +} + +.table-visibility-toggle { + display: grid; + width: 28px; + height: 28px; + place-items: center; + border: 0; + border-radius: 6px; + padding: 0; + background: transparent; + color: var(--muted); +} + +.table-visibility-toggle:hover:not(:disabled) { + background: #252a28; + color: var(--text); +} + +.table-visibility-toggle:disabled { + opacity: 0.42; +} + +.eye-icon { + width: 15px; + height: 15px; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.8; +} + +.table-list-icon, +.table-icon { + display: inline-block; + width: 14px; + height: 14px; + border: 1px solid currentColor; + border-radius: 2px; + opacity: 0.86; + background: + linear-gradient(90deg, transparent 45%, currentColor 45%, currentColor 55%, transparent 55%), + linear-gradient(transparent 45%, currentColor 45%, currentColor 55%, transparent 55%); +} + +.canvas { + position: relative; + min-width: 0; + overflow: hidden; + background: + radial-gradient(circle, rgba(255, 255, 255, 0.11) 1px, transparent 1.2px), var(--canvas); + background-size: 22px 22px; + cursor: grab; + touch-action: none; + user-select: none; +} + +.canvas.is-panning { + cursor: grabbing; +} + +.world { + position: absolute; + top: 0; + left: 0; + transform-origin: 0 0; +} + +.edges, +.nodes { + position: absolute; + top: 0; + left: 0; +} + +.edges { + overflow: visible; + pointer-events: none; +} + +.edge { + fill: none; + stroke: #4c5351; + stroke-width: 2; +} + +.edge.is-selected { + filter: drop-shadow(0 0 7px var(--accent-glow)); + stroke: var(--accent); + stroke-width: 3; +} + +.edge.is-diff-added { + stroke: var(--diff-added); +} + +.edge.is-diff-changed { + stroke: var(--diff-changed); +} + +.edge.is-diff-removed { + stroke: var(--diff-removed); + stroke-dasharray: 8 5; +} + +.edge.is-selected.is-diff-added, +.edge.is-selected.is-diff-changed, +.edge.is-selected.is-diff-removed { + stroke-width: 3; +} + +.edge-cardinality { + pointer-events: none; +} + +.edge-cardinality line, +.edge-cardinality circle { + fill: var(--canvas); + stroke: #4c5351; + stroke-linecap: square; + stroke-linejoin: round; + stroke-width: 2; +} + +.edge-cardinality.is-selected line, +.edge-cardinality.is-selected circle { + fill: var(--canvas); + stroke: var(--accent); + stroke-width: 3; +} + +.edge-cardinality.is-diff-added line, +.edge-cardinality.is-diff-added circle { + stroke: var(--diff-added); +} + +.edge-cardinality.is-diff-changed line, +.edge-cardinality.is-diff-changed circle { + stroke: var(--diff-changed); +} + +.edge-cardinality.is-diff-removed line, +.edge-cardinality.is-diff-removed circle { + stroke: var(--diff-removed); +} + +.table-card { + appearance: none; + position: absolute; + display: flex; + flex-direction: column; + width: var(--card-width); + min-height: 62px; + align-items: stretch; + padding: 0; + border: 1px solid var(--line-strong); + border-radius: 8px; + background: var(--panel-raised); + box-shadow: 0 12px 26px rgba(0, 0, 0, 0.32); + color: var(--muted); + cursor: grab; + font: inherit; + text-align: left; + touch-action: none; + user-select: none; +} + +.table-card:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; +} + +.table-card.is-selected { + border-color: var(--accent); + box-shadow: + 0 0 0 1px var(--accent), + 0 0 26px var(--accent-glow), + 0 18px 32px rgba(0, 0, 0, 0.44); + color: var(--text); +} + +.table-card.is-related { + border-color: #55625d; + color: var(--text); +} + +.table-card.is-muted { + opacity: 0.36; +} + +.table-card.is-diff-added { + border-color: var(--diff-added); + box-shadow: + 0 0 0 1px rgba(78, 217, 131, 0.32), + 0 16px 30px rgba(0, 0, 0, 0.38); +} + +.table-card.is-diff-changed { + border-color: var(--diff-changed); + box-shadow: + 0 0 0 1px rgba(242, 184, 75, 0.3), + 0 16px 30px rgba(0, 0, 0, 0.38); +} + +.table-card.is-diff-removed { + border-color: var(--diff-removed); + box-shadow: + 0 0 0 1px rgba(255, 107, 95, 0.32), + 0 16px 30px rgba(0, 0, 0, 0.38); +} + +.table-card.is-dragging { + z-index: 4; + cursor: grabbing; +} + +.table-head { + display: flex; + min-width: 0; + min-height: 60px; + align-items: center; + gap: 10px; + padding: 0 16px; +} + +.table-card.is-diff-added .table-head { + background: linear-gradient(90deg, var(--diff-added-bg), transparent); +} + +.table-card.is-diff-changed .table-head { + background: linear-gradient(90deg, var(--diff-changed-bg), transparent); +} + +.table-card.is-diff-removed .table-head { + background: linear-gradient(90deg, var(--diff-removed-bg), transparent); +} + +.table-card.is-diff-removed .table-name { + text-decoration: line-through; + text-decoration-color: var(--diff-removed); +} + +.table-name { + min-width: 0; + overflow: hidden; + font-size: 16px; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +} + +.table-fields { + display: grid; + border-top: 1px solid var(--line); +} + +.table-field { + display: grid; + min-width: 0; + height: 28px; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; + align-items: center; + padding: 0 10px 0 16px; + color: var(--muted); + font-size: 11px; +} + +.table-field + .table-field { + border-top: 1px solid #303534; +} + +.table-field.is-key { + color: var(--text); +} + +.table-field.is-diff-added { + background: var(--diff-added-bg); + color: var(--text); +} + +.table-field.is-diff-changed { + background: var(--diff-changed-bg); + color: var(--text); +} + +.table-field.is-diff-removed { + background: var(--diff-removed-bg); + color: var(--text); +} + +.table-field.is-diff-removed .field-name-text { + text-decoration: line-through; + text-decoration-color: var(--diff-removed); +} + +.field-type { + max-width: 92px; + overflow: hidden; + color: #7f8784; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 10px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.details-inner { + display: grid; + gap: 18px; + padding: 16px; +} + +.details h2, +.details h3 { + margin: 0; + line-height: 1.2; +} + +.details h2 { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; +} + +.details h3 { + color: var(--text); + font-size: 14px; +} + +.details p { + margin: 6px 0 0; + color: var(--muted); + font-size: 13px; + line-height: 1.45; +} + +.details-section { + display: grid; + gap: 10px; + border-top: 1px solid var(--line); + padding-top: 16px; +} + +.detail-list { + display: grid; + gap: 8px; +} + +.detail-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: start; + border: 1px solid var(--line); + border-radius: 4px; + padding: 9px; + background: #2a2d2c; +} + +.detail-row.is-diff-added { + border-color: rgba(78, 217, 131, 0.58); + background: linear-gradient(90deg, var(--diff-added-bg), #2a2d2c 72%); +} + +.detail-row.is-diff-changed { + border-color: rgba(242, 184, 75, 0.58); + background: linear-gradient(90deg, var(--diff-changed-bg), #2a2d2c 72%); +} + +.detail-row.is-diff-removed { + border-color: rgba(255, 107, 95, 0.58); + background: linear-gradient(90deg, var(--diff-removed-bg), #2a2d2c 72%); +} + +.detail-row.is-diff-removed strong { + text-decoration: line-through; + text-decoration-color: var(--diff-removed); +} + +.detail-row strong { + display: block; + overflow-wrap: anywhere; + font-size: 12px; +} + +.detail-row code, +.pill { + border-radius: 4px; + background: #363a39; + padding: 2px 6px; + color: var(--text); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 11px; +} + +.pill-wrap { + display: flex; + flex-wrap: wrap; + gap: 5px; + margin-top: 7px; +} + +.muted { + color: var(--muted); +} + +.empty-state, +.status { + position: absolute; + left: 50%; + transform: translateX(-50%); + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + box-shadow: var(--shadow); +} + +.empty-state { + top: 45%; + padding: 16px 20px; + color: var(--muted); +} + +.status { + bottom: 78px; + max-width: min(560px, calc(100% - 32px)); + padding: 10px 12px; + color: var(--danger); + font-size: 12px; +} + +.canvas-toolbar { + display: flex; + position: absolute; + bottom: 18px; + left: 50%; + z-index: 3; + align-items: center; + gap: 10px; + height: 42px; + border: 1px solid var(--line); + border-radius: 10px; + padding: 0 12px; + background: rgba(38, 41, 40, 0.96); + box-shadow: var(--shadow); + transform: translateX(-50%); +} + +.canvas-toolbar button { + width: 28px; + height: 28px; + border: 0; + padding: 0; + background: transparent; + color: var(--text); + font-size: 17px; +} + +#fit-view { + width: auto; + padding: 0 6px; + font-size: 13px; + font-weight: 700; +} + +.show-mode-control { + position: relative; + display: flex; +} + +.show-mode-button { + display: flex; + width: auto !important; + min-width: 104px; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 0 8px !important; + color: var(--text); + font-size: 12px !important; + font-weight: 700; +} + +.show-mode-caret { + width: 7px; + height: 7px; + border-right: 1.5px solid currentColor; + border-bottom: 1.5px solid currentColor; + transform: translateY(-2px) rotate(45deg); +} + +.show-mode-menu { + position: absolute; + right: 0; + bottom: calc(100% + 10px); + z-index: 4; + display: grid; + width: 158px; + gap: 2px; + border: 1px solid var(--line); + border-radius: 8px; + padding: 5px; + background: rgba(32, 35, 34, 0.98); + box-shadow: var(--shadow); +} + +.show-mode-menu[hidden] { + display: none; +} + +.show-mode-option { + display: grid; + width: 100% !important; + height: 30px !important; + grid-template-columns: minmax(0, 1fr) 14px; + gap: 8px; + align-items: center; + border-radius: 6px !important; + padding: 0 8px !important; + color: var(--muted) !important; + font-size: 12px !important; + text-align: left; +} + +.show-mode-option:hover, +.show-mode-option[aria-checked="true"] { + background: #2a2e2d !important; + color: var(--text) !important; +} + +.show-mode-check { + display: grid; + place-items: center; +} + +.check-icon { + width: 13px; + height: 13px; + fill: none; + stroke: var(--accent); + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.3; +} + +#zoom-label { + width: 48px; + color: var(--text); + font-size: 13px; + font-weight: 700; + text-align: center; +} + +.toolbar-divider { + width: 1px; + height: 24px; + background: var(--line); +} + +@media (max-width: 980px) { + .toolbar { + grid-template-columns: minmax(180px, 1fr) minmax(220px, 320px) auto; + } + + .main { + grid-template-rows: minmax(0, 1fr) minmax(220px, 36vh); + grid-template-columns: 220px minmax(0, 1fr); + } + + .main.is-details-collapsed { + grid-template-rows: minmax(0, 1fr); + grid-template-columns: 220px minmax(0, 1fr); + } + + .details { + grid-column: 1 / -1; + border-top: 1px solid var(--line); + border-left: 0; + } +} + +@media (max-width: 700px) { + html, + body, + #app { + height: auto; + min-height: 100%; + } + + .app { + grid-template-rows: auto auto; + min-height: 100%; + } + + .toolbar { + grid-template-columns: 1fr auto; + gap: 10px; + padding: 8px; + } + + .toolbar-actions { + grid-column: 1 / -1; + justify-content: stretch; + } + + .view-mode-control { + flex: 1 1 auto; + } + + .toolbar-search { + grid-column: 1 / -1; + } + + .brand-mark { + display: none; + } + + .brand h1 { + font-size: 16px; + } + + .brand p { + overflow-wrap: anywhere; + white-space: normal; + } + + .main { + min-height: auto; + grid-template-rows: auto minmax(520px, 64vh) auto; + grid-template-columns: 1fr; + } + + .main.is-details-collapsed { + grid-template-rows: auto minmax(520px, 64vh); + grid-template-columns: 1fr; + } + + .table-nav { + max-height: 240px; + border-right: 0; + border-bottom: 1px solid var(--line); + } + + .canvas { + min-height: 520px; + } + + .details { + max-height: none; + overflow: visible; + } +} diff --git a/packages/sdk-tailordb-erd-plugin/src/viewer.test.ts b/packages/sdk-tailordb-erd-plugin/src/viewer.test.ts new file mode 100644 index 000000000..a4720bf90 --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/src/viewer.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, test } from "vitest"; +import { buildViewerHtml } from "./viewer"; +import type { TailorDbErdSchema } from "./types"; + +function buildSchema(overrides: Partial = {}): TailorDbErdSchema { + return { + version: 1, + namespace: "tailordb", + generatedAt: "2026-01-01T00:00:00.000Z", + revision: "test-revision", + source: "local", + cleanRoom: { implementation: "tailor", notes: [] }, + tables: [ + { + name: "User", + pluralForm: "users", + columns: [], + indexes: [], + forwardRelationships: [], + backwardRelationships: [], + }, + ], + relations: [], + ...overrides, + }; +} + +// Extract and parse the embedded schema JSON the same way the viewer (and any +// external tooling such as a future ERD diff) would. +function extractEmbeddedSchema(html: string): unknown { + const match = html.match(/" }), + }); + + expect(html).not.toContain(""); + expect(html).toContain("\\u003c/script>\\u003cimg src=x>"); + // The escaped value round-trips back to the original via JSON.parse. + expect(extractEmbeddedSchema(html)).toMatchObject({ namespace: "" }); + }); + + test("preserves U+2028/U+2029 in embedded values via JSON round-trip", () => { + const lineSep = String.fromCharCode(0x2028); + const paraSep = String.fromCharCode(0x2029); + const description = `line${lineSep}para${paraSep}end`; + const html = buildViewerHtml({ + schema: buildSchema({ + tables: [ + { + name: "User", + pluralForm: "users", + description, + columns: [], + indexes: [], + forwardRelationships: [], + backwardRelationships: [], + }, + ], + }), + }); + + const parsed = extractEmbeddedSchema(html) as TailorDbErdSchema; + expect(parsed.tables[0]?.description).toBe(description); + }); + + test("embeds optional diff data for the viewer", () => { + const html = buildViewerHtml({ + schema: buildSchema(), + diff: { + namespace: "tailordb", + baseRevision: "base", + headRevision: "head", + changed: true, + summary: { added: 1, changed: 0, removed: 0 }, + changes: [{ action: "added", entity: "table", path: "User", detail: "Table added" }], + }, + }); + + expect(html).toContain(''; + +export interface WriteViewerDistOptions { + schema: TailorDbErdSchema; + distDir: string; +} + +export interface BuildViewerHtmlOptions { + schema: TailorDbErdSchema; + currentSchema?: TailorDbErdSchema; + diff?: unknown; + title?: string; +} + +function escapeHtml(value: string): string { + return value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} + +function assetDirCandidates(): string[] { + const currentDir = path.dirname(fileURLToPath(import.meta.url)); + return [path.join(currentDir, "viewer-assets")]; +} + +/** + * Resolve the packaged ERD viewer asset directory. + * @returns Absolute path to the viewer asset directory. + */ +export function resolveViewerAssetsDir(): string { + for (const candidate of assetDirCandidates()) { + if (fs.existsSync(path.join(candidate, "index.html"))) { + return candidate; + } + } + + throw new Error(`ERD viewer assets not found. Checked: ${assetDirCandidates().join(", ")}`); +} + +/** + * Build the self-contained ERD viewer HTML document. CSS, JS, and the schema + * are inlined as separately extractable blocks: a ``) + .replace( + APP_SCRIPT, + () => `${embedScript}${currentSchemaScript}${diffScript}\n ${inlineScript}`, + ); +} + +/** + * Write the self-contained TailorDB ERD viewer to `/index.html`. + * @param options - Viewer dist write options. + */ +export function writeViewerDist(options: WriteViewerDistOptions): void { + fs.rmSync(options.distDir, { recursive: true, force: true }); + fs.mkdirSync(options.distDir, { recursive: true }); + fs.writeFileSync( + path.join(options.distDir, "index.html"), + buildViewerHtml({ schema: options.schema }), + "utf8", + ); +} diff --git a/packages/sdk-tailordb-erd-plugin/tsconfig.json b/packages/sdk-tailordb-erd-plugin/tsconfig.json new file mode 100644 index 000000000..4bafc4f43 --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "types": ["node"], + "incremental": true, + "tsBuildInfoFile": "./.tsbuildinfo" + }, + "include": ["src/**/*.ts", "tsdown.config.ts", "vitest.config.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/sdk-tailordb-erd-plugin/tsdown.config.ts b/packages/sdk-tailordb-erd-plugin/tsdown.config.ts new file mode 100644 index 000000000..62882d5ea --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/tsdown.config.ts @@ -0,0 +1,22 @@ +import { cpSync, rmSync } from "node:fs"; +import path from "node:path"; +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + target: "node22", + platform: "node", + clean: true, + outDir: "dist", + tsconfig: "./tsconfig.json", + deps: { neverBundle: [/^@tailor-platform\/sdk$/, /^@tailor-platform\/sdk\//] }, + outExtensions: () => ({ + js: ".js", + }), + onSuccess: (config) => { + const target = path.resolve(config.outDir, "viewer-assets"); + rmSync(target, { recursive: true, force: true }); + cpSync(path.resolve("src/viewer-assets"), target, { recursive: true }); + }, +}); diff --git a/packages/sdk-tailordb-erd-plugin/vitest.config.ts b/packages/sdk-tailordb-erd-plugin/vitest.config.ts new file mode 100644 index 000000000..b8ea3230b --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/?(*.)+(spec|test).ts"], + exclude: ["**/node_modules/**", "**/dist/**"], + environment: "node", + globals: true, + watch: false, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa900329b..52bb805db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -646,6 +646,58 @@ importers: specifier: 4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.0.16(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) + packages/sdk-tailordb-erd-plugin: + dependencies: + chalk: + specifier: 5.6.2 + version: 5.6.2 + chokidar: + specifier: 5.0.0 + version: 5.0.0 + mime-types: + specifier: 3.0.2 + version: 3.0.2 + open: + specifier: 11.0.0 + version: 11.0.0 + pathe: + specifier: 2.0.3 + version: 2.0.3 + pkg-types: + specifier: 2.3.1 + version: 2.3.1 + politty: + specifier: 0.11.2 + version: 0.11.2(@clack/prompts@1.7.0)(@inquirer/prompts@8.5.2(@types/node@24.13.3))(zod@4.4.3) + zod: + specifier: 4.4.3 + version: 4.4.3 + devDependencies: + '@tailor-platform/sdk': + specifier: workspace:^ + version: link:../sdk + '@types/mime-types': + specifier: 3.0.1 + version: 3.0.1 + '@types/node': + specifier: 24.13.3 + version: 24.13.3 + eslint-plugin-zod: + specifier: 4.7.0 + version: 4.7.0(eslint@10.5.0(jiti@2.7.0))(oxlint@1.73.0(oxlint-tsgolint@0.24.0))(typescript@6.0.3)(zod@4.4.3) + oxlint: + specifier: 1.73.0 + version: 1.73.0(oxlint-tsgolint@0.24.0) + tsdown: + specifier: 0.22.4 + version: 0.22.4(@typescript/native-preview@7.0.0-dev.20260707.2)(oxc-resolver@11.21.3)(publint@0.3.21)(tsx@4.23.0)(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vitest: + specifier: 4.1.10 + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.0.16(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) + packages/tailor-proto: dependencies: '@bufbuild/protobuf': From 733845732fabff504c32b725f6919be6167bc7a1 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 08:46:28 +0900 Subject: [PATCH 555/618] feat(sdk)!: dispatch tailordb erd to the sdk-tailordb-erd-plugin CLI plugin Remove the built-in tailordb erd command group. tailor tailordb erd now dispatches to the tailor-tailordb-erd executable installed by @tailor-platform/sdk-tailordb-erd-plugin, and the CLI suggests the install command when the plugin is missing. Update the CLI docs, the repository ERD schema workflow, and the example project accordingly. --- .changeset/add-erd-plugin-package.md | 5 + .changeset/cli-plugin-schema-api.md | 5 + .changeset/extract-erd-plugin.md | 5 + .github/workflows/erd-schema.yml | 6 +- example/package.json | 1 + packages/sdk/docs/cli-reference.md | 9 +- packages/sdk/docs/cli-reference.template.md | 4 +- packages/sdk/docs/cli/tailordb.md | 148 +- packages/sdk/docs/cli/tailordb.template.md | 51 +- packages/sdk/docs/github-actions.md | 8 + packages/sdk/knip.ts | 1 - .../cli/commands/setup/workflow-lint.test.ts | 2 +- .../src/cli/commands/tailordb/erd/deploy.ts | 75 - .../tailordb/erd/diff-command.test.ts | 107 -- .../cli/commands/tailordb/erd/diff-command.ts | 143 -- .../cli/commands/tailordb/erd/diff.test.ts | 362 ----- .../sdk/src/cli/commands/tailordb/erd/diff.ts | 427 ----- .../src/cli/commands/tailordb/erd/export.ts | 217 --- .../src/cli/commands/tailordb/erd/index.ts | 16 - .../tailordb/erd/local-schema.test.ts | 31 - .../cli/commands/tailordb/erd/local-schema.ts | 61 - .../cli/commands/tailordb/erd/schema.test.ts | 315 ---- .../src/cli/commands/tailordb/erd/schema.ts | 288 ---- .../cli/commands/tailordb/erd/serve.test.ts | 48 - .../src/cli/commands/tailordb/erd/serve.ts | 492 ------ .../src/cli/commands/tailordb/erd/types.ts | 104 -- .../src/cli/commands/tailordb/erd/utils.ts | 42 - .../tailordb/erd/viewer-assets/app.js | 1424 ----------------- .../tailordb/erd/viewer-assets/index.html | 77 - .../tailordb/erd/viewer-assets/serve.json | 13 - .../tailordb/erd/viewer-assets/styles.css | 1036 ------------ .../cli/commands/tailordb/erd/viewer.test.ts | 107 -- .../src/cli/commands/tailordb/erd/viewer.ts | 121 -- .../sdk/src/cli/commands/tailordb/index.ts | 4 +- packages/sdk/src/cli/completion.test.ts | 15 +- packages/sdk/src/cli/index.ts | 21 +- packages/sdk/src/cli/shared/beta.ts | 2 +- packages/sdk/src/cli/shared/plugin.ts | 9 + .../sdk/src/cli/shared/readonly-guard.test.ts | 4 - packages/sdk/tsdown.config.ts | 10 +- pnpm-lock.yaml | 3 + 41 files changed, 87 insertions(+), 5732 deletions(-) create mode 100644 .changeset/add-erd-plugin-package.md create mode 100644 .changeset/cli-plugin-schema-api.md create mode 100644 .changeset/extract-erd-plugin.md delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/deploy.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/diff-command.test.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/diff-command.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/diff.test.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/diff.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/export.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/index.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/local-schema.test.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/local-schema.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/schema.test.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/schema.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/serve.test.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/serve.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/types.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/utils.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/viewer-assets/app.js delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/viewer-assets/index.html delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/viewer-assets/serve.json delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/viewer-assets/styles.css delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/viewer.test.ts delete mode 100644 packages/sdk/src/cli/commands/tailordb/erd/viewer.ts diff --git a/.changeset/add-erd-plugin-package.md b/.changeset/add-erd-plugin-package.md new file mode 100644 index 000000000..dcac7863b --- /dev/null +++ b/.changeset/add-erd-plugin-package.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-tailordb-erd-plugin": minor +--- + +New Tailor CLI plugin providing the `tailor tailordb erd` commands (export, diff, serve, deploy), extracted from `@tailor-platform/sdk`. diff --git a/.changeset/cli-plugin-schema-api.md b/.changeset/cli-plugin-schema-api.md new file mode 100644 index 000000000..0cf677b15 --- /dev/null +++ b/.changeset/cli-plugin-schema-api.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": minor +--- + +Add `loadTailorDBNamespaces`, `deployStaticWebsite`, `assertWritable`, and `isPluginGeneratedType` (with their related types) to `@tailor-platform/sdk/cli`, so CLI plugins and scripts can load local TailorDB schema data and deploy static websites through the public API. diff --git a/.changeset/extract-erd-plugin.md b/.changeset/extract-erd-plugin.md new file mode 100644 index 000000000..02f5e6fbd --- /dev/null +++ b/.changeset/extract-erd-plugin.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Remove the built-in `tailordb erd` commands. They are now provided by the `@tailor-platform/sdk-tailordb-erd-plugin` CLI plugin: install it with `npm install -D @tailor-platform/sdk-tailordb-erd-plugin` and keep running `tailor tailordb erd ` as before — the CLI dispatches to the plugin automatically and suggests the install command when the plugin is missing. The `erdSite` TailorDB setting is unchanged. diff --git a/.github/workflows/erd-schema.yml b/.github/workflows/erd-schema.yml index 07086143d..00b9ff489 100644 --- a/.github/workflows/erd-schema.yml +++ b/.github/workflows/erd-schema.yml @@ -5,12 +5,12 @@ on: push: branches: [main, v2] paths: - - packages/sdk/src/cli/commands/tailordb/erd/** + - packages/sdk-tailordb-erd-plugin/** - example/** - .github/workflows/erd-schema.yml pull_request: paths: - - packages/sdk/src/cli/commands/tailordb/erd/** + - packages/sdk-tailordb-erd-plugin/** - example/** - .github/workflows/erd-schema.yml @@ -93,7 +93,7 @@ jobs: preview-artifact-name: ${{ matrix.namespace }}.html always-relevant-paths: | example/tailor.config.ts - packages/sdk/src/cli/commands/tailordb/erd/ + packages/sdk-tailordb-erd-plugin/ relevant-path-prefix: example/ github-token: ${{ github.token }} diff --git a/example/package.json b/example/package.json index 8f3f83240..ebd1a0d49 100644 --- a/example/package.json +++ b/example/package.json @@ -34,6 +34,7 @@ "devDependencies": { "@connectrpc/connect": "2.1.2", "@connectrpc/connect-node": "2.1.2", + "@tailor-platform/sdk-tailordb-erd-plugin": "workspace:^", "@types/node": "24.13.3", "@typescript/native-preview": "7.0.0-dev.20260707.2", "eslint-plugin-zod": "4.7.0", diff --git a/packages/sdk/docs/cli-reference.md b/packages/sdk/docs/cli-reference.md index 9bb022cd6..50eac5291 100644 --- a/packages/sdk/docs/cli-reference.md +++ b/packages/sdk/docs/cli-reference.md @@ -117,7 +117,9 @@ tailor hello world --loud ``` This also works under a built-in command group. The command path is joined with hyphens, so a plugin -nested under `tailordb` is named `tailor-tailordb-erd`: +nested under `tailordb` is named `tailor-tailordb-erd`. This is how the +[`@tailor-platform/sdk-tailordb-erd-plugin`](https://www.npmjs.com/package/@tailor-platform/sdk-tailordb-erd-plugin) +package provides the `tailordb erd` commands: ```bash # Runs `tailor-tailordb-erd` with: export @@ -195,11 +197,6 @@ Commands for managing TailorDB tables, data, and schema migrations. | [tailordb migration set](./cli/tailordb.md#tailordb-migration-set) | Set migration checkpoint to a specific number. | | [tailordb migration status](./cli/tailordb.md#tailordb-migration-status) | Show the current migration status for TailorDB namespaces, including applied and pending migrations. | | [tailordb migration sync](./cli/tailordb.md#tailordb-migration-sync) | Sync remote TailorDB schema to a specific migration snapshot (recovery from --no-schema-check drift). | -| [tailordb erd](./cli/tailordb.md#tailordb-erd) | Generate TailorDB ERD viewer artifacts from local TailorDB schema. (beta) | -| [tailordb erd export](./cli/tailordb.md#tailordb-erd-export) | Export TailorDB ERD static viewer from local TailorDB schema. | -| [tailordb erd diff](./cli/tailordb.md#tailordb-erd-diff) | Render TailorDB ERD schema diff HTML from exported ERD viewers. | -| [tailordb erd serve](./cli/tailordb.md#tailordb-erd-serve) | Generate and serve TailorDB ERD locally with watch reload. (beta) | -| [tailordb erd deploy](./cli/tailordb.md#tailordb-erd-deploy) | Deploy ERD static website for TailorDB namespace(s). | ### [Query Commands](./cli/query.md) diff --git a/packages/sdk/docs/cli-reference.template.md b/packages/sdk/docs/cli-reference.template.md index ca7af9783..d9726a2ef 100644 --- a/packages/sdk/docs/cli-reference.template.md +++ b/packages/sdk/docs/cli-reference.template.md @@ -110,7 +110,9 @@ tailor hello world --loud ``` This also works under a built-in command group. The command path is joined with hyphens, so a plugin -nested under `tailordb` is named `tailor-tailordb-erd`: +nested under `tailordb` is named `tailor-tailordb-erd`. This is how the +[`@tailor-platform/sdk-tailordb-erd-plugin`](https://www.npmjs.com/package/@tailor-platform/sdk-tailordb-erd-plugin) +package provides the `tailordb erd` commands: ```bash # Runs `tailor-tailordb-erd` with: export diff --git a/packages/sdk/docs/cli/tailordb.md b/packages/sdk/docs/cli/tailordb.md index 824f38a8f..c39415aec 100644 --- a/packages/sdk/docs/cli/tailordb.md +++ b/packages/sdk/docs/cli/tailordb.md @@ -16,11 +16,10 @@ See [Global Options](../cli-reference.md#global-options) for options available t **Commands** -| Command | Description | -| ------------------------------------------- | ------------------------------------------------------------------------- | -| [`tailordb truncate`](#tailordb-truncate) | Truncate (delete all records from) TailorDB tables. | -| [`tailordb migration`](#tailordb-migration) | Manage TailorDB schema migrations. | -| [`tailordb erd`](#tailordb-erd) | Generate TailorDB ERD viewer artifacts from local TailorDB schema. (beta) | +| Command | Description | +| ------------------------------------------- | --------------------------------------------------- | +| [`tailordb truncate`](#tailordb-truncate) | Truncate (delete all records from) TailorDB tables. | +| [`tailordb migration`](#tailordb-migration) | Manage TailorDB schema migrations. | ### tailordb truncate @@ -232,142 +231,11 @@ See [Global Options](../cli-reference.md#global-options) for options available t ### tailordb erd -Generate TailorDB ERD viewer artifacts from local TailorDB schema. (beta) - -**Usage** - -``` -tailor tailordb erd -``` - -See [Global Options](../cli-reference.md#global-options) for options available to all commands. - -**Commands** - -| Command | Description | -| --------------------------------------------- | ----------------------------------------------------------------- | -| [`tailordb erd export`](#tailordb-erd-export) | Export TailorDB ERD static viewer from local TailorDB schema. | -| [`tailordb erd diff`](#tailordb-erd-diff) | Render TailorDB ERD schema diff HTML from exported ERD viewers. | -| [`tailordb erd serve`](#tailordb-erd-serve) | Generate and serve TailorDB ERD locally with watch reload. (beta) | -| [`tailordb erd deploy`](#tailordb-erd-deploy) | Deploy ERD static website for TailorDB namespace(s). | - -#### tailordb erd export - -Export TailorDB ERD static viewer from local TailorDB schema. - -**Usage** - -``` -tailor tailordb erd export [options] -``` - -**Options** - -| Option | Alias | Description | Required | Default | Env | -| ------------------------- | ----- | ---------------------------------------------------------------------------------------------- | -------- | -------------------- | -------------------- | -| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | -| `--namespace ` | `-n` | TailorDB namespace name (optional if only one namespace is defined in config) | No | - | - | -| `--output ` | `-o` | Output directory path for TailorDB ERD viewer files (writes to `//dist`) | No | `".tailor/erd"` | - | - -See [Global Options](../cli-reference.md#global-options) for options available to all commands. - -#### tailordb erd diff - -Render TailorDB ERD schema diff HTML from exported ERD viewers. - -**Usage** - -``` -tailor tailordb erd diff [options] -``` - -**Options** - -| Option | Alias | Description | Required | Default | -| ----------------------------- | ----- | ----------------------------------------------------------------------- | -------- | ------- | -| `--base-html ` | - | Base ERD viewer HTML file | No | - | -| `--head-html ` | - | Head ERD viewer HTML file | No | - | -| `--namespace ` | `-n` | TailorDB namespace name (defaults to the provided ERD schema namespace) | No | - | -| `--output ` | `-o` | Output ERD diff HTML file | Yes | - | -| `--output-json ` | - | Optional output JSON file for the computed diff | No | - | - -See [Global Options](../cli-reference.md#global-options) for options available to all commands. - -#### tailordb erd serve - -Generate and serve TailorDB ERD locally with watch reload. (beta) - -**Usage** - -``` -tailor tailordb erd serve [options] -``` - -**Options** - -| Option | Alias | Description | Required | Default | Env | -| ------------------------- | ----- | ------------------------------------------------------------------------- | -------- | -------------------- | -------------------- | -| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | -| `--namespace ` | `-n` | TailorDB namespace name (uses first namespace in config if not specified) | No | - | - | -| `--port ` | - | Local server port (0 selects a free port) | No | `0` | - | -| `--open` | - | Open the ERD viewer in the default browser | No | `false` | - | - -See [Global Options](../cli-reference.md#global-options) for options available to all commands. - -#### tailordb erd deploy - -Deploy ERD static website for TailorDB namespace(s). - -**Usage** - -``` -tailor tailordb erd deploy [options] -``` - -**Options** - -| Option | Alias | Description | Required | Default | Env | -| ------------------------------- | ----- | ----------------------------------------------------------------------------------- | -------- | -------------------- | ------------------------------ | -| `--workspace-id ` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` | -| `--profile ` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` | -| `--config ` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` | -| `--namespace ` | `-n` | TailorDB namespace name (optional - deploys all namespaces with erdSite if omitted) | No | - | - | - -See [Global Options](../cli-reference.md#global-options) for options available to all commands. - -**Notes:** - -- ERD commands build from the local TailorDB schema, including plugin-generated TailorDB types. -- `tailordb erd export` writes a self-contained `index.html` viewer to `//dist`. -- `tailordb erd diff` compares exported self-contained viewer HTML files and writes a visual ERD viewer that can switch between the current schema and the highlighted diff. -- `tailordb erd serve` watches the config file and TailorDB type files, then reloads the browser viewer when the rebuilt `index.html` reports a new embedded schema revision. -- `tailordb erd deploy` still requires `erdSite` in `tailor.config.ts` because it uploads the generated viewer to a configured Static Website. - -**Usage Examples:** +The `tailordb erd` commands (export, diff, serve, deploy) are provided by the [`@tailor-platform/sdk-tailordb-erd-plugin`](https://www.npmjs.com/package/@tailor-platform/sdk-tailordb-erd-plugin) CLI plugin. Install it next to the SDK and keep running `tailor tailordb erd ` as before: ```bash -# Deploy ERD for all namespaces with erdSite configured -tailor tailordb erd deploy - -# Deploy ERD for a specific namespace -tailor tailordb erd deploy --namespace myNamespace - -# Deploy ERD with JSON output -tailor tailordb erd deploy --json +npm install -D @tailor-platform/sdk-tailordb-erd-plugin +tailor tailordb erd export --namespace myNamespace ``` -**Notes:** - -- This command is a beta feature and may introduce breaking changes in future releases -- Requires `erdSite` to be configured in `tailor.config.ts` for each namespace you want to deploy -- Example config: - ```typescript - export default defineConfig({ - db: { - myNamespace: { - // ... table definitions - erdSite: "my-erd-site-name", - }, - }, - }); - ``` +See the plugin's README for the full command reference. diff --git a/packages/sdk/docs/cli/tailordb.template.md b/packages/sdk/docs/cli/tailordb.template.md index 869ee7a72..4106d5dc0 100644 --- a/packages/sdk/docs/cli/tailordb.template.md +++ b/packages/sdk/docs/cli/tailordb.template.md @@ -70,54 +70,13 @@ Note: Migration scripts are automatically executed during `tailor deploy`. See [ **See also:** For migration concepts, configuration, workflow, and troubleshooting, see the [TailorDB Migrations guide](../services/tailordb-migration.md). -{{politty:command:tailordb erd:heading}} +### tailordb erd -{{politty:command:tailordb erd:description}} - -{{politty:command:tailordb erd:usage}} - -{{politty:command:tailordb erd:global-options-link}} - -{{politty:command:tailordb erd:subcommands}} - -{{politty:command:tailordb erd export}} -{{politty:command:tailordb erd diff}} -{{politty:command:tailordb erd serve}} -{{politty:command:tailordb erd deploy}} - -**Notes:** - -- ERD commands build from the local TailorDB schema, including plugin-generated TailorDB types. -- `tailordb erd export` writes a self-contained `index.html` viewer to `//dist`. -- `tailordb erd diff` compares exported self-contained viewer HTML files and writes a visual ERD viewer that can switch between the current schema and the highlighted diff. -- `tailordb erd serve` watches the config file and TailorDB type files, then reloads the browser viewer when the rebuilt `index.html` reports a new embedded schema revision. -- `tailordb erd deploy` still requires `erdSite` in `tailor.config.ts` because it uploads the generated viewer to a configured Static Website. - -**Usage Examples:** +The `tailordb erd` commands (export, diff, serve, deploy) are provided by the [`@tailor-platform/sdk-tailordb-erd-plugin`](https://www.npmjs.com/package/@tailor-platform/sdk-tailordb-erd-plugin) CLI plugin. Install it next to the SDK and keep running `tailor tailordb erd ` as before: ```bash -# Deploy ERD for all namespaces with erdSite configured -tailor tailordb erd deploy - -# Deploy ERD for a specific namespace -tailor tailordb erd deploy --namespace myNamespace - -# Deploy ERD with JSON output -tailor tailordb erd deploy --json +npm install -D @tailor-platform/sdk-tailordb-erd-plugin +tailor tailordb erd export --namespace myNamespace ``` -**Notes:** - -- This command is a beta feature and may introduce breaking changes in future releases -- Requires `erdSite` to be configured in `tailor.config.ts` for each namespace you want to deploy -- Example config: - ```typescript - export default defineConfig({ - db: { - myNamespace: { - // ... table definitions - erdSite: "my-erd-site-name", - }, - }, - }); - ``` +See the plugin's README for the full command reference. diff --git a/packages/sdk/docs/github-actions.md b/packages/sdk/docs/github-actions.md index 1232e71c9..ffe694ec4 100644 --- a/packages/sdk/docs/github-actions.md +++ b/packages/sdk/docs/github-actions.md @@ -72,6 +72,14 @@ to pull requests: tailor setup -n my-app-stg --erd-preview ``` +The generated workflow runs `tailor tailordb erd`, which is provided by the +`@tailor-platform/sdk-tailordb-erd-plugin` CLI plugin — install it as a +dev-dependency in your project: + +```bash +npm install -D @tailor-platform/sdk-tailordb-erd-plugin +``` + The generated workflow builds one self-contained ERD viewer HTML file for each owned TailorDB namespace in `tailor.config.ts`. The viewer compares the pull request merge result with the base branch, can switch between the current schema diff --git a/packages/sdk/knip.ts b/packages/sdk/knip.ts index 139a519cc..43bf9cc28 100644 --- a/packages/sdk/knip.ts +++ b/packages/sdk/knip.ts @@ -9,7 +9,6 @@ export default { "scripts/**", "e2e/fixtures/**", "src/cli/commands/deploy/__test_fixtures__/**", - "src/cli/commands/tailordb/erd/viewer-assets/**", "src/cli/ts-hook.d.mts", "src/types/*.ts", "src/vitest/integration/vitest.config.ts", diff --git a/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts b/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts index 30418c201..7c9e727b5 100644 --- a/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts +++ b/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts @@ -124,7 +124,7 @@ describe("repository ERD schema workflow", () => { const content = fs.readFileSync(ERD_SCHEMA_WORKFLOW, "utf-8"); expect(content).toContain("example/tailor.config.ts"); - expect(content).toContain("packages/sdk/src/cli/commands/tailordb/erd/"); + expect(content).toContain("packages/sdk-tailordb-erd-plugin/"); expect(content).toContain("relevant-path-prefix: example/"); }); diff --git a/packages/sdk/src/cli/commands/tailordb/erd/deploy.ts b/packages/sdk/src/cli/commands/tailordb/erd/deploy.ts deleted file mode 100644 index 9853bb23d..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/deploy.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { arg } from "politty"; -import { z } from "zod"; -import { deploymentArgs } from "#/cli/shared/args"; -import { defineAppCommand } from "#/cli/shared/command"; -import { logger } from "#/cli/shared/logger"; -import { assertWritable } from "#/cli/shared/readonly-guard"; -import { deployStaticWebsite, logSkippedFiles } from "../../staticwebsite/deploy"; -import { prepareErdBuilds } from "./export"; -import { initErdDeployContext } from "./utils"; - -export const erdDeployCommand = defineAppCommand({ - name: "deploy", - description: "Deploy ERD static website for TailorDB namespace(s).", - args: z.strictObject({ - ...deploymentArgs, - namespace: arg(z.string().optional(), { - alias: "n", - description: - "TailorDB namespace name (optional - deploys all namespaces with erdSite if omitted)", - }), - }), - run: async (args) => { - await assertWritable({ profile: args.profile }); - const { client, workspaceId } = await initErdDeployContext(args); - const buildResults = await prepareErdBuilds({ - configPath: args.config, - namespace: args.namespace, - requireErdSite: true, - }); - - const deployResults = await Promise.all( - buildResults.map(async (result) => { - if (!result.erdSite) { - throw new Error( - `No erdSite configured for namespace "${result.namespace}". ` + - `Add erdSite: "" to db.${result.namespace} in tailor.config.ts.`, - ); - } - - if (!args.json) { - logger.info( - `Deploying ERD for namespace "${result.namespace}" to site "${result.erdSite}"...`, - ); - } - - const { url, skippedFiles } = await deployStaticWebsite( - client, - workspaceId, - result.erdSite, - result.distDir, - !args.json, - ); - - return { - namespace: result.namespace, - erdSite: result.erdSite, - url, - skippedFiles, - }; - }), - ); - logger.newline(); - - if (args.json) { - logger.out(deployResults); - } else { - for (const result of deployResults) { - logSkippedFiles(result.skippedFiles); - logger.newline(); - logger.success(`ERD site "${result.erdSite}" deployed successfully.`); - logger.out(result.url); - } - } - }, -}); diff --git a/packages/sdk/src/cli/commands/tailordb/erd/diff-command.test.ts b/packages/sdk/src/cli/commands/tailordb/erd/diff-command.test.ts deleted file mode 100644 index 73588360b..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/diff-command.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "pathe"; -import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { writeErdDiff } from "./diff-command"; -import type { TailorDbErdSchema } from "./types"; - -let tempDir: string; - -function schema(overrides: Partial = {}): TailorDbErdSchema { - return { - version: 1, - namespace: "tailordb", - generatedAt: "2026-01-01T00:00:00.000Z", - revision: "revision", - source: "local", - cleanRoom: { implementation: "tailor", notes: [] }, - tables: [], - relations: [], - ...overrides, - }; -} - -function htmlWithSchema(value: TailorDbErdSchema): string { - return ``; -} - -describe("writeErdDiff", () => { - beforeEach(() => { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tailordb-erd-diff-")); - }); - - afterEach(() => { - fs.rmSync(tempDir, { recursive: true, force: true }); - }); - - test("writes diff HTML and JSON files", () => { - const baseHtml = path.join(tempDir, "base.html"); - const headHtml = path.join(tempDir, "head.html"); - const outputHtml = path.join(tempDir, "out", "diff.html"); - const outputJson = path.join(tempDir, "out", "diff.json"); - - fs.writeFileSync(baseHtml, htmlWithSchema(schema()), "utf8"); - fs.writeFileSync( - headHtml, - htmlWithSchema( - schema({ - revision: "head-revision", - tables: [ - { - name: "User", - pluralForm: "users", - columns: [], - indexes: [], - forwardRelationships: [], - backwardRelationships: [], - }, - ], - }), - ), - "utf8", - ); - - const result = writeErdDiff({ baseHtml, headHtml, outputHtml, outputJson }); - - const output = fs.readFileSync(outputHtml, "utf8"); - expect(output).toContain("TailorDB ERD diff - tailordb"); - expect(output).toContain('id="erd-schema"'); - expect(output).toContain('id="erd-current-schema"'); - expect(output).toContain('id="erd-diff"'); - expect(output).toContain("function renderNodes()"); - expect(JSON.parse(fs.readFileSync(outputJson, "utf8"))).toMatchObject({ - namespace: "tailordb", - summary: { added: 1, changed: 0, removed: 0 }, - }); - expect(result.diff.changed).toBe(true); - }); - - test("uses an empty base when only head HTML is supplied", () => { - const headHtml = path.join(tempDir, "head.html"); - const outputHtml = path.join(tempDir, "diff.html"); - - fs.writeFileSync( - headHtml, - htmlWithSchema( - schema({ - tables: [ - { - name: "User", - pluralForm: "users", - columns: [], - indexes: [], - forwardRelationships: [], - backwardRelationships: [], - }, - ], - }), - ), - "utf8", - ); - - const result = writeErdDiff({ headHtml, outputHtml }); - - expect(result.diff.baseRevision).toBe("missing-base"); - expect(result.diff.summary).toEqual({ added: 1, changed: 0, removed: 0 }); - }); -}); diff --git a/packages/sdk/src/cli/commands/tailordb/erd/diff-command.ts b/packages/sdk/src/cli/commands/tailordb/erd/diff-command.ts deleted file mode 100644 index 30e60226d..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/diff-command.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "pathe"; -import { arg } from "politty"; -import { z } from "zod"; -import { defineAppCommand } from "#/cli/shared/command"; -import { logger } from "#/cli/shared/logger"; -import { - buildErdDiffViewerSchema, - buildErdSchemaDiff, - createEmptyErdSchema, - extractEmbeddedErdSchema, - renderErdDiffHtml, - type ErdSchemaDiff, -} from "./diff"; -import { initErdCommand } from "./utils"; -import type { TailorDbErdSchema } from "./types"; - -export interface WriteErdDiffOptions { - baseHtml?: string; - headHtml?: string; - namespace?: string; - outputHtml: string; - outputJson?: string; -} - -export interface WriteErdDiffResult { - namespace: string; - outputHtml: string; - outputJson?: string; - diff: ErdSchemaDiff; -} - -interface ResolveNamespaceOptions { - namespace?: string; - base?: TailorDbErdSchema; - head?: TailorDbErdSchema; -} - -function readSchema(filePath: string | undefined): TailorDbErdSchema | undefined { - if (!filePath) return undefined; - return extractEmbeddedErdSchema(fs.readFileSync(filePath, "utf8")); -} - -function resolveNamespace(options: ResolveNamespaceOptions): string { - const namespace = options.namespace ?? options.head?.namespace ?? options.base?.namespace; - if (!namespace) { - throw new Error("Missing --namespace when one side of the ERD diff is omitted."); - } - if (options.base && options.base.namespace !== namespace) { - throw new Error( - `Base ERD namespace "${options.base.namespace}" does not match "${namespace}".`, - ); - } - if (options.head && options.head.namespace !== namespace) { - throw new Error( - `Head ERD namespace "${options.head.namespace}" does not match "${namespace}".`, - ); - } - return namespace; -} - -function writeFile(filePath: string, content: string): void { - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, content, "utf8"); -} - -export function writeErdDiff(options: WriteErdDiffOptions): WriteErdDiffResult { - const base = readSchema(options.baseHtml); - const head = readSchema(options.headHtml); - if (!base && !head) { - throw new Error("At least one of --base-html or --head-html is required."); - } - - const namespace = resolveNamespace({ namespace: options.namespace, base, head }); - const baseSchema = base ?? createEmptyErdSchema({ namespace, revision: "missing-base" }); - const headSchema = head ?? createEmptyErdSchema({ namespace, revision: "missing-head" }); - const diff = buildErdSchemaDiff({ base: baseSchema, head: headSchema }); - const viewerSchema = buildErdDiffViewerSchema({ base: baseSchema, head: headSchema }); - - writeFile( - options.outputHtml, - renderErdDiffHtml({ schema: viewerSchema, currentSchema: headSchema, diff }), - ); - if (options.outputJson) { - writeFile(options.outputJson, `${JSON.stringify(diff, null, 2)}\n`); - } - - return { - namespace, - outputHtml: options.outputHtml, - outputJson: options.outputJson, - diff, - }; -} - -export const erdDiffCommand = defineAppCommand({ - name: "diff", - description: "Render TailorDB ERD schema diff HTML from exported ERD viewers.", - args: z.strictObject({ - "base-html": arg(z.string().optional(), { - description: "Base ERD viewer HTML file", - completion: { type: "file", matcher: [".html"] }, - }), - "head-html": arg(z.string().optional(), { - description: "Head ERD viewer HTML file", - completion: { type: "file", matcher: [".html"] }, - }), - namespace: arg(z.string().optional(), { - alias: "n", - description: "TailorDB namespace name (defaults to the provided ERD schema namespace)", - }), - output: arg(z.string().min(1), { - alias: "o", - description: "Output ERD diff HTML file", - completion: { type: "file", matcher: [".html"] }, - }), - "output-json": arg(z.string().optional(), { - description: "Optional output JSON file for the computed diff", - completion: { type: "file", matcher: [".json"] }, - }), - }), - run: (args) => { - initErdCommand(); - const result = writeErdDiff({ - baseHtml: args["base-html"], - headHtml: args["head-html"], - namespace: args.namespace, - outputHtml: args.output, - outputJson: args["output-json"], - }); - - if (args.json) { - logger.out({ - namespace: result.namespace, - outputHtml: result.outputHtml, - outputJson: result.outputJson, - summary: result.diff.summary, - }); - } else { - logger.success(`Wrote ERD diff to ${path.relative(process.cwd(), result.outputHtml)}`); - } - }, -}); diff --git a/packages/sdk/src/cli/commands/tailordb/erd/diff.test.ts b/packages/sdk/src/cli/commands/tailordb/erd/diff.test.ts deleted file mode 100644 index 5e28e792a..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/diff.test.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { - buildErdSchemaDiff, - buildErdDiffViewerSchema, - createEmptyErdSchema, - extractEmbeddedErdSchema, - renderErdDiffHtml, -} from "./diff"; -import { buildViewerHtml } from "./viewer"; -import type { TailorDbErdSchema, TailorDbErdTable } from "./types"; - -function table(name: string, overrides: Partial = {}): TailorDbErdTable { - return { - name, - pluralForm: `${name.toLowerCase()}s`, - columns: [ - { - name: "id", - type: "uuid", - required: true, - array: false, - primaryKey: true, - unique: true, - }, - ], - indexes: [], - forwardRelationships: [], - backwardRelationships: [], - ...overrides, - }; -} - -function schema(overrides: Partial = {}): TailorDbErdSchema { - return { - version: 1, - namespace: "tailordb", - generatedAt: "2026-01-01T00:00:00.000Z", - revision: "base-revision", - source: "local", - cleanRoom: { implementation: "tailor", notes: [] }, - tables: [table("User")], - relations: [], - ...overrides, - }; -} - -function extractJsonBlock(html: string, id: string): T { - const pattern = new RegExp(``); - const match = pattern.exec(html); - if (!match?.[1]) { - throw new Error(`JSON block not found: ${id}`); - } - return JSON.parse(match[1]) as T; -} - -describe("extractEmbeddedErdSchema", () => { - test("parses the viewer schema data block", () => { - const embedded = schema({ namespace: "" }); - const html = ``; - - expect(extractEmbeddedErdSchema(html)).toMatchObject({ - namespace: "", - revision: "base-revision", - }); - }); - - test("reports a missing schema data block", () => { - expect(() => extractEmbeddedErdSchema("")).toThrow("ERD schema block not found"); - }); - - test("preserves dollar replacement patterns in embedded schema values", () => { - const description = "Prices use $$, literal match $&, left context $`, right context $'."; - const html = buildViewerHtml({ - schema: schema({ - tables: [table("Invoice", { description })], - }), - }); - - expect(extractEmbeddedErdSchema(html).tables[0]?.description).toBe(description); - }); -}); - -describe("buildErdSchemaDiff", () => { - test("ignores generation timestamp-only changes", () => { - const diff = buildErdSchemaDiff({ - base: schema({ generatedAt: "2026-01-01T00:00:00.000Z" }), - head: schema({ generatedAt: "2026-01-02T00:00:00.000Z" }), - }); - - expect(diff.changed).toBe(false); - expect(diff.summary).toEqual({ added: 0, changed: 0, removed: 0 }); - expect(diff.changes).toEqual([]); - }); - - test("classifies table, column, index, and relation changes", () => { - const base = schema({ - revision: "base-revision", - tables: [ - table("Order"), - table("User", { - columns: [ - { - name: "id", - type: "uuid", - required: true, - array: false, - primaryKey: true, - unique: true, - }, - { - name: "name", - type: "string", - required: true, - array: false, - }, - ], - indexes: [{ name: "idx_user_name", fields: ["name"], unique: false }], - }), - ], - relations: [ - { - name: "User.organizationId->Organization.id", - sourceTable: "User", - sourceColumns: ["organizationId"], - targetTable: "Organization", - targetColumns: ["id"], - required: true, - unique: false, - kind: "foreignKey", - }, - ], - }); - const head = schema({ - revision: "head-revision", - tables: [ - table("Invoice"), - table("User", { - columns: [ - { - name: "id", - type: "uuid", - required: true, - array: false, - primaryKey: true, - unique: true, - }, - { - name: "name", - type: "text", - required: true, - array: false, - }, - { - name: "email", - type: "string", - required: false, - array: false, - }, - ], - indexes: [{ name: "idx_user_name", fields: ["name"], unique: true }], - }), - ], - relations: [ - { - name: "User.organizationId->Organization.id", - sourceTable: "User", - sourceColumns: ["organizationId"], - targetTable: "Organization", - targetColumns: ["id"], - required: false, - unique: false, - kind: "foreignKey", - }, - ], - }); - - const diff = buildErdSchemaDiff({ base, head }); - - expect(diff.changed).toBe(true); - expect(diff.summary).toEqual({ added: 2, changed: 3, removed: 1 }); - expect(diff.changes).toEqual([ - { - action: "removed", - entity: "table", - path: "Order", - detail: "Table removed", - }, - { - action: "added", - entity: "table", - path: "Invoice", - detail: "Table added", - }, - { - action: "changed", - entity: "column", - path: "User.name", - detail: "Changed fields: type", - }, - { - action: "added", - entity: "column", - path: "User.email", - detail: "Column added", - }, - { - action: "changed", - entity: "index", - path: "User.idx_user_name", - detail: "Changed fields: unique", - }, - { - action: "changed", - entity: "relation", - path: "User.organizationId->Organization.id", - detail: "Changed fields: required", - }, - ]); - }); - - test("represents a missing base namespace as added tables", () => { - const diff = buildErdSchemaDiff({ - base: createEmptyErdSchema({ namespace: "tailordb", revision: "missing-base" }), - head: schema({ tables: [table("Account"), table("User")] }), - }); - - expect(diff.summary).toEqual({ added: 2, changed: 0, removed: 0 }); - expect(diff.changes.map((change) => change.path)).toEqual(["Account", "User"]); - }); - - test("detects relationship metadata changes on tables", () => { - const base = schema({ - tables: [ - table("User", { - forwardRelationships: [ - { - name: "orders", - targetType: "Order", - targetField: "userId", - sourceField: "id", - isArray: true, - description: "Old label", - }, - ], - }), - ], - }); - const head = schema({ - tables: [ - table("User", { - forwardRelationships: [ - { - name: "orders", - targetType: "Order", - targetField: "userId", - sourceField: "id", - isArray: true, - description: "New label", - }, - ], - }), - ], - }); - - const diff = buildErdSchemaDiff({ base, head }); - - expect(diff.changed).toBe(true); - expect(diff.changes).toContainEqual({ - action: "changed", - entity: "table", - path: "User", - detail: "Changed fields: forwardRelationships", - }); - }); -}); - -describe("ERD diff rendering", () => { - test("keeps removed tables and columns visible for diff highlighting", () => { - const base = schema({ - tables: [ - table("Order"), - table("User", { - columns: [ - { - name: "id", - type: "uuid", - required: true, - array: false, - primaryKey: true, - unique: true, - }, - { - name: "legacyCode", - type: "string", - required: false, - array: false, - }, - ], - }), - ], - }); - const head = schema({ - tables: [ - table("Invoice"), - table("User", { - columns: [ - { - name: "id", - type: "uuid", - required: true, - array: false, - primaryKey: true, - unique: true, - }, - ], - }), - ], - }); - - const viewerSchema = buildErdDiffViewerSchema({ base, head }); - - expect(viewerSchema.tables.map((item) => item.name).toSorted()).toEqual([ - "Invoice", - "Order", - "User", - ]); - expect(viewerSchema.tables.find((item) => item.name === "User")?.columns).toContainEqual( - expect.objectContaining({ name: "legacyCode" }), - ); - }); - - test("renders the existing ERD viewer with embedded diff metadata", () => { - const head = schema({ - revision: "head-revision", - tables: [table("Account"), table("User")], - }); - const diff = buildErdSchemaDiff({ - base: schema(), - head, - }); - const viewerSchema = buildErdDiffViewerSchema({ - base: schema(), - head, - }); - const html = renderErdDiffHtml({ schema: viewerSchema, currentSchema: head, diff }); - expect(html).toContain("TailorDB ERD diff - tailordb"); - expect(html).toContain('id="erd-schema"'); - expect(html).toContain('id="erd-current-schema"'); - expect(html).toContain('([\s\S]*?)<\/script>/; - -export type ErdDiffAction = "added" | "changed" | "removed"; -export type ErdDiffEntity = "column" | "index" | "relation" | "table"; - -export interface ErdDiffChange { - action: ErdDiffAction; - entity: ErdDiffEntity; - path: string; - detail: string; -} - -export interface ErdDiffSummary { - added: number; - changed: number; - removed: number; -} - -export interface ErdSchemaDiff { - namespace: string; - baseRevision: string; - headRevision: string; - changed: boolean; - summary: ErdDiffSummary; - changes: ErdDiffChange[]; -} - -export interface BuildErdSchemaDiffOptions { - base: TailorDbErdSchema; - head: TailorDbErdSchema; -} - -export interface BuildErdDiffViewerSchemaOptions { - base: TailorDbErdSchema; - head: TailorDbErdSchema; -} - -export interface CreateEmptyErdSchemaOptions { - namespace: string; - revision: string; -} - -export interface RenderErdDiffHtmlOptions { - schema: TailorDbErdSchema; - currentSchema: TailorDbErdSchema; - diff: ErdSchemaDiff; -} - -function stableValue(value: unknown): unknown { - if (Array.isArray(value)) { - return value.map((item) => stableValue(item)); - } - if (value && typeof value === "object") { - return Object.fromEntries( - Object.entries(value) - .toSorted(([a], [b]) => a.localeCompare(b)) - .map(([key, item]) => [key, stableValue(item)]), - ); - } - return value; -} - -function stableJson(value: unknown): string { - return JSON.stringify(stableValue(value)); -} - -function changedFields(base: object, head: object): string[] { - const keys = [...new Set([...Object.keys(base), ...Object.keys(head)])].toSorted((a, b) => - a.localeCompare(b), - ); - return keys.filter( - (key) => - stableJson((base as Record)[key]) !== - stableJson((head as Record)[key]), - ); -} - -function detailForChangedFields(fields: string[]): string { - return `Changed fields: ${fields.join(", ")}`; -} - -function mapByName(items: readonly T[]): Map { - return new Map(items.map((item) => [item.name, item])); -} - -function compareCommonNamed( - baseItems: readonly T[], - headItems: readonly T[], - addChange: (name: string, fields: string[]) => void, -): void { - const baseByName = mapByName(baseItems); - const headByName = mapByName(headItems); - const commonNames = [...baseByName.keys()] - .filter((name) => headByName.has(name)) - .toSorted((a, b) => a.localeCompare(b)); - - for (const name of commonNames) { - const base = baseByName.get(name); - const head = headByName.get(name); - if (!base || !head) continue; - - const fields = changedFields(base, head); - if (fields.length > 0) { - addChange(name, fields); - } - } -} - -function removedNames( - baseItems: readonly T[], - headItems: readonly T[], -): string[] { - const headByName = mapByName(headItems); - return baseItems - .map((item) => item.name) - .filter((name) => !headByName.has(name)) - .toSorted((a, b) => a.localeCompare(b)); -} - -function addedNames( - baseItems: readonly T[], - headItems: readonly T[], -): string[] { - const baseByName = mapByName(baseItems); - return headItems - .map((item) => item.name) - .filter((name) => !baseByName.has(name)) - .toSorted((a, b) => a.localeCompare(b)); -} - -function tableMetadata(table: TailorDbErdTable): object { - return { - backwardRelationships: table.backwardRelationships, - description: table.description, - forwardRelationships: table.forwardRelationships, - pluralForm: table.pluralForm, - source: table.source, - }; -} - -function pushChange(changes: ErdDiffChange[], change: ErdDiffChange): void { - changes.push(change); -} - -function diffColumns( - changes: ErdDiffChange[], - tableName: string, - baseColumns: readonly TailorDbErdColumn[], - headColumns: readonly TailorDbErdColumn[], -): void { - compareCommonNamed(baseColumns, headColumns, (name, fields) => { - pushChange(changes, { - action: "changed", - entity: "column", - path: `${tableName}.${name}`, - detail: detailForChangedFields(fields), - }); - }); - for (const name of removedNames(baseColumns, headColumns)) { - pushChange(changes, { - action: "removed", - entity: "column", - path: `${tableName}.${name}`, - detail: "Column removed", - }); - } - for (const name of addedNames(baseColumns, headColumns)) { - pushChange(changes, { - action: "added", - entity: "column", - path: `${tableName}.${name}`, - detail: "Column added", - }); - } -} - -function diffIndexes( - changes: ErdDiffChange[], - tableName: string, - baseIndexes: readonly TailorDbErdIndex[], - headIndexes: readonly TailorDbErdIndex[], -): void { - compareCommonNamed(baseIndexes, headIndexes, (name, fields) => { - pushChange(changes, { - action: "changed", - entity: "index", - path: `${tableName}.${name}`, - detail: detailForChangedFields(fields), - }); - }); - for (const name of removedNames(baseIndexes, headIndexes)) { - pushChange(changes, { - action: "removed", - entity: "index", - path: `${tableName}.${name}`, - detail: "Index removed", - }); - } - for (const name of addedNames(baseIndexes, headIndexes)) { - pushChange(changes, { - action: "added", - entity: "index", - path: `${tableName}.${name}`, - detail: "Index added", - }); - } -} - -function diffTables( - changes: ErdDiffChange[], - baseTables: readonly TailorDbErdTable[], - headTables: readonly TailorDbErdTable[], -): void { - const baseByName = mapByName(baseTables); - const headByName = mapByName(headTables); - - for (const name of removedNames(baseTables, headTables)) { - pushChange(changes, { - action: "removed", - entity: "table", - path: name, - detail: "Table removed", - }); - } - for (const name of addedNames(baseTables, headTables)) { - pushChange(changes, { - action: "added", - entity: "table", - path: name, - detail: "Table added", - }); - } - - const commonNames = [...baseByName.keys()] - .filter((name) => headByName.has(name)) - .toSorted((a, b) => a.localeCompare(b)); - for (const name of commonNames) { - const base = baseByName.get(name); - const head = headByName.get(name); - if (!base || !head) continue; - - const tableFields = changedFields(tableMetadata(base), tableMetadata(head)); - if (tableFields.length > 0) { - pushChange(changes, { - action: "changed", - entity: "table", - path: name, - detail: detailForChangedFields(tableFields), - }); - } - diffColumns(changes, name, base.columns, head.columns); - diffIndexes(changes, name, base.indexes, head.indexes); - } -} - -function diffRelations( - changes: ErdDiffChange[], - baseRelations: readonly TailorDbErdRelation[], - headRelations: readonly TailorDbErdRelation[], -): void { - compareCommonNamed(baseRelations, headRelations, (name, fields) => { - pushChange(changes, { - action: "changed", - entity: "relation", - path: name, - detail: detailForChangedFields(fields), - }); - }); - for (const name of removedNames(baseRelations, headRelations)) { - pushChange(changes, { - action: "removed", - entity: "relation", - path: name, - detail: "Relation removed", - }); - } - for (const name of addedNames(baseRelations, headRelations)) { - pushChange(changes, { - action: "added", - entity: "relation", - path: name, - detail: "Relation added", - }); - } -} - -function summarize(changes: readonly ErdDiffChange[]): ErdDiffSummary { - return { - added: changes.filter((change) => change.action === "added").length, - changed: changes.filter((change) => change.action === "changed").length, - removed: changes.filter((change) => change.action === "removed").length, - }; -} - -export function extractEmbeddedErdSchema(html: string): TailorDbErdSchema { - const match = SCHEMA_BLOCK_PATTERN.exec(html); - if (!match?.[1]) { - throw new Error("ERD schema block not found."); - } - - try { - return JSON.parse(match[1]) as TailorDbErdSchema; - } catch (error) { - throw new Error(`Failed to parse ERD schema block: ${String(error)}`, { cause: error }); - } -} - -export function createEmptyErdSchema(options: CreateEmptyErdSchemaOptions): TailorDbErdSchema { - return { - version: 1, - namespace: options.namespace, - generatedAt: new Date(0).toISOString(), - revision: options.revision, - source: "local", - cleanRoom: { - implementation: "tailor", - notes: ["Synthetic empty schema used for ERD diff generation."], - }, - tables: [], - relations: [], - }; -} - -export function buildErdSchemaDiff(options: BuildErdSchemaDiffOptions): ErdSchemaDiff { - const { base, head } = options; - if (base.namespace !== head.namespace) { - throw new Error( - `Cannot diff ERD schemas from different namespaces: ${base.namespace}, ${head.namespace}`, - ); - } - - const changes: ErdDiffChange[] = []; - diffTables(changes, base.tables, head.tables); - diffRelations(changes, base.relations, head.relations); - const summary = summarize(changes); - - return { - namespace: head.namespace, - baseRevision: base.revision, - headRevision: head.revision, - changed: changes.length > 0, - summary, - changes, - }; -} - -function mergeNamedByHead( - baseItems: readonly T[], - headItems: readonly T[], -): T[] { - const headByName = mapByName(headItems); - const removedItems = baseItems - .filter((item) => !headByName.has(item.name)) - .toSorted((a, b) => a.name.localeCompare(b.name)); - return [...headItems, ...removedItems]; -} - -function mergeDiffViewerTable( - baseTable: TailorDbErdTable, - headTable: TailorDbErdTable, -): TailorDbErdTable { - return { - ...headTable, - columns: mergeNamedByHead(baseTable.columns, headTable.columns), - indexes: mergeNamedByHead(baseTable.indexes, headTable.indexes), - }; -} - -function mergeDiffViewerTables( - baseTables: readonly TailorDbErdTable[], - headTables: readonly TailorDbErdTable[], -): TailorDbErdTable[] { - const baseByName = mapByName(baseTables); - const headByName = mapByName(headTables); - const merged = headTables.map((headTable) => { - const baseTable = baseByName.get(headTable.name); - return baseTable ? mergeDiffViewerTable(baseTable, headTable) : headTable; - }); - const removedTables = baseTables - .filter((table) => !headByName.has(table.name)) - .toSorted((a, b) => a.name.localeCompare(b.name)); - return [...merged, ...removedTables]; -} - -/** - * Build the schema rendered by the diff viewer. The ordinary ERD viewer only - * knows how to draw objects present in its schema, so the diff schema keeps - * base-only tables, columns, indexes, and relations visible for removal - * highlighting while preferring head-side metadata for unchanged objects. - * @param options - Base and head schemas to merge for diff rendering. - * @returns A TailorDB ERD schema suitable for the visual diff viewer. - */ -export function buildErdDiffViewerSchema( - options: BuildErdDiffViewerSchemaOptions, -): TailorDbErdSchema { - const { base, head } = options; - if (base.namespace !== head.namespace) { - throw new Error( - `Cannot diff ERD schemas from different namespaces: ${base.namespace}, ${head.namespace}`, - ); - } - - return { - ...head, - tables: mergeDiffViewerTables(base.tables, head.tables), - relations: mergeNamedByHead(base.relations, head.relations), - }; -} - -export function renderErdDiffHtml(options: RenderErdDiffHtmlOptions): string { - return buildViewerHtml({ - schema: options.schema, - currentSchema: options.currentSchema, - diff: options.diff, - title: `TailorDB ERD diff - ${options.diff.namespace}`, - }); -} diff --git a/packages/sdk/src/cli/commands/tailordb/erd/export.ts b/packages/sdk/src/cli/commands/tailordb/erd/export.ts deleted file mode 100644 index 7278ff84c..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/export.ts +++ /dev/null @@ -1,217 +0,0 @@ -import * as path from "pathe"; -import { arg } from "politty"; -import { z } from "zod"; -import { configArg } from "#/cli/shared/args"; -import { defineAppCommand } from "#/cli/shared/command"; -import { logger } from "#/cli/shared/logger"; -import { loadLocalErdSchema, type LocalErdSchemaContext } from "./local-schema"; -import { buildTailorDbErdSchema } from "./schema"; -import { initErdCommand } from "./utils"; -import { writeViewerDist } from "./viewer"; -import type { TailorDBNamespaceData } from "#/plugin/types"; - -const DEFAULT_ERD_BASE_DIR = ".tailor/erd"; - -interface ResolveTargetsOptions { - context: LocalErdSchemaContext; - namespace?: string; - outputDir: string; - requireErdSite?: boolean; -} - -interface ErdTarget { - namespaceData: TailorDBNamespaceData; - erdSite?: string; - distDir: string; -} - -interface ErdBuildsOptions { - configPath?: string; - namespace?: string; - outputDir?: string; - requireErdSite?: boolean; -} - -interface ErdBuildsFromContextOptions { - context: LocalErdSchemaContext; - namespace?: string; - outputDir?: string; - requireErdSite?: boolean; -} - -export interface ErdBuildResult { - namespace: string; - erdSite?: string; - distDir: string; -} - -function getErdSite(context: LocalErdSchemaContext, namespace: string): string | undefined { - const dbConfig = context.config.db?.[namespace]; - if (!dbConfig || "external" in dbConfig) { - return undefined; - } - return dbConfig.erdSite; -} - -function resolveExplicitTarget(options: ResolveTargetsOptions): ErdTarget { - const namespaceData = options.context.namespaces.find( - (candidate) => candidate.namespace === options.namespace, - ); - if (!namespaceData) { - const available = options.context.namespaces.map((candidate) => candidate.namespace).join(", "); - throw new Error( - `TailorDB namespace "${options.namespace}" not found in local config.db.` + - (available ? ` Available owned namespaces: ${available}` : ""), - ); - } - - const erdSite = getErdSite(options.context, namespaceData.namespace); - if (options.requireErdSite && !erdSite) { - throw new Error( - `No erdSite configured for namespace "${namespaceData.namespace}". ` + - `Add erdSite: "" to db.${namespaceData.namespace} in tailor.config.ts.`, - ); - } - - return toTarget(options.outputDir, namespaceData, erdSite); -} - -function resolveAllTargets(options: ResolveTargetsOptions): ErdTarget[] { - const namespaces = options.context.namespaces.filter( - (namespaceData) => - !options.requireErdSite || getErdSite(options.context, namespaceData.namespace), - ); - if (namespaces.length === 0) { - throw new Error( - options.requireErdSite - ? "No namespaces with erdSite configured found. " + - 'Add erdSite: "" to db. in tailor.config.ts.' - : "No TailorDB namespaces found in config. Please define db services in tailor.config.ts.", - ); - } - - logger.info( - `Found ${namespaces.length} namespace(s)${options.requireErdSite ? " with erdSite configured" : ""}.`, - ); - return namespaces.map((namespaceData) => - toTarget( - options.outputDir, - namespaceData, - getErdSite(options.context, namespaceData.namespace), - ), - ); -} - -function toTarget( - outputDir: string, - namespaceData: TailorDBNamespaceData, - erdSite: string | undefined, -): ErdTarget { - const distDir = path.join(outputDir, namespaceData.namespace, "dist"); - return { - namespaceData, - erdSite, - distDir, - }; -} - -function resolveTargets(options: ResolveTargetsOptions): ErdTarget[] { - if (options.namespace) { - return [resolveExplicitTarget(options)]; - } - return resolveAllTargets(options); -} - -function prepareErdBuild(target: ErdTarget): ErdBuildResult { - const schema = buildTailorDbErdSchema({ namespaceData: target.namespaceData }); - writeViewerDist({ schema, distDir: target.distDir }); - - const relativePath = path.relative(process.cwd(), target.distDir); - logger.success(`Built ERD to ${relativePath}`); - - return { - namespace: target.namespaceData.namespace, - erdSite: target.erdSite, - distDir: target.distDir, - }; -} - -/** - * Prepare TailorDB ERD static viewer builds for one or more namespaces. - * @param options - Build options. - * @returns Build results by namespace. - */ -export async function prepareErdBuilds(options: ErdBuildsOptions): Promise { - const context = await loadLocalErdSchema({ - configPath: options.configPath, - namespaces: options.namespace ? [options.namespace] : undefined, - requireErdSite: options.requireErdSite, - }); - return prepareErdBuildsFromContext({ - context, - namespace: options.namespace, - outputDir: options.outputDir, - requireErdSite: options.requireErdSite, - }); -} - -/** - * Prepare TailorDB ERD static viewer builds from an already loaded schema context. - * @param options - Build options. - * @returns Build results by namespace. - */ -export function prepareErdBuildsFromContext( - options: ErdBuildsFromContextOptions, -): ErdBuildResult[] { - const outputDir = path.resolve(process.cwd(), options.outputDir ?? DEFAULT_ERD_BASE_DIR); - const targets = resolveTargets({ - context: options.context, - namespace: options.namespace, - outputDir, - requireErdSite: options.requireErdSite, - }); - - return targets.map((target) => prepareErdBuild(target)); -} - -export const erdExportCommand = defineAppCommand({ - name: "export", - description: "Export TailorDB ERD static viewer from local TailorDB schema.", - args: z.strictObject({ - ...configArg, - namespace: arg(z.string().optional(), { - alias: "n", - description: "TailorDB namespace name (optional if only one namespace is defined in config)", - }), - output: arg(z.string().default(DEFAULT_ERD_BASE_DIR), { - alias: "o", - description: - "Output directory path for TailorDB ERD viewer files (writes to `//dist`)", - completion: { type: "directory" }, - }), - }), - run: async (args) => { - initErdCommand(); - - const results = await prepareErdBuilds({ - configPath: args.config, - namespace: args.namespace, - outputDir: args.output, - }); - - logger.newline(); - if (args.json) { - logger.out( - results.map((result) => ({ - namespace: result.namespace, - distDir: result.distDir, - })), - ); - } else { - for (const result of results) { - logger.out(`Exported ERD for namespace "${result.namespace}"`); - logger.out(` - ERD viewer: ${path.join(result.distDir, "index.html")}`); - } - } - }, -}); diff --git a/packages/sdk/src/cli/commands/tailordb/erd/index.ts b/packages/sdk/src/cli/commands/tailordb/erd/index.ts deleted file mode 100644 index 95f1f8ef8..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { defineCommand } from "politty"; -import { erdDeployCommand } from "./deploy"; -import { erdDiffCommand } from "./diff-command"; -import { erdExportCommand } from "./export"; -import { erdServeCommand } from "./serve"; - -export const erdCommand = defineCommand({ - name: "erd", - description: "Generate TailorDB ERD viewer artifacts from local TailorDB schema. (beta)", - subCommands: { - export: erdExportCommand, - diff: erdDiffCommand, - serve: erdServeCommand, - deploy: erdDeployCommand, - }, -}); diff --git a/packages/sdk/src/cli/commands/tailordb/erd/local-schema.test.ts b/packages/sdk/src/cli/commands/tailordb/erd/local-schema.test.ts deleted file mode 100644 index e1797bc63..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/local-schema.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { resolveLocalErdSchemaNamespaces } from "./local-schema"; -import type { LoadedConfig } from "#/cli/shared/config-loader"; - -describe("resolveLocalErdSchemaNamespaces", () => { - const config = { - path: "/tmp/tailor.config.ts", - db: { - main: { files: ["tailordb/main/*.ts"], erdSite: "main-erd" }, - admin: { files: ["tailordb/admin/*.ts"] }, - external: { external: true }, - }, - } as unknown as LoadedConfig; - - test("loads only erdSite namespaces when required and no namespace is explicit", () => { - expect(resolveLocalErdSchemaNamespaces(config, { requireErdSite: true })).toEqual(["main"]); - }); - - test("keeps explicit namespaces even when erdSite is required", () => { - expect( - resolveLocalErdSchemaNamespaces(config, { - namespaces: ["admin"], - requireErdSite: true, - }), - ).toEqual(["admin"]); - }); - - test("loads all owned namespaces when erdSite is not required", () => { - expect(resolveLocalErdSchemaNamespaces(config, {})).toBeUndefined(); - }); -}); diff --git a/packages/sdk/src/cli/commands/tailordb/erd/local-schema.ts b/packages/sdk/src/cli/commands/tailordb/erd/local-schema.ts deleted file mode 100644 index dabcfc543..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/local-schema.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { loadTailorDBNamespaces } from "#/cli/shared/tailordb-namespaces"; -import type { LoadedConfig } from "#/cli/shared/config-loader"; -import type { TailorDBNamespaceData } from "#/plugin/types"; - -export interface LoadLocalErdSchemaOptions { - configPath?: string; - namespaces?: string[]; - requireErdSite?: boolean; -} - -export interface LocalErdSchemaContext { - config: LoadedConfig; - namespaces: TailorDBNamespaceData[]; -} - -export interface ResolveLocalErdSchemaNamespacesOptions { - /** Explicit namespace selection. */ - namespaces?: string[]; - /** Limit implicit selection to owned namespaces with erdSite configured. */ - requireErdSite?: boolean; -} - -/** - * Resolve TailorDB namespaces that need local type loading for ERD generation. - * @param config - Loaded Tailor config. - * @param options - Namespace selection options. - * @returns Namespace names to load, or undefined to load all owned namespaces. - */ -export function resolveLocalErdSchemaNamespaces( - config: LoadedConfig, - options: ResolveLocalErdSchemaNamespacesOptions, -): string[] | undefined { - if (options.namespaces) { - return options.namespaces; - } - if (!options.requireErdSite) { - return undefined; - } - - return Object.entries(config.db ?? {}).flatMap(([namespace, dbConfig]) => - "external" in dbConfig || !dbConfig.erdSite ? [] : [namespace], - ); -} - -/** - * Load local TailorDB namespaces exactly as SDK generation/deploy sees them. - * @param options - Local schema loading options. - * @returns Loaded TailorDB namespace data. - */ -export async function loadLocalErdSchema( - options: LoadLocalErdSchemaOptions, -): Promise { - return await loadTailorDBNamespaces({ - configPath: options.configPath, - namespaces: (config) => - resolveLocalErdSchemaNamespaces(config, { - namespaces: options.namespaces, - requireErdSite: options.requireErdSite, - }), - }); -} diff --git a/packages/sdk/src/cli/commands/tailordb/erd/schema.test.ts b/packages/sdk/src/cli/commands/tailordb/erd/schema.test.ts deleted file mode 100644 index 85d7e07dc..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/schema.test.ts +++ /dev/null @@ -1,315 +0,0 @@ -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "pathe"; -import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; -import { buildTailorDbErdSchema, writeTailorDbErdSchemaToFile } from "./schema"; -import type { - OperatorFieldConfig, - ParsedField, - TailorDBType, -} from "#/parser/service/tailordb/types"; -import type { TailorDBNamespaceData } from "#/plugin/types"; - -function createField( - name: string, - config: Partial, - relation?: ParsedField["relation"], -): ParsedField { - return { - name, - config: { - type: config.type ?? "string", - required: config.required, - description: config.description, - allowedValues: config.allowedValues, - array: config.array, - index: config.index, - unique: config.unique, - foreignKey: config.foreignKey, - foreignKeyType: config.foreignKeyType, - foreignKeyField: config.foreignKeyField, - rawRelation: config.rawRelation, - validate: config.validate, - hooks: config.hooks, - serial: config.serial, - scale: config.scale, - fields: config.fields, - }, - relation, - }; -} - -function createType( - name: string, - fields: Record = {}, - options: Partial = {}, -): TailorDBType { - return { - name, - pluralForm: options.pluralForm ?? `${name}s`, - description: options.description, - fields, - forwardRelationships: options.forwardRelationships ?? {}, - backwardRelationships: options.backwardRelationships ?? {}, - settings: options.settings ?? {}, - permissions: options.permissions ?? {}, - indexes: options.indexes, - files: options.files, - }; -} - -function createNamespace(types: Record): TailorDBNamespaceData { - return { - namespace: "shop", - types, - sourceInfo: new Map(), - pluginAttachments: new Map(), - }; -} - -describe("buildTailorDbErdSchema", () => { - test("maps TailorDB types into TailorDB ERD schema v1", () => { - const customer = createType( - "Customer", - { - name: createField("name", { - type: "string", - required: true, - description: "Customer name", - }), - status: createField("status", { - type: "enum", - required: false, - allowedValues: [ - { value: "active", description: "Can place orders" }, - { value: "inactive" }, - ], - }), - tags: createField("tags", { - type: "string", - array: true, - }), - }, - { - description: "Customer records", - indexes: { - idx_customer_name: { fields: ["name"] }, - }, - }, - ); - - const schema = buildTailorDbErdSchema({ - namespaceData: createNamespace({ Customer: customer }), - generatedAt: "2026-01-01T00:00:00.000Z", - }); - - expect(schema).toMatchObject({ - version: 1, - namespace: "shop", - source: "local", - generatedAt: "2026-01-01T00:00:00.000Z", - }); - expect(schema.cleanRoom.notes.join(" ")).toContain("does not copy Liam source code"); - expect(schema.tables[0]!).toMatchObject({ - name: "Customer", - pluralForm: "Customers", - description: "Customer records", - }); - expect(schema.tables[0]!.columns).toEqual([ - { - name: "id", - type: "uuid", - required: true, - array: false, - primaryKey: true, - unique: true, - }, - { - name: "name", - type: "string", - required: true, - array: false, - description: "Customer name", - index: true, - indexNames: ["idx_customer_name"], - }, - { - name: "status", - type: "enum", - required: false, - array: false, - enumValues: ["active", "inactive"], - enumValueDescriptions: { active: "Can place orders" }, - }, - { - name: "tags", - type: "string", - required: true, - array: true, - }, - ]); - }); - - test("builds relations from parsed TailorDB relation metadata", () => { - const customer = createType("Customer"); - const order = createType("Order", { - customerId: createField( - "customerId", - { - type: "uuid", - required: true, - foreignKey: true, - foreignKeyType: "Customer", - foreignKeyField: "id", - rawRelation: { - type: "n-1", - toward: { type: "Customer", as: "customer", key: "id" }, - backward: "orders", - }, - }, - { - targetType: "Customer", - forwardName: "customer", - backwardName: "orders", - key: "id", - unique: false, - }, - ), - }); - - const schema = buildTailorDbErdSchema({ - namespaceData: createNamespace({ Customer: customer, Order: order }), - generatedAt: "2026-01-01T00:00:00.000Z", - }); - - expect(schema.relations).toEqual([ - { - name: "Order.customerId->Customer.id", - sourceTable: "Order", - sourceColumns: ["customerId"], - targetTable: "Customer", - targetColumns: ["id"], - required: true, - unique: false, - kind: "relation", - relationType: "n-1", - forwardName: "customer", - backwardName: "orders", - }, - ]); - expect(schema.tables.find((table) => table.name === "Order")?.columns[1]!.relation).toEqual({ - targetTable: "Customer", - targetColumn: "id", - kind: "relation", - required: true, - relationType: "n-1", - forwardName: "customer", - backwardName: "orders", - }); - }); - - test("does not duplicate the implicit parsed id field", () => { - const user = createType("User", { - id: createField("id", { - type: "uuid", - required: true, - }), - email: createField("email", { - type: "string", - required: true, - }), - }); - - const schema = buildTailorDbErdSchema({ - namespaceData: createNamespace({ User: user }), - generatedAt: "2026-01-01T00:00:00.000Z", - }); - - expect(schema.tables[0]!.columns.map((column) => column.name)).toEqual(["id", "email"]); - expect(schema.tables[0]!.columns[0]!).toMatchObject({ - name: "id", - primaryKey: true, - unique: true, - }); - }); - - test("includes plugin source metadata without local file paths", () => { - const namespace = createNamespace({ - AuditLog: createType("AuditLog"), - }); - namespace.sourceInfo = new Map([ - [ - "AuditLog", - { - exportName: "AuditLog", - pluginId: "audit-plugin", - pluginImportPath: "@example/audit", - originalFilePath: "/Users/example/project/tailordb/user.ts", - originalExportName: "User", - generatedTypeKind: "history", - namespace: "shop", - }, - ], - ]); - - const schema = buildTailorDbErdSchema({ - namespaceData: namespace, - generatedAt: "2026-01-01T00:00:00.000Z", - }); - - expect(schema.tables[0]!.source).toEqual({ - kind: "plugin", - exportName: "AuditLog", - pluginId: "audit-plugin", - pluginImportPath: "@example/audit", - originalExportName: "User", - generatedTypeKind: "history", - namespace: "shop", - }); - expect(JSON.stringify(schema)).not.toContain("/Users/example"); - }); - - test("keeps revisions stable when only generatedAt changes", () => { - const namespace = createNamespace({ User: createType("User") }); - - const first = buildTailorDbErdSchema({ - namespaceData: namespace, - generatedAt: "2026-01-01T00:00:00.000Z", - }); - const second = buildTailorDbErdSchema({ - namespaceData: namespace, - generatedAt: "2026-01-02T00:00:00.000Z", - }); - - expect(first.revision).toBe(second.revision); - }); -}); - -describe("writeTailorDbErdSchemaToFile", () => { - let tempDir: string; - - beforeEach(() => { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tailordb-erd-schema-")); - }); - - afterEach(() => { - fs.rmSync(tempDir, { recursive: true, force: true }); - vi.restoreAllMocks(); - }); - - test("writes schema JSON to disk", () => { - const schema = buildTailorDbErdSchema({ - namespaceData: createNamespace({ User: createType("User") }), - generatedAt: "2026-01-01T00:00:00.000Z", - }); - const outputPath = path.join(tempDir, "schema.json"); - - writeTailorDbErdSchemaToFile({ schema, outputPath }); - - expect(JSON.parse(fs.readFileSync(outputPath, "utf8"))).toMatchObject({ - version: 1, - namespace: "shop", - tables: [{ name: "User" }], - }); - }); -}); diff --git a/packages/sdk/src/cli/commands/tailordb/erd/schema.ts b/packages/sdk/src/cli/commands/tailordb/erd/schema.ts deleted file mode 100644 index 494c6aa11..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/schema.ts +++ /dev/null @@ -1,288 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "pathe"; -import { hashContent } from "#/cli/cache/hasher"; -import { logger } from "#/cli/shared/logger"; -import { isPluginGeneratedType } from "#/parser/service/tailordb/type-source"; -import type { - OperatorFieldConfig, - ParsedField, - TailorDBType, - TypeSourceInfoEntry, -} from "#/parser/service/tailordb/types"; -import type { TailorDBNamespaceData } from "#/plugin/types"; -import type { - TailorDbErdColumn, - TailorDbErdColumnRelation, - TailorDbErdIndex, - TailorDbErdRelation, - TailorDbErdRelationship, - TailorDbErdSchema, - TailorDbErdSource, - TailorDbErdTable, - TailorDbErdTypeSource, -} from "./types"; - -const CLEAN_ROOM_NOTES = [ - "Generated by a TailorDB-specific viewer implementation.", - "The implementation is based on TailorDB schema contracts, public Liam documentation, Liam CLI help, and black-box generated-output observation.", - "It does not copy Liam source code, generated JavaScript/CSS, parser internals, or layout internals.", -]; - -interface BuildTailorDbErdSchemaOptions { - namespaceData: TailorDBNamespaceData; - generatedAt?: string; - source?: TailorDbErdSource; -} - -interface WriteTailorDbErdSchemaOptions { - schema: TailorDbErdSchema; - outputPath: string; -} - -interface BuildColumnOptions { - fieldName: string; - fieldConfig: OperatorFieldConfig; - parsedField?: ParsedField; - indexEntries: Array<[string, TailorDbErdIndex]>; -} - -function buildRevision(schema: Omit): string { - return hashContent(JSON.stringify(schema)).slice(0, 16); -} - -function toTypeSource(source: TypeSourceInfoEntry | undefined): TailorDbErdTypeSource | undefined { - if (!source) return undefined; - if (isPluginGeneratedType(source)) { - return { - kind: "plugin", - exportName: source.exportName, - pluginId: source.pluginId, - pluginImportPath: source.pluginImportPath, - originalExportName: source.originalExportName, - generatedTypeKind: source.generatedTypeKind, - namespace: source.namespace, - }; - } - return { - kind: "user", - exportName: source.exportName, - }; -} - -function toRelationships( - relationships: Record, -): TailorDbErdRelationship[] { - return Object.entries(relationships) - .toSorted(([a], [b]) => a.localeCompare(b)) - .map(([name, relationship]) => ({ - name, - targetType: relationship.targetType, - targetField: relationship.targetField, - sourceField: relationship.sourceField, - isArray: relationship.isArray, - ...(relationship.description && { description: relationship.description }), - })); -} - -function toIndexes(type: TailorDBType): TailorDbErdIndex[] { - return Object.entries(type.indexes ?? {}) - .toSorted(([a], [b]) => a.localeCompare(b)) - .map(([name, index]) => ({ - name, - fields: [...index.fields], - unique: index.unique === true, - })); -} - -function toColumnRelation(options: BuildColumnOptions): TailorDbErdColumnRelation | undefined { - const { fieldConfig, parsedField } = options; - if (!fieldConfig.foreignKey || !fieldConfig.foreignKeyType) { - return undefined; - } - - const required = fieldConfig.required !== false; - const kind = parsedField?.relation || fieldConfig.rawRelation ? "relation" : "foreignKey"; - return { - targetTable: fieldConfig.foreignKeyType, - targetColumn: fieldConfig.foreignKeyField || "id", - kind, - required, - ...(fieldConfig.rawRelation?.type && { relationType: fieldConfig.rawRelation.type }), - ...(parsedField?.relation?.forwardName && { forwardName: parsedField.relation.forwardName }), - ...(parsedField?.relation?.backwardName && { - backwardName: parsedField.relation.backwardName, - }), - }; -} - -function toColumn(options: BuildColumnOptions): TailorDbErdColumn { - const { fieldName, fieldConfig } = options; - const indexNames = options.indexEntries - .filter(([, index]) => index.fields.includes(fieldName)) - .map(([name]) => name); - const uniqueIndexNames = options.indexEntries - .filter(([, index]) => index.unique && index.fields.includes(fieldName)) - .map(([name]) => name); - - const enumValues = fieldConfig.allowedValues?.map((value) => value.value) ?? []; - const enumValueDescriptions = Object.fromEntries( - (fieldConfig.allowedValues ?? []).flatMap((value) => - value.description ? [[value.value, value.description]] : [], - ), - ); - const nestedFields = Object.entries(fieldConfig.fields ?? {}).map(([nestedName, nestedConfig]) => - toColumn({ - fieldName: nestedName, - fieldConfig: nestedConfig, - indexEntries: [], - }), - ); - const relation = toColumnRelation(options); - - return { - name: fieldName, - type: fieldConfig.type || "string", - required: fieldConfig.required !== false, - array: fieldConfig.array === true, - ...(fieldConfig.description && { description: fieldConfig.description }), - ...(fieldConfig.unique && { unique: true }), - ...((fieldConfig.index || indexNames.length > 0) && { index: true }), - ...(indexNames.length > 0 && { indexNames }), - ...(uniqueIndexNames.length > 0 && { uniqueIndexNames }), - ...(enumValues.length > 0 && { enumValues }), - ...(Object.keys(enumValueDescriptions).length > 0 && { enumValueDescriptions }), - ...(fieldConfig.vector && { vector: true }), - ...(fieldConfig.serial && { serial: { ...fieldConfig.serial } }), - ...(fieldConfig.scale !== undefined && { scale: fieldConfig.scale }), - ...(fieldConfig.validate?.length && { validations: fieldConfig.validate.length }), - ...(fieldConfig.hooks && { - hooks: { - ...(fieldConfig.hooks.create && { create: true }), - ...(fieldConfig.hooks.update && { update: true }), - }, - }), - ...(nestedFields.length > 0 && { fields: nestedFields }), - ...(relation && { relation }), - }; -} - -function toTable(type: TailorDBType, source: TypeSourceInfoEntry | undefined): TailorDbErdTable { - const indexes = toIndexes(type); - const indexEntries: Array<[string, TailorDbErdIndex]> = indexes.map((index) => [ - index.name, - index, - ]); - const fieldColumns = Object.entries(type.fields) - .filter(([fieldName]) => fieldName !== "id") - .map(([fieldName, field]) => - toColumn({ - fieldName, - fieldConfig: field.config, - parsedField: field, - indexEntries, - }), - ); - - const typeSource = toTypeSource(source); - - return { - name: type.name, - pluralForm: type.pluralForm, - ...(type.description && { description: type.description }), - ...(typeSource && { source: typeSource }), - columns: [ - { - name: "id", - type: "uuid", - required: true, - array: false, - primaryKey: true, - unique: true, - }, - ...fieldColumns, - ], - indexes, - forwardRelationships: toRelationships(type.forwardRelationships), - backwardRelationships: toRelationships(type.backwardRelationships), - }; -} - -function toRelation(sourceTable: string, field: ParsedField): TailorDbErdRelation | undefined { - const relation = toColumnRelation({ - fieldName: field.name, - fieldConfig: field.config, - parsedField: field, - indexEntries: [], - }); - if (!relation) return undefined; - - return { - name: `${sourceTable}.${field.name}->${relation.targetTable}.${relation.targetColumn}`, - sourceTable, - sourceColumns: [field.name], - targetTable: relation.targetTable, - targetColumns: [relation.targetColumn], - required: relation.required, - unique: field.config.unique === true || field.relation?.unique === true, - kind: relation.kind, - ...(relation.relationType && { relationType: relation.relationType }), - ...(relation.forwardName && { forwardName: relation.forwardName }), - ...(relation.backwardName && { backwardName: relation.backwardName }), - }; -} - -function buildRelations(types: Record): TailorDbErdRelation[] { - const relations: TailorDbErdRelation[] = []; - for (const type of Object.values(types)) { - for (const field of Object.values(type.fields)) { - const relation = toRelation(type.name, field); - if (relation) { - relations.push(relation); - } - } - } - return relations.toSorted((a, b) => a.name.localeCompare(b.name)); -} - -/** - * Build the TailorDB ERD viewer schema for one namespace. - * @param options - Schema build options. - * @returns TailorDB ERD viewer schema. - */ -export function buildTailorDbErdSchema(options: BuildTailorDbErdSchemaOptions): TailorDbErdSchema { - const { namespaceData } = options; - const tables = Object.values(namespaceData.types) - .toSorted((a, b) => a.name.localeCompare(b.name)) - .map((type) => toTable(type, namespaceData.sourceInfo.get(type.name))); - - const schemaWithoutRevision = { - version: 1 as const, - namespace: namespaceData.namespace, - source: options.source ?? "local", - cleanRoom: { - implementation: "tailor" as const, - notes: CLEAN_ROOM_NOTES, - }, - tables, - relations: buildRelations(namespaceData.types), - }; - - return { - ...schemaWithoutRevision, - generatedAt: options.generatedAt ?? new Date().toISOString(), - revision: buildRevision(schemaWithoutRevision), - }; -} - -/** - * Writes a TailorDB ERD viewer schema to disk. - * @param options - Schema write options. - */ -export function writeTailorDbErdSchemaToFile(options: WriteTailorDbErdSchemaOptions): void { - const json = JSON.stringify(options.schema, null, 2); - fs.mkdirSync(path.dirname(options.outputPath), { recursive: true }); - fs.writeFileSync(options.outputPath, json, "utf8"); - - const relativePath = path.relative(process.cwd(), options.outputPath); - logger.success(`Wrote ERD schema to ${relativePath}`); -} diff --git a/packages/sdk/src/cli/commands/tailordb/erd/serve.test.ts b/packages/sdk/src/cli/commands/tailordb/erd/serve.test.ts deleted file mode 100644 index 04f9ead5b..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/serve.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "pathe"; -import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { resolveWatchPaths } from "./serve"; -import type { ErdBuildResult } from "./export"; -import type { LocalErdSchemaContext } from "./local-schema"; - -describe("resolveWatchPaths", () => { - let tempDir: string; - - beforeEach(() => { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tailordb-erd-watch-")); - }); - - afterEach(() => { - fs.rmSync(tempDir, { recursive: true, force: true }); - }); - - test("expands TailorDB file globs and includes the literal base directory", async () => { - const configPath = path.join(tempDir, "tailor.config.ts"); - const typeDir = path.join(tempDir, "tailordb"); - const typeFile = path.join(typeDir, "user.ts"); - fs.writeFileSync(configPath, "export default {};"); - fs.mkdirSync(typeDir); - fs.writeFileSync(typeFile, "export const User = {};"); - - const context = { - config: { - path: configPath, - db: { - main: { - files: [path.join(typeDir, "*.ts")], - }, - }, - }, - namespaces: [], - } as unknown as LocalErdSchemaContext; - const results = [{ namespace: "main" }] as ErdBuildResult[]; - - const paths = await resolveWatchPaths(context, results); - - expect(paths).toContain(configPath); - expect(paths).toContain(typeFile); - expect(paths).toContain(typeDir); - expect(paths).not.toContain(path.join(typeDir, "*.ts")); - }); -}); diff --git a/packages/sdk/src/cli/commands/tailordb/erd/serve.ts b/packages/sdk/src/cli/commands/tailordb/erd/serve.ts deleted file mode 100644 index 7694b32c3..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/serve.ts +++ /dev/null @@ -1,492 +0,0 @@ -import { spawn } from "node:child_process"; -import * as fs from "node:fs"; -import { glob } from "node:fs/promises"; -import * as http from "node:http"; -import { watch, type FSWatcher } from "chokidar"; -import { lookup as lookupMime } from "mime-types"; -import open from "open"; -import * as path from "pathe"; -import { arg } from "politty"; -import { z } from "zod"; -import { configArg } from "#/cli/shared/args"; -import { defineAppCommand } from "#/cli/shared/command"; -import { loadConfig, type LoadedConfig } from "#/cli/shared/config-loader"; -import { logger } from "#/cli/shared/logger"; -import { prepareErdBuildsFromContext, type ErdBuildResult } from "./export"; -import { loadLocalErdSchema, type LocalErdSchemaContext } from "./local-schema"; -import { initErdCommand } from "./utils"; - -const DEFAULT_ERD_BASE_DIR = ".tailor/erd"; -const LOCAL_HOST = "127.0.0.1"; - -interface StaticServerResult { - server: http.Server; - url: string; -} - -interface StartStaticServerOptions { - distDir: string; - port: number; -} - -interface WatchOptions { - configPath?: string; - namespace?: string; - outputDir: string; - initialContext: LocalErdSchemaContext; - initialResults: ErdBuildResult[]; -} - -interface FreshErdExportOptions { - configPath?: string; - namespace?: string; - outputDir: string; -} - -interface ErdExportJsonResult { - namespace: string; - distDir: string; -} - -interface OpenStaticFileResult { - filePath: string; - fd: number; -} - -const GLOB_CHARS = /[*?[\]{}()!+@]/; - -function formatServeCommand(namespace: string): string { - return `tailor tailordb erd serve --namespace ${namespace}`; -} - -function getCacheControl(filePath: string): string { - return filePath.endsWith(".html") || filePath.endsWith(".json") - ? "no-cache" - : "public, max-age=3600"; -} - -function resolveRequestPath(distDir: string, requestUrl: string | undefined): string | undefined { - const url = new URL(requestUrl ?? "/", "http://localhost"); - let pathname: string; - try { - pathname = decodeURIComponent(url.pathname); - } catch { - return undefined; - } - - if (pathname === "/" || pathname.endsWith("/")) { - pathname = path.join(pathname, "index.html"); - } - - const root = path.resolve(distDir); - const filePath = path.resolve(root, `.${pathname}`); - if (filePath !== root && !filePath.startsWith(`${root}${path.sep}`)) { - return undefined; - } - return filePath; -} - -function openStaticFile(filePath: string): OpenStaticFileResult | undefined { - let fd: number | undefined; - try { - fd = fs.openSync(filePath, "r"); - if (!fs.fstatSync(fd).isFile()) { - fs.closeSync(fd); - return undefined; - } - return { filePath, fd }; - } catch { - if (fd !== undefined) { - fs.closeSync(fd); - } - return undefined; - } -} - -function serveFile(distDir: string, req: http.IncomingMessage, res: http.ServerResponse): void { - const filePath = resolveRequestPath(distDir, req.url); - if (!filePath) { - res.writeHead(403); - res.end("Forbidden"); - return; - } - - const fallbackPath = path.join(distDir, "index.html"); - const target = openStaticFile(filePath) ?? openStaticFile(fallbackPath); - if (!target) { - res.writeHead(503, { - "Content-Type": "text/plain; charset=utf-8", - "Cache-Control": "no-cache", - "Retry-After": "1", - }); - res.end("ERD build is refreshing. Please retry."); - return; - } - - const mimeType = lookupMime(target.filePath) || "application/octet-stream"; - const stream = fs.createReadStream(target.filePath, { - fd: target.fd, - autoClose: true, - }); - stream.on("error", () => { - if (!res.headersSent) { - res.writeHead(503, { - "Content-Type": "text/plain; charset=utf-8", - "Cache-Control": "no-cache", - "Retry-After": "1", - }); - res.end("ERD build is refreshing. Please retry."); - return; - } - res.destroy(); - }); - res.writeHead(200, { - "Content-Type": mimeType, - "Cache-Control": getCacheControl(target.filePath), - }); - stream.pipe(res); -} - -async function startStaticServer(options: StartStaticServerOptions): Promise { - const server = http.createServer((req, res) => { - serveFile(options.distDir, req, res); - }); - - return await new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(options.port, LOCAL_HOST, () => { - server.off("error", reject); - const address = server.address(); - if (!address || typeof address === "string") { - reject(new Error("Failed to determine ERD server address.")); - return; - } - resolve({ - server, - url: `http://${LOCAL_HOST}:${address.port}`, - }); - }); - }); -} - -function getWatchPatterns(config: LoadedConfig, results: ErdBuildResult[]): string[] { - const namespaces = new Set(results.map((result) => result.namespace)); - const patterns = [config.path]; - for (const namespace of namespaces) { - const dbConfig = config.db?.[namespace]; - if (dbConfig && !("external" in dbConfig)) { - patterns.push(...dbConfig.files); - } - } - return [...new Set(patterns)]; -} - -function hasGlobPattern(pattern: string): boolean { - return GLOB_CHARS.test(pattern); -} - -function globBaseDir(pattern: string): string { - const absolutePattern = path.resolve(pattern); - const parsed = path.parse(absolutePattern); - const relativePattern = absolutePattern.slice(parsed.root.length); - const literalParts: string[] = []; - for (const part of relativePattern.split(path.sep)) { - if (!part || GLOB_CHARS.test(part)) break; - literalParts.push(part); - } - - const literalPath = - literalParts.length > 0 ? path.join(parsed.root, ...literalParts) : parsed.root; - if (!literalPath || literalPath === parsed.root) return parsed.root || process.cwd(); - if (!fs.existsSync(literalPath)) return path.dirname(literalPath); - return fs.statSync(literalPath).isDirectory() ? literalPath : path.dirname(literalPath); -} - -async function expandWatchPattern(pattern: string): Promise { - if (!hasGlobPattern(pattern)) { - return [path.resolve(pattern)]; - } - - const paths = new Set(); - for await (const file of glob(pattern)) { - paths.add(path.resolve(file)); - } - - const baseDir = globBaseDir(pattern); - if (fs.existsSync(baseDir) && fs.statSync(baseDir).isDirectory()) { - paths.add(baseDir); - } - return [...paths]; -} - -async function resolveWatchPathsFromConfig( - config: LoadedConfig, - results: ErdBuildResult[], -): Promise { - const paths = new Set(); - for (const pattern of getWatchPatterns(config, results)) { - for (const watchPath of await expandWatchPattern(pattern)) { - paths.add(watchPath); - } - } - return [...paths]; -} - -export async function resolveWatchPaths( - context: LocalErdSchemaContext, - results: ErdBuildResult[], -): Promise { - return await resolveWatchPathsFromConfig(context.config, results); -} - -function parseFreshErdExportResults(stdout: string): ErdBuildResult[] { - const lines = stdout - .trim() - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean); - for (const line of lines.toReversed()) { - try { - const parsed = JSON.parse(line) as unknown; - if (!Array.isArray(parsed)) continue; - return parsed.map((entry): ErdBuildResult => { - const result = entry as Partial; - if (typeof result.namespace !== "string" || typeof result.distDir !== "string") { - throw new Error("Invalid ERD export JSON output."); - } - return { - namespace: result.namespace, - distDir: result.distDir, - }; - }); - } catch { - continue; - } - } - throw new Error("Failed to parse ERD export JSON output."); -} - -function freshErdExportArgs(options: FreshErdExportOptions): string[] { - const cliEntry = process.argv[1]; - if (!cliEntry) { - throw new Error("Cannot rebuild ERD schema in a fresh process: CLI entrypoint is unavailable."); - } - - const args = [cliEntry, "tailordb", "erd", "export", "--output", options.outputDir, "--json"]; - if (options.configPath) { - args.push("--config", options.configPath); - } - if (options.namespace) { - args.push("--namespace", options.namespace); - } - return args; -} - -async function runFreshErdExport(options: FreshErdExportOptions): Promise { - return await new Promise((resolve, reject) => { - const child = spawn(process.execPath, freshErdExportArgs(options), { - cwd: process.cwd(), - env: process.env, - stdio: ["ignore", "pipe", "pipe"], - }); - let stdout = ""; - let stderr = ""; - child.stdout.setEncoding("utf8"); - child.stderr.setEncoding("utf8"); - child.stdout.on("data", (chunk: string) => { - stdout += chunk; - }); - child.stderr.on("data", (chunk: string) => { - stderr += chunk; - }); - child.on("error", reject); - child.on("close", (code, signal) => { - if (code !== 0) { - const detail = stderr.trim() || signal || `exit code ${code}`; - reject(new Error(`Fresh ERD export failed: ${detail}`)); - return; - } - try { - resolve(parseFreshErdExportResults(stdout)); - } catch (error) { - reject(error); - } - }); - }); -} - -function selectPrimaryResult(results: ErdBuildResult[]): ErdBuildResult { - const [primary, ...rest] = results; - if (!primary) { - throw new Error("No ERD build results found."); - } - - logger.info(`Serving ERD for namespace "${primary.namespace}".`); - if (rest.length > 0) { - const commands = rest.map((result) => ` - ${formatServeCommand(result.namespace)}`).join("\n"); - logger.warn(`Multiple namespaces found. To serve another namespace, run:\n${commands}`); - } - - return primary; -} - -async function createErdWatcher(options: WatchOptions): Promise { - let rebuilding = false; - let pending = false; - let watchPaths = await resolveWatchPaths(options.initialContext, options.initialResults); - let importNonce = 0; - - const watcher = watch(watchPaths, { - ignored: /node_modules/, - ignoreInitial: true, - awaitWriteFinish: { - stabilityThreshold: 100, - pollInterval: 100, - }, - }); - - async function rebuild(): Promise { - if (rebuilding) { - pending = true; - return; - } - - rebuilding = true; - try { - const results = await runFreshErdExport({ - configPath: options.configPath, - namespace: options.namespace, - outputDir: options.outputDir, - }); - const { config } = await loadConfig(options.configPath, { - importNonce: String((importNonce += 1)), - }); - const nextWatchPaths = await resolveWatchPathsFromConfig(config, results); - watcher.unwatch(watchPaths); - watcher.add(nextWatchPaths); - watchPaths = nextWatchPaths; - logger.success( - `Rebuilt ERD schema (${results.map((result) => result.namespace).join(", ")})`, - { - mode: "stream", - }, - ); - } catch (error) { - logger.error("Failed to rebuild ERD schema. Serving the last successful build.", { - mode: "stream", - }); - logger.error(String(error)); - } finally { - rebuilding = false; - if (pending) { - pending = false; - await rebuild(); - } - } - } - - let debounceTimer: NodeJS.Timeout | undefined; - const scheduleRebuild = (changedPath: string) => { - logger.info(`Schema source changed: ${path.relative(process.cwd(), changedPath)}`, { - mode: "stream", - }); - if (debounceTimer) { - clearTimeout(debounceTimer); - } - debounceTimer = setTimeout(() => { - rebuild(); - }, 150); - }; - - watcher.on("add", scheduleRebuild); - watcher.on("change", scheduleRebuild); - watcher.on("unlink", scheduleRebuild); - watcher.on("error", (error) => { - logger.error(`ERD watcher error: ${String(error)}`, { mode: "stream" }); - }); - - return watcher; -} - -async function waitForShutdown(server: http.Server, watcher: FSWatcher): Promise { - return await new Promise((resolve) => { - const shutdown = () => { - watcher.close().finally(() => { - server.close(() => { - logger.info("ERD server stopped."); - resolve(); - }); - }); - }; - - process.once("SIGINT", shutdown); - process.once("SIGTERM", shutdown); - }); -} - -export const erdServeCommand = defineAppCommand({ - name: "serve", - description: "Generate and serve TailorDB ERD locally with watch reload. (beta)", - args: z.strictObject({ - ...configArg, - namespace: arg(z.string().optional(), { - alias: "n", - description: "TailorDB namespace name (uses first namespace in config if not specified)", - }), - port: arg(z.coerce.number().int().min(0).max(65535).default(0), { - description: "Local server port (0 selects a free port)", - }), - open: arg(z.boolean().default(false), { - description: "Open the ERD viewer in the default browser", - }), - }), - run: async (args) => { - initErdCommand(); - - const outputDir = path.resolve(process.cwd(), DEFAULT_ERD_BASE_DIR); - const context = await loadLocalErdSchema({ - configPath: args.config, - namespaces: args.namespace ? [args.namespace] : undefined, - }); - const results = prepareErdBuildsFromContext({ - context, - namespace: args.namespace, - outputDir, - }); - const primary = selectPrimaryResult(results); - const { server, url } = await startStaticServer({ - distDir: primary.distDir, - port: args.port, - }); - const watchUrl = `${url}/?watch=1`; - const watcher = await createErdWatcher({ - configPath: args.config, - namespace: args.namespace, - outputDir, - initialContext: context, - initialResults: results, - }); - - logger.newline(); - if (args.json) { - logger.out({ - namespace: primary.namespace, - url: watchUrl, - distDir: primary.distDir, - }); - } else { - logger.success("ERD server started."); - logger.out(watchUrl); - } - - if (args.open) { - try { - await open(watchUrl); - } catch { - logger.warn("Failed to open browser automatically. Please open the URL above manually."); - } - } - - await waitForShutdown(server, watcher); - }, -}); diff --git a/packages/sdk/src/cli/commands/tailordb/erd/types.ts b/packages/sdk/src/cli/commands/tailordb/erd/types.ts deleted file mode 100644 index e89c0e0ad..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/types.ts +++ /dev/null @@ -1,104 +0,0 @@ -export type TailorDbErdSource = "local"; - -export interface TailorDbErdTypeSource { - kind: "user" | "plugin"; - exportName?: string; - pluginId?: string; - pluginImportPath?: string; - originalExportName?: string; - generatedTypeKind?: string; - namespace?: string; -} - -export interface TailorDbErdColumnRelation { - targetTable: string; - targetColumn: string; - kind: "foreignKey" | "relation"; - required: boolean; - relationType?: string; - forwardName?: string; - backwardName?: string; -} - -export interface TailorDbErdColumn { - name: string; - type: string; - required: boolean; - array: boolean; - description?: string; - primaryKey?: boolean; - unique?: boolean; - index?: boolean; - indexNames?: string[]; - uniqueIndexNames?: string[]; - enumValues?: string[]; - enumValueDescriptions?: Record; - vector?: boolean; - serial?: { - start: number; - maxValue?: number; - format?: string; - }; - scale?: number; - validations?: number; - hooks?: { - create?: boolean; - update?: boolean; - }; - fields?: TailorDbErdColumn[]; - relation?: TailorDbErdColumnRelation; -} - -export interface TailorDbErdIndex { - name: string; - fields: string[]; - unique: boolean; -} - -export interface TailorDbErdRelationship { - name: string; - targetType: string; - targetField: string; - sourceField: string; - isArray: boolean; - description?: string; -} - -export interface TailorDbErdTable { - name: string; - pluralForm: string; - description?: string; - source?: TailorDbErdTypeSource; - columns: TailorDbErdColumn[]; - indexes: TailorDbErdIndex[]; - forwardRelationships: TailorDbErdRelationship[]; - backwardRelationships: TailorDbErdRelationship[]; -} - -export interface TailorDbErdRelation { - name: string; - sourceTable: string; - sourceColumns: string[]; - targetTable: string; - targetColumns: string[]; - required: boolean; - unique: boolean; - kind: "foreignKey" | "relation"; - relationType?: string; - forwardName?: string; - backwardName?: string; -} - -export interface TailorDbErdSchema { - version: 1; - namespace: string; - generatedAt: string; - revision: string; - source: TailorDbErdSource; - cleanRoom: { - implementation: "tailor"; - notes: string[]; - }; - tables: TailorDbErdTable[]; - relations: TailorDbErdRelation[]; -} diff --git a/packages/sdk/src/cli/commands/tailordb/erd/utils.ts b/packages/sdk/src/cli/commands/tailordb/erd/utils.ts deleted file mode 100644 index 9234dd501..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { logBetaWarning } from "#/cli/shared/beta"; -import { initOperatorClient } from "#/cli/shared/client"; -import { loadAccessToken, loadWorkspaceId } from "#/cli/shared/context"; -import type { OperatorClient } from "#/cli/shared/client"; - -export interface ErdDeployContext { - client: OperatorClient; - workspaceId: string; -} - -type ErdDeployContextOptions = { - profile?: string; - workspaceId?: string; -}; - -/** - * Initialize shared ERD command behavior. - */ -export function initErdCommand(): void { - logBetaWarning("tailordb erd"); -} - -/** - * Initialize platform context for ERD deployment. - * @param args - CLI arguments. - * @returns Initialized deploy context. - */ -export async function initErdDeployContext( - args: ErdDeployContextOptions, -): Promise { - initErdCommand(); - const accessToken = await loadAccessToken({ - profile: args.profile, - }); - const client = await initOperatorClient(accessToken); - const workspaceId = await loadWorkspaceId({ - workspaceId: args.workspaceId, - profile: args.profile, - }); - - return { client, workspaceId }; -} diff --git a/packages/sdk/src/cli/commands/tailordb/erd/viewer-assets/app.js b/packages/sdk/src/cli/commands/tailordb/erd/viewer-assets/app.js deleted file mode 100644 index ba5fc454d..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/viewer-assets/app.js +++ /dev/null @@ -1,1424 +0,0 @@ -const TABLE_WIDTH = 260; -const TABLE_HEIGHT = 62; -const FIELD_ROW_HEIGHT = 28; -const FIELD_SECTION_BORDER_HEIGHT = 1; -const X_GAP = 240; -const Y_GAP = 56; -const CARDINALITY_MARKER_WIDTH = 50; -const CROW_FOOT_TIP_OFFSET = 0; -const CROW_FOOT_JOIN_OFFSET = 18; -const CARDINALITY_OUTER_OFFSET = 32; -const DRAG_THRESHOLD = 4; -const FIT_PADDING = 80; -const MIN_ZOOM = 0.25; -const MAX_ZOOM = 2.2; -const DEFAULT_SHOW_MODE = "TABLE_NAME"; -const DEFAULT_VIEW_MODE = "diff"; -const SHOW_MODE_OPTIONS = [ - { value: "ALL_FIELDS", label: "All Fields" }, - { value: "TABLE_NAME", label: "Table Name" }, - { value: "KEY_ONLY", label: "Key Only" }, -]; - -const elements = { - main: document.querySelector(".main"), - namespace: document.getElementById("namespace"), - revision: document.getElementById("revision"), - search: document.getElementById("search"), - tableSummary: document.getElementById("table-count-summary"), - toggleAllTables: document.getElementById("toggle-all-tables"), - tableList: document.getElementById("table-list"), - canvas: document.getElementById("canvas"), - world: document.getElementById("world"), - edges: document.getElementById("edges"), - nodes: document.getElementById("nodes"), - details: document.getElementById("details"), - emptyState: document.getElementById("empty-state"), - status: document.getElementById("status"), - zoomIn: document.getElementById("zoom-in"), - zoomOut: document.getElementById("zoom-out"), - zoomLabel: document.getElementById("zoom-label"), - fitView: document.getElementById("fit-view"), - showMode: document.getElementById("show-mode"), - showModeMenu: document.getElementById("show-mode-menu"), - viewModeControl: document.getElementById("view-mode-control"), - viewModeCurrent: document.getElementById("view-mode-current"), - viewModeDiff: document.getElementById("view-mode-diff"), - copyLink: document.getElementById("copy-link"), -}; - -let schema; -let diffViewSchema; -let currentViewSchema; -let diffMetadata; -let erdDiff; -let layout; -let selectedTable; -let searchText = ""; -let viewport = { x: 32, y: 32, z: 1 }; -let hasZoomFromHash = false; -let userAdjustedViewport = false; -let hashUpdatesDisabled = false; -let showMode = DEFAULT_SHOW_MODE; -let activeCardDrag; -let activeCanvasPan; -let activeViewportAnimation; -let suppressNextCanvasClick = false; -let viewMode = DEFAULT_VIEW_MODE; -const manualNodePositions = new Map(); -const hiddenTableNames = new Set(); -let diffIndex = new Map(); -let tableDiffIndex = new Map(); - -const DIFF_LABELS = { - added: "Added", - changed: "Changed", - removed: "Removed", -}; - -const DIFF_MARKS = { - added: "+", - changed: "~", - removed: "-", -}; - -const DIFF_ACTION_RANK = { - added: 2, - changed: 1, - removed: 3, -}; - -function escapeHtml(value) { - return String(value ?? "") - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """); -} - -function clamp(value, min, max) { - return Math.min(max, Math.max(min, value)); -} - -function tableByName(name) { - return schema?.tables.find((table) => table.name === name); -} - -function strongestDiffAction(current, next) { - if (!current) return next; - if (!next) return current; - return DIFF_ACTION_RANK[next] > DIFF_ACTION_RANK[current] ? next : current; -} - -function diffKey(entity, path) { - return `${entity}:${path}`; -} - -function diffChange(entity, path) { - return diffIndex.get(diffKey(entity, path)); -} - -function setTableDiffAction(tableName, action) { - if (!tableName || !action) return; - tableDiffIndex.set(tableName, strongestDiffAction(tableDiffIndex.get(tableName), action)); -} - -function tableNameFromEntityPath(path) { - return path.split(".")[0]; -} - -function tableNamesFromRelationChange(change) { - const relation = schema?.relations.find((item) => item.name === change.path); - if (relation) return [relation.sourceTable, relation.targetTable]; - - const [source, target] = change.path.split("->"); - return [tableNameFromEntityPath(source || ""), tableNameFromEntityPath(target || "")].filter( - Boolean, - ); -} - -function setDiff(nextDiff) { - erdDiff = nextDiff; - diffIndex = new Map(); - tableDiffIndex = new Map(); - if (!nextDiff?.changes) return; - - for (const change of nextDiff.changes) { - diffIndex.set(diffKey(change.entity, change.path), change); - if (change.entity === "table") { - setTableDiffAction(change.path, change.action); - continue; - } - if (change.entity === "column" || change.entity === "index") { - setTableDiffAction(tableNameFromEntityPath(change.path), "changed"); - continue; - } - if (change.entity === "relation") { - for (const tableName of tableNamesFromRelationChange(change)) { - setTableDiffAction(tableName, "changed"); - } - } - } -} - -function diffClass(action) { - return action ? `is-diff-${action}` : ""; -} - -function diffBadge(action, detail) { - if (!action) return ""; - const title = detail ? `${DIFF_LABELS[action]}: ${detail}` : DIFF_LABELS[action]; - return `${DIFF_MARKS[action]}`; -} - -function diffDetail(change) { - if (!change?.detail) return ""; - return `${escapeHtml(change.detail)}`; -} - -function tableDiffChange(tableName) { - return diffChange("table", tableName); -} - -function tableDiffAction(tableName) { - return tableDiffIndex.get(tableName); -} - -function columnDiffChange(tableName, columnName) { - return diffChange("column", `${tableName}.${columnName}`); -} - -function indexDiffChange(tableName, indexName) { - return diffChange("index", `${tableName}.${indexName}`); -} - -function relationDiffChange(relation) { - return diffChange("relation", relation.name); -} - -function relationDiffAction(relation) { - return relationDiffChange(relation)?.action; -} - -function showModeOption(value) { - return SHOW_MODE_OPTIONS.find((option) => option.value === value); -} - -function isTableHidden(tableName) { - return hiddenTableNames.has(tableName); -} - -function visibleTables() { - return schema.tables.filter((table) => !isTableHidden(table.name)); -} - -function visibleTableNames() { - return visibleTables().map((table) => table.name); -} - -function readHashState() { - const params = new URLSearchParams(location.hash.slice(1)); - selectedTable = params.get("table") || undefined; - const nextShowMode = params.get("show"); - if (showModeOption(nextShowMode)) { - showMode = nextShowMode; - } - const nextViewMode = params.get("view"); - if (nextViewMode === "current" || nextViewMode === "diff") { - viewMode = nextViewMode; - } - hiddenTableNames.clear(); - for (const tableName of (params.get("hidden") || "").split(",")) { - if (tableName) hiddenTableNames.add(tableName); - } - const z = Number(params.get("z")); - if (params.has("z") && Number.isFinite(z)) { - viewport = { ...viewport, z: clamp(z, MIN_ZOOM, MAX_ZOOM) }; - hasZoomFromHash = true; - } -} - -function writeHashState() { - if (hashUpdatesDisabled) return; - const params = new URLSearchParams(); - if (selectedTable) params.set("table", selectedTable); - if (showMode !== DEFAULT_SHOW_MODE) params.set("show", showMode); - if (hiddenTableNames.size > 0) { - params.set("hidden", [...hiddenTableNames].toSorted((a, b) => a.localeCompare(b)).join(",")); - } - if (canSwitchViewMode() && viewMode !== DEFAULT_VIEW_MODE) { - params.set("view", viewMode); - } - params.set("z", viewport.z.toFixed(3)); - try { - history.replaceState(null, "", `#${params.toString()}`); - } catch { - // Disable further hash writes once they fail (e.g. sandboxed artifact - // preview) so panning/zooming does not throw on every frame. - hashUpdatesDisabled = true; - } -} - -const SCHEMA_BLOCK_PATTERN = - / - - diff --git a/packages/sdk/src/cli/commands/tailordb/erd/viewer-assets/serve.json b/packages/sdk/src/cli/commands/tailordb/erd/viewer-assets/serve.json deleted file mode 100644 index 31a8ab4e3..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/viewer-assets/serve.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "headers": [ - { - "source": "**/*.@(html|json)", - "headers": [ - { - "key": "Cache-Control", - "value": "no-cache" - } - ] - } - ] -} diff --git a/packages/sdk/src/cli/commands/tailordb/erd/viewer-assets/styles.css b/packages/sdk/src/cli/commands/tailordb/erd/viewer-assets/styles.css deleted file mode 100644 index 747087dc3..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/viewer-assets/styles.css +++ /dev/null @@ -1,1036 +0,0 @@ -:root { - color-scheme: dark; - --bg: #111313; - --canvas: #101111; - --panel: #202322; - --panel-raised: #262928; - --line: #343938; - --line-strong: #555d5b; - --text: #e7ecea; - --muted: #a0a7a4; - --subtle: #2d3130; - --accent: #4950e5; - --accent-weak: rgba(73, 80, 229, 0.18); - --accent-glow: rgba(73, 80, 229, 0.34); - --warn: #f2b84b; - --danger: #ff6b5f; - --diff-added: #4ed983; - --diff-added-bg: rgba(78, 217, 131, 0.14); - --diff-changed: #f2b84b; - --diff-changed-bg: rgba(242, 184, 75, 0.14); - --diff-removed: #ff6b5f; - --diff-removed-bg: rgba(255, 107, 95, 0.14); - --shadow: 0 16px 34px rgba(0, 0, 0, 0.42); - --card-width: 260px; - --toolbar-height: 56px; -} - -* { - box-sizing: border-box; -} - -html, -body, -#app { - width: 100%; - height: 100%; - margin: 0; -} - -body { - background: var(--bg); - color: var(--text); - font-family: - Inter, - ui-sans-serif, - system-ui, - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - sans-serif; - letter-spacing: 0; -} - -button, -input { - font: inherit; -} - -button { - height: 34px; - border: 1px solid var(--line); - border-radius: 7px; - background: #1a1d1c; - color: var(--text); - cursor: pointer; -} - -button:hover { - border-color: var(--line-strong); - background: #242827; -} - -.app { - display: grid; - grid-template-rows: var(--toolbar-height) 1fr; - min-width: 0; -} - -.toolbar { - display: grid; - grid-template-columns: minmax(220px, 1fr) minmax(240px, 360px) minmax(120px, 1fr); - align-items: center; - gap: 18px; - padding: 8px 16px; - border-bottom: 1px solid var(--line); - background: #222525; -} - -.brand { - display: flex; - align-items: center; - gap: 12px; - min-width: 0; -} - -.brand-mark { - display: inline-grid; - width: 30px; - height: 30px; - place-items: center; - border-radius: 35%; - background: var(--accent); - color: #fff; - font-weight: 800; -} - -.brand h1 { - margin: 0; - overflow: hidden; - font-size: 16px; - font-weight: 700; - line-height: 1.2; - text-overflow: ellipsis; - white-space: nowrap; -} - -.brand p { - margin: 2px 0 0; - overflow: hidden; - color: var(--muted); - font-size: 11px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.toolbar-search input { - width: 100%; - height: 34px; - border: 1px solid var(--line); - border-radius: 7px; - padding: 0 12px; - outline: none; - background: #141716; - color: var(--text); -} - -.toolbar-search input::placeholder { - color: #777e7b; -} - -.toolbar-search input:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-weak); -} - -.toolbar-actions { - display: flex; - align-items: center; - gap: 10px; - justify-content: flex-end; -} - -.view-mode-control { - display: inline-grid; - grid-template-columns: repeat(2, minmax(72px, 1fr)); - min-width: 152px; - border: 1px solid var(--line); - border-radius: 8px; - padding: 2px; - background: #171a19; -} - -.view-mode-control[hidden] { - display: none; -} - -.view-mode-button { - height: 28px; - border: 0; - border-radius: 6px; - background: transparent; - color: var(--muted); - font-size: 12px; - font-weight: 800; -} - -.view-mode-button:hover { - background: #252a28; - color: var(--text); -} - -.view-mode-button[aria-pressed="true"] { - background: var(--accent); - color: #fff; -} - -.primary-action { - min-width: 104px; - border-color: var(--accent); - background: var(--accent); - color: #fff; - font-weight: 800; -} - -.primary-action:hover { - background: #5f65ee; -} - -.main { - display: grid; - min-height: 0; - grid-template-columns: 240px minmax(0, 1fr) 360px; -} - -.main.is-details-collapsed { - grid-template-columns: 240px minmax(0, 1fr); -} - -.table-nav, -.details { - min-width: 0; - overflow: auto; - border-color: var(--line); - background: var(--panel); -} - -.table-nav { - border-right: 1px solid var(--line); -} - -.details { - border-left: 1px solid var(--line); -} - -.nav-head { - display: flex; - position: sticky; - top: 0; - z-index: 2; - align-items: center; - justify-content: space-between; - gap: 8px; - height: 52px; - padding: 0 12px; - border-bottom: 1px solid var(--line); - background: var(--panel); -} - -.nav-title { - display: grid; - min-width: 0; - gap: 2px; -} - -.nav-head h2 { - margin: 0; - font-size: 13px; -} - -.nav-head span { - color: var(--muted); - font-size: 12px; -} - -.table-list { - display: grid; - gap: 1px; - padding: 8px 0; -} - -.table-list-row { - display: grid; - grid-template-columns: minmax(0, 1fr) 32px; - align-items: center; - min-width: 0; -} - -.table-select { - display: grid; - grid-template-columns: 16px minmax(0, 1fr); - align-items: center; - gap: 8px; - width: 100%; - height: 34px; - border: 0; - border-radius: 0; - padding: 0 12px; - background: transparent; - color: var(--muted); - text-align: left; -} - -.table-select:hover { - background: #252a28; - color: var(--text); -} - -.table-select[aria-current="true"] { - background: linear-gradient(90deg, var(--accent-weak), rgba(73, 80, 229, 0.04)); - color: var(--text); - font-weight: 700; -} - -.table-list-label, -.table-name-wrap, -.field-name, -.detail-name { - display: flex; - min-width: 0; - align-items: center; - gap: 6px; -} - -.table-name-wrap { - flex: 1 1 auto; -} - -.table-list-name, -.field-name-text { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.diff-badge { - display: inline-grid; - flex: 0 0 auto; - min-width: 16px; - height: 16px; - place-items: center; - border: 1px solid currentColor; - border-radius: 999px; - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - font-size: 11px; - font-weight: 800; - line-height: 1; -} - -.diff-badge-added { - background: var(--diff-added-bg); - color: var(--diff-added); -} - -.diff-badge-changed { - background: var(--diff-changed-bg); - color: var(--diff-changed); -} - -.diff-badge-removed { - background: var(--diff-removed-bg); - color: var(--diff-removed); -} - -.diff-detail { - display: block; - margin-top: 5px; - color: var(--muted); - font-size: 11px; - line-height: 1.35; - overflow-wrap: anywhere; -} - -.detail-row.is-diff-added .diff-detail { - color: var(--diff-added); -} - -.detail-row.is-diff-changed .diff-detail { - color: var(--diff-changed); -} - -.detail-row.is-diff-removed .diff-detail { - color: var(--diff-removed); -} - -.table-list-row.is-diff-added .table-select { - background: linear-gradient(90deg, var(--diff-added-bg), transparent); - color: var(--text); -} - -.table-list-row.is-diff-changed .table-select { - background: linear-gradient(90deg, var(--diff-changed-bg), transparent); - color: var(--text); -} - -.table-list-row.is-diff-removed .table-select { - background: linear-gradient(90deg, var(--diff-removed-bg), transparent); - color: var(--text); -} - -.table-list-row.is-hidden .table-select { - color: #69716e; -} - -.table-visibility-toggle { - display: grid; - width: 28px; - height: 28px; - place-items: center; - border: 0; - border-radius: 6px; - padding: 0; - background: transparent; - color: var(--muted); -} - -.table-visibility-toggle:hover:not(:disabled) { - background: #252a28; - color: var(--text); -} - -.table-visibility-toggle:disabled { - opacity: 0.42; -} - -.eye-icon { - width: 15px; - height: 15px; - fill: none; - stroke: currentColor; - stroke-linecap: round; - stroke-linejoin: round; - stroke-width: 1.8; -} - -.table-list-icon, -.table-icon { - display: inline-block; - width: 14px; - height: 14px; - border: 1px solid currentColor; - border-radius: 2px; - opacity: 0.86; - background: - linear-gradient(90deg, transparent 45%, currentColor 45%, currentColor 55%, transparent 55%), - linear-gradient(transparent 45%, currentColor 45%, currentColor 55%, transparent 55%); -} - -.canvas { - position: relative; - min-width: 0; - overflow: hidden; - background: - radial-gradient(circle, rgba(255, 255, 255, 0.11) 1px, transparent 1.2px), var(--canvas); - background-size: 22px 22px; - cursor: grab; - touch-action: none; - user-select: none; -} - -.canvas.is-panning { - cursor: grabbing; -} - -.world { - position: absolute; - top: 0; - left: 0; - transform-origin: 0 0; -} - -.edges, -.nodes { - position: absolute; - top: 0; - left: 0; -} - -.edges { - overflow: visible; - pointer-events: none; -} - -.edge { - fill: none; - stroke: #4c5351; - stroke-width: 2; -} - -.edge.is-selected { - filter: drop-shadow(0 0 7px var(--accent-glow)); - stroke: var(--accent); - stroke-width: 3; -} - -.edge.is-diff-added { - stroke: var(--diff-added); -} - -.edge.is-diff-changed { - stroke: var(--diff-changed); -} - -.edge.is-diff-removed { - stroke: var(--diff-removed); - stroke-dasharray: 8 5; -} - -.edge.is-selected.is-diff-added, -.edge.is-selected.is-diff-changed, -.edge.is-selected.is-diff-removed { - stroke-width: 3; -} - -.edge-cardinality { - pointer-events: none; -} - -.edge-cardinality line, -.edge-cardinality circle { - fill: var(--canvas); - stroke: #4c5351; - stroke-linecap: square; - stroke-linejoin: round; - stroke-width: 2; -} - -.edge-cardinality.is-selected line, -.edge-cardinality.is-selected circle { - fill: var(--canvas); - stroke: var(--accent); - stroke-width: 3; -} - -.edge-cardinality.is-diff-added line, -.edge-cardinality.is-diff-added circle { - stroke: var(--diff-added); -} - -.edge-cardinality.is-diff-changed line, -.edge-cardinality.is-diff-changed circle { - stroke: var(--diff-changed); -} - -.edge-cardinality.is-diff-removed line, -.edge-cardinality.is-diff-removed circle { - stroke: var(--diff-removed); -} - -.table-card { - appearance: none; - position: absolute; - display: flex; - flex-direction: column; - width: var(--card-width); - min-height: 62px; - align-items: stretch; - padding: 0; - border: 1px solid var(--line-strong); - border-radius: 8px; - background: var(--panel-raised); - box-shadow: 0 12px 26px rgba(0, 0, 0, 0.32); - color: var(--muted); - cursor: grab; - font: inherit; - text-align: left; - touch-action: none; - user-select: none; -} - -.table-card:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 3px; -} - -.table-card.is-selected { - border-color: var(--accent); - box-shadow: - 0 0 0 1px var(--accent), - 0 0 26px var(--accent-glow), - 0 18px 32px rgba(0, 0, 0, 0.44); - color: var(--text); -} - -.table-card.is-related { - border-color: #55625d; - color: var(--text); -} - -.table-card.is-muted { - opacity: 0.36; -} - -.table-card.is-diff-added { - border-color: var(--diff-added); - box-shadow: - 0 0 0 1px rgba(78, 217, 131, 0.32), - 0 16px 30px rgba(0, 0, 0, 0.38); -} - -.table-card.is-diff-changed { - border-color: var(--diff-changed); - box-shadow: - 0 0 0 1px rgba(242, 184, 75, 0.3), - 0 16px 30px rgba(0, 0, 0, 0.38); -} - -.table-card.is-diff-removed { - border-color: var(--diff-removed); - box-shadow: - 0 0 0 1px rgba(255, 107, 95, 0.32), - 0 16px 30px rgba(0, 0, 0, 0.38); -} - -.table-card.is-dragging { - z-index: 4; - cursor: grabbing; -} - -.table-head { - display: flex; - min-width: 0; - min-height: 60px; - align-items: center; - gap: 10px; - padding: 0 16px; -} - -.table-card.is-diff-added .table-head { - background: linear-gradient(90deg, var(--diff-added-bg), transparent); -} - -.table-card.is-diff-changed .table-head { - background: linear-gradient(90deg, var(--diff-changed-bg), transparent); -} - -.table-card.is-diff-removed .table-head { - background: linear-gradient(90deg, var(--diff-removed-bg), transparent); -} - -.table-card.is-diff-removed .table-name { - text-decoration: line-through; - text-decoration-color: var(--diff-removed); -} - -.table-name { - min-width: 0; - overflow: hidden; - font-size: 16px; - font-weight: 700; - text-overflow: ellipsis; - white-space: nowrap; -} - -.table-fields { - display: grid; - border-top: 1px solid var(--line); -} - -.table-field { - display: grid; - min-width: 0; - height: 28px; - grid-template-columns: minmax(0, 1fr) auto; - gap: 8px; - align-items: center; - padding: 0 10px 0 16px; - color: var(--muted); - font-size: 11px; -} - -.table-field + .table-field { - border-top: 1px solid #303534; -} - -.table-field.is-key { - color: var(--text); -} - -.table-field.is-diff-added { - background: var(--diff-added-bg); - color: var(--text); -} - -.table-field.is-diff-changed { - background: var(--diff-changed-bg); - color: var(--text); -} - -.table-field.is-diff-removed { - background: var(--diff-removed-bg); - color: var(--text); -} - -.table-field.is-diff-removed .field-name-text { - text-decoration: line-through; - text-decoration-color: var(--diff-removed); -} - -.field-type { - max-width: 92px; - overflow: hidden; - color: #7f8784; - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - font-size: 10px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.details-inner { - display: grid; - gap: 18px; - padding: 16px; -} - -.details h2, -.details h3 { - margin: 0; - line-height: 1.2; -} - -.details h2 { - display: flex; - align-items: center; - gap: 8px; - font-size: 16px; -} - -.details h3 { - color: var(--text); - font-size: 14px; -} - -.details p { - margin: 6px 0 0; - color: var(--muted); - font-size: 13px; - line-height: 1.45; -} - -.details-section { - display: grid; - gap: 10px; - border-top: 1px solid var(--line); - padding-top: 16px; -} - -.detail-list { - display: grid; - gap: 8px; -} - -.detail-row { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 10px; - align-items: start; - border: 1px solid var(--line); - border-radius: 4px; - padding: 9px; - background: #2a2d2c; -} - -.detail-row.is-diff-added { - border-color: rgba(78, 217, 131, 0.58); - background: linear-gradient(90deg, var(--diff-added-bg), #2a2d2c 72%); -} - -.detail-row.is-diff-changed { - border-color: rgba(242, 184, 75, 0.58); - background: linear-gradient(90deg, var(--diff-changed-bg), #2a2d2c 72%); -} - -.detail-row.is-diff-removed { - border-color: rgba(255, 107, 95, 0.58); - background: linear-gradient(90deg, var(--diff-removed-bg), #2a2d2c 72%); -} - -.detail-row.is-diff-removed strong { - text-decoration: line-through; - text-decoration-color: var(--diff-removed); -} - -.detail-row strong { - display: block; - overflow-wrap: anywhere; - font-size: 12px; -} - -.detail-row code, -.pill { - border-radius: 4px; - background: #363a39; - padding: 2px 6px; - color: var(--text); - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - font-size: 11px; -} - -.pill-wrap { - display: flex; - flex-wrap: wrap; - gap: 5px; - margin-top: 7px; -} - -.muted { - color: var(--muted); -} - -.empty-state, -.status { - position: absolute; - left: 50%; - transform: translateX(-50%); - border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); - box-shadow: var(--shadow); -} - -.empty-state { - top: 45%; - padding: 16px 20px; - color: var(--muted); -} - -.status { - bottom: 78px; - max-width: min(560px, calc(100% - 32px)); - padding: 10px 12px; - color: var(--danger); - font-size: 12px; -} - -.canvas-toolbar { - display: flex; - position: absolute; - bottom: 18px; - left: 50%; - z-index: 3; - align-items: center; - gap: 10px; - height: 42px; - border: 1px solid var(--line); - border-radius: 10px; - padding: 0 12px; - background: rgba(38, 41, 40, 0.96); - box-shadow: var(--shadow); - transform: translateX(-50%); -} - -.canvas-toolbar button { - width: 28px; - height: 28px; - border: 0; - padding: 0; - background: transparent; - color: var(--text); - font-size: 17px; -} - -#fit-view { - width: auto; - padding: 0 6px; - font-size: 13px; - font-weight: 700; -} - -.show-mode-control { - position: relative; - display: flex; -} - -.show-mode-button { - display: flex; - width: auto !important; - min-width: 104px; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 0 8px !important; - color: var(--text); - font-size: 12px !important; - font-weight: 700; -} - -.show-mode-caret { - width: 7px; - height: 7px; - border-right: 1.5px solid currentColor; - border-bottom: 1.5px solid currentColor; - transform: translateY(-2px) rotate(45deg); -} - -.show-mode-menu { - position: absolute; - right: 0; - bottom: calc(100% + 10px); - z-index: 4; - display: grid; - width: 158px; - gap: 2px; - border: 1px solid var(--line); - border-radius: 8px; - padding: 5px; - background: rgba(32, 35, 34, 0.98); - box-shadow: var(--shadow); -} - -.show-mode-menu[hidden] { - display: none; -} - -.show-mode-option { - display: grid; - width: 100% !important; - height: 30px !important; - grid-template-columns: minmax(0, 1fr) 14px; - gap: 8px; - align-items: center; - border-radius: 6px !important; - padding: 0 8px !important; - color: var(--muted) !important; - font-size: 12px !important; - text-align: left; -} - -.show-mode-option:hover, -.show-mode-option[aria-checked="true"] { - background: #2a2e2d !important; - color: var(--text) !important; -} - -.show-mode-check { - display: grid; - place-items: center; -} - -.check-icon { - width: 13px; - height: 13px; - fill: none; - stroke: var(--accent); - stroke-linecap: round; - stroke-linejoin: round; - stroke-width: 2.3; -} - -#zoom-label { - width: 48px; - color: var(--text); - font-size: 13px; - font-weight: 700; - text-align: center; -} - -.toolbar-divider { - width: 1px; - height: 24px; - background: var(--line); -} - -@media (max-width: 980px) { - .toolbar { - grid-template-columns: minmax(180px, 1fr) minmax(220px, 320px) auto; - } - - .main { - grid-template-rows: minmax(0, 1fr) minmax(220px, 36vh); - grid-template-columns: 220px minmax(0, 1fr); - } - - .main.is-details-collapsed { - grid-template-rows: minmax(0, 1fr); - grid-template-columns: 220px minmax(0, 1fr); - } - - .details { - grid-column: 1 / -1; - border-top: 1px solid var(--line); - border-left: 0; - } -} - -@media (max-width: 700px) { - html, - body, - #app { - height: auto; - min-height: 100%; - } - - .app { - grid-template-rows: auto auto; - min-height: 100%; - } - - .toolbar { - grid-template-columns: 1fr auto; - gap: 10px; - padding: 8px; - } - - .toolbar-actions { - grid-column: 1 / -1; - justify-content: stretch; - } - - .view-mode-control { - flex: 1 1 auto; - } - - .toolbar-search { - grid-column: 1 / -1; - } - - .brand-mark { - display: none; - } - - .brand h1 { - font-size: 16px; - } - - .brand p { - overflow-wrap: anywhere; - white-space: normal; - } - - .main { - min-height: auto; - grid-template-rows: auto minmax(520px, 64vh) auto; - grid-template-columns: 1fr; - } - - .main.is-details-collapsed { - grid-template-rows: auto minmax(520px, 64vh); - grid-template-columns: 1fr; - } - - .table-nav { - max-height: 240px; - border-right: 0; - border-bottom: 1px solid var(--line); - } - - .canvas { - min-height: 520px; - } - - .details { - max-height: none; - overflow: visible; - } -} diff --git a/packages/sdk/src/cli/commands/tailordb/erd/viewer.test.ts b/packages/sdk/src/cli/commands/tailordb/erd/viewer.test.ts deleted file mode 100644 index a4720bf90..000000000 --- a/packages/sdk/src/cli/commands/tailordb/erd/viewer.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { buildViewerHtml } from "./viewer"; -import type { TailorDbErdSchema } from "./types"; - -function buildSchema(overrides: Partial = {}): TailorDbErdSchema { - return { - version: 1, - namespace: "tailordb", - generatedAt: "2026-01-01T00:00:00.000Z", - revision: "test-revision", - source: "local", - cleanRoom: { implementation: "tailor", notes: [] }, - tables: [ - { - name: "User", - pluralForm: "users", - columns: [], - indexes: [], - forwardRelationships: [], - backwardRelationships: [], - }, - ], - relations: [], - ...overrides, - }; -} - -// Extract and parse the embedded schema JSON the same way the viewer (and any -// external tooling such as a future ERD diff) would. -function extractEmbeddedSchema(html: string): unknown { - const match = html.match(/" }), - }); - - expect(html).not.toContain(""); - expect(html).toContain("\\u003c/script>\\u003cimg src=x>"); - // The escaped value round-trips back to the original via JSON.parse. - expect(extractEmbeddedSchema(html)).toMatchObject({ namespace: "" }); - }); - - test("preserves U+2028/U+2029 in embedded values via JSON round-trip", () => { - const lineSep = String.fromCharCode(0x2028); - const paraSep = String.fromCharCode(0x2029); - const description = `line${lineSep}para${paraSep}end`; - const html = buildViewerHtml({ - schema: buildSchema({ - tables: [ - { - name: "User", - pluralForm: "users", - description, - columns: [], - indexes: [], - forwardRelationships: [], - backwardRelationships: [], - }, - ], - }), - }); - - const parsed = extractEmbeddedSchema(html) as TailorDbErdSchema; - expect(parsed.tables[0]?.description).toBe(description); - }); - - test("embeds optional diff data for the viewer", () => { - const html = buildViewerHtml({ - schema: buildSchema(), - diff: { - namespace: "tailordb", - baseRevision: "base", - headRevision: "head", - changed: true, - summary: { added: 1, changed: 0, removed: 0 }, - changes: [{ action: "added", entity: "table", path: "User", detail: "Table added" }], - }, - }); - - expect(html).toContain(''; - -export interface WriteViewerDistOptions { - schema: TailorDbErdSchema; - distDir: string; -} - -export interface BuildViewerHtmlOptions { - schema: TailorDbErdSchema; - currentSchema?: TailorDbErdSchema; - diff?: unknown; - title?: string; -} - -function escapeHtml(value: string): string { - return value - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """); -} - -function assetDirCandidates(): string[] { - const currentDir = path.dirname(fileURLToPath(import.meta.url)); - return [ - path.join(currentDir, "viewer-assets"), - path.join(currentDir, VIEWER_ASSETS_DIR), - path.join(currentDir, "commands", "tailordb", "erd", VIEWER_ASSETS_DIR), - path.resolve(process.cwd(), "packages/sdk/src/cli/commands/tailordb/erd/viewer-assets"), - ]; -} - -/** - * Resolve the packaged ERD viewer asset directory. - * @returns Absolute path to the viewer asset directory. - */ -export function resolveViewerAssetsDir(): string { - for (const candidate of assetDirCandidates()) { - if (fs.existsSync(path.join(candidate, "index.html"))) { - return candidate; - } - } - - throw new Error(`ERD viewer assets not found. Checked: ${assetDirCandidates().join(", ")}`); -} - -/** - * Build the self-contained ERD viewer HTML document. CSS, JS, and the schema - * are inlined as separately extractable blocks: a ``) - .replace( - APP_SCRIPT, - () => `${embedScript}${currentSchemaScript}${diffScript}\n ${inlineScript}`, - ); -} - -/** - * Write the self-contained TailorDB ERD viewer to `/index.html`. - * @param options - Viewer dist write options. - */ -export function writeViewerDist(options: WriteViewerDistOptions): void { - fs.rmSync(options.distDir, { recursive: true, force: true }); - fs.mkdirSync(options.distDir, { recursive: true }); - fs.writeFileSync( - path.join(options.distDir, "index.html"), - buildViewerHtml({ schema: options.schema }), - "utf8", - ); -} diff --git a/packages/sdk/src/cli/commands/tailordb/index.ts b/packages/sdk/src/cli/commands/tailordb/index.ts index 43de0d14e..2ec5fc285 100644 --- a/packages/sdk/src/cli/commands/tailordb/index.ts +++ b/packages/sdk/src/cli/commands/tailordb/index.ts @@ -1,14 +1,14 @@ import { defineCommand } from "politty"; -import { erdCommand } from "./erd"; import { migrationCommand } from "./migrate"; import { truncateCommand } from "./truncate"; export const tailordbCommand = defineCommand({ name: "tailordb", description: "Manage TailorDB tables and data.", + notes: + "The `tailordb erd` commands are provided by the @tailor-platform/sdk-tailordb-erd-plugin CLI plugin.", subCommands: { truncate: truncateCommand, migration: migrationCommand, - erd: erdCommand, }, }); diff --git a/packages/sdk/src/cli/completion.test.ts b/packages/sdk/src/cli/completion.test.ts index fc0082b99..36f596f57 100644 --- a/packages/sdk/src/cli/completion.test.ts +++ b/packages/sdk/src/cli/completion.test.ts @@ -41,7 +41,6 @@ describe("shell completion", () => { test("completes nested subcommands for tailordb", async () => { const values = await completeValues(["tailordb", ""]); - expect(values).toContain("erd"); expect(values).toContain("migration"); expect(values).toContain("truncate"); }); @@ -77,13 +76,13 @@ describe("shell completion", () => { }); describe("directory completion", () => { - test.each([ - ["staticwebsite deploy --dir", ["staticwebsite", "deploy", "--dir", ""]], - ["tailordb erd export --output", ["tailordb", "erd", "export", "--output", ""]], - ])("triggers directory completion for %s", async (_label, args) => { - const result = await complete(args); - expect(result.directive & CompletionDirective.DirectoryCompletion).toBeTruthy(); - }); + test.each([["staticwebsite deploy --dir", ["staticwebsite", "deploy", "--dir", ""]]])( + "triggers directory completion for %s", + async (_label, args) => { + const result = await complete(args); + expect(result.directive & CompletionDirective.DirectoryCompletion).toBeTruthy(); + }, + ); }); describe("no file completion", () => { diff --git a/packages/sdk/src/cli/index.ts b/packages/sdk/src/cli/index.ts index 03c018faa..6ef2bec52 100644 --- a/packages/sdk/src/cli/index.ts +++ b/packages/sdk/src/cli/index.ts @@ -39,7 +39,7 @@ import { commonArgs, isVerbose } from "./shared/args"; import { isCLIError } from "./shared/errors"; import { logger } from "./shared/logger"; import { readPackageJson } from "./shared/package-json"; -import { dispatchPlugin } from "./shared/plugin"; +import { dispatchPlugin, KNOWN_PLUGIN_PACKAGES } from "./shared/plugin"; import { registerTsHook } from "./shared/register-ts-hook"; await registerTsHook(new URL("./ts-hook.mjs", import.meta.url)); @@ -158,14 +158,27 @@ runMain(mainCommand, { displayErrors: false, // CLI plugin dispatch: an unknown subcommand at any level execs the external // `tailor--` binary, forwarding args and injecting context. - onUnknownSubcommand: ({ commandPath, name, args }) => - dispatchPlugin({ + onUnknownSubcommand: async ({ commandPath, name, args }) => { + const exitCode = await dispatchPlugin({ commandPath, name, args, cliName, profile: process.env.TAILOR_PLATFORM_PROFILE, - }), + }); + if (exitCode !== undefined) { + return exitCode; + } + const knownPackage = KNOWN_PLUGIN_PACKAGES[[...commandPath, name].join("-")]; + if (!knownPackage) { + return undefined; + } + logger.error( + `"${cliName} ${[...commandPath, name].join(" ")}" is provided by the ${knownPackage} CLI plugin, which is not installed.`, + ); + logger.info(`Install it with: npm install -D ${knownPackage}`); + return 1; + }, cleanup: async ({ error }) => { if (error) { if (isCLIError(error)) { diff --git a/packages/sdk/src/cli/shared/beta.ts b/packages/sdk/src/cli/shared/beta.ts index 3702bfddc..c0286d9a8 100644 --- a/packages/sdk/src/cli/shared/beta.ts +++ b/packages/sdk/src/cli/shared/beta.ts @@ -2,7 +2,7 @@ import { logger } from "./logger"; /** * Warn that a feature is in beta. - * @param {string} featureName - Name of the beta feature (e.g., "tailordb erd", "tailordb migration") + * @param {string} featureName - Name of the beta feature (e.g., "tailordb migration") */ export function logBetaWarning(featureName: string): void { logger.warn( diff --git a/packages/sdk/src/cli/shared/plugin.ts b/packages/sdk/src/cli/shared/plugin.ts index a0f40929b..4acfba48d 100644 --- a/packages/sdk/src/cli/shared/plugin.ts +++ b/packages/sdk/src/cli/shared/plugin.ts @@ -13,6 +13,15 @@ import { import { logger } from "./logger"; import { readPackageJson } from "./package-json"; +/** + * npm packages known to provide a plugin slug, keyed by the slug the + * dispatcher resolves (command path joined with `-`). Used to suggest an + * install command when the plugin executable is not found. + */ +export const KNOWN_PLUGIN_PACKAGES: Record = { + "tailordb-erd": "@tailor-platform/sdk-tailordb-erd-plugin", +}; + /** * A plugin discovered on the filesystem. `tailor ` dispatches to the * external `tailor-` executable. diff --git a/packages/sdk/src/cli/shared/readonly-guard.test.ts b/packages/sdk/src/cli/shared/readonly-guard.test.ts index d5b008a90..968cf79cf 100644 --- a/packages/sdk/src/cli/shared/readonly-guard.test.ts +++ b/packages/sdk/src/cli/shared/readonly-guard.test.ts @@ -108,10 +108,6 @@ const READ_OR_LOCAL_COMMAND_PATHS = new Set([ "staticwebsite/domain/list.ts", // TailorDB (read-only / local ops) "tailordb/index.ts", - "tailordb/erd/index.ts", - "tailordb/erd/diff-command.ts", - "tailordb/erd/export.ts", - "tailordb/erd/serve.ts", "tailordb/migrate/index.ts", "tailordb/migrate/generate.ts", "tailordb/migrate/script.ts", diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index fab194128..4e4af08f3 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -1,17 +1,10 @@ -import { cpSync, rmSync } from "node:fs"; +import { cpSync } from "node:fs"; import path from "node:path"; import Sonda from "sonda/rolldown"; import { defineConfig, type TsdownPluginOption } from "tsdown"; import { entry } from "./scripts/build-entries.mjs"; import { loadYamlText } from "./scripts/yaml-text-plugin.mjs"; -function copyErdViewerAssets(outDir: string): void { - const source = path.resolve("src/cli/commands/tailordb/erd/viewer-assets"); - const target = path.resolve(outDir, "cli/erd-viewer-assets"); - rmSync(target, { recursive: true, force: true }); - cpSync(source, target, { recursive: true }); -} - function yamlText() { return { name: "yaml-text", @@ -67,7 +60,6 @@ export default defineConfig([ deps: { neverBundle: externalDeps }, plugins: jsPlugins, onSuccess: (config) => { - copyErdViewerAssets(config.outDir); cpSync(path.resolve("src/cli/ts-hook.mjs"), path.join(config.outDir, "cli/ts-hook.mjs")); cpSync(path.resolve("src/cli/ts-hook.d.mts"), path.join(config.outDir, "cli/ts-hook.d.mts")); }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52bb805db..8074c1100 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ importers: '@connectrpc/connect-node': specifier: 2.1.2 version: 2.1.2(@bufbuild/protobuf@2.12.1)(@connectrpc/connect@2.1.2(@bufbuild/protobuf@2.12.1)) + '@tailor-platform/sdk-tailordb-erd-plugin': + specifier: workspace:^ + version: link:../packages/sdk-tailordb-erd-plugin '@types/node': specifier: 24.13.3 version: 24.13.3 From 7349e305a160a5b15defecf59d8a9bee09a48833 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 08:48:08 +0900 Subject: [PATCH 556/618] refactor(sdk-tailordb-erd-plugin): merge duplicate type imports --- packages/sdk-tailordb-erd-plugin/src/local-schema.ts | 3 +-- packages/sdk-tailordb-erd-plugin/src/schema.test.ts | 8 ++++++-- packages/sdk-tailordb-erd-plugin/src/schema.ts | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/sdk-tailordb-erd-plugin/src/local-schema.ts b/packages/sdk-tailordb-erd-plugin/src/local-schema.ts index 907e3c9cb..514b550cd 100644 --- a/packages/sdk-tailordb-erd-plugin/src/local-schema.ts +++ b/packages/sdk-tailordb-erd-plugin/src/local-schema.ts @@ -1,6 +1,5 @@ import { loadTailorDBNamespaces } from "@tailor-platform/sdk/cli"; -import type { LoadedConfig } from "@tailor-platform/sdk/cli"; -import type { TailorDBNamespaceData } from "@tailor-platform/sdk/cli"; +import type { LoadedConfig, TailorDBNamespaceData } from "@tailor-platform/sdk/cli"; export interface LoadLocalErdSchemaOptions { configPath?: string; diff --git a/packages/sdk-tailordb-erd-plugin/src/schema.test.ts b/packages/sdk-tailordb-erd-plugin/src/schema.test.ts index 2ffc31d7d..7e9d89ab3 100644 --- a/packages/sdk-tailordb-erd-plugin/src/schema.test.ts +++ b/packages/sdk-tailordb-erd-plugin/src/schema.test.ts @@ -3,8 +3,12 @@ import * as os from "node:os"; import * as path from "pathe"; import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { buildTailorDbErdSchema, writeTailorDbErdSchemaToFile } from "./schema"; -import type { OperatorFieldConfig, ParsedField, TailorDBType } from "@tailor-platform/sdk/cli"; -import type { TailorDBNamespaceData } from "@tailor-platform/sdk/cli"; +import type { + OperatorFieldConfig, + ParsedField, + TailorDBNamespaceData, + TailorDBType, +} from "@tailor-platform/sdk/cli"; function createField( name: string, diff --git a/packages/sdk-tailordb-erd-plugin/src/schema.ts b/packages/sdk-tailordb-erd-plugin/src/schema.ts index 1b2386460..1c4b49e35 100644 --- a/packages/sdk-tailordb-erd-plugin/src/schema.ts +++ b/packages/sdk-tailordb-erd-plugin/src/schema.ts @@ -17,10 +17,10 @@ import type { import type { OperatorFieldConfig, ParsedField, + TailorDBNamespaceData, TailorDBType, TypeSourceInfoEntry, } from "@tailor-platform/sdk/cli"; -import type { TailorDBNamespaceData } from "@tailor-platform/sdk/cli"; const CLEAN_ROOM_NOTES = [ "Generated by a TailorDB-specific viewer implementation.", From ec81931f7ae9fec65282e2d834a6949d59756f60 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 08:51:51 +0900 Subject: [PATCH 557/618] fix(sdk-tailordb-erd-plugin): render SDK CLIError details in plugin error output --- packages/sdk-tailordb-erd-plugin/src/index.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/sdk-tailordb-erd-plugin/src/index.ts b/packages/sdk-tailordb-erd-plugin/src/index.ts index 6f71fe52c..38c0b185b 100644 --- a/packages/sdk-tailordb-erd-plugin/src/index.ts +++ b/packages/sdk-tailordb-erd-plugin/src/index.ts @@ -10,6 +10,15 @@ import { erdDiffCommand } from "./diff-command"; import { erdExportCommand } from "./export"; import { erdServeCommand } from "./serve"; import { commonArgs } from "./shared/args"; +import { logger } from "./shared/logger"; + +function hasFormat(error: unknown): error is { format(): string } { + return ( + typeof error === "object" && + error !== null && + typeof (error as { format?: unknown }).format === "function" + ); +} const packageRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); const packageJson = await readPackageJSON(packageRoot); @@ -31,4 +40,16 @@ void runMain(mainCommand, { version: packageJson.version ?? "0.0.0", // strip unknown keys globalArgs: z.object(commonArgs), + displayErrors: false, + // Render the SDK's CLIError format (details/suggestion) like the host CLI does. + cleanup: ({ error }) => { + if (!error) return; + if (hasFormat(error)) { + logger.log(error.format()); + } else if (error instanceof Error) { + logger.error(error.message); + } else { + logger.error(`Unknown error: ${String(error)}`); + } + }, }); From 144d7251a252a10611124c973c2d7d387d2a02c3 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 08:53:08 +0900 Subject: [PATCH 558/618] fix(ci): build the ERD plugin before running erd-schema jobs --- .github/workflows/erd-schema.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/erd-schema.yml b/.github/workflows/erd-schema.yml index 00b9ff489..ee6ec6b9a 100644 --- a/.github/workflows/erd-schema.yml +++ b/.github/workflows/erd-schema.yml @@ -42,6 +42,11 @@ jobs: - uses: ./.github/actions/install-deps + # install-deps only builds the SDK; `tailor tailordb erd` dispatches to + # this plugin's bin, which must also point at a real dist file. + - name: Build ERD plugin + run: pnpm --filter @tailor-platform/sdk-tailordb-erd-plugin run build + - uses: tailor-platform/actions/erd-schema-export@0bd69a0fc9aab3f818b636aa111995441a2c1a03 # v2 branch (unreleased) with: namespace: ${{ matrix.namespace }} @@ -80,6 +85,11 @@ jobs: - name: Install deps uses: ./.github/actions/install-deps + # install-deps only builds the SDK; `tailor tailordb erd` dispatches to + # this plugin's bin, which must also point at a real dist file. + - name: Build ERD plugin + run: pnpm --filter @tailor-platform/sdk-tailordb-erd-plugin run build + - uses: tailor-platform/actions/erd-schema-preview@0bd69a0fc9aab3f818b636aa111995441a2c1a03 # v2 branch (unreleased) with: namespace: ${{ matrix.namespace }} From e245c4518618009647edebd0b91dbd8a9bb0711a Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 09:11:48 +0900 Subject: [PATCH 559/618] fix(sdk): resolve plugin context from forwarded --profile and align plugin CI config - dispatchPlugin now resolves the injected token/workspace/user from an explicit --profile/-p in the forwarded args, so plugin commands run against the requested profile instead of the current one - name the plugin package's Vitest project "unit" so the workspace-wide --project 'unit*' filter matches - trigger the ERD schema workflow on tailordb-namespaces.ts changes - wire the plugin's --verbose flag to stack-trace output on errors --- .github/workflows/erd-schema.yml | 3 ++ packages/sdk-tailordb-erd-plugin/src/index.ts | 3 ++ .../src/shared/args.ts | 3 ++ .../src/shared/logger.ts | 14 ++++++++ .../sdk-tailordb-erd-plugin/vitest.config.ts | 12 +++++-- packages/sdk/src/cli/shared/plugin.test.ts | 18 ++++++++++- packages/sdk/src/cli/shared/plugin.ts | 32 ++++++++++++++++++- 7 files changed, 81 insertions(+), 4 deletions(-) diff --git a/.github/workflows/erd-schema.yml b/.github/workflows/erd-schema.yml index ee6ec6b9a..852bcf915 100644 --- a/.github/workflows/erd-schema.yml +++ b/.github/workflows/erd-schema.yml @@ -6,11 +6,13 @@ on: branches: [main, v2] paths: - packages/sdk-tailordb-erd-plugin/** + - packages/sdk/src/cli/shared/tailordb-namespaces.ts - example/** - .github/workflows/erd-schema.yml pull_request: paths: - packages/sdk-tailordb-erd-plugin/** + - packages/sdk/src/cli/shared/tailordb-namespaces.ts - example/** - .github/workflows/erd-schema.yml @@ -104,6 +106,7 @@ jobs: always-relevant-paths: | example/tailor.config.ts packages/sdk-tailordb-erd-plugin/ + packages/sdk/src/cli/shared/tailordb-namespaces.ts relevant-path-prefix: example/ github-token: ${{ github.token }} diff --git a/packages/sdk-tailordb-erd-plugin/src/index.ts b/packages/sdk-tailordb-erd-plugin/src/index.ts index 38c0b185b..7822bef77 100644 --- a/packages/sdk-tailordb-erd-plugin/src/index.ts +++ b/packages/sdk-tailordb-erd-plugin/src/index.ts @@ -51,5 +51,8 @@ void runMain(mainCommand, { } else { logger.error(`Unknown error: ${String(error)}`); } + if (error instanceof Error && error.stack) { + logger.debug(`\nStack trace:\n${error.stack}`); + } }, }); diff --git a/packages/sdk-tailordb-erd-plugin/src/shared/args.ts b/packages/sdk-tailordb-erd-plugin/src/shared/args.ts index 45e980315..5ed9b5dee 100644 --- a/packages/sdk-tailordb-erd-plugin/src/shared/args.ts +++ b/packages/sdk-tailordb-erd-plugin/src/shared/args.ts @@ -65,6 +65,9 @@ export const commonArgs = { }), verbose: arg(z.boolean().default(false), { description: "Enable verbose logging", + effect: (value) => { + logger.verbose = value; + }, }), json: arg(z.boolean().default(false), { alias: "j", diff --git a/packages/sdk-tailordb-erd-plugin/src/shared/logger.ts b/packages/sdk-tailordb-erd-plugin/src/shared/logger.ts index 400fe9b5c..24c4e6f17 100644 --- a/packages/sdk-tailordb-erd-plugin/src/shared/logger.ts +++ b/packages/sdk-tailordb-erd-plugin/src/shared/logger.ts @@ -25,6 +25,7 @@ const TYPE_COLORS: Record string> = { // In JSON mode, all logs go to stderr to keep stdout clean for JSON data let _jsonMode = false; +let _verbose = false; function writeLog(type: string, message: string, opts?: LogOptions): void { const mode = opts?.mode ?? "default"; @@ -49,6 +50,13 @@ export const logger = { _jsonMode = value; }, + get verbose(): boolean { + return _verbose; + }, + set verbose(value: boolean) { + _verbose = value; + }, + info(message: string, opts?: LogOptions): void { writeLog("info", message, opts); }, @@ -73,6 +81,12 @@ export const logger = { process.stderr.write("\n"); }, + debug(message: string): void { + if (_verbose) { + writeLog("log", chalk.gray(message), { mode: "plain" }); + } + }, + out(data: string | object | object[]): void { if (typeof data === "string") { process.stdout.write(data.endsWith("\n") ? data : `${data}\n`); diff --git a/packages/sdk-tailordb-erd-plugin/vitest.config.ts b/packages/sdk-tailordb-erd-plugin/vitest.config.ts index b8ea3230b..7878e9578 100644 --- a/packages/sdk-tailordb-erd-plugin/vitest.config.ts +++ b/packages/sdk-tailordb-erd-plugin/vitest.config.ts @@ -2,8 +2,16 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - include: ["src/**/?(*.)+(spec|test).ts"], - exclude: ["**/node_modules/**", "**/dist/**"], + projects: [ + { + extends: true, + test: { + name: "unit", + include: ["src/**/?(*.)+(spec|test).ts"], + exclude: ["**/node_modules/**", "**/dist/**"], + }, + }, + ], environment: "node", globals: true, watch: false, diff --git a/packages/sdk/src/cli/shared/plugin.test.ts b/packages/sdk/src/cli/shared/plugin.test.ts index 1059915a6..7b5c0fa32 100644 --- a/packages/sdk/src/cli/shared/plugin.test.ts +++ b/packages/sdk/src/cli/shared/plugin.test.ts @@ -3,7 +3,7 @@ import * as os from "node:os"; import * as path from "pathe"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { getOAuth2ClientId, getPlatformBaseUrl } from "./client"; -import { dispatchPlugin, listPlugins, resolvePlugin } from "./plugin"; +import { dispatchPlugin, explicitProfileFromArgs, listPlugins, resolvePlugin } from "./plugin"; const contextMocks = vi.hoisted(() => ({ loadAccessToken: vi.fn(), @@ -325,3 +325,19 @@ describe("dispatchPlugin", () => { expect(await dispatchPlugin({ name: "missing", args: [], cliName: CLI })).toBeUndefined(); }); }); + +describe("explicitProfileFromArgs", () => { + test.each([ + [["deploy", "--profile", "staging"], "staging"], + [["deploy", "-p", "staging"], "staging"], + [["deploy", "--profile=staging"], "staging"], + [["deploy", "-p=staging"], "staging"], + [["--profile", "a", "deploy", "--profile", "b"], "b"], + [["deploy"], undefined], + [["deploy", "--profile"], undefined], + [["deploy", "--profile", "--json"], undefined], + [["deploy", "--", "--profile", "staging"], undefined], + ])("extracts profile from %j", (args, expected) => { + expect(explicitProfileFromArgs(args)).toBe(expected); + }); +}); diff --git a/packages/sdk/src/cli/shared/plugin.ts b/packages/sdk/src/cli/shared/plugin.ts index 4acfba48d..76987bcf4 100644 --- a/packages/sdk/src/cli/shared/plugin.ts +++ b/packages/sdk/src/cli/shared/plugin.ts @@ -354,6 +354,35 @@ function buildSpawnTarget( return { command: sh, args: ["-c", 'command "$@"', "--", pluginPath, ...args], shell: false }; } +/** + * Extract an explicit `--profile`/`-p` value from args forwarded to a plugin, + * so the injected platform context matches the profile the plugin will use. + * Scanning stops at a `--` terminator; the last occurrence wins. + * @param args - Args forwarded to the plugin + * @returns The explicit profile value, or undefined when not present + */ +export function explicitProfileFromArgs(args: readonly string[]): string | undefined { + let profile: string | undefined; + for (let i = 0; i < args.length; i++) { + const token = args[i]; + if (token === undefined || token === "--") break; + if (token === "--profile" || token === "-p") { + const next = args[i + 1]; + if (next !== undefined && !next.startsWith("-")) { + profile = next; + i++; + } + continue; + } + if (token.startsWith("--profile=")) { + profile = token.slice("--profile=".length); + } else if (token.startsWith("-p=")) { + profile = token.slice("-p=".length); + } + } + return profile || undefined; +} + /** * Resolve and execute a plugin, forwarding stdio and propagating its exit code. * @param params - Dispatch parameters @@ -378,7 +407,8 @@ export async function dispatchPlugin(params: { return undefined; } - const env = { ...process.env, ...(await buildPluginEnv({ profile: params.profile })) }; + const profile = explicitProfileFromArgs(params.args) ?? params.profile; + const env = { ...process.env, ...(await buildPluginEnv({ profile })) }; const { command, args, shell } = buildSpawnTarget(plugin.path, params.args); return await new Promise((resolve) => { From 31e30b04e5c4029087d19a002915292a626381ec Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 09:26:31 +0900 Subject: [PATCH 560/618] fix(ci): publish the ERD plugin in pkg.pr.new previews and document plugin flag placement --- .github/workflows/pkg-pr-new.yml | 2 +- packages/sdk/docs/cli-reference.md | 3 +++ packages/sdk/docs/cli-reference.template.md | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml index 913e93725..f00d2c35e 100644 --- a/.github/workflows/pkg-pr-new.yml +++ b/.github/workflows/pkg-pr-new.yml @@ -70,7 +70,7 @@ jobs: pnpm --config.verify-deps-before-run=false --filter @tailor-platform/create-sdk build - name: Publish to pkg.pr.new - run: pnpm --config.verify-deps-before-run=false exec pkg-pr-new publish --pnpm --compact --commentWithSha --packageManager=pnpm "packages/sdk" "packages/create-sdk" # zizmor: ignore[use-trusted-publishing] + run: pnpm --config.verify-deps-before-run=false exec pkg-pr-new publish --pnpm --compact --commentWithSha --packageManager=pnpm "packages/sdk" "packages/create-sdk" "packages/sdk-tailordb-erd-plugin" # zizmor: ignore[use-trusted-publishing] init-smoke: needs: [publish] diff --git a/packages/sdk/docs/cli-reference.md b/packages/sdk/docs/cli-reference.md index 50eac5291..5df163c08 100644 --- a/packages/sdk/docs/cli-reference.md +++ b/packages/sdk/docs/cli-reference.md @@ -135,6 +135,9 @@ Resolution rules: - **Lookup order:** the project's `node_modules/.bin` (nearest first, walking up from the current directory), then your `PATH`. So a plugin installed as a project dev-dependency takes precedence over a globally installed one. +- **Place global flags after the plugin command.** Only the arguments following the plugin name are + forwarded; a global flag placed before it (e.g. `tailor --json tailordb erd export`) is consumed by + the host CLI and does not reach the plugin. Write `tailor tailordb erd export --json` instead. Because resolution is based on `node_modules/.bin` and `PATH`, any package manager that populates `node_modules/.bin` works for project-local plugins — npm, pnpm (its content-addressable store is diff --git a/packages/sdk/docs/cli-reference.template.md b/packages/sdk/docs/cli-reference.template.md index d9726a2ef..42214bb18 100644 --- a/packages/sdk/docs/cli-reference.template.md +++ b/packages/sdk/docs/cli-reference.template.md @@ -128,6 +128,9 @@ Resolution rules: - **Lookup order:** the project's `node_modules/.bin` (nearest first, walking up from the current directory), then your `PATH`. So a plugin installed as a project dev-dependency takes precedence over a globally installed one. +- **Place global flags after the plugin command.** Only the arguments following the plugin name are + forwarded; a global flag placed before it (e.g. `tailor --json tailordb erd export`) is consumed by + the host CLI and does not reach the plugin. Write `tailor tailordb erd export --json` instead. Because resolution is based on `node_modules/.bin` and `PATH`, any package manager that populates `node_modules/.bin` works for project-local plugins — npm, pnpm (its content-addressable store is From 56146ed23f42f1df6a96335ba494716590236835 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 09:50:50 +0900 Subject: [PATCH 561/618] fix(sdk): skip platform context injection when a plugin gets an explicit env-file Also register the ERD plugin package in the changeset gate, relink its bin after the CI build, build it on pnpm pack via prepack, and drop it from pkg.pr.new previews until its first npm release (--compact requires a published package). --- .github/workflows/changeset-check.yml | 1 + .github/workflows/erd-schema.yml | 16 +++-- .github/workflows/pkg-pr-new.yml | 2 +- packages/sdk-tailordb-erd-plugin/package.json | 2 +- packages/sdk/src/cli/shared/plugin.test.ts | 21 ++++++- packages/sdk/src/cli/shared/plugin.ts | 61 +++++++++++++++---- 6 files changed, 83 insertions(+), 20 deletions(-) diff --git a/.github/workflows/changeset-check.yml b/.github/workflows/changeset-check.yml index 7df22cd31..e07708d9c 100644 --- a/.github/workflows/changeset-check.yml +++ b/.github/workflows/changeset-check.yml @@ -41,6 +41,7 @@ jobs: packages: - 'packages/sdk/**' - 'packages/create-sdk/**' + - 'packages/sdk-tailordb-erd-plugin/**' - 'packages/tailor-proto/**' - name: Fail if no changeset diff --git a/.github/workflows/erd-schema.yml b/.github/workflows/erd-schema.yml index 852bcf915..f0ec2f536 100644 --- a/.github/workflows/erd-schema.yml +++ b/.github/workflows/erd-schema.yml @@ -45,9 +45,13 @@ jobs: - uses: ./.github/actions/install-deps # install-deps only builds the SDK; `tailor tailordb erd` dispatches to - # this plugin's bin, which must also point at a real dist file. + # this plugin's bin, which must also point at a real dist file. Relink + # bins afterwards — pnpm skips bin links whose target is missing at + # install time. - name: Build ERD plugin - run: pnpm --filter @tailor-platform/sdk-tailordb-erd-plugin run build + run: | + pnpm --filter @tailor-platform/sdk-tailordb-erd-plugin run build + pnpm rebuild --pending - uses: tailor-platform/actions/erd-schema-export@0bd69a0fc9aab3f818b636aa111995441a2c1a03 # v2 branch (unreleased) with: @@ -88,9 +92,13 @@ jobs: uses: ./.github/actions/install-deps # install-deps only builds the SDK; `tailor tailordb erd` dispatches to - # this plugin's bin, which must also point at a real dist file. + # this plugin's bin, which must also point at a real dist file. Relink + # bins afterwards — pnpm skips bin links whose target is missing at + # install time. - name: Build ERD plugin - run: pnpm --filter @tailor-platform/sdk-tailordb-erd-plugin run build + run: | + pnpm --filter @tailor-platform/sdk-tailordb-erd-plugin run build + pnpm rebuild --pending - uses: tailor-platform/actions/erd-schema-preview@0bd69a0fc9aab3f818b636aa111995441a2c1a03 # v2 branch (unreleased) with: diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml index f00d2c35e..913e93725 100644 --- a/.github/workflows/pkg-pr-new.yml +++ b/.github/workflows/pkg-pr-new.yml @@ -70,7 +70,7 @@ jobs: pnpm --config.verify-deps-before-run=false --filter @tailor-platform/create-sdk build - name: Publish to pkg.pr.new - run: pnpm --config.verify-deps-before-run=false exec pkg-pr-new publish --pnpm --compact --commentWithSha --packageManager=pnpm "packages/sdk" "packages/create-sdk" "packages/sdk-tailordb-erd-plugin" # zizmor: ignore[use-trusted-publishing] + run: pnpm --config.verify-deps-before-run=false exec pkg-pr-new publish --pnpm --compact --commentWithSha --packageManager=pnpm "packages/sdk" "packages/create-sdk" # zizmor: ignore[use-trusted-publishing] init-smoke: needs: [publish] diff --git a/packages/sdk-tailordb-erd-plugin/package.json b/packages/sdk-tailordb-erd-plugin/package.json index f9bcb9628..69615cbf3 100644 --- a/packages/sdk-tailordb-erd-plugin/package.json +++ b/packages/sdk-tailordb-erd-plugin/package.json @@ -23,7 +23,7 @@ "lint:fix": "oxlint . --fix", "typecheck": "tsc --noEmit", "test": "vitest", - "prepublish": "pnpm run build", + "prepack": "pnpm run build", "publint": "publint --strict" }, "dependencies": { diff --git a/packages/sdk/src/cli/shared/plugin.test.ts b/packages/sdk/src/cli/shared/plugin.test.ts index 7b5c0fa32..a6c381125 100644 --- a/packages/sdk/src/cli/shared/plugin.test.ts +++ b/packages/sdk/src/cli/shared/plugin.test.ts @@ -3,7 +3,13 @@ import * as os from "node:os"; import * as path from "pathe"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { getOAuth2ClientId, getPlatformBaseUrl } from "./client"; -import { dispatchPlugin, explicitProfileFromArgs, listPlugins, resolvePlugin } from "./plugin"; +import { + dispatchPlugin, + explicitProfileFromArgs, + hasEnvFileFlag, + listPlugins, + resolvePlugin, +} from "./plugin"; const contextMocks = vi.hoisted(() => ({ loadAccessToken: vi.fn(), @@ -341,3 +347,16 @@ describe("explicitProfileFromArgs", () => { expect(explicitProfileFromArgs(args)).toBe(expected); }); }); + +describe("hasEnvFileFlag", () => { + test.each([ + [["deploy", "--env-file", ".env"], true], + [["deploy", "-e", ".env"], true], + [["deploy", "--env-file=.env"], true], + [["deploy", "--env-file-if-exists", ".env"], true], + [["deploy"], false], + [["deploy", "--", "--env-file", ".env"], false], + ])("detects env-file flag in %j", (args, expected) => { + expect(hasEnvFileFlag(args)).toBe(expected); + }); +}); diff --git a/packages/sdk/src/cli/shared/plugin.ts b/packages/sdk/src/cli/shared/plugin.ts index 76987bcf4..dad50bbbf 100644 --- a/packages/sdk/src/cli/shared/plugin.ts +++ b/packages/sdk/src/cli/shared/plugin.ts @@ -223,6 +223,8 @@ export function listPlugins(cliName: string): DiscoveredPlugin[] { interface PluginContextOptions { /** Active profile used to resolve the workspace, user, and token. */ profile?: string | undefined; + /** Skip platform context (URL, client ID, workspace, user, token) injection. */ + skipPlatformContext?: boolean; } /** @@ -248,18 +250,7 @@ async function resolveActiveUser(profile?: string): Promise */ async function buildPluginEnv(options: PluginContextOptions = {}): Promise> { const { profile } = options; - // Resolve the active profile's platform settings so the injected URL and - // OAuth client match the profile the token and workspace belong to. - let platformConfig: PlatformClientConfig | undefined; - try { - platformConfig = await loadPlatformClientConfig({ profile, allowMissingProfile: true }); - } catch { - // Fall back to env/default platform settings when the profile is unreadable. - } - const env: Record = { - TAILOR_PLATFORM_URL: getPlatformBaseUrl(platformConfig), - TAILOR_PLATFORM_OAUTH2_CLIENT_ID: getOAuth2ClientId(platformConfig), - }; + const env: Record = {}; const binPath = process.argv[1]; if (binPath) { @@ -278,6 +269,24 @@ async function buildPluginEnv(options: PluginContextOptions = {}): Promise` for an unknown subcommand now executes an external `tailor-` executable found on your PATH or in `node_modules/.bin` (project-local takes precedence), forwarding all following arguments. This also works for unknown subcommands nested under a known command — e.g. `tailor tailordb erd` dispatches to `tailor-tailordb-erd`. Builtins always take precedence, matching stops at the first unknown segment, and a command that takes its own arguments is never replaced by a plugin. The plugin receives the current Tailor Platform context via environment variables (`TAILOR_PLATFORM_TOKEN`, `TAILOR_PLATFORM_URL`, `TAILOR_PLATFORM_OAUTH2_CLIENT_ID`, `TAILOR_PLATFORM_WORKSPACE_ID`, `TAILOR_PLATFORM_USER`, `TAILOR_CONFIG_PATH`, `TAILOR_VERSION`, `TAILOR_BIN`); token, workspace, and user are best-effort, so auth-free plugins still run when you are not logged in. +Add CLI plugin support (beta). Running `tailor ` for an unknown subcommand now executes an external `tailor-` executable found on your PATH or in `node_modules/.bin` (project-local takes precedence), forwarding all following arguments. This also works for unknown subcommands nested under a known command — e.g. `tailor tailordb erd` dispatches to `tailor-tailordb-erd`. Builtins always take precedence, matching stops at the first unknown segment, and a command that takes its own arguments is never replaced by a plugin. The plugin receives the current Tailor Platform context via environment variables (`TAILOR_PLATFORM_TOKEN`, `TAILOR_PLATFORM_URL`, `TAILOR_PLATFORM_OAUTH2_CLIENT_ID`, `TAILOR_PLATFORM_WORKSPACE_ID`, `TAILOR_PLATFORM_USER`, `TAILOR_CONFIG_PATH`, `TAILOR_VERSION`, `TAILOR_BIN`); token, workspace, and user are best-effort, so auth-free plugins still run when you are not logged in. When the forwarded arguments include an explicit `--profile`/`-p`, the injected context is resolved for that profile; when they include `--env-file`/`-e`/`--env-file-if-exists`, platform context injection is skipped so the env file's values take effect in the plugin. Also adds: diff --git a/packages/sdk/src/cli/shared/plugin.test.ts b/packages/sdk/src/cli/shared/plugin.test.ts index a6c381125..1145d08c9 100644 --- a/packages/sdk/src/cli/shared/plugin.test.ts +++ b/packages/sdk/src/cli/shared/plugin.test.ts @@ -271,6 +271,50 @@ describe("dispatchPlugin", () => { expect(env.TAILOR_PLATFORM_OAUTH2_CLIENT_ID).toBe("cpoc_staging"); }); + test("resolves injected context from an explicit --profile in forwarded args", async () => { + const project = path.join(tempDir, "project"); + writeCapturePlugin(path.join(project, "node_modules", ".bin"), `${CLI}-hello`, outFile); + process.chdir(project); + process.env.PATH = ""; + + const code = await dispatchPlugin({ + name: "hello", + args: ["deploy", "--profile", "staging"], + cliName: CLI, + profile: "prod", + }); + + expect(code).toBe(0); + expect(contextMocks.loadAccessToken).toHaveBeenCalledWith({ profile: "staging" }); + expect(contextMocks.loadWorkspaceId).toHaveBeenCalledWith({ profile: "staging" }); + }); + + test("skips platform context injection when the forwarded args carry an env-file flag", async () => { + const project = path.join(tempDir, "project"); + writeCapturePlugin(path.join(project, "node_modules", ".bin"), `${CLI}-hello`, outFile); + process.chdir(project); + process.env.PATH = ""; + // Node itself also parses --env-file after the script path + // (https://github.com/nodejs/node/issues/54232), so the file must exist for + // the capture plugin process to start. + fs.writeFileSync(path.join(project, ".env.staging"), ""); + + const code = await dispatchPlugin({ + name: "hello", + args: ["deploy", "--env-file", ".env.staging"], + cliName: CLI, + }); + + expect(code).toBe(0); + const { env } = readCapture(); + expect(env.TAILOR_PLATFORM_TOKEN).toBeUndefined(); + expect(env.TAILOR_PLATFORM_WORKSPACE_ID).toBeUndefined(); + expect(env.TAILOR_PLATFORM_USER).toBeUndefined(); + expect(env.TAILOR_PLATFORM_URL).toBeUndefined(); + expect(env.TAILOR_PLATFORM_OAUTH2_CLIENT_ID).toBeUndefined(); + expect(env.TAILOR_CONFIG_PATH).toBe("/proj/tailor.config.ts"); + }); + test("omits best-effort context when it cannot be resolved but still dispatches", async () => { contextMocks.loadAccessToken.mockRejectedValue(new Error("not logged in")); contextMocks.loadWorkspaceId.mockRejectedValue(new Error("no workspace")); From 31d3092e60ae8e1a40ebca3684b52d097788ec8e Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 11:07:18 +0900 Subject: [PATCH 563/618] docs(sdk): drop npm links to the unpublished ERD plugin package --- packages/sdk/docs/cli-reference.md | 2 +- packages/sdk/docs/cli-reference.template.md | 2 +- packages/sdk/docs/cli/tailordb.md | 2 +- packages/sdk/docs/cli/tailordb.template.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/sdk/docs/cli-reference.md b/packages/sdk/docs/cli-reference.md index 5df163c08..11f6976cf 100644 --- a/packages/sdk/docs/cli-reference.md +++ b/packages/sdk/docs/cli-reference.md @@ -118,7 +118,7 @@ tailor hello world --loud This also works under a built-in command group. The command path is joined with hyphens, so a plugin nested under `tailordb` is named `tailor-tailordb-erd`. This is how the -[`@tailor-platform/sdk-tailordb-erd-plugin`](https://www.npmjs.com/package/@tailor-platform/sdk-tailordb-erd-plugin) +`@tailor-platform/sdk-tailordb-erd-plugin` package provides the `tailordb erd` commands: ```bash diff --git a/packages/sdk/docs/cli-reference.template.md b/packages/sdk/docs/cli-reference.template.md index 42214bb18..5348a9209 100644 --- a/packages/sdk/docs/cli-reference.template.md +++ b/packages/sdk/docs/cli-reference.template.md @@ -111,7 +111,7 @@ tailor hello world --loud This also works under a built-in command group. The command path is joined with hyphens, so a plugin nested under `tailordb` is named `tailor-tailordb-erd`. This is how the -[`@tailor-platform/sdk-tailordb-erd-plugin`](https://www.npmjs.com/package/@tailor-platform/sdk-tailordb-erd-plugin) +`@tailor-platform/sdk-tailordb-erd-plugin` package provides the `tailordb erd` commands: ```bash diff --git a/packages/sdk/docs/cli/tailordb.md b/packages/sdk/docs/cli/tailordb.md index c39415aec..f10fd0d52 100644 --- a/packages/sdk/docs/cli/tailordb.md +++ b/packages/sdk/docs/cli/tailordb.md @@ -231,7 +231,7 @@ See [Global Options](../cli-reference.md#global-options) for options available t ### tailordb erd -The `tailordb erd` commands (export, diff, serve, deploy) are provided by the [`@tailor-platform/sdk-tailordb-erd-plugin`](https://www.npmjs.com/package/@tailor-platform/sdk-tailordb-erd-plugin) CLI plugin. Install it next to the SDK and keep running `tailor tailordb erd ` as before: +The `tailordb erd` commands (export, diff, serve, deploy) are provided by the `@tailor-platform/sdk-tailordb-erd-plugin` CLI plugin. Install it next to the SDK and keep running `tailor tailordb erd ` as before: ```bash npm install -D @tailor-platform/sdk-tailordb-erd-plugin diff --git a/packages/sdk/docs/cli/tailordb.template.md b/packages/sdk/docs/cli/tailordb.template.md index 4106d5dc0..b159143b0 100644 --- a/packages/sdk/docs/cli/tailordb.template.md +++ b/packages/sdk/docs/cli/tailordb.template.md @@ -72,7 +72,7 @@ Note: Migration scripts are automatically executed during `tailor deploy`. See [ ### tailordb erd -The `tailordb erd` commands (export, diff, serve, deploy) are provided by the [`@tailor-platform/sdk-tailordb-erd-plugin`](https://www.npmjs.com/package/@tailor-platform/sdk-tailordb-erd-plugin) CLI plugin. Install it next to the SDK and keep running `tailor tailordb erd ` as before: +The `tailordb erd` commands (export, diff, serve, deploy) are provided by the `@tailor-platform/sdk-tailordb-erd-plugin` CLI plugin. Install it next to the SDK and keep running `tailor tailordb erd ` as before: ```bash npm install -D @tailor-platform/sdk-tailordb-erd-plugin From 13164471cc934da03dd64640b2fb2457c79b4413 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 15 Jul 2026 16:54:15 +0900 Subject: [PATCH 564/618] fix(tailordb): make issues() field param non-generic The .validate() callback's issues() field parameter used a self-generic

> signature. Comparing two such generic call signatures requires TypeScript to check the inner constraint for identity rather than assignability, which spuriously rejected structurally-equal TailorDBType instances built from the same custom fields but derived through different generic instantiation paths (e.g. a table passed across a module factory boundary), surfacing as TS2719 ("Two different types with this name exist, but they are unrelated"). Making the parameter a plain (non-generic) union keeps strict field-path checking while using ordinary assignability for these comparisons. --- .../fix-validate-issues-generic-field.md | 5 ++++ .../services/tailordb/schema.test.ts | 25 +++++++++++++++++++ .../src/configure/services/tailordb/types.ts | 4 ++- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-validate-issues-generic-field.md diff --git a/.changeset/fix-validate-issues-generic-field.md b/.changeset/fix-validate-issues-generic-field.md new file mode 100644 index 000000000..4136e102a --- /dev/null +++ b/.changeset/fix-validate-issues-generic-field.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": patch +--- + +Fix a spurious `TS2719` type error when a `db.table()` type built with custom fields is passed across module boundaries (e.g. into another package's factory function) and its `.validate()` callback's `issues()` field parameter is compared for assignability. The `issues()` field argument type no longer uses a self-generic parameter, so structurally-equal `TailorDBType` instances derived through different generic instantiation paths are recognized as compatible. diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index bf7ccfd1c..53559ff79 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -1357,6 +1357,31 @@ describe("TailorDBType type-level validate (function form) tests", () => { }); }); + test("table with custom fields stays assignable across generic module factory boundaries", () => { + function defineItemModule>(params: { + fields: CustomFields; + }) { + return { + item: db.table("Item", { + unitId: db.string(), + ...params.fields, + }), + }; + } + + const itemModule = defineItemModule({ + fields: { customField: db.string({ optional: true }).description("test") }, + }); + + function defineProductModule>(params: { + item: ReturnType>["item"]; + }) { + return params; + } + + defineProductModule({ item: itemModule.item }); + }); + test("type-level validate function stores in metadata", () => { const type = db .table("Test", { diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index 89f6cad29..7c102b207 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -201,7 +201,9 @@ export type TypeValidateFn< oldRecord: DeepReadonly | null; invoker: TailorPrincipal | null; }, - issues:

>>(field: P, message: string) => void, + // `field` stays non-generic: a self-generic `issues

` makes TS compare + // call signatures by identity, spuriously rejecting equal Fields from separate module paths. + issues: (field: DottedPaths>, message: string) => void, ) => void; type TypeCreateHookFn< From 76f68879bc2b6c146d87b991e0fff044b259d74b Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 15 Jul 2026 17:03:55 +0900 Subject: [PATCH 565/618] fix(tailordb): drop rationale comment from issues() field param Comment restated design rationale already covered by the commit message and regression test; per the code-comments convention, source comments should express constraints the code can't otherwise convey, not PR rationale. --- packages/sdk/src/configure/services/tailordb/types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index 7c102b207..16a5be936 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -201,8 +201,6 @@ export type TypeValidateFn< oldRecord: DeepReadonly | null; invoker: TailorPrincipal | null; }, - // `field` stays non-generic: a self-generic `issues

` makes TS compare - // call signatures by identity, spuriously rejecting equal Fields from separate module paths. issues: (field: DottedPaths>, message: string) => void, ) => void; From d135703ecb61eb1176da754c43df093fa6f83b74 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 15 Jul 2026 20:03:52 +0900 Subject: [PATCH 566/618] test(tailordb): rename cross-factory assignability test to match scope Rename to make clear this documents assignability when a factory's parameter type is derived via explicit type-argument passthrough, rather than serving as a direct regression guard for the TS2719 false-positive reported upstream. A standalone repro of that exact compiler behavior could not be constructed without the consuming package's full module composition. --- packages/sdk/src/configure/services/tailordb/schema.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 53559ff79..09668f33f 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -1357,7 +1357,7 @@ describe("TailorDBType type-level validate (function form) tests", () => { }); }); - test("table with custom fields stays assignable across generic module factory boundaries", () => { + test("table type built inside a generic factory stays assignable when threaded through another generic factory's explicitly-parameterized signature", () => { function defineItemModule>(params: { fields: CustomFields; }) { From c971797c9bfa035a43771c46f2b1c3bd93f989a9 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 15 Jul 2026 21:59:22 +0900 Subject: [PATCH 567/618] fix(workflow): remove pre-alignment trigger* names, rename .trigger() to .start() Remove the pre-alignment `tailor.workflow` names `triggerWorkflow`, `triggerJobFunction`, and `resumeWorkflow` (and their `TriggerWorkflowOptions` / `TriggerJobFunctionOptions` option types) from `@tailor-platform/sdk/runtime`, the ambient `@tailor-platform/sdk/runtime/globals` types, and the `mockWorkflow()` test facade. Use the canonical `startWorkflow`, `startJobFunction`, and `resumeWorkflowExecution` names instead. Also rename `Workflow.trigger()` (from `createWorkflow()`) and `WorkflowJob.trigger()` (from `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary. `mockWorkflow()`'s `wf.job()`/`wf.workflow()` mocks, `wf.setTriggerHandler` / `wf.triggeredJobs`, and the CLI bundler's trigger detection/codegen are updated to match. Adds the `v2/workflow-trigger-rename` codemod (automatic) for the low-level rename, and registers `v2/workflow-start-rename` (manual, guided by prompt) for the `.trigger()` -> `.start()` call-site rename, since distinguishing a workflow/job `.trigger()` call from an unrelated object's own method isn't reliably decidable from a single file's syntax. --- .changeset/workflow-start-rename.md | 17 ++ .changeset/workflow-trigger-dispatch.md | 2 +- .changeset/workflow-trigger-rename.md | 19 ++ example/e2e/resolver.test.ts | 32 +-- .../{triggerWorkflow.ts => startWorkflow.ts} | 10 +- example/tests/bundled_execution.test.ts | 8 +- example/workflows/order-processing.ts | 8 +- example/workflows/sample.ts | 4 +- .../create-sdk/templates/workflow/README.md | 4 +- .../src/workflow/order-fulfillment.test.ts | 20 +- .../src/workflow/order-fulfillment.ts | 6 +- .../v2/workflow-trigger-rename/codemod.yaml | 7 + .../scripts/transform.ts | 102 +++++++++ .../tests/aliased-import/expected.ts | 5 + .../tests/aliased-import/input.ts | 5 + .../tests/ambient-global/expected.ts | 7 + .../tests/ambient-global/input.ts | 7 + .../tests/no-match/input.ts | 7 + .../tests/runtime-import/expected.ts | 6 + .../tests/runtime-import/input.ts | 6 + .../expected.ts | 5 + .../runtime-workflow-subpath-import/input.ts | 5 + .../tests/shadowed-tailor/input.ts | 9 + .../tests/shadowed-workflow/input.ts | 10 + packages/sdk-codemod/src/registry.ts | 101 +++++++-- packages/sdk-codemod/src/transform.test.ts | 4 + packages/sdk-codemod/tsdown.config.ts | 2 + packages/sdk/docs/cli/function.md | 4 +- packages/sdk/docs/migration/v2.md | 113 +++++++++- packages/sdk/docs/runtime.md | 2 +- packages/sdk/docs/services/auth.md | 10 +- packages/sdk/docs/services/workflow.md | 82 +++---- packages/sdk/docs/testing.md | 18 +- .../workflows/processOrder.ts | 6 +- .../src/cli/commands/deploy/workflow.test.ts | 4 +- .../src/cli/commands/function/detect.test.ts | 10 +- .../sdk/src/cli/commands/function/test-run.ts | 4 +- .../src/cli/services/service-brand.test.ts | 4 +- .../services/workflow/ast-transformer.test.ts | 206 +++++++++--------- .../src/cli/services/workflow/ast-utils.ts | 6 +- .../src/cli/services/workflow/bundler.test.ts | 34 +-- .../sdk/src/cli/services/workflow/bundler.ts | 10 +- .../src/cli/services/workflow/service.test.ts | 6 +- .../sdk/src/cli/services/workflow/service.ts | 8 +- .../services/workflow/trigger-transformer.ts | 8 +- .../src/cli/shared/trigger-context.test.ts | 54 ++--- .../configure/services/executor/executor.ts | 2 +- .../configure/services/executor/operation.ts | 2 +- .../configure/services/workflow/job.test.ts | 66 +++--- .../src/configure/services/workflow/job.ts | 40 ++-- .../configure/services/workflow/registry.ts | 28 +-- .../configure/services/workflow/workflow.ts | 26 +-- .../sdk/src/parser/service/workflow/schema.ts | 6 +- packages/sdk/src/runtime/globals.test.ts | 12 - packages/sdk/src/runtime/globals.ts | 4 - packages/sdk/src/runtime/workflow.test.ts | 100 ++------- packages/sdk/src/runtime/workflow.ts | 132 +---------- packages/sdk/src/types/workflow.generated.ts | 8 +- packages/sdk/src/utils/test/mock.ts | 32 +-- packages/sdk/src/vitest/globals.ts | 2 +- .../vitest/integration/should-pass.test.ts | 2 +- .../sdk/src/vitest/mock-ergonomics.test.ts | 22 +- packages/sdk/src/vitest/mock-types.test.ts | 12 - packages/sdk/src/vitest/mock.test.ts | 134 ++++-------- packages/sdk/src/vitest/mocks/workflow.ts | 159 ++++++-------- packages/sdk/src/vitest/workflow-local.ts | 67 +++--- packages/sdk/src/vitest/workflow-runtime.ts | 14 +- 67 files changed, 979 insertions(+), 898 deletions(-) create mode 100644 .changeset/workflow-start-rename.md create mode 100644 .changeset/workflow-trigger-rename.md rename example/resolvers/{triggerWorkflow.ts => startWorkflow.ts} (66%) create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/aliased-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/aliased-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/ambient-global/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/ambient-global/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/no-match/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-workflow-subpath-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-workflow-subpath-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/shadowed-tailor/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/shadowed-workflow/input.ts diff --git a/.changeset/workflow-start-rename.md b/.changeset/workflow-start-rename.md new file mode 100644 index 000000000..28c647e26 --- /dev/null +++ b/.changeset/workflow-start-rename.md @@ -0,0 +1,17 @@ +--- +"@tailor-platform/sdk": major +"@tailor-platform/create-sdk": major +"@tailor-platform/sdk-codemod": patch +--- + +Rename `Workflow.trigger()` (returned by `createWorkflow()`) and `WorkflowJob.trigger()` (returned by `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary: + +```diff + const inventory = checkInventory.trigger({ orderId: input.orderId }); ++const inventory = checkInventory.start({ orderId: input.orderId }); + +-const workflowRunId = await orderProcessingWorkflow.trigger(args, { invoker: "manager" }); ++const workflowRunId = await orderProcessingWorkflow.start(args, { invoker: "manager" }); +``` + +`mockWorkflow()`'s `wf.job(definition)` / `wf.workflow(definition)` now return a mock of the `.start` method, and `wf.setTriggerHandler` / `wf.triggeredJobs` are renamed to `wf.setStartHandler` / `wf.startedJobs`. No codemod ships for the `.trigger()` → `.start()` call-site rename itself — see the `v2/workflow-start-rename` migration guide entry for manual migration steps. diff --git a/.changeset/workflow-trigger-dispatch.md b/.changeset/workflow-trigger-dispatch.md index 553133b4b..bd6841bee 100644 --- a/.changeset/workflow-trigger-dispatch.md +++ b/.changeset/workflow-trigger-dispatch.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk": major --- -Align workflow job `.trigger()` with the platform runtime. Job triggers now require a mocked workflow runtime in tests instead of running job bodies locally, and `trigger()` returns the job result directly instead of a Promise wrapper. Use `mockWorkflow()` to mock trigger results in tests, or `runWorkflowLocally()` for full-chain local workflow tests. +Align workflow job `.start()` (previously `.trigger()`) with the platform runtime. Job starts now require a mocked workflow runtime in tests instead of running job bodies locally, and `start()` returns the job result directly instead of a Promise wrapper. Use `mockWorkflow()` to mock start results in tests, or `runWorkflowLocally()` for full-chain local workflow tests. diff --git a/.changeset/workflow-trigger-rename.md b/.changeset/workflow-trigger-rename.md new file mode 100644 index 000000000..c9f260bea --- /dev/null +++ b/.changeset/workflow-trigger-rename.md @@ -0,0 +1,19 @@ +--- +"@tailor-platform/sdk": major +"@tailor-platform/sdk-codemod": patch +--- + +Remove the pre-alignment `tailor.workflow` names `triggerWorkflow`, `triggerJobFunction`, and `resumeWorkflow` (and their `TriggerWorkflowOptions` / `TriggerJobFunctionOptions` option types) from `@tailor-platform/sdk/runtime`, the ambient `@tailor-platform/sdk/runtime/globals` types, and the `mockWorkflow()` test facade. Use the canonical names instead: + +```diff + import { workflow } from "@tailor-platform/sdk/runtime"; + +-await workflow.triggerWorkflow("myWorkflow", { data: "value" }); ++await workflow.startWorkflow("myWorkflow", { data: "value" }); +-workflow.triggerJobFunction("myJob", { data: "value" }); ++workflow.startJobFunction("myJob", { data: "value" }); +-await workflow.resumeWorkflow("execution-id"); ++await workflow.resumeWorkflowExecution("execution-id"); +``` + +Run the `v2/workflow-trigger-rename` codemod to migrate call sites automatically. diff --git a/example/e2e/resolver.test.ts b/example/e2e/resolver.test.ts index 423ea61b2..f33a4caeb 100644 --- a/example/e2e/resolver.test.ts +++ b/example/e2e/resolver.test.ts @@ -433,14 +433,14 @@ describe("dataplane", () => { }); }); - describe("triggerOrderProcessing", () => { - test("triggers workflow and returns workflowRunId", async () => { + describe("startOrderProcessing", () => { + test("starts workflow and returns workflowRunId", async () => { const orderId = randomUUID(); const customerId = randomUUID(); const mutation = gql` mutation { - triggerOrderProcessing( + startOrderProcessing( orderId: "${orderId}" customerId: "${customerId}" ) { @@ -452,15 +452,15 @@ describe("dataplane", () => { const result = await graphQLClient.rawRequest(mutation); expect(result.errors).toBeUndefined(); expect(result.data).toEqual({ - triggerOrderProcessing: { + startOrderProcessing: { workflowRunId: expect.any(String), - message: `Workflow triggered for order ${orderId}`, + message: `Workflow started for order ${orderId}`, }, }); // Verify workflowRunId is a valid UUID - const workflowRunId = (result.data as { triggerOrderProcessing: { workflowRunId: string } }) - .triggerOrderProcessing.workflowRunId; + const workflowRunId = (result.data as { startOrderProcessing: { workflowRunId: string } }) + .startOrderProcessing.workflowRunId; expect(workflowRunId).toMatch( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, ); @@ -475,33 +475,33 @@ describe("dataplane", () => { pipelineResolverView: PipelineResolverView.FULL, }); - const triggerResolver = pipelineResolvers.find((e) => e.name === "triggerOrderProcessing"); - expect(triggerResolver).toBeDefined(); - expect(triggerResolver).toMatchObject({ - name: "triggerOrderProcessing", - description: "Trigger the order processing workflow", + const startResolver = pipelineResolvers.find((e) => e.name === "startOrderProcessing"); + expect(startResolver).toBeDefined(); + expect(startResolver).toMatchObject({ + name: "startOrderProcessing", + description: "Start the order processing workflow", operationType: "mutation", }); // Verify inputs - const inputNames = triggerResolver?.inputs?.map((i) => i.name) ?? []; + const inputNames = startResolver?.inputs?.map((i) => i.name) ?? []; expect(inputNames).toContain("orderId"); expect(inputNames).toContain("customerId"); // Verify orderId input - const orderIdInput = triggerResolver?.inputs?.find((i) => i.name === "orderId"); + const orderIdInput = startResolver?.inputs?.find((i) => i.name === "orderId"); expect(orderIdInput?.description).toBe("Order ID to process"); expect(orderIdInput?.type?.kind).toBe("ScalarType"); expect(orderIdInput?.type?.name).toBe("String"); // Verify customerId input - const customerIdInput = triggerResolver?.inputs?.find((i) => i.name === "customerId"); + const customerIdInput = startResolver?.inputs?.find((i) => i.name === "customerId"); expect(customerIdInput?.description).toBe("Customer ID for the order"); expect(customerIdInput?.type?.kind).toBe("ScalarType"); expect(customerIdInput?.type?.name).toBe("ID"); // Verify response - const responseFields = triggerResolver?.response?.type?.fields ?? []; + const responseFields = startResolver?.response?.type?.fields ?? []; const workflowRunIdField = responseFields.find((f) => f.name === "workflowRunId"); expect(workflowRunIdField).toBeDefined(); const messageField = responseFields.find((f) => f.name === "message"); diff --git a/example/resolvers/triggerWorkflow.ts b/example/resolvers/startWorkflow.ts similarity index 66% rename from example/resolvers/triggerWorkflow.ts rename to example/resolvers/startWorkflow.ts index 028b493fb..2a58e6030 100644 --- a/example/resolvers/triggerWorkflow.ts +++ b/example/resolvers/startWorkflow.ts @@ -2,16 +2,16 @@ import { createResolver, t } from "@tailor-platform/sdk"; import orderProcessingWorkflow from "../workflows/order-processing"; export default createResolver({ - name: "triggerOrderProcessing", - description: "Trigger the order processing workflow", + name: "startOrderProcessing", + description: "Start the order processing workflow", operation: "mutation", input: { orderId: t.string().description("Order ID to process"), customerId: t.uuid().description("Customer ID for the order"), }, body: async ({ input }) => { - // Trigger the workflow with invoker (machine user name is type-narrowed via tailor.d.ts) - const workflowRunId = await orderProcessingWorkflow.trigger( + // Start the workflow with invoker (machine user name is type-narrowed via tailor.d.ts) + const workflowRunId = await orderProcessingWorkflow.start( { orderId: input.orderId, customerId: input.customerId, @@ -21,7 +21,7 @@ export default createResolver({ return { workflowRunId, - message: `Workflow triggered for order ${input.orderId}`, + message: `Workflow started for order ${input.orderId}`, }; }, output: t.object({ diff --git a/example/tests/bundled_execution.test.ts b/example/tests/bundled_execution.test.ts index 3d214bf25..e47541502 100644 --- a/example/tests/bundled_execution.test.ts +++ b/example/tests/bundled_execution.test.ts @@ -48,7 +48,7 @@ describe("bundled execution tests", () => { "resolvers/add.js": 5459 + sizeBuffer, "resolvers/showUserInfo.js": 5999 + sizeBuffer, "resolvers/stepChain.js": 182391 + sizeBuffer, - "resolvers/triggerOrderProcessing.js": 5692 + sizeBuffer, + "resolvers/startOrderProcessing.js": 5692 + sizeBuffer, // workflow-jobs: Kysely jobs (~158KB), date-fns jobs (~20KB), simple jobs (<2KB) "workflow-jobs/check-inventory.js": 19967 + sizeBuffer, "workflow-jobs/fetch-customer.js": 157770 + sizeBuffer, @@ -210,7 +210,7 @@ describe("bundled execution tests", () => { processedAt: "2025-01-01 12:00:00", }); - expect(wf.triggeredJobs).toEqual([ + expect(wf.startedJobs).toEqual([ { jobName: "fetch-customer", args: { customerId: "customer-456" } }, { jobName: "send-notification", @@ -263,7 +263,7 @@ describe("bundled execution tests", () => { } }); - test("workflow-jobs/validate-order.js triggers check-inventory job", async () => { + test("workflow-jobs/validate-order.js starts check-inventory job", async () => { using wf = mockWorkflow(); wf.setJobHandler((jobName) => { if (jobName === "check-inventory") { @@ -280,7 +280,7 @@ describe("bundled execution tests", () => { paymentResult: null, }); - expect(wf.triggeredJobs).toEqual([ + expect(wf.startedJobs).toEqual([ { jobName: "check-inventory", args: undefined }, { jobName: "process-payment", args: undefined }, ]); diff --git a/example/workflows/order-processing.ts b/example/workflows/order-processing.ts index 375f16099..6bba8a814 100644 --- a/example/workflows/order-processing.ts +++ b/example/workflows/order-processing.ts @@ -10,8 +10,8 @@ export const processOrder = createWorkflowJob({ // Log env for demonstration console.log("Environment:", env); - // Fetch customer information using trigger - const customer = fetchCustomer.trigger({ + // Fetch customer information using start + const customer = fetchCustomer.start({ customerId: input.customerId, }); @@ -19,8 +19,8 @@ export const processOrder = createWorkflowJob({ throw new Error(`Customer ${input.customerId} not found`); } - // Send notification to customer using trigger - const notification = sendNotification.trigger({ + // Send notification to customer using start + const notification = sendNotification.start({ message: `Your order ${input.orderId} is being processed`, recipient: customer.email, }); diff --git a/example/workflows/sample.ts b/example/workflows/sample.ts index 8033ecc02..e6cbfddca 100644 --- a/example/workflows/sample.ts +++ b/example/workflows/sample.ts @@ -23,8 +23,8 @@ export const validate_order = createWorkflowJob({ name: "validate-order", body: (input: { orderId: string }) => { console.log("Order ID:", input.orderId); - const inventoryResult = check_inventory.trigger(); - const paymentResult = process_payment.trigger(); + const inventoryResult = check_inventory.start(); + const paymentResult = process_payment.start(); return { inventoryResult, paymentResult }; }, }); diff --git a/packages/create-sdk/templates/workflow/README.md b/packages/create-sdk/templates/workflow/README.md index 7fdf55a06..393a7b41b 100644 --- a/packages/create-sdk/templates/workflow/README.md +++ b/packages/create-sdk/templates/workflow/README.md @@ -1,11 +1,11 @@ # Workflow Template -Demonstrates workflow patterns with job chaining, trigger testing, and dependency injection. +Demonstrates workflow patterns with job chaining, start testing, and dependency injection. ## Features - Workflow with multiple jobs (`createWorkflow`, `createWorkflowJob`) -- Job chaining via `.trigger()` +- Job chaining via `.start()` - Database operations in workflow jobs (DI pattern) - Integration testing with `runWorkflowLocally()` diff --git a/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts b/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts index 6389972f9..6d195a34f 100644 --- a/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts +++ b/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.test.ts @@ -48,18 +48,18 @@ describe("order fulfillment workflow", () => { }); }); - describe("orchestration tests with mocked triggers", () => { + describe("orchestration tests with mocked starts", () => { test("fulfillOrder chains all jobs", async () => { - using _validateSpy = vi.spyOn(validateOrder, "trigger").mockReturnValue({ + using _validateSpy = vi.spyOn(validateOrder, "start").mockReturnValue({ valid: true, orderId: "order-1", }); - using _paymentSpy = vi.spyOn(processPayment, "trigger").mockReturnValue({ + using _paymentSpy = vi.spyOn(processPayment, "start").mockReturnValue({ transactionId: "txn-order-1", amount: 100, status: "completed" as const, }); - using _confirmSpy = vi.spyOn(sendConfirmation, "trigger").mockReturnValue({ + using _confirmSpy = vi.spyOn(sendConfirmation, "start").mockReturnValue({ orderId: "order-1", transactionId: "txn-order-1", confirmed: true, @@ -70,15 +70,15 @@ describe("order fulfillment workflow", () => { { env: {}, invoker: null }, ); - expect(validateOrder.trigger).toHaveBeenCalledWith({ + expect(validateOrder.start).toHaveBeenCalledWith({ orderId: "order-1", amount: 100, }); - expect(processPayment.trigger).toHaveBeenCalledWith({ + expect(processPayment.start).toHaveBeenCalledWith({ orderId: "order-1", amount: 100, }); - expect(sendConfirmation.trigger).toHaveBeenCalledWith({ + expect(sendConfirmation.start).toHaveBeenCalledWith({ orderId: "order-1", transactionId: "txn-order-1", }); @@ -91,16 +91,16 @@ describe("order fulfillment workflow", () => { }); test("workflow.mainJob.body() chains all jobs", async () => { - using _validateSpy = vi.spyOn(validateOrder, "trigger").mockReturnValue({ + using _validateSpy = vi.spyOn(validateOrder, "start").mockReturnValue({ valid: true, orderId: "order-2", }); - using _paymentSpy = vi.spyOn(processPayment, "trigger").mockReturnValue({ + using _paymentSpy = vi.spyOn(processPayment, "start").mockReturnValue({ transactionId: "txn-order-2", amount: 200, status: "completed" as const, }); - using _confirmSpy = vi.spyOn(sendConfirmation, "trigger").mockReturnValue({ + using _confirmSpy = vi.spyOn(sendConfirmation, "start").mockReturnValue({ orderId: "order-2", transactionId: "txn-order-2", confirmed: true, diff --git a/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.ts b/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.ts index 5e6de9b58..ed053c80d 100644 --- a/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.ts +++ b/packages/create-sdk/templates/workflow/src/workflow/order-fulfillment.ts @@ -35,7 +35,7 @@ export const sendConfirmation = createWorkflowJob({ export const fulfillOrder = createWorkflowJob({ name: "fulfill-order", body: (input: { orderId: string; amount: number }) => { - const validation = validateOrder.trigger({ + const validation = validateOrder.start({ orderId: input.orderId, amount: input.amount, }); @@ -44,12 +44,12 @@ export const fulfillOrder = createWorkflowJob({ throw new Error("Order validation failed"); } - const payment = processPayment.trigger({ + const payment = processPayment.start({ orderId: input.orderId, amount: input.amount, }); - const confirmation = sendConfirmation.trigger({ + const confirmation = sendConfirmation.start({ orderId: input.orderId, transactionId: payment.transactionId, }); diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/codemod.yaml b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/codemod.yaml new file mode 100644 index 000000000..083e26e87 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/workflow-trigger-rename" +version: "1.0.0" +description: "Rename tailor.workflow triggerWorkflow/triggerJobFunction/resumeWorkflow call sites to startWorkflow/startJobFunction/resumeWorkflowExecution" +engine: jssg +language: typescript +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/scripts/transform.ts new file mode 100644 index 000000000..4e707498e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/scripts/transform.ts @@ -0,0 +1,102 @@ +import { parse, Lang } from "@ast-grep/napi"; +import { + findImportStatements, + importBindings, + localDeclarationNames, +} from "../../../../src/ast-grep-helpers"; +import type { Edit, SgNode } from "@ast-grep/napi"; + +const RENAMES: Record = { + triggerWorkflow: "startWorkflow", + triggerJobFunction: "startJobFunction", + resumeWorkflow: "resumeWorkflowExecution", +}; + +const WORKFLOW_MODULE_SOURCES = new Set([ + "@tailor-platform/sdk/runtime", + "@tailor-platform/sdk/runtime/workflow", +]); + +function quickFilter(source: string): boolean { + return Object.keys(RENAMES).some((name) => source.includes(name)); +} + +function sourceLang(filePath: string, source: string): Lang { + const lower = filePath.toLowerCase(); + if (lower.endsWith(".tsx") || lower.endsWith(".jsx")) return Lang.Tsx; + return source.includes("") ? Lang.Tsx : Lang.TypeScript; +} + +function collectWorkflowLocals(root: SgNode, imports: SgNode[]): Set { + const locals = new Set(); + for (const importStmt of imports) { + for (const binding of importBindings(importStmt)) { + if (binding.importedName === "workflow" && WORKFLOW_MODULE_SOURCES.has(binding.source)) { + locals.add(binding.localName); + } + } + } + + // Coarse safety check: a local name also declared elsewhere in the file + // (shadowing the import) is dropped rather than range-tracked. + const declaredNames = localDeclarationNames(root); + for (const name of locals) { + if (declaredNames.has(name)) locals.delete(name); + } + return locals; +} + +function tailorIsAmbientGlobal(root: SgNode, imports: SgNode[]): boolean { + const declaredNames = localDeclarationNames(root); + if (declaredNames.has("tailor")) return false; + return !imports.some((stmt) => importBindings(stmt).some((b) => b.localName === "tailor")); +} + +function isAmbientTailorWorkflow(object: SgNode | null): boolean { + if (!object || object.kind() !== "member_expression") return false; + const base = object.field("object"); + const property = object.field("property"); + return ( + base?.kind() === "identifier" && base.text() === "tailor" && property?.text() === "workflow" + ); +} + +/** + * Rewrite `.triggerWorkflow(`, `.triggerJobFunction(`, and `.resumeWorkflow(` + * member accesses to their canonical `start*`/`resumeWorkflowExecution` names, + * either on the ambient `tailor.workflow` global or on a `workflow` value + * imported from `@tailor-platform/sdk/runtime(/workflow)`. + * @param source - File contents + * @param filePath - Absolute path to the file + * @returns Transformed source or null when nothing matched. + */ +export default function transform(source: string, filePath?: string): string | null { + if (!quickFilter(source)) return null; + + const root = parse(sourceLang(filePath ?? "", source), source).root(); + const imports = findImportStatements(root); + const workflowLocals = collectWorkflowLocals(root, imports); + const tailorIsAmbient = tailorIsAmbientGlobal(root, imports); + + const edits: Edit[] = []; + for (const member of root.findAll({ rule: { kind: "member_expression" } })) { + const property = member.field("property"); + if (!property || property.kind() !== "property_identifier") continue; + const newName = RENAMES[property.text()]; + if (!newName) continue; + + const object = member.field("object"); + if (!object) continue; + + const isWorkflowImportReceiver = + object.kind() === "identifier" && workflowLocals.has(object.text()); + const isAmbientReceiver = tailorIsAmbient && isAmbientTailorWorkflow(object); + if (!isWorkflowImportReceiver && !isAmbientReceiver) continue; + + edits.push(property.replace(newName)); + } + + if (edits.length === 0) return null; + const result = root.commitEdits(edits); + return result === source ? null : result; +} diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/aliased-import/expected.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/aliased-import/expected.ts new file mode 100644 index 000000000..21f466560 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/aliased-import/expected.ts @@ -0,0 +1,5 @@ +import { workflow as wf } from "@tailor-platform/sdk/runtime"; + +export async function resume(executionId: string): Promise { + return await wf.resumeWorkflowExecution(executionId); +} diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/aliased-import/input.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/aliased-import/input.ts new file mode 100644 index 000000000..02fa1c6e7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/aliased-import/input.ts @@ -0,0 +1,5 @@ +import { workflow as wf } from "@tailor-platform/sdk/runtime"; + +export async function resume(executionId: string): Promise { + return await wf.resumeWorkflow(executionId); +} diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/ambient-global/expected.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/ambient-global/expected.ts new file mode 100644 index 000000000..0034c66a7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/ambient-global/expected.ts @@ -0,0 +1,7 @@ +import "@tailor-platform/sdk/runtime/globals"; + +export async function run(): Promise { + await tailor.workflow.startWorkflow("myWorkflow", { data: "value" }); + tailor.workflow.startJobFunction("myJob", { data: "value" }); + await tailor.workflow.resumeWorkflowExecution("execution-id"); +} diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/ambient-global/input.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/ambient-global/input.ts new file mode 100644 index 000000000..61c2cde3c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/ambient-global/input.ts @@ -0,0 +1,7 @@ +import "@tailor-platform/sdk/runtime/globals"; + +export async function run(): Promise { + await tailor.workflow.triggerWorkflow("myWorkflow", { data: "value" }); + tailor.workflow.triggerJobFunction("myJob", { data: "value" }); + await tailor.workflow.resumeWorkflow("execution-id"); +} diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/no-match/input.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/no-match/input.ts new file mode 100644 index 000000000..b423611f4 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/no-match/input.ts @@ -0,0 +1,7 @@ +const localWorkflowClient = { + triggerWorkflow(name: string): string { + return name; + }, +}; + +localWorkflowClient.triggerWorkflow("unrelated"); diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-import/expected.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-import/expected.ts new file mode 100644 index 000000000..db7aaacf6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-import/expected.ts @@ -0,0 +1,6 @@ +import { workflow } from "@tailor-platform/sdk/runtime"; + +export async function run(): Promise { + const executionId = await workflow.startWorkflow("myWorkflow", { data: "value" }); + return executionId; +} diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-import/input.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-import/input.ts new file mode 100644 index 000000000..82f45d195 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-import/input.ts @@ -0,0 +1,6 @@ +import { workflow } from "@tailor-platform/sdk/runtime"; + +export async function run(): Promise { + const executionId = await workflow.triggerWorkflow("myWorkflow", { data: "value" }); + return executionId; +} diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-workflow-subpath-import/expected.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-workflow-subpath-import/expected.ts new file mode 100644 index 000000000..80f36db5a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-workflow-subpath-import/expected.ts @@ -0,0 +1,5 @@ +import { workflow } from "@tailor-platform/sdk/runtime/workflow"; + +export function runJob(): unknown { + return workflow.startJobFunction("myJob", { data: "value" }); +} diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-workflow-subpath-import/input.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-workflow-subpath-import/input.ts new file mode 100644 index 000000000..7ea7091bb --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/runtime-workflow-subpath-import/input.ts @@ -0,0 +1,5 @@ +import { workflow } from "@tailor-platform/sdk/runtime/workflow"; + +export function runJob(): unknown { + return workflow.triggerJobFunction("myJob", { data: "value" }); +} diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/shadowed-tailor/input.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/shadowed-tailor/input.ts new file mode 100644 index 000000000..c1e862b38 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/shadowed-tailor/input.ts @@ -0,0 +1,9 @@ +interface LocalTailor { + workflow: { + triggerWorkflow: (name: string) => Promise; + }; +} + +function run(tailor: LocalTailor): Promise { + return tailor.workflow.triggerWorkflow("local"); +} diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/shadowed-workflow/input.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/shadowed-workflow/input.ts new file mode 100644 index 000000000..e47a413ec --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/shadowed-workflow/input.ts @@ -0,0 +1,10 @@ +import { workflow } from "@tailor-platform/sdk/runtime"; + +function run(): unknown { + const workflow = getLocalWorkflow(); + return workflow.triggerWorkflow("local"); +} + +function getLocalWorkflow(): { triggerWorkflow: (name: string) => string } { + return { triggerWorkflow: (name) => name }; +} diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index ad4e3df88..e2bbebe39 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -416,7 +416,7 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/auth-invoker-unwrap", name: 'auth.invoker("name") → invoker: "name"', description: - 'Rename statically identified SDK `authInvoker` options to `invoker`, replace `auth.invoker("name")` there with the bare `"name"` string, and drop the `auth` import when no other reference remains. Ambiguous workflow `.trigger()` calls are left for manual review. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle.', + 'Rename statically identified SDK `authInvoker` options to `invoker`, replace `auth.invoker("name")` there with the bare `"name"` string, and drop the `auth` import when no other reference remains. Ambiguous workflow `.start()` calls are left for manual review. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle.', since: "1.0.0", until: "2.0.0", prereleaseUntil: V2_NEXT_2, @@ -449,8 +449,8 @@ export const allCodemods: CodemodPackage[] = [ " machine user name string; platform/runtime authInvoker payloads still expect", " the object form.", "2. Rename remaining authInvoker option keys to invoker only for SDK resolver,", - " executor, workflow.trigger(), or startWorkflow() options. Keep platform/runtime", - " payload keys such as tailor.workflow.triggerWorkflow(..., { authInvoker: ... }).", + " executor, workflow.start(), or startWorkflow() options. Keep platform/runtime", + " payload keys such as tailor.workflow.startWorkflow(..., { authInvoker: ... }).", "3. After removing every auth.invoker usage in a file, delete the now-unused auth", " import (keeping it pulls Node-only config modules into runtime bundles); leave", " the import if auth is still referenced elsewhere.", @@ -717,6 +717,41 @@ export const allCodemods: CodemodPackage[] = [ }, ], }, + { + id: "v2/workflow-trigger-rename", + name: "workflow.triggerWorkflow/triggerJobFunction/resumeWorkflow → startWorkflow/startJobFunction/resumeWorkflowExecution", + description: + "Rename tailor.workflow call sites from the pre-alignment triggerWorkflow/triggerJobFunction/resumeWorkflow names to the canonical startWorkflow/startJobFunction/resumeWorkflowExecution names, on both the ambient tailor.workflow global and a workflow value imported from @tailor-platform/sdk/runtime(/workflow).", + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/workflow-trigger-rename/scripts/transform.js", + legacyPatterns: ["triggerWorkflow", "triggerJobFunction", "resumeWorkflow"], + examples: [ + { + before: + 'import { workflow } from "@tailor-platform/sdk/runtime";\n\nawait workflow.triggerWorkflow("myWorkflow", { data: "value" });', + after: + 'import { workflow } from "@tailor-platform/sdk/runtime";\n\nawait workflow.startWorkflow("myWorkflow", { data: "value" });', + }, + ], + prompt: [ + "The pre-alignment tailor.workflow names triggerWorkflow, triggerJobFunction, and", + "resumeWorkflow are removed from the SDK's type surface in v2; use the canonical", + "startWorkflow, startJobFunction, and resumeWorkflowExecution names instead. The", + "codemod rewrites direct member-access call sites on the ambient tailor.workflow", + "global and on a workflow value imported from @tailor-platform/sdk/runtime or", + "@tailor-platform/sdk/runtime/workflow (including aliased imports). It skips a", + "file entirely when a local declaration shadows the workflow import or the", + "ambient tailor name, to avoid rewriting an unrelated same-named value — review", + "those manually.", + "", + "Also review, and migrate by hand:", + "- Destructured references (e.g. const { triggerWorkflow } = workflow) — the", + " codemod only rewrites direct member-access calls.", + "- Imported TriggerWorkflowOptions / TriggerJobFunctionOptions types — rename", + " them to StartWorkflowOptions / StartJobFunctionOptions.", + ].join("\n"), + }, { id: "v2/open-download-stream", name: "openDownloadStream → downloadStream", @@ -824,28 +859,68 @@ export const allCodemods: CodemodPackage[] = [ }, { id: "v2/workflow-trigger-dispatch", - name: "Workflow .trigger() and trigger tests", + name: "Workflow job start() and start tests", description: - "Workflow job `.trigger()` now aligns with the platform runtime: it returns the job result directly instead of a Promise wrapper, and tests no longer run job bodies locally. Mock trigger responses with `mockWorkflow()` (`setJobHandler` / `enqueueResult`, assert via `triggeredJobs`), or use `runWorkflowLocally()` for a full-chain local run.", + "Workflow job `.start()` (previously `.trigger()`) now aligns with the platform runtime: it returns the job result directly instead of a Promise wrapper, and tests no longer run job bodies locally. Mock start responses with `mockWorkflow()` (`setJobHandler` / `enqueueResult`, assert via `startedJobs`), or use `runWorkflowLocally()` for a full-chain local run.", since: "1.0.0", until: "2.0.0", prereleaseUntil: V2_NEXT_1, - suspiciousPatterns: [".trigger("], + suspiciousPatterns: [".trigger(", ".start("], examples: [ { caption: "Tests must mock the workflow runtime instead of running bodies locally:", - before: - 'const result = await orderJob.trigger({ id });\nexpect(result.status).toBe("done");', + before: 'const result = await orderJob.start({ id });\nexpect(result.status).toBe("done");', after: - 'using wf = mockWorkflow();\nwf.setJobHandler((jobName) => (jobName === "order-job" ? { status: "done" } : null));\nconst result = await orderJob.trigger({ id });\nexpect(result.status).toBe("done");', + 'using wf = mockWorkflow();\nwf.setJobHandler((jobName) => (jobName === "order-job" ? { status: "done" } : null));\nconst result = await orderJob.start({ id });\nexpect(result.status).toBe("done");', }, ], prompt: [ - "Workflow job .trigger() now uses the platform workflow runtime instead of running", + "Workflow job .start() now uses the platform workflow runtime instead of running", "the job body locally. In tests, acquire `using wf = mockWorkflow()` and provide", - "trigger responses (setJobHandler / enqueueResult), or use runWorkflowLocally() for a", - "full-chain local run; an unmocked trigger now throws. Outside tests, treat the", - "trigger result as the job output directly (no Promise wrapper to unwrap).", + "start responses (setJobHandler / enqueueResult), or use runWorkflowLocally() for a", + "full-chain local run; an unmocked start now throws. Outside tests, treat the", + "start result as the job output directly (no Promise wrapper to unwrap).", + ].join("\n"), + }, + { + id: "v2/workflow-start-rename", + name: "Workflow.trigger()/WorkflowJob.trigger() → .start()", + description: + "Rename `Workflow.trigger()` (returned by `createWorkflow()`) and `WorkflowJob.trigger()` (returned by `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary. No codemod ships for this rename: distinguishing a workflow/job `.trigger()` call from an unrelated object's own `.trigger()` method requires resolving the receiver back to a `createWorkflow`/`createWorkflowJob` result, which is not reliably decidable from a single file's syntax.", + since: "1.0.0", + until: "2.0.0", + suspiciousPatterns: [".trigger("], + examples: [ + { + before: [ + "const inventory = checkInventory.trigger({ orderId: input.orderId });", + 'const workflowRunId = await orderProcessingWorkflow.trigger(args, { invoker: "manager" });', + ].join("\n"), + after: [ + "const inventory = checkInventory.start({ orderId: input.orderId });", + 'const workflowRunId = await orderProcessingWorkflow.start(args, { invoker: "manager" });', + ].join("\n"), + }, + ], + prompt: [ + "In Tailor SDK v2, the ergonomic .trigger() method on a createWorkflow() or", + "createWorkflowJob() result is renamed to .start(). This is unrelated to the", + "separate tailor.workflow.triggerWorkflow/triggerJobFunction/resumeWorkflow removal", + "(see the workflow-trigger-rename codemod) — this rename targets the SDK's own", + "ergonomic wrapper, not the low-level platform call.", + "", + "For each flagged `.trigger(` call in these files:", + "1. Confirm the receiver is a workflow or job object — typically a local const", + " assigned from createWorkflow(...)/createWorkflowJob(...), a named import of one,", + " or the default import of a workflow module. Skip receivers that are unrelated", + " objects with their own .trigger() method (state machines, event emitters, etc.).", + "2. Rename the call from .trigger(...) to .start(...); the argument list is unchanged.", + "3. Update any mock/test code that reads WorkflowJob['trigger'] / Workflow['trigger']", + " as a type, or that mocks the ergonomic method via a wrapper — for example,", + " `wf.job(definition)` / `wf.workflow(definition)` from mockWorkflow() now return a", + " mock of the `.start` method.", + '4. Update prose/docs/comments that say "trigger the workflow/job" to "start" only', + " where they describe this SDK verb specifically, not unrelated event terminology.", ].join("\n"), }, { diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index 590b858d5..5daf91c10 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -138,4 +138,8 @@ describe("codemod transforms", () => { test("v2/db-type-to-table transforms correctly", async () => { await expect(runFixtureCases("v2/db-type-to-table")).resolves.toBeUndefined(); }); + + test("v2/workflow-trigger-rename transforms correctly", async () => { + await expect(runFixtureCases("v2/workflow-trigger-rename")).resolves.toBeUndefined(); + }); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index 2eab53773..8b3c296ae 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -50,6 +50,8 @@ export default defineConfig([ "v2/rename-bin/scripts/transform": "codemods/v2/rename-bin/scripts/transform.ts", "v2/wait-point-rename/scripts/transform": "codemods/v2/wait-point-rename/scripts/transform.ts", + "v2/workflow-trigger-rename/scripts/transform": + "codemods/v2/workflow-trigger-rename/scripts/transform.ts", }, format: ["esm"], target: "node18", diff --git a/packages/sdk/docs/cli/function.md b/packages/sdk/docs/cli/function.md index bcc74b55e..e5db796fc 100644 --- a/packages/sdk/docs/cli/function.md +++ b/packages/sdk/docs/cli/function.md @@ -178,5 +178,5 @@ You can pass either a source file (`.ts`) or a pre-bundled file (`.js`). When a `.js` file is provided, detection and bundling are skipped and the file is executed as-is. > [!WARNING] -> Workflow job `.trigger()` calls do not work in test-run mode. -> Triggered jobs are not executed; only the target job's `body` function runs in isolation. +> Workflow job `.start()` calls do not work in test-run mode. +> Started jobs are not executed; only the target job's `body` function runs in isolation. diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index d49ede61e..4c4f0343b 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -350,7 +350,7 @@ Do not change behavior beyond the auth.invoker() removal. **Migration:** Partially automatic -Rename statically identified SDK `authInvoker` options to `invoker`, replace `auth.invoker("name")` there with the bare `"name"` string, and drop the `auth` import when no other reference remains. Ambiguous workflow `.trigger()` calls are left for manual review. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle. +Rename statically identified SDK `authInvoker` options to `invoker`, replace `auth.invoker("name")` there with the bare `"name"` string, and drop the `auth` import when no other reference remains. Ambiguous workflow `.start()` calls are left for manual review. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle. Before: @@ -378,8 +378,8 @@ For each remaining auth.invoker() call: machine user name string; platform/runtime authInvoker payloads still expect the object form. 2. Rename remaining authInvoker option keys to invoker only for SDK resolver, - executor, workflow.trigger(), or startWorkflow() options. Keep platform/runtime - payload keys such as tailor.workflow.triggerWorkflow(..., { authInvoker: ... }). + executor, workflow.start(), or startWorkflow() options. Keep platform/runtime + payload keys such as tailor.workflow.startWorkflow(..., { authInvoker: ... }). 3. After removing every auth.invoker usage in a file, delete the now-unused auth import (keeping it pulls Node-only config modules into runtime bundles); leave the import if auth is still referenced elsewhere. @@ -694,6 +694,51 @@ export const { approval } = createWaitPoints((define) => ({ })); ``` +## workflow.triggerWorkflow/triggerJobFunction/resumeWorkflow → startWorkflow/startJobFunction/resumeWorkflowExecution + +**Migration:** Partially automatic + +Rename tailor.workflow call sites from the pre-alignment triggerWorkflow/triggerJobFunction/resumeWorkflow names to the canonical startWorkflow/startJobFunction/resumeWorkflowExecution names, on both the ambient tailor.workflow global and a workflow value imported from @tailor-platform/sdk/runtime(/workflow). + +Before: + +```ts +import { workflow } from "@tailor-platform/sdk/runtime"; + +await workflow.triggerWorkflow("myWorkflow", { data: "value" }); +``` + +After: + +```ts +import { workflow } from "@tailor-platform/sdk/runtime"; + +await workflow.startWorkflow("myWorkflow", { data: "value" }); +``` + +

+Prompt for an AI agent (to finish the cases the codemod could not migrate) + +```text +The pre-alignment tailor.workflow names triggerWorkflow, triggerJobFunction, and +resumeWorkflow are removed from the SDK's type surface in v2; use the canonical +startWorkflow, startJobFunction, and resumeWorkflowExecution names instead. The +codemod rewrites direct member-access call sites on the ambient tailor.workflow +global and on a workflow value imported from @tailor-platform/sdk/runtime or +@tailor-platform/sdk/runtime/workflow (including aliased imports). It skips a +file entirely when a local declaration shadows the workflow import or the +ambient tailor name, to avoid rewriting an unrelated same-named value — review +those manually. + +Also review, and migrate by hand: +- Destructured references (e.g. const { triggerWorkflow } = workflow) — the + codemod only rewrites direct member-access calls. +- Imported TriggerWorkflowOptions / TriggerJobFunctionOptions types — rename + them to StartWorkflowOptions / StartJobFunctionOptions. +``` + +
+ ## openDownloadStream → downloadStream **Migration:** Manual @@ -787,18 +832,18 @@ string literals. -## Workflow .trigger() and trigger tests +## Workflow job start() and start tests **Migration:** Manual -Workflow job `.trigger()` now aligns with the platform runtime: it returns the job result directly instead of a Promise wrapper, and tests no longer run job bodies locally. Mock trigger responses with `mockWorkflow()` (`setJobHandler` / `enqueueResult`, assert via `triggeredJobs`), or use `runWorkflowLocally()` for a full-chain local run. +Workflow job `.start()` (previously `.trigger()`) now aligns with the platform runtime: it returns the job result directly instead of a Promise wrapper, and tests no longer run job bodies locally. Mock start responses with `mockWorkflow()` (`setJobHandler` / `enqueueResult`, assert via `startedJobs`), or use `runWorkflowLocally()` for a full-chain local run. Tests must mock the workflow runtime instead of running bodies locally: Before: ```ts -const result = await orderJob.trigger({ id }); +const result = await orderJob.start({ id }); expect(result.status).toBe("done"); ``` @@ -807,7 +852,7 @@ After: ```ts using wf = mockWorkflow(); wf.setJobHandler((jobName) => (jobName === "order-job" ? { status: "done" } : null)); -const result = await orderJob.trigger({ id }); +const result = await orderJob.start({ id }); expect(result.status).toBe("done"); ``` @@ -815,11 +860,57 @@ expect(result.status).toBe("done"); Prompt for an AI agent (to perform this migration) ```text -Workflow job .trigger() now uses the platform workflow runtime instead of running +Workflow job .start() now uses the platform workflow runtime instead of running the job body locally. In tests, acquire `using wf = mockWorkflow()` and provide -trigger responses (setJobHandler / enqueueResult), or use runWorkflowLocally() for a -full-chain local run; an unmocked trigger now throws. Outside tests, treat the -trigger result as the job output directly (no Promise wrapper to unwrap). +start responses (setJobHandler / enqueueResult), or use runWorkflowLocally() for a +full-chain local run; an unmocked start now throws. Outside tests, treat the +start result as the job output directly (no Promise wrapper to unwrap). +``` + + + +## Workflow.trigger()/WorkflowJob.trigger() → .start() + +**Migration:** Manual + +Rename `Workflow.trigger()` (returned by `createWorkflow()`) and `WorkflowJob.trigger()` (returned by `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary. No codemod ships for this rename: distinguishing a workflow/job `.trigger()` call from an unrelated object's own `.trigger()` method requires resolving the receiver back to a `createWorkflow`/`createWorkflowJob` result, which is not reliably decidable from a single file's syntax. + +Before: + +```ts +const inventory = checkInventory.trigger({ orderId: input.orderId }); +const workflowRunId = await orderProcessingWorkflow.trigger(args, { invoker: "manager" }); +``` + +After: + +```ts +const inventory = checkInventory.start({ orderId: input.orderId }); +const workflowRunId = await orderProcessingWorkflow.start(args, { invoker: "manager" }); +``` + +
+Prompt for an AI agent (to perform this migration) + +```text +In Tailor SDK v2, the ergonomic .trigger() method on a createWorkflow() or +createWorkflowJob() result is renamed to .start(). This is unrelated to the +separate tailor.workflow.triggerWorkflow/triggerJobFunction/resumeWorkflow removal +(see the workflow-trigger-rename codemod) — this rename targets the SDK's own +ergonomic wrapper, not the low-level platform call. + +For each flagged `.trigger(` call in these files: +1. Confirm the receiver is a workflow or job object — typically a local const + assigned from createWorkflow(...)/createWorkflowJob(...), a named import of one, + or the default import of a workflow module. Skip receivers that are unrelated + objects with their own .trigger() method (state machines, event emitters, etc.). +2. Rename the call from .trigger(...) to .start(...); the argument list is unchanged. +3. Update any mock/test code that reads WorkflowJob['trigger'] / Workflow['trigger'] + as a type, or that mocks the ergonomic method via a wrapper — for example, + `wf.job(definition)` / `wf.workflow(definition)` from mockWorkflow() now return a + mock of the `.start` method. +4. Update prose/docs/comments that say "trigger the workflow/job" to "start" only + where they describe this SDK verb specifically, not unrelated event terminology. ```
diff --git a/packages/sdk/docs/runtime.md b/packages/sdk/docs/runtime.md index 38b353c38..b3765c0a0 100644 --- a/packages/sdk/docs/runtime.md +++ b/packages/sdk/docs/runtime.md @@ -81,7 +81,7 @@ The runtime entry re-exports the following namespaces. Detailed signatures, para - `secretmanager` — secret-vault access (`getSecret`, `getSecrets`) - `authconnection` — OAuth-style connection tokens (`getConnectionToken`) - `idp` — IdP user management (`new Client({ namespace })`) -- `workflow` — workflow & job control (`startWorkflow`, `resumeWorkflowExecution`, `startJobFunction`, `wait`, `resolve`; the pre-alignment names `triggerWorkflow`, `resumeWorkflow`, `triggerJobFunction` are kept as frozen aliases) +- `workflow` — workflow & job control (`startWorkflow`, `resumeWorkflowExecution`, `startJobFunction`, `wait`, `resolve`) - `context` — execution context (`getInvoker`) - `file` — `tailordb.file` BLOB API (`upload`, `download`, `downloadAsBase64`, `delete`, `getMetadata`, `downloadStream`, `uploadStream`) - `aigateway` — AI Gateway URL resolution (`get`) diff --git a/packages/sdk/docs/services/auth.md b/packages/sdk/docs/services/auth.md index fe71c60d8..e721f2791 100644 --- a/packages/sdk/docs/services/auth.md +++ b/packages/sdk/docs/services/auth.md @@ -269,7 +269,7 @@ tailor machineuser token ### Specifying a machine user invoker -Resolvers, executors, and `workflow.trigger()` accept an `invoker` option that chooses which machine user runs the operation. Pass the machine user name as a plain string — it is type-narrowed to the names you registered in `machineUsers`. +Resolvers, executors, and `workflow.start()` accept an `invoker` option that chooses which machine user runs the operation. Pass the machine user name as a plain string — it is type-narrowed to the names you registered in `machineUsers`. ```typescript // tailor.config.ts @@ -284,19 +284,19 @@ export const auth = defineAuth("my-auth", { ``` ```typescript -// resolvers/trigger-workflow.ts +// resolvers/start-workflow.ts import { createResolver, t } from "@tailor-platform/sdk"; import myWorkflow from "../workflows/my-workflow"; export default createResolver({ - name: "triggerMyWorkflow", + name: "startMyWorkflow", operation: "mutation", input: { id: t.string(), }, body: async ({ input }) => { - // Trigger workflow with machine user permissions - const workflowRunId = await myWorkflow.trigger( + // Start workflow with machine user permissions + const workflowRunId = await myWorkflow.start( { id: input.id }, { invoker: "admin-machine-user" }, ); diff --git a/packages/sdk/docs/services/workflow.md b/packages/sdk/docs/services/workflow.md index b349ef8c2..466150ffc 100644 --- a/packages/sdk/docs/services/workflow.md +++ b/packages/sdk/docs/services/workflow.md @@ -10,7 +10,7 @@ Workflows provide: - Durable execution with automatic state management - Resume capabilities from failure points - Access to TailorDB via Kysely query builder -- Job triggering to compose multi-step logic +- Job starting to compose multi-step logic For the official Tailor Platform documentation, see [Workflow Guide](https://docs.tailor.tech/guides/workflow). @@ -26,12 +26,12 @@ All workflow components must follow these rules: - **Job name uniqueness**: Job names must be unique across the entire project (not just within one file) - **mainJob required**: Every workflow must specify a `mainJob` -| Rule | Description | -| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| `createWorkflow` result must be default export | Workflow files must export the workflow as default | -| All jobs must be named exports | Includes `mainJob` and any job triggered via `.trigger()` (even if referenced only within the same file) | -| Job `name` values must be unique | Job names must be unique across the entire project | -| `mainJob` is required | Every workflow must specify a `mainJob` | +| Rule | Description | +| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `createWorkflow` result must be default export | Workflow files must export the workflow as default | +| All jobs must be named exports | Includes `mainJob` and any job started via `.start()` (even if referenced only within the same file) | +| Job `name` values must be unique | Job names must be unique across the entire project | +| `mainJob` is required | Every workflow must specify a `mainJob` | ## Creating a Workflow Job @@ -92,11 +92,11 @@ export const nullJob = createWorkflowJob({ These constraints are enforced at compile time — you will get a type error if you use an unsupported type. -## Triggering Jobs +## Starting Jobs -Use `.trigger()` to start other jobs from within a job. +Use `.start()` to start other jobs from within a job. -Jobs are triggered by calling `.trigger()` on the other job object (no `deps` and no `jobs` object in the context). +Jobs are started by calling `.start()` on the other job object (no `deps` and no `jobs` object in the context). ```typescript import { createWorkflowJob } from "@tailor-platform/sdk"; @@ -106,10 +106,10 @@ import { sendNotification } from "./jobs/send-notification"; export const mainJob = createWorkflowJob({ name: "main-job", body: (input: { customerId: string }) => { - const customer = fetchCustomer.trigger({ + const customer = fetchCustomer.start({ customerId: input.customerId, }); - const notification = sendNotification.trigger({ + const notification = sendNotification.start({ message: "Order processed", recipient: customer.email, }); @@ -120,51 +120,51 @@ export const mainJob = createWorkflowJob({ ### 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. +Workflow jobs use a **suspend/resume execution model**. When a job calls `.start()`, the runtime suspends the current job, executes the started job, and then **re-executes the calling job from the beginning** with cached results from previous starts. -This means that **job code must be deterministic** — every re-execution must produce the same sequence of `.trigger()` calls with the same arguments in the same order. +This means that **job code must be deterministic** — every re-execution must produce the same sequence of `.start()` calls with the same arguments in the same order. -Using `.trigger()` inside a loop works correctly, as long as the loop is deterministic: +Using `.start()` inside a loop works correctly, as long as the loop is deterministic: ```typescript // ✅ OK: deterministic loop — same calls in the same order on every execution const regions = ["us", "eu", "ap"]; for (const region of regions) { - const result = fetchData.trigger({ region }); + const result = fetchData.start({ region }); results.push(result); } ``` ```typescript // ❌ Bad: non-deterministic — argument changes between executions -processJob.trigger({ timestamp: Date.now() }); +processJob.start({ timestamp: Date.now() }); // ✅ OK: call Date.now() in separated job -const timestamp = timestampJob.trigger(); -processJob.trigger({ timestamp }); +const timestamp = timestampJob.start(); +processJob.start({ timestamp }); ``` ```typescript // ❌ Bad: non-deterministic — external data may change between executions const items = await fetch("https://api.example.com/items").then((r) => r.json()); for (const item of items) { - processItem.trigger({ id: item.id }); + processItem.start({ id: item.id }); } // ✅ OK: call fetch("https://api.example.com/items").then((r) => r.json()); in separated job -const items = fetchItemsJob.trigger(); +const items = fetchItemsJob.start(); for (const item of items) { - processItem.trigger({ id: item.id }); + processItem.start({ id: item.id }); } ``` -If the runtime detects that a `.trigger()` call at the same position has different arguments than the previous execution, it will throw an **argument hash mismatch error**. +If the runtime detects that a `.start()` call at the same position has different arguments than the previous execution, it will throw an **argument hash mismatch error**. **Guidelines:** -- Do not use non-deterministic values (random numbers, timestamps, external API responses) as `.trigger()` arguments. -- Do not use conditions that may change between executions to decide whether to call `.trigger()`. -- Any data that varies between executions should be fetched **inside the triggered job**, not passed as an argument from the calling job. +- Do not use non-deterministic values (random numbers, timestamps, external API responses) as `.start()` arguments. +- Do not use conditions that may change between executions to decide whether to call `.start()`. +- Any data that varies between executions should be fetched **inside the started job**, not passed as an argument from the calling job. ## Workflow Definition @@ -181,12 +181,12 @@ export const processOrder = createWorkflowJob({ body: (input: { customerId: string }, { env, invoker }) => { // `env` contains values from `tailor.config.ts` -> `env`. // `invoker` is the principal running this job, or the machine user - // configured through the trigger `invoker` option; `null` for anonymous calls. - // Trigger other jobs by calling .trigger() on the job object. - const customer = fetchCustomer.trigger({ + // configured through the start `invoker` option; `null` for anonymous calls. + // Start other jobs by calling .start() on the job object. + const customer = fetchCustomer.start({ customerId: input.customerId, }); - sendNotification.trigger({ + sendNotification.start({ message: "Order processed", recipient: customer.email, }); @@ -353,7 +353,7 @@ export default createWorkflow({ ## Execution Policies -Execution policies apply a per-key concurrency cap to workflow job function dispatches. Declare them at the workspace level and pass a matching key when triggering a job; the platform serializes dispatches that resolve to the same key and suspends any that would exceed the cap until slots free up. +Execution policies apply a per-key concurrency cap to workflow job function dispatches. Declare them at the workspace level and pass a matching key when starting a job; the platform serializes dispatches that resolve to the same key and suspends any that would exceed the cap until slots free up. ### Declaring Policies @@ -401,7 +401,7 @@ An exact-key policy applies to dispatches whose runtime key equals the policy ke ### Referencing a Policy from a Workflow -Pass the runtime key through the `executionPolicyKey` option on `job.trigger()` or `tailor.workflow.startJobFunction()` (or its frozen alias `triggerJobFunction`). For exact-key policies, use `.key` directly — it's typed so only a value that came from a declared policy can be passed. For wildcard policies (`matchType: "prefix"`), there is no `.key` — call `.keyFor(suffix)` to build the concrete key. `keyFor` joins the prefix and suffix with `.` by default; override it with `separator` — the second argument to `defineWorkflowExecutionPolicies` (applies to every policy in the group), or a `def` field on a single `defineWorkflowExecutionPolicy`. +Pass the runtime key through the `executionPolicyKey` option on `job.start()` or `tailor.workflow.startJobFunction()`. For exact-key policies, use `.key` directly — it's typed so only a value that came from a declared policy can be passed. For wildcard policies (`matchType: "prefix"`), there is no `.key` — call `.keyFor(suffix)` to build the concrete key. `keyFor` joins the prefix and suffix with `.` by default; override it with `separator` — the second argument to `defineWorkflowExecutionPolicies` (applies to every policy in the group), or a `def` field on a single `defineWorkflowExecutionPolicy`. ```typescript import { createWorkflowJob } from "@tailor-platform/sdk"; @@ -413,13 +413,13 @@ export const mainJob = createWorkflowJob({ name: "main-job", body: async (input: { tenantId: string }) => { // Exact key policy: pass .key directly. - await sendNotification.trigger( + await sendNotification.start( { message: "Order processed" }, { executionPolicyKey: executionPolicies.premium.key }, ); // Wildcard policy: build the concrete key with keyFor(). - await fetchTenant.trigger( + await fetchTenant.start( { tenantId: input.tenantId }, { executionPolicyKey: executionPolicies.tenantApi.keyFor(input.tenantId) }, ); @@ -427,13 +427,13 @@ export const mainJob = createWorkflowJob({ }); ``` -The same `executionPolicyKey` option is available on `tailor.workflow.startJobFunction(name, args, options)` (or the frozen alias `tailor.workflow.triggerJobFunction`) for jobs invoked by name. +The same `executionPolicyKey` option is available on `tailor.workflow.startJobFunction(name, args, options)` for jobs invoked by name. -## Triggering a Workflow from a Resolver +## Starting a Workflow from a Resolver -You can start a workflow execution from a resolver using `workflow.trigger()`. +You can start a workflow execution from a resolver using `workflow.start()`. -- `workflow.trigger(args, options?)` returns a workflow run ID (`Promise`). +- `workflow.start(args, options?)` returns a workflow run ID (`Promise`). - To run with machine-user permissions, pass `{ invoker: "" }`. The name is type-narrowed to the machine users defined in your auth config. ```typescript @@ -441,14 +441,14 @@ import { createResolver, t } from "@tailor-platform/sdk"; import orderProcessingWorkflow from "../workflows/order-processing"; export default createResolver({ - name: "triggerOrderProcessing", + name: "startOrderProcessing", operation: "mutation", input: { orderId: t.string(), customerId: t.string(), }, body: async ({ input }) => { - const workflowRunId = await orderProcessingWorkflow.trigger( + const workflowRunId = await orderProcessingWorkflow.start( { orderId: input.orderId, customerId: input.customerId }, { invoker: "manager-machine-user" }, ); @@ -461,7 +461,7 @@ export default createResolver({ }); ``` -See the full working example in the repository: [example/resolvers/triggerWorkflow.ts](https://github.com/tailor-platform/sdk/blob/main/example/resolvers/triggerWorkflow.ts). +See the full working example in the repository: [example/resolvers/startWorkflow.ts](https://github.com/tailor-platform/sdk/blob/main/example/resolvers/startWorkflow.ts). ## File Organization diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index f9796c542..d1fd5eea2 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -13,7 +13,7 @@ Unit-test entrypoints exposed by the SDK: - `resolver.body({ input, caller, invoker, env })` — invoke a resolver - `workflowJob.body(input, { env, invoker })` — invoke a workflow job body directly -- `workflowJob.trigger(input)` — chain a workflow job through the workflow runtime +- `workflowJob.start(input)` — chain a workflow job through the workflow runtime - `runWorkflowLocally(workflow, args)` — run a workflow chain locally with real job bodies - `executor.operation.body({ ...args, invoker })` — invoke a function-kind executor @@ -146,13 +146,13 @@ Pass `{ onUnhandled: "error" }` to make an unmatched query fail instead of retur ### Workflow Mock -Workflow job `.trigger()` calls use the platform workflow runtime. Acquire `mockWorkflow()` when you want to provide trigger responses with `setJobHandler` / `enqueueResult` or assert on `triggeredJobs`. If no response is configured, the mock throws so missing job mocks fail loudly. Use `job(definition)` or `workflow(definition)` to get a stable, fully typed Vitest mock for one definition: +Workflow job `.start()` calls use the platform workflow runtime. Acquire `mockWorkflow()` when you want to provide start responses with `setJobHandler` / `enqueueResult` or assert on `startedJobs`. If no response is configured, the mock throws so missing job mocks fail loudly. Use `job(definition)` or `workflow(definition)` to get a stable, fully typed Vitest mock for one definition: ```typescript import { mockWorkflow } from "@tailor-platform/sdk/vitest"; import { processPayment, validateOrder } from "./jobs"; -test("workflow triggers jobs", async () => { +test("workflow starts jobs", async () => { using wf = mockWorkflow(); const validate = wf.job(validateOrder); const payment = wf.job(processPayment); @@ -166,7 +166,7 @@ test("workflow triggers jobs", async () => { }); ``` -Unconfigured definition mocks continue to run their real implementations. The lower-level `triggerJobFunction`, `triggerWorkflow`, `resumeWorkflow`, `wait`, and `resolve` mocks and the existing `setJobHandler`, `enqueueResult`, `enqueueResults`, and call-record helpers remain available. +Unconfigured definition mocks continue to run their real implementations. The lower-level `startJobFunction`, `startWorkflow`, `resumeWorkflowExecution`, `wait`, and `resolve` mocks and the existing `setJobHandler`, `enqueueResult`, `enqueueResults`, and call-record helpers remain available. Use `waitPoint(definition)` for typed wait-point control: @@ -374,7 +374,7 @@ export default defineConfig({ ## Unit Tests -Unit tests call `.body()` (or `.trigger()`) directly on a resolver, workflow job, or executor and stub any platform-provided globals they touch. +Unit tests call `.body()` (or `.start()`) directly on a resolver, workflow job, or executor and stub any platform-provided globals they touch. ### Testing Resolvers @@ -669,7 +669,7 @@ To exercise the full chain (executor → helper → TailorDB), drop the spy and ### Testing Workflow Jobs -Workflow jobs expose the same `.body()` entrypoint as resolvers, plus `.trigger()` for calling them from another job or a test. +Workflow jobs expose the same `.body()` entrypoint as resolvers, plus `.start()` for calling them from another job or a test. #### Simple job @@ -696,7 +696,7 @@ describe("validateOrder", () => { }); ``` -#### Jobs that trigger other jobs +#### Jobs that start other jobs Use `mockWorkflow().job(definition)` to replace dependent jobs with deterministic results: @@ -781,7 +781,7 @@ The lower-level `setWaitHandler` and `waitCalls` APIs remain available when one #### Running a full workflow locally -To exercise the full chain with real job bodies, call `runWorkflowLocally(workflow, args)`. Dependent jobs run their real `.body()` functions, and trigger args/results cross the same JSON boundary as the platform, so a non-serializable payload fails the test exactly as it would in production: +To exercise the full chain with real job bodies, call `runWorkflowLocally(workflow, args)`. Dependent jobs run their real `.body()` functions, and start args/results cross the same JSON boundary as the platform, so a non-serializable payload fails the test exactly as it would in production: ```typescript import { runWorkflowLocally } from "@tailor-platform/sdk/vitest"; @@ -803,7 +803,7 @@ Pass `{ env }` as the third argument when job bodies need configuration values d If you already acquired `mockWorkflow()`, you can also call `wf.setEnv(...)` to reuse the same env across local workflow runs. -Like the platform runtime, the local runner re-runs the orchestrator body once per `.trigger()` call (N triggers means N+1 passes), so any side effects outside the trigger results fire on every pass. Keep the body deterministic and move repeatable side effects into the triggered jobs. +Like the platform runtime, the local runner re-runs the orchestrator body once per `.start()` call (N starts means N+1 passes), so any side effects outside the start results fire on every pass. Keep the body deterministic and move repeatable side effects into the started jobs. This helper is still a local runner. Use E2E tests when you need to verify deployed workflow scheduling, suspension, or replay behavior. diff --git a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/workflows/processOrder.ts b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/workflows/processOrder.ts index 69b24d169..13fa6532e 100644 --- a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/workflows/processOrder.ts +++ b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/workflows/processOrder.ts @@ -19,14 +19,14 @@ export const notifyUser = createWorkflowJob({ export const processOrder = createWorkflowJob({ name: "process-order", body: (input: { orderId: string; userEmail: string }) => { - const details = fetchDetails.trigger({ orderId: input.orderId }); - // trigger return may be undefined when the upstream job produces no value + const details = fetchDetails.start({ orderId: input.orderId }); + // start return may be undefined when the upstream job produces no value // oxlint-disable-next-line typescript/no-unnecessary-condition if (!details) { throw new Error(`Order ${input.orderId} not found`); } - const notification = notifyUser.trigger({ + const notification = notifyUser.start({ message: `Order ${input.orderId} processed`, recipient: input.userEmail, }); diff --git a/packages/sdk/src/cli/commands/deploy/workflow.test.ts b/packages/sdk/src/cli/commands/deploy/workflow.test.ts index e43aba9bf..a1bc48871 100644 --- a/packages/sdk/src/cli/commands/deploy/workflow.test.ts +++ b/packages/sdk/src/cli/commands/deploy/workflow.test.ts @@ -42,7 +42,7 @@ describe("planWorkflow", () => { function createMockJob(name: string): WorkflowJob { return { name, - trigger: () => {}, + start: () => {}, body: () => {}, }; } @@ -761,7 +761,7 @@ describe("formatWorkflowChangeEntries", () => { workspaceId: "ws", workflow: { name: "order-processing", - mainJob: { name: "process-order", body: () => {}, trigger: () => {} }, + mainJob: { name: "process-order", body: () => {}, start: () => {} }, }, usedJobNames: ["process-order"], metaRequest: { trn: "t", labels: {} }, diff --git a/packages/sdk/src/cli/commands/function/detect.test.ts b/packages/sdk/src/cli/commands/function/detect.test.ts index 96ca916c9..847afc6c1 100644 --- a/packages/sdk/src/cli/commands/function/detect.test.ts +++ b/packages/sdk/src/cli/commands/function/detect.test.ts @@ -185,13 +185,13 @@ export default { const multiJobSource = ` export const job_a = { name: "job-a", - trigger: () => {}, + start: () => {}, body: (input) => input, }; export const job_b = { name: "job-b", - trigger: () => {}, + start: () => {}, body: (input) => input, }; `; @@ -202,13 +202,13 @@ export const job_b = { ` export const my_job = { name: "my-job", - trigger: () => {}, + start: () => {}, body: (input) => input, }; export default { name: "my-workflow", - mainJob: { name: "my-job", trigger: () => {}, body: () => {} }, + mainJob: { name: "my-job", start: () => {}, body: () => {} }, }; `, ); @@ -242,7 +242,7 @@ export default { ` export const my_job = { name: "my-job", - trigger: () => {}, + start: () => {}, body: (input) => input, }; `, diff --git a/packages/sdk/src/cli/commands/function/test-run.ts b/packages/sdk/src/cli/commands/function/test-run.ts index 4ad685a46..e8f8b40e8 100644 --- a/packages/sdk/src/cli/commands/function/test-run.ts +++ b/packages/sdk/src/cli/commands/function/test-run.ts @@ -61,8 +61,8 @@ export const testRunCommand = defineAppCommand({ When a \`.js\` file is provided, detection and bundling are skipped and the file is executed as-is. > [!WARNING] -> Workflow job \`.trigger()\` calls do not work in test-run mode. -> Triggered jobs are not executed; only the target job's \`body\` function runs in isolation.`, +> Workflow job \`.start()\` calls do not work in test-run mode. +> Started jobs are not executed; only the target job's \`body\` function runs in isolation.`, examples: [ { cmd: 'resolvers/add.ts --arg \'{"a":1,"b":2}\'', diff --git a/packages/sdk/src/cli/services/service-brand.test.ts b/packages/sdk/src/cli/services/service-brand.test.ts index cc195c9c4..53a3fc7bd 100644 --- a/packages/sdk/src/cli/services/service-brand.test.ts +++ b/packages/sdk/src/cli/services/service-brand.test.ts @@ -150,7 +150,7 @@ describe("service brand-based error categorization", () => { "workflow", { name: "approvalFlow", - mainJob: { name: "startApproval", trigger: () => {}, body: () => {} }, + mainJob: { name: "startApproval", start: () => {}, body: () => {} }, }, { name: "approvalFlow" }, "approvalFlow", @@ -162,7 +162,7 @@ describe("service brand-based error categorization", () => { itLoadsBrandedValues( WorkflowJobSchema, "workflow-job", - { name: "processOrder", trigger: () => {}, body: () => {} }, + { name: "processOrder", start: () => {}, body: () => {} }, { name: "processOrder", body: "not a function" }, "processOrder", { helperFn: () => {} }, diff --git a/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts b/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts index db545f3ae..56df32eab 100644 --- a/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts +++ b/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts @@ -85,7 +85,7 @@ const job1 = createWorkflowJob({ const job2 = createWorkflowJob({ name: "job-two", body: async (input, { env }) => { - return await job1.trigger(); + return await job1.start(); } }); `; @@ -391,7 +391,7 @@ const fetchData = createWorkflowJob({ const mainJob = createWorkflowJob({ name: "main-job", body: async (input, { env }) => { - const result = await fetchData.trigger({ id: input.id }); + const result = await fetchData.start({ id: input.id }); return result; } }); @@ -408,9 +408,7 @@ const mainJob = createWorkflowJob({ allJobsMap, ); - expect(result).toContain( - 'tailor.workflow.triggerJobFunction("fetch-data", { id: input.id })', - ); + expect(result).toContain('tailor.workflow.startJobFunction("fetch-data", { id: input.id })'); // fetchData declaration is removed (const fetchData = ...) expect(result).not.toContain("const fetchData"); }); @@ -427,7 +425,7 @@ const fetchData = createWorkflowJob({ const mainJob = createWorkflowJob({ name: "main-job", body: async (input) => { - return await fetchData.trigger({ id: input.id }, { executionPolicyKey: "premium" }); + return await fetchData.start({ id: input.id }, { executionPolicyKey: "premium" }); } }); `; @@ -444,7 +442,7 @@ const mainJob = createWorkflowJob({ ); expect(result).toContain( - 'tailor.workflow.triggerJobFunction("fetch-data", { id: input.id }, { executionPolicyKey: "premium" })', + 'tailor.workflow.startJobFunction("fetch-data", { id: input.id }, { executionPolicyKey: "premium" })', ); }); @@ -463,7 +461,7 @@ const heavyJob = createWorkflowJob({ const mainJob = createWorkflowJob({ name: "main-job", body: async (input, { env }) => { - const result = await heavyJob.trigger(); + const result = await heavyJob.start(); return { result: "main" }; } }); @@ -487,7 +485,7 @@ const mainJob = createWorkflowJob({ // mainJob body is preserved expect(result).toContain('result: "main"'); // trigger is transformed (job name appears in triggerJobFunction call) - expect(result).toContain('tailor.workflow.triggerJobFunction("heavy-job", undefined)'); + expect(result).toContain('tailor.workflow.startJobFunction("heavy-job", undefined)'); }); test("removes declarations of multiple other jobs", () => { @@ -507,8 +505,8 @@ const job2 = createWorkflowJob({ const mainJob = createWorkflowJob({ name: "main-job", body: async () => { - await job1.trigger(); - await job2.trigger(); + await job1.start(); + await job2.start(); return "main"; } }); @@ -534,8 +532,8 @@ const mainJob = createWorkflowJob({ // heavy code is removed (part of job1/job2 body) expect(result).not.toContain("heavy code"); // triggers are transformed (job names appear in triggerJobFunction calls) - expect(result).toContain('tailor.workflow.triggerJobFunction("job-one", undefined)'); - expect(result).toContain('tailor.workflow.triggerJobFunction("job-two", undefined)'); + expect(result).toContain('tailor.workflow.startJobFunction("job-one", undefined)'); + expect(result).toContain('tailor.workflow.startJobFunction("job-two", undefined)'); }); test("does not transform trigger calls inside fallback-removed job bodies", () => { @@ -550,7 +548,7 @@ const nestedJob = createWorkflowJob({ createWorkflowJob({ name: "fallback-job", body: async () => { - await nestedJob.trigger({ id: 1 }); + await nestedJob.start({ id: 1 }); return "fallback"; } }); @@ -563,8 +561,8 @@ const mainJob = createWorkflowJob({ const result = transformWorkflowSource(source, "main-job"); expect(result).toContain("body: () => {}"); - expect(result).not.toContain("nestedJob.trigger"); - expect(result).not.toContain("triggerJobFunction"); + expect(result).not.toContain("nestedJob.start"); + expect(result).not.toContain("startJobFunction"); }); test("does not modify jobs without trigger calls", () => { @@ -589,16 +587,16 @@ import { createWorkflowJob } from "@tailor-platform/sdk"; export const mainJob = createWorkflowJob({ name: "main-job", body: async () => { - const step = { trigger: async () => "local" }; - return await step.trigger(); + const step = { start: async () => "local" }; + return await step.start(); }, }); `; const result = transformWorkflowSource(source, "main-job", "mainJob", ["step"]); - expect(result).toContain('const step = { trigger: async () => "local" }'); - expect(result).toContain("step.trigger()"); + expect(result).toContain('const step = { start: async () => "local" }'); + expect(result).toContain("step.start()"); }); test("removes createWorkflow default export", () => { @@ -613,7 +611,7 @@ export const check_inventory = createWorkflowJob({ export const validate_order = createWorkflowJob({ name: "validate-order", body: (input) => { - const inventoryResult = check_inventory.trigger(); + const inventoryResult = check_inventory.start(); return { inventoryResult }; } }); @@ -759,9 +757,9 @@ const workflow2 = createWorkflow({ name: "workflow-two", mainJob: job2 }); describe("AST Transformer - transformFunctionTriggers", () => { describe("workflow trigger transformation", () => { - test("transforms workflow.trigger() calls to tailor.workflow.triggerWorkflow()", () => { + test("transforms workflow.start() calls to tailor.workflow.startWorkflow()", () => { const source = ` -const workflowRunId = await orderWorkflow.trigger( +const workflowRunId = await orderWorkflow.start( { orderId: "123", customerId: "456" }, { invoker: "admin" } ); @@ -771,28 +769,28 @@ const workflowRunId = await orderWorkflow.trigger( const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); - expect(result).toContain('tailor.workflow.triggerWorkflow("order-processing"'); + expect(result).toContain('tailor.workflow.startWorkflow("order-processing"'); expect(result).toContain('{ orderId: "123", customerId: "456" }'); expect(result).toContain('{ invoker: "admin" }'); }); - test("transforms workflow.trigger() with shorthand invoker", () => { + test("transforms workflow.start() with shorthand invoker", () => { const source = ` const invoker = "admin"; -const result = await myWorkflow.trigger({ id: 1 }, { invoker }); +const result = await myWorkflow.start({ id: 1 }, { invoker }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); - expect(result).toContain('tailor.workflow.triggerWorkflow("my-workflow"'); + expect(result).toContain('tailor.workflow.startWorkflow("my-workflow"'); expect(result).toContain("{ invoker }"); }); - test("transforms workflow.trigger() without options and omits the helper", () => { + test("transforms workflow.start() without options and omits the helper", () => { const source = ` -const result = await myWorkflow.trigger({ id: 1 }); +const result = await myWorkflow.start({ id: 1 }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -806,13 +804,13 @@ const result = await myWorkflow.trigger({ id: 1 }); "my-auth", ); - expect(result).toContain('tailor.workflow.triggerWorkflow("my-workflow", { id: 1 })'); + expect(result).toContain('tailor.workflow.startWorkflow("my-workflow", { id: 1 })'); expect(result).not.toContain("__tailor_normalizeTriggerOptions"); }); test("wraps a string-literal invoker with the runtime normalizer when authNamespace is provided", () => { const source = ` -const result = await myWorkflow.trigger({ id: 1 }, { invoker: "kiosk" }); +const result = await myWorkflow.start({ id: 1 }, { invoker: "kiosk" }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -826,7 +824,7 @@ const result = await myWorkflow.trigger({ id: 1 }, { invoker: "kiosk" }); "my-auth", ); - expect(result).toContain('tailor.workflow.triggerWorkflow("my-workflow"'); + expect(result).toContain('tailor.workflow.startWorkflow("my-workflow"'); expect(result).toContain('__tailor_normalizeTriggerOptions({ invoker: "kiosk" })'); // Helper injected at the top of the file with the namespace baked in expect(result).toContain( @@ -837,7 +835,7 @@ const result = await myWorkflow.trigger({ id: 1 }, { invoker: "kiosk" }); test("wraps a variable-reference invoker with the runtime normalizer", () => { const source = ` const machineUser = "kiosk"; -const result = await myWorkflow.trigger({ id: 1 }, { invoker: machineUser }); +const result = await myWorkflow.start({ id: 1 }, { invoker: machineUser }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -857,7 +855,7 @@ const result = await myWorkflow.trigger({ id: 1 }, { invoker: machineUser }); test("wraps a shorthand invoker with the runtime normalizer", () => { const source = ` const invoker = "kiosk"; -const result = await myWorkflow.trigger({ id: 1 }, { invoker }); +const result = await myWorkflow.start({ id: 1 }, { invoker }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -877,7 +875,7 @@ const result = await myWorkflow.trigger({ id: 1 }, { invoker }); test("wraps a variable options argument with the runtime normalizer", () => { const source = ` const opts = { invoker: "kiosk" }; -const result = await myWorkflow.trigger({ id: 1 }, opts); +const result = await myWorkflow.start({ id: 1 }, opts); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -892,13 +890,13 @@ const result = await myWorkflow.trigger({ id: 1 }, opts); ); expect(result).toContain( - 'tailor.workflow.triggerWorkflow("my-workflow", { id: 1 }, __tailor_normalizeTriggerOptions(opts))', + 'tailor.workflow.startWorkflow("my-workflow", { id: 1 }, __tailor_normalizeTriggerOptions(opts))', ); }); test("wraps spread options with the runtime normalizer", () => { const source = ` -const result = await myWorkflow.trigger({ id: 1 }, { ...base }); +const result = await myWorkflow.start({ id: 1 }, { ...base }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -915,9 +913,9 @@ const result = await myWorkflow.trigger({ id: 1 }, { ...base }); expect(result).toContain("__tailor_normalizeTriggerOptions({ ...base })"); }); - test("transforms workflow.trigger() with an empty options object", () => { + test("transforms workflow.start() with an empty options object", () => { const source = ` -const result = await myWorkflow.trigger({ id: 1 }, {}); +const result = await myWorkflow.start({ id: 1 }, {}); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -932,14 +930,14 @@ const result = await myWorkflow.trigger({ id: 1 }, {}); ); expect(result).toContain( - 'tailor.workflow.triggerWorkflow("my-workflow", { id: 1 }, __tailor_normalizeTriggerOptions({}))', + 'tailor.workflow.startWorkflow("my-workflow", { id: 1 }, __tailor_normalizeTriggerOptions({}))', ); }); test("injects the normalizer helper only once per file even for multiple trigger calls", () => { const source = ` -await myWorkflow.trigger({ id: 1 }, { invoker: "kiosk" }); -await myWorkflow.trigger({ id: 2 }, { invoker: "batch" }); +await myWorkflow.start({ id: 1 }, { invoker: "kiosk" }); +await myWorkflow.start({ id: 2 }, { invoker: "batch" }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -959,7 +957,7 @@ await myWorkflow.trigger({ id: 2 }, { invoker: "batch" }); test("keeps options unchanged and omits the helper when authNamespace is not provided", () => { const source = ` -const result = await myWorkflow.trigger({ id: 1 }, { invoker: "kiosk" }); +const result = await myWorkflow.start({ id: 1 }, { invoker: "kiosk" }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); @@ -967,40 +965,40 @@ const result = await myWorkflow.trigger({ id: 1 }, { invoker: "kiosk" }); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain( - 'tailor.workflow.triggerWorkflow("my-workflow", { id: 1 }, { invoker: "kiosk" })', + 'tailor.workflow.startWorkflow("my-workflow", { id: 1 }, { invoker: "kiosk" })', ); expect(result).not.toContain("__tailor_normalizeTriggerOptions"); }); }); describe("job trigger transformation", () => { - test("transforms job.trigger() calls to tailor.workflow.triggerJobFunction()", () => { + test("transforms job.start() calls to tailor.workflow.startJobFunction()", () => { const source = ` -const result = await fetchCustomer.trigger({ customerId: "123" }); +const result = await fetchCustomer.start({ customerId: "123" }); `; const workflowNameMap = new Map(); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); - expect(result).toContain('tailor.workflow.triggerJobFunction("fetch-customer"'); + expect(result).toContain('tailor.workflow.startJobFunction("fetch-customer"'); expect(result).toContain('{ customerId: "123" }'); }); - test("transforms job.trigger() without arguments", () => { + test("transforms job.start() without arguments", () => { const source = ` -const result = await simpleJob.trigger(); +const result = await simpleJob.start(); `; const workflowNameMap = new Map(); const jobNameMap = new Map([["simpleJob", "simple-job"]]); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); - expect(result).toContain('tailor.workflow.triggerJobFunction("simple-job", undefined)'); + expect(result).toContain('tailor.workflow.startJobFunction("simple-job", undefined)'); }); test("forwards a second options argument to triggerJobFunction", () => { const source = ` -const result = await fetchCustomer.trigger({ id: "123" }, { executionPolicyKey: "premium" }); +const result = await fetchCustomer.start({ id: "123" }, { executionPolicyKey: "premium" }); `; const workflowNameMap = new Map(); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); @@ -1008,13 +1006,13 @@ const result = await fetchCustomer.trigger({ id: "123" }, { executionPolicyKey: const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain( - 'tailor.workflow.triggerJobFunction("fetch-customer", { id: "123" }, { executionPolicyKey: "premium" })', + 'tailor.workflow.startJobFunction("fetch-customer", { id: "123" }, { executionPolicyKey: "premium" })', ); }); test("omits the options argument when the caller passes only args", () => { const source = ` -const result = await fetchCustomer.trigger({ id: "123" }); +const result = await fetchCustomer.start({ id: "123" }); `; const workflowNameMap = new Map(); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); @@ -1022,21 +1020,19 @@ const result = await fetchCustomer.trigger({ id: "123" }); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); // Args only, no trailing options argument. - expect(result).toContain( - 'tailor.workflow.triggerJobFunction("fetch-customer", { id: "123" })', - ); + expect(result).toContain('tailor.workflow.startJobFunction("fetch-customer", { id: "123" })'); expect(result).not.toContain('{ id: "123" }, '); }); }); describe("false positive prevention", () => { - test("does not transform .trigger() calls on unknown identifiers", () => { + test("does not transform .start() calls on unknown identifiers", () => { const source = ` // This should NOT be transformed -const result = await someRandomObject.trigger({ data: "test" }); +const result = await someRandomObject.start({ data: "test" }); // Neither should this -const event = button.trigger("click"); +const event = button.start("click"); `; const workflowNameMap = new Map(); const jobNameMap = new Map(); @@ -1044,21 +1040,21 @@ const event = button.trigger("click"); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); // Should remain unchanged - expect(result).toContain('someRandomObject.trigger({ data: "test" })'); - expect(result).toContain('button.trigger("click")'); + expect(result).toContain('someRandomObject.start({ data: "test" })'); + expect(result).toContain('button.start("click")'); expect(result).not.toContain("tailor.workflow"); }); test("only transforms trigger calls for known workflows and jobs", () => { const source = ` // Known workflow - should be transformed -const wfResult = await orderWorkflow.trigger({ id: 1 }, { invoker: "admin" }); +const wfResult = await orderWorkflow.start({ id: 1 }, { invoker: "admin" }); // Known job - should be transformed -const jobResult = await fetchData.trigger({ id: 2 }); +const jobResult = await fetchData.start({ id: 2 }); // Unknown - should NOT be transformed -const unknown = await randomThing.trigger({ id: 3 }); +const unknown = await randomThing.start({ id: 3 }); `; const workflowNameMap = new Map([["orderWorkflow", "order-processing"]]); const jobNameMap = new Map([["fetchData", "fetch-data"]]); @@ -1066,24 +1062,24 @@ const unknown = await randomThing.trigger({ id: 3 }); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); // Known workflow transformed - expect(result).toContain('tailor.workflow.triggerWorkflow("order-processing"'); + expect(result).toContain('tailor.workflow.startWorkflow("order-processing"'); // Known job transformed - expect(result).toContain('tailor.workflow.triggerJobFunction("fetch-data"'); + expect(result).toContain('tailor.workflow.startJobFunction("fetch-data"'); // Unknown NOT transformed - expect(result).toContain("randomThing.trigger({ id: 3 })"); + expect(result).toContain("randomThing.start({ id: 3 })"); }); - test("transforms workflow.trigger() regardless of argument count", () => { + test("transforms workflow.start() regardless of argument count", () => { const source = ` -const result = await myWorkflow.trigger({ id: 1 }); +const result = await myWorkflow.start({ id: 1 }); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); - expect(result).toContain('tailor.workflow.triggerWorkflow("my-workflow", { id: 1 })'); - expect(result).not.toContain("myWorkflow.trigger"); + expect(result).toContain('tailor.workflow.startWorkflow("my-workflow", { id: 1 })'); + expect(result).not.toContain("myWorkflow.start"); }); }); @@ -1092,10 +1088,10 @@ const result = await myWorkflow.trigger({ id: 1 }); const source = ` async function processOrder(orderId: string) { // Trigger a job to fetch data - const data = await fetchCustomer.trigger({ id: orderId }); + const data = await fetchCustomer.start({ id: orderId }); // Then trigger a workflow for processing - const workflowRunId = await orderWorkflow.trigger( + const workflowRunId = await orderWorkflow.start( { orderId, data }, { invoker: "system" } ); @@ -1108,15 +1104,15 @@ async function processOrder(orderId: string) { const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); - expect(result).toContain('tailor.workflow.triggerJobFunction("fetch-customer"'); - expect(result).toContain('tailor.workflow.triggerWorkflow("order-processing"'); + expect(result).toContain('tailor.workflow.startJobFunction("fetch-customer"'); + expect(result).toContain('tailor.workflow.startWorkflow("order-processing"'); }); }); describe("direct job trigger transformation", () => { - test("replaces job.trigger() with triggerJobFunction() and preserves await", () => { + test("replaces job.start() with startJobFunction() and preserves await", () => { const source = ` -const customer = await fetchCustomer.trigger({ customerId: "123" }); +const customer = await fetchCustomer.start({ customerId: "123" }); console.log(customer); `; const workflowNameMap = new Map(); @@ -1125,14 +1121,14 @@ console.log(customer); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain( - 'const customer = await tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" })', + 'const customer = await tailor.workflow.startJobFunction("fetch-customer", { customerId: "123" })', ); }); - test("replaces multiple job.trigger() calls directly", () => { + test("replaces multiple job.start() calls directly", () => { const source = ` -const customer = await fetchCustomer.trigger({ customerId: "123" }); -const notification = await sendNotification.trigger({ message: "Hello" }); +const customer = await fetchCustomer.start({ customerId: "123" }); +const notification = await sendNotification.start({ message: "Hello" }); `; const workflowNameMap = new Map(); const jobNameMap = new Map([ @@ -1142,26 +1138,26 @@ const notification = await sendNotification.trigger({ message: "Hello" }); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); - expect(result).toContain('tailor.workflow.triggerJobFunction("fetch-customer"'); - expect(result).toContain('tailor.workflow.triggerJobFunction("send-notification"'); + expect(result).toContain('tailor.workflow.startJobFunction("fetch-customer"'); + expect(result).toContain('tailor.workflow.startJobFunction("send-notification"'); }); - test("does not wrap workflow.trigger() calls (already async)", () => { + test("does not wrap workflow.start() calls (already async)", () => { const source = ` -const executionId = await orderWorkflow.trigger({ orderId: "123" }, { invoker }); +const executionId = await orderWorkflow.start({ orderId: "123" }, { invoker }); `; const workflowNameMap = new Map([["orderWorkflow", "order-processing"]]); const jobNameMap = new Map(); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); - expect(result).toContain('await tailor.workflow.triggerWorkflow("order-processing"'); - expect(result).not.toContain("(async () => tailor.workflow.triggerWorkflow"); + expect(result).toContain('await tailor.workflow.startWorkflow("order-processing"'); + expect(result).not.toContain("(async () => tailor.workflow.startWorkflow"); }); - test("replaces job.trigger() without await with the raw platform result", () => { + test("replaces job.start() without await with the raw platform result", () => { const source = ` -const customerPromise = fetchCustomer.trigger({ customerId: "123" }); +const customerPromise = fetchCustomer.start({ customerId: "123" }); `; const workflowNameMap = new Map(); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); @@ -1169,16 +1165,16 @@ const customerPromise = fetchCustomer.trigger({ customerId: "123" }); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain( - 'const customerPromise = tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" })', + 'const customerPromise = tailor.workflow.startJobFunction("fetch-customer", { customerId: "123" })', ); expect(result).not.toMatch(/\bawait\b/); }); - test("replaces job.trigger() inside Promise.all array elements", () => { + test("replaces job.start() inside Promise.all array elements", () => { const source = ` const [customer, notification] = await Promise.all([ - fetchCustomer.trigger({ customerId: "123" }), - sendNotification.trigger({ message: "Hello" }), + fetchCustomer.start({ customerId: "123" }), + sendNotification.start({ message: "Hello" }), ]); `; const workflowNameMap = new Map(); @@ -1190,17 +1186,17 @@ const [customer, notification] = await Promise.all([ const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain( - 'tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" })', + 'tailor.workflow.startJobFunction("fetch-customer", { customerId: "123" })', ); expect(result).toContain( - 'tailor.workflow.triggerJobFunction("send-notification", { message: "Hello" })', + 'tailor.workflow.startJobFunction("send-notification", { message: "Hello" })', ); expect(result).toContain("await Promise.all(["); }); - test("replaces job.trigger() before .then() chains without preserving Promise wrapping", () => { + test("replaces job.start() before .then() chains without preserving Promise wrapping", () => { const source = ` -fetchCustomer.trigger({ customerId: "123" }).then((customer) => { +fetchCustomer.start({ customerId: "123" }).then((customer) => { console.log(customer); }); `; @@ -1210,13 +1206,13 @@ fetchCustomer.trigger({ customerId: "123" }).then((customer) => { const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain( - 'tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" }).then(', + 'tailor.workflow.startJobFunction("fetch-customer", { customerId: "123" }).then(', ); }); test("transforms only the outer call when a known trigger is nested inside another", () => { const source = ` -await myWorkflow.trigger(fetchCustomer.trigger({ customerId: "123" })); +await myWorkflow.start(fetchCustomer.start({ customerId: "123" })); `; const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); @@ -1225,16 +1221,16 @@ await myWorkflow.trigger(fetchCustomer.trigger({ customerId: "123" })); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain( - 'await tailor.workflow.triggerWorkflow("my-workflow", fetchCustomer.trigger({ customerId: "123" }));', + 'await tailor.workflow.startWorkflow("my-workflow", fetchCustomer.start({ customerId: "123" }));', ); expect(warnSpy).toHaveBeenCalledWith( - 'Nested trigger call "fetchCustomer.trigger(...)" inside "myWorkflow.trigger(...)" cannot be converted. Move it to a separate statement and pass the result instead.', + 'Nested start call "fetchCustomer.start(...)" inside "myWorkflow.start(...)" cannot be converted. Move it to a separate statement and pass the result instead.', ); }); - test("replaces job.trigger() nested inside an unknown .trigger() argument", () => { + test("replaces job.start() nested inside an unknown .start() argument", () => { const source = ` -unknown.trigger(fetchCustomer.trigger({ customerId: "123" })); +unknown.start(fetchCustomer.start({ customerId: "123" })); `; const workflowNameMap = new Map(); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); @@ -1242,9 +1238,9 @@ unknown.trigger(fetchCustomer.trigger({ customerId: "123" })); const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); expect(result).toContain( - 'tailor.workflow.triggerJobFunction("fetch-customer", { customerId: "123" })', + 'tailor.workflow.startJobFunction("fetch-customer", { customerId: "123" })', ); - expect(result).toContain("unknown.trigger("); + expect(result).toContain("unknown.start("); }); }); }); diff --git a/packages/sdk/src/cli/services/workflow/ast-utils.ts b/packages/sdk/src/cli/services/workflow/ast-utils.ts index 13261bff8..771b772ce 100644 --- a/packages/sdk/src/cli/services/workflow/ast-utils.ts +++ b/packages/sdk/src/cli/services/workflow/ast-utils.ts @@ -100,10 +100,10 @@ function argumentSourceText(arg: unknown, sourceText: string): string | undefine } /** - * Get metadata for a static `identifier.trigger(...)` call. + * Get metadata for a static `identifier.start(...)` call. * @param node - AST node to inspect * @param sourceText - Source code text - * @returns Trigger call metadata, or null when the node is not a trigger call + * @returns Start call metadata, or null when the node is not a start call */ export function getTriggerCallInfo( node: ASTNode | null | undefined, @@ -124,7 +124,7 @@ export function getTriggerCallInfo( // callee may be a ComputedMemberExpression at runtime // oxlint-disable-next-line typescript/no-unnecessary-condition memberExpr.computed || - memberExpr.property.name !== "trigger" || + memberExpr.property.name !== "start" || memberExpr.object.type !== "Identifier" ) { return null; diff --git a/packages/sdk/src/cli/services/workflow/bundler.test.ts b/packages/sdk/src/cli/services/workflow/bundler.test.ts index 0b5d48cd4..ab061897b 100644 --- a/packages/sdk/src/cli/services/workflow/bundler.test.ts +++ b/packages/sdk/src/cli/services/workflow/bundler.test.ts @@ -41,7 +41,7 @@ import { createWorkflow, createWorkflowJob } from "@tailor-platform/sdk"; export const step = createWorkflowJob({ name: "step-a", body: async () => "a" }); export const mainA = createWorkflowJob({ name: "main-a", - body: async () => await step.trigger(), + body: async () => await step.start(), }); export default createWorkflow({ name: "workflow-a", mainJob: mainA }); `, @@ -54,7 +54,7 @@ import { createWorkflow, createWorkflowJob } from "@tailor-platform/sdk"; export const step = createWorkflowJob({ name: "step-b", body: async () => "b" }); export const mainB = createWorkflowJob({ name: "main-b", - body: async () => await step.trigger(), + body: async () => await step.start(), }); export default createWorkflow({ name: "workflow-b", mainJob: mainB }); `, @@ -75,8 +75,8 @@ export default createWorkflow({ name: "workflow-b", mainJob: mainB }); expect(result.mainJobDeps["main-a"]).toEqual(["main-a", "step-a"]); expect(result.usedJobNames).toEqual(["step-a", "main-a"]); - expect(result.bundledCode.get("main-a")).toMatch(/triggerJobFunction\([`'"]step-a/); - expect(result.bundledCode.get("main-a")).not.toMatch(/triggerJobFunction\([`'"]step-b/); + expect(result.bundledCode.get("main-a")).toMatch(/startJobFunction\([`'"]step-a/); + expect(result.bundledCode.get("main-a")).not.toMatch(/startJobFunction\([`'"]step-b/); }); test("includes jobs referenced through aliased named imports", async () => { @@ -98,7 +98,7 @@ import { step as importedStep } from "./jobs"; export const mainJob = createWorkflowJob({ name: "main-job", - body: async () => await importedStep.trigger(), + body: async () => await importedStep.start(), }); export default createWorkflow({ name: "workflow", mainJob }); `, @@ -117,7 +117,7 @@ export default createWorkflow({ name: "workflow", mainJob }); expect(result.mainJobDeps["main-job"]).toEqual(["main-job", "step-a"]); expect(result.usedJobNames).toEqual(["step-a", "main-job"]); - expect(result.bundledCode.get("main-job")).toMatch(/triggerJobFunction\([`'"]step-a/); + expect(result.bundledCode.get("main-job")).toMatch(/startJobFunction\([`'"]step-a/); }); test("does not include a job whose binding is shadowed by a parameter", async () => { @@ -131,7 +131,7 @@ import { createWorkflow, createWorkflowJob } from "@tailor-platform/sdk"; export const step = createWorkflowJob({ name: "step-a", body: async () => "a" }); export const mainJob = createWorkflowJob({ name: "main-job", - body: async (step: { trigger(): Promise }) => await step.trigger(), + body: async (step: { start(): Promise }) => await step.start(), }); export default createWorkflow({ name: "workflow", mainJob }); `, @@ -150,7 +150,7 @@ export default createWorkflow({ name: "workflow", mainJob }); expect(result.mainJobDeps["main-job"]).toEqual(["main-job"]); expect(result.usedJobNames).toEqual(["main-job"]); - expect(result.bundledCode.get("main-job")).not.toContain("triggerJobFunction"); + expect(result.bundledCode.get("main-job")).not.toContain("startJobFunction"); }); }); @@ -206,7 +206,7 @@ import simpleWorkflow from "${importPath}"; export const callerJob = createWorkflowJob({ name: "caller-job", body: async () => { - const executionId = await simpleWorkflow.trigger(${triggerArgs}); + const executionId = await simpleWorkflow.start(${triggerArgs}); return { executionId }; }, }); @@ -256,17 +256,17 @@ export default createWorkflow({ test.each([ { label: "cross-file default import", ext: "ts", importPath: "./simple" }, { label: ".mts dependency files", ext: "mts", importPath: "./simple.mjs" }, - ])("transforms workflow.trigger() from $label", async (options) => { + ])("transforms workflow.start() from $label", async (options) => { const { ext, importPath } = options; const result = await buildBundleFixture({ ext, importPath }); expect(result.bundledCode.has("caller-job")).toBe(true); const callerCode = result.bundledCode.get("caller-job")!; - // The trigger call should be transformed to triggerWorkflow - expect(callerCode).toContain("triggerWorkflow"); - // The raw simpleWorkflow.trigger() should NOT remain in the bundle - expect(callerCode).not.toContain("simpleWorkflow.trigger"); + // The trigger call should be transformed to startWorkflow + expect(callerCode).toContain("startWorkflow"); + // The raw simpleWorkflow.start() should NOT remain in the bundle + expect(callerCode).not.toContain("simpleWorkflow.start"); }); test("strips platform-bundle-only symbols from cross-file default import", async () => { @@ -284,7 +284,7 @@ export default createWorkflow({ } }); - test("transforms workflow.trigger() without an options argument", async () => { + test("transforms workflow.start() without an options argument", async () => { const result = await buildBundleFixture({ ext: "ts", importPath: "./simple", @@ -294,8 +294,8 @@ export default createWorkflow({ expect(result.bundledCode.has("caller-job")).toBe(true); const callerCode = result.bundledCode.get("caller-job")!; - expect(callerCode).toContain("triggerWorkflow"); - expect(callerCode).not.toContain("simpleWorkflow.trigger"); + expect(callerCode).toContain("startWorkflow"); + expect(callerCode).not.toContain("simpleWorkflow.start"); }); }); }); diff --git a/packages/sdk/src/cli/services/workflow/bundler.ts b/packages/sdk/src/cli/services/workflow/bundler.ts index b8315153e..583c79478 100644 --- a/packages/sdk/src/cli/services/workflow/bundler.ts +++ b/packages/sdk/src/cli/services/workflow/bundler.ts @@ -144,7 +144,7 @@ interface FilterUsedJobsResult { * Filter jobs to only include those that are actually used. * A job is "used" if: * - It's a mainJob of a workflow - * - It's called via .trigger() from another used job (transitively) + * - It's called via .start() from another used job (transitively) * * Also returns a map of mainJob -> all jobs it depends on (for metadata). * @param allJobs - All available job infos @@ -334,11 +334,11 @@ async function bundleSingleJob( }, }, handler(code, id) { - // Only transform source files that contain workflow jobs or trigger calls + // Only transform source files that contain workflow jobs or start calls if ( !code.includes("createWorkflowJob") && !code.includes("createWorkflow") && - !code.includes(".trigger(") + !code.includes(".start(") ) { return null; } @@ -358,8 +358,8 @@ async function bundleSingleJob( ); } - // Apply workflow.trigger / job.trigger transformation. - if (transformed.includes(".trigger(")) { + // Apply workflow.start / job.start transformation. + if (transformed.includes(".start(")) { transformed = transformFunctionTriggers(transformed, triggerContext, id); } diff --git a/packages/sdk/src/cli/services/workflow/service.test.ts b/packages/sdk/src/cli/services/workflow/service.test.ts index 2e07611f6..a57079f4a 100644 --- a/packages/sdk/src/cli/services/workflow/service.test.ts +++ b/packages/sdk/src/cli/services/workflow/service.test.ts @@ -5,7 +5,7 @@ import { tempCwd } from "#/cli/shared/test-helpers/temp-cwd"; import { createWorkflowService } from "./service"; describe("createWorkflowService", () => { - test("does not strip runtime trigger from an unbranded default export", async () => { + test("does not strip runtime start from an unbranded default export", async () => { using tmp = tempCwd("sdk-workflow-service-"); const workflowFile = path.join(tmp.dir, "workflow.mjs"); fs.writeFileSync( @@ -13,14 +13,14 @@ describe("createWorkflowService", () => { ` export const mainJob = { name: "main-job", - trigger: () => {}, + start: () => {}, body: () => {}, }; export default { name: "looks-like-workflow", mainJob, - trigger: () => {}, + start: () => {}, }; `, ); diff --git a/packages/sdk/src/cli/services/workflow/service.ts b/packages/sdk/src/cli/services/workflow/service.ts index be7bf11c0..1ea1f75ef 100644 --- a/packages/sdk/src/cli/services/workflow/service.ts +++ b/packages/sdk/src/cli/services/workflow/service.ts @@ -13,16 +13,16 @@ export interface CollectedJob { sourceFile: string; } -function stripRuntimeTrigger(workflow: unknown): unknown { +function stripRuntimeStart(workflow: unknown): unknown { if ( !isSdkBranded(workflow, "workflow") || workflow === null || typeof workflow !== "object" || - !("trigger" in workflow) + !("start" in workflow) ) { return workflow; } - const { trigger: _trigger, ...rest } = workflow as Record; + const { start: _start, ...rest } = workflow as Record; return rest; } @@ -190,7 +190,7 @@ async function loadFileContent(filePath: string): Promise<{ for (const [exportName, exportValue] of Object.entries(module)) { // Check if it's a workflow (default export) if (exportName === "default") { - const workflowResult = WorkflowSchema.safeParse(stripRuntimeTrigger(exportValue)); + const workflowResult = WorkflowSchema.safeParse(stripRuntimeStart(exportValue)); if (workflowResult.success) { workflow = workflowResult.data; } else if (isSdkBranded(exportValue, ["workflow", "workflow-job"])) { diff --git a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts b/packages/sdk/src/cli/services/workflow/trigger-transformer.ts index 36762f42a..a77e4166c 100644 --- a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts +++ b/packages/sdk/src/cli/services/workflow/trigger-transformer.ts @@ -331,7 +331,7 @@ export function transformFunctionTriggers( for (const { call, parent } of nestedTriggerCalls) { logger.warn( - `Nested trigger call "${call.identifierName}.trigger(...)" inside "${parent.identifierName}.trigger(...)" cannot be converted. Move it to a separate statement and pass the result instead.`, + `Nested start call "${call.identifierName}.start(...)" inside "${parent.identifierName}.start(...)" cannot be converted. Move it to a separate statement and pass the result instead.`, ); } @@ -352,10 +352,10 @@ export function transformFunctionTriggers( optionsPart = `, ${call.optionsText}`; } } - transformedCall = `tailor.workflow.triggerWorkflow(${JSON.stringify(call.targetName)}, ${call.argsText || "undefined"}${optionsPart})`; + transformedCall = `tailor.workflow.startWorkflow(${JSON.stringify(call.targetName)}, ${call.argsText || "undefined"}${optionsPart})`; } else { const optionsPart = call.optionsText !== undefined ? `, ${call.optionsText}` : ""; - transformedCall = `tailor.workflow.triggerJobFunction(${JSON.stringify(call.targetName)}, ${call.argsText || "undefined"}${optionsPart})`; + transformedCall = `tailor.workflow.startJobFunction(${JSON.stringify(call.targetName)}, ${call.argsText || "undefined"}${optionsPart})`; } replacements.push({ start: call.callRange.start, @@ -380,7 +380,7 @@ export function createTriggerTransformPlugin( transform: { filter: { id: { include: [/\.(ts|mts|cts|js|mjs|cjs)$/] } }, handler(code, id) { - if (!code.includes(".trigger(")) return null; + if (!code.includes(".start(")) return null; return { code: transformFunctionTriggers(code, triggerContext, id) }; }, }, diff --git a/packages/sdk/src/cli/shared/trigger-context.test.ts b/packages/sdk/src/cli/shared/trigger-context.test.ts index 156fc7041..de78b85c1 100644 --- a/packages/sdk/src/cli/shared/trigger-context.test.ts +++ b/packages/sdk/src/cli/shared/trigger-context.test.ts @@ -112,23 +112,23 @@ export const step = createWorkflowJob({ name: "step-b", body: async () => "b" }) test("resolves duplicate local export names from the current module", async () => { const { context, firstPath, firstSource } = await createDuplicateExportContext(); - const result = transform(`${firstSource}\nawait step.trigger();\n`, firstPath, context); + const result = transform(`${firstSource}\nawait step.start();\n`, firstPath, context); - expect(result).toContain('tailor.workflow.triggerJobFunction("step-a", undefined)'); - expect(result).not.toContain('tailor.workflow.triggerJobFunction("step-b"'); + expect(result).toContain('tailor.workflow.startJobFunction("step-a", undefined)'); + expect(result).not.toContain('tailor.workflow.startJobFunction("step-b"'); }); test("resolves aliased named imports", async () => { const { context, tempDir } = await createDuplicateExportContext(); const source = ` import { step as importedStep } from "./first"; -await importedStep.trigger(); +await importedStep.start(); `; const result = transform(source, path.join(tempDir, "caller.ts"), context); - expect(result).toContain('tailor.workflow.triggerJobFunction("step-a", undefined)'); - expect(result).not.toContain("importedStep.trigger()"); + expect(result).toContain('tailor.workflow.startJobFunction("step-a", undefined)'); + expect(result).not.toContain("importedStep.start()"); }); test("resolves relative default workflow imports", async () => { @@ -145,13 +145,13 @@ export default createWorkflow({ name: "workflow-a", mainJob }); const context = await buildTriggerContext({ files: [workflowPath] }); const source = ` import workflow from "./workflow"; -await workflow.trigger(); +await workflow.start(); `; const result = transform(source, path.join(tempDir, "caller.ts"), context); expect(result).toContain('import workflow from "./workflow"'); - expect(result).toContain('tailor.workflow.triggerWorkflow("workflow-a", undefined)'); + expect(result).toContain('tailor.workflow.startWorkflow("workflow-a", undefined)'); }); test("preserves a namespace import paired with a transformed default import", async () => { @@ -169,60 +169,60 @@ export default createWorkflow({ name: "workflow-a", mainJob }); const source = ` import workflow, * as helpers from "./workflow"; console.log(helpers); -await workflow.trigger(); +await workflow.start(); `; const result = transform(source, path.join(tempDir, "caller.ts"), context); expect(result).toContain('import workflow, * as helpers from "./workflow"'); expect(result).toContain("console.log(helpers)"); - expect(result).toContain('tailor.workflow.triggerWorkflow("workflow-a", undefined)'); + expect(result).toContain('tailor.workflow.startWorkflow("workflow-a", undefined)'); }); test("does not transform an imported job shadowed by a parameter", async () => { const { context, tempDir } = await createDuplicateExportContext(); const source = ` import { step } from "./first"; -await step.trigger({ source: "outer" }); -async function run(step: { trigger(): Promise }) { - return step.trigger(); +await step.start({ source: "outer" }); +async function run(step: { start(): Promise }) { + return step.start(); } `; const result = transform(source, path.join(tempDir, "caller.ts"), context); - expect(result).toContain('triggerJobFunction("step-a", { source: "outer" })'); - expect(result).toContain("return step.trigger()"); + expect(result).toContain('startJobFunction("step-a", { source: "outer" })'); + expect(result).toContain("return step.start()"); }); test("does not transform an imported job shadowed by a local variable", async () => { const { context, tempDir } = await createDuplicateExportContext(); const source = ` import { step } from "./first"; -await step.trigger({ source: "outer" }); +await step.start({ source: "outer" }); async function run() { - const step = { trigger: async () => "local" }; - return step.trigger(); + const step = { start: async () => "local" }; + return step.start(); } `; const result = transform(source, path.join(tempDir, "caller.ts"), context); - expect(result).toContain('triggerJobFunction("step-a", { source: "outer" })'); - expect(result).toContain("return step.trigger()"); + expect(result).toContain('startJobFunction("step-a", { source: "outer" })'); + expect(result).toContain("return step.start()"); }); - test("does not transform a local object with a trigger method", async () => { + test("does not transform a local object with a start method", async () => { const { context, tempDir } = await createDuplicateExportContext(); const source = ` -const step = { trigger: async () => "local" }; -await step.trigger(); +const step = { start: async () => "local" }; +await step.start(); `; const result = transform(source, path.join(tempDir, "caller.ts"), context); - expect(result).toContain("await step.trigger()"); - expect(result).not.toContain("tailor.workflow.triggerJobFunction"); + expect(result).toContain("await step.start()"); + expect(result).not.toContain("tailor.workflow.startJobFunction"); }); test("escapes job and workflow names in generated calls", async () => { @@ -231,8 +231,8 @@ await step.trigger(); const jobName = 'step"\\quoted'; currentModule?.localBindings.set("step", { kind: "job", name: jobName }); - const result = transform(`${firstSource}\nawait step.trigger();\n`, firstPath, context); + const result = transform(`${firstSource}\nawait step.start();\n`, firstPath, context); - expect(result).toContain(`triggerJobFunction(${JSON.stringify(jobName)}, undefined)`); + expect(result).toContain(`startJobFunction(${JSON.stringify(jobName)}, undefined)`); }); }); diff --git a/packages/sdk/src/configure/services/executor/executor.ts b/packages/sdk/src/configure/services/executor/executor.ts index 42ec21b27..2de555b3f 100644 --- a/packages/sdk/src/configure/services/executor/executor.ts +++ b/packages/sdk/src/configure/services/executor/executor.ts @@ -8,7 +8,7 @@ import type { Trigger } from "./trigger"; /** * Extract mainJob's Input type from Workflow. */ -type WorkflowInput = Parameters[0]; +type WorkflowInput = Parameters[0]; type TriggerArgs> = T extends { __args: infer Args } ? Args : never; diff --git a/packages/sdk/src/configure/services/executor/operation.ts b/packages/sdk/src/configure/services/executor/operation.ts index 6b92d008b..75dc540d8 100644 --- a/packages/sdk/src/configure/services/executor/operation.ts +++ b/packages/sdk/src/configure/services/executor/operation.ts @@ -285,7 +285,7 @@ export type WebhookOperation = Omit< * Extract mainJob's Input type from Workflow. * Workflow -> Job is WorkflowJob -> Input */ -type WorkflowInput = Parameters[0]; +type WorkflowInput = Parameters[0]; /** Workflow-triggering executor operation. Triggers a workflow in response to an event. */ export type WorkflowOperation = Omit< diff --git a/packages/sdk/src/configure/services/workflow/job.test.ts b/packages/sdk/src/configure/services/workflow/job.test.ts index 2ca82f851..cb8d8e0a7 100644 --- a/packages/sdk/src/configure/services/workflow/job.test.ts +++ b/packages/sdk/src/configure/services/workflow/job.test.ts @@ -16,7 +16,7 @@ async function withRegisteredJobRuntime(run: () => Promise): Promise { const root = globalThis as { tailor?: { workflow?: { - triggerJobFunction: (name: string, args?: unknown) => unknown; + startJobFunction: (name: string, args?: unknown) => unknown; }; }; }; @@ -25,7 +25,7 @@ async function withRegisteredJobRuntime(run: () => Promise): Promise { root.tailor = { ...previousTailor, workflow: { - triggerJobFunction: (name, args) => { + startJobFunction: (name, args) => { const body = getRegisteredJob(name); if (!body) return null; const out = body(platformSerialize(args), buildJobContext()); @@ -53,7 +53,7 @@ describe("WorkflowJob type inference", () => { name: "test", body: () => ({ status: "ok" as const, count: 42 }), }); - type Output = ReturnType; + type Output = ReturnType; expectTypeOf().toEqualTypeOf<{ status: "ok"; count: number }>(); }); @@ -62,7 +62,7 @@ describe("WorkflowJob type inference", () => { name: "test", body: (input: { type: "a" | "b" }) => ({ result: input.type }), }); - type Input = Parameters[0]; + type Input = Parameters[0]; expectTypeOf().toEqualTypeOf<{ type: "a" | "b" }>(); }); @@ -75,7 +75,7 @@ describe("WorkflowJob type inference", () => { name: "test", body: (input: UserInput) => ({ greeting: `Hello ${input.name}` }), }); - type Input = Parameters[0]; + type Input = Parameters[0]; expectTypeOf().toEqualTypeOf(); }); @@ -88,7 +88,7 @@ describe("WorkflowJob type inference", () => { name: "test", body: (): UserOutput => ({ id: "123", created: true }), }); - type Output = ReturnType; + type Output = ReturnType; expectTypeOf().toEqualTypeOf(); }); @@ -123,7 +123,7 @@ describe("WorkflowJob type inference", () => { }); const parent = createWorkflowJob({ name: "propagate-parent-invoker-without-get-builtin-module", - body: async () => await child.trigger(), + body: async () => await child.start(), }); await withRegisteredJobRuntime(async () => { @@ -138,7 +138,7 @@ describe("WorkflowJob type inference", () => { } }); - test("direct body calls propagate invoker to triggered child jobs", async () => { + test("direct body calls propagate invoker to started child jobs", async () => { const invoker: TailorPrincipal = { id: "principal-1", type: "user", @@ -152,7 +152,7 @@ describe("WorkflowJob type inference", () => { }); const parent = createWorkflowJob({ name: "propagate-parent-invoker", - body: async () => await child.trigger(), + body: async () => await child.start(), }); await withRegisteredJobRuntime(async () => { @@ -160,7 +160,7 @@ describe("WorkflowJob type inference", () => { }); }); - test("concurrent direct body calls isolate invokers for child triggers", async () => { + test("concurrent direct body calls isolate invokers for child starts", async () => { const firstInvoker: TailorPrincipal = { id: "principal-1", type: "user", @@ -195,7 +195,7 @@ describe("WorkflowJob type inference", () => { name: "capture-concurrent-parent-invoker", body: async (input: { gate: "first" | "second" }) => { await gates[input.gate]; - return await child.trigger(); + return await child.start(); }, }); @@ -210,7 +210,7 @@ describe("WorkflowJob type inference", () => { }); }); - test("trigger reads the runtime invoker when no body context is active", async () => { + test("start reads the runtime invoker when no body context is active", async () => { const previousTailor = (globalThis as { tailor?: unknown }).tailor; (globalThis as { tailor?: unknown }).tailor = { context: { @@ -230,7 +230,7 @@ describe("WorkflowJob type inference", () => { }); await withRegisteredJobRuntime(async () => { - expect(job.trigger()).toBe("runtime-principal"); + expect(job.start()).toBe("runtime-principal"); }); } finally { (globalThis as { tailor?: unknown }).tailor = previousTailor; @@ -424,13 +424,13 @@ describe("WorkflowJob type constraints", () => { }); }); - describe("trigger return type", () => { + describe("start return type", () => { test("returns Output as-is (no Jsonify transformation)", () => { const job = createWorkflowJob({ name: "test", body: () => ({ result: "ok", count: 42, active: true as boolean }), }); - expectTypeOf(job.trigger).returns.toEqualTypeOf<{ + expectTypeOf(job.start).returns.toEqualTypeOf<{ result: string; count: number; active: boolean; @@ -447,7 +447,7 @@ describe("WorkflowJob type constraints", () => { }, }), }); - expectTypeOf(job.trigger).returns.toEqualTypeOf<{ + expectTypeOf(job.start).returns.toEqualTypeOf<{ data: { id: string; tags: string[]; @@ -460,7 +460,7 @@ describe("WorkflowJob type constraints", () => { name: "test", body: () => undefined, }); - expectTypeOf(job.trigger).returns.toEqualTypeOf(); + expectTypeOf(job.start).returns.toEqualTypeOf(); }); test("returns T | undefined for T | undefined output", () => { @@ -470,27 +470,27 @@ describe("WorkflowJob type constraints", () => { return Math.random() > 0.5 ? { value: 1 } : undefined; }, }); - expectTypeOf(job.trigger).returns.toEqualTypeOf<{ value: number } | undefined>(); + expectTypeOf(job.start).returns.toEqualTypeOf<{ value: number } | undefined>(); }); }); - describe("input presence affects trigger signature", () => { - test("trigger takes no arguments when input is undefined", () => { + describe("input presence affects start signature", () => { + test("start takes no arguments when input is undefined", () => { const job = createWorkflowJob({ name: "test", body: () => ({ result: "ok" }), }); - const _trigger: () => { result: string } = job.trigger; - expectTypeOf(_trigger).toBeFunction(); + const _start: () => { result: string } = job.start; + expectTypeOf(_start).toBeFunction(); }); - test("trigger requires input when body has input parameter", () => { + test("start requires input when body has input parameter", () => { const job = createWorkflowJob({ name: "test", body: (input: { id: string }) => ({ result: input.id }), }); - const _trigger: (input: { id: string }) => { result: string } = job.trigger; - expectTypeOf(_trigger).toBeFunction(); + const _start: (input: { id: string }) => { result: string } = job.start; + expectTypeOf(_start).toBeFunction(); }); }); @@ -503,10 +503,10 @@ describe("WorkflowJob type constraints", () => { expectTypeOf().toEqualTypeOf<"test">(); }); - test("trigger return preserves Output as-is", () => { + test("start return preserves Output as-is", () => { type Job = WorkflowJob<"test", undefined, { id: string; result: string }>; - expectTypeOf>().toEqualTypeOf<{ + expectTypeOf>().toEqualTypeOf<{ id: string; result: string; }>(); @@ -581,24 +581,24 @@ describe("WorkflowJob type constraints", () => { }); // Plain `node` environment (no `tailor-runtime`, no `mockWorkflow()`), so -// `.trigger()` should not execute job bodies locally. -describe("trigger without tailor.workflow", () => { - test("job trigger throws instead of running the registered body", () => { +// `.start()` should not execute job bodies locally. +describe("start without tailor.workflow", () => { + test("job start throws instead of running the registered body", () => { const double = createWorkflowJob({ name: "fallback-double", body: (input: { n: number }) => ({ doubled: input.n * 2 }), }); - expect(() => double.trigger({ n: 21 })).toThrow(/tailor\.workflow is not available/); + expect(() => double.start({ n: 21 })).toThrow(/tailor\.workflow is not available/); }); - test("workflow trigger rejects instead of running the main job", async () => { + test("workflow start rejects instead of running the main job", async () => { const main = createWorkflowJob({ name: "fallback-main", body: (input: { n: number }) => ({ total: input.n + 1 }), }); const workflow = createWorkflow({ name: "fallback-wf", mainJob: main }); - await expect(workflow.trigger({ n: 0 })).rejects.toThrow(/tailor\.workflow is not available/); + await expect(workflow.start({ n: 0 })).rejects.toThrow(/tailor\.workflow is not available/); }); }); diff --git a/packages/sdk/src/configure/services/workflow/job.ts b/packages/sdk/src/configure/services/workflow/job.ts index bef508edf..5905372cb 100644 --- a/packages/sdk/src/configure/services/workflow/job.ts +++ b/packages/sdk/src/configure/services/workflow/job.ts @@ -1,8 +1,8 @@ import { brandValue } from "#/utils/brand"; -import { dispatchTriggerJob, registerJob, type RegisteredJobBody } from "./registry"; +import { dispatchStartJob, registerJob, type RegisteredJobBody } from "./registry"; import { withWorkflowTestInvoker } from "./test-env-key"; import type { TailorEnv, TailorPrincipal } from "#/runtime/types"; -import type { TriggerJobFunctionOptions } from "#/runtime/workflow"; +import type { StartJobFunctionOptions } from "#/runtime/workflow"; import type { JsonCompatible, TypeLevelError } from "#/types/helpers"; /** @@ -33,31 +33,31 @@ type JobBody = [null] extends [I] : TypeLevelError<"Input must be JsonValue-compatible (plain objects/arrays; no class instances or functions)">; /** - * WorkflowJob represents a job that can be triggered in a workflow. + * WorkflowJob represents a job that can be started from a workflow. * * Type constraints: * - Input: Must be JsonValue-compatible (plain objects/arrays; no class instances or functions) or undefined. * - Output: Must be JsonValue-compatible (plain objects/arrays; no class instances or functions), undefined, or void. - * - Trigger returns `Awaited` as-is (no Promise or Jsonify transformation). + * - Start returns `Awaited` as-is (no Promise or Jsonify transformation). */ export interface WorkflowJob { name: Name; /** - * Trigger this job with the given input and return the job's output value. + * Start this job with the given input and return the job's output value. * Accepts an optional second argument to pass `executionPolicyKey` for * platform-side concurrency enforcement. * @example * body: async (input) => { - * const a = jobA.trigger({ id: input.id }); - * const b = jobB.trigger({ id: input.id }, { + * const a = jobA.start({ id: input.id }); + * const b = jobB.start({ id: input.id }, { * executionPolicyKey: `tenant-api.${input.tenantId}`, * }); * return { a, b }; * } */ - trigger: [Input] extends [undefined] - ? (input?: undefined, options?: TriggerJobFunctionOptions) => Awaited - : (input: Input, options?: TriggerJobFunctionOptions) => Awaited; + start: [Input] extends [undefined] + ? (input?: undefined, options?: StartJobFunctionOptions) => Awaited + : (input: Input, options?: StartJobFunctionOptions) => Awaited; body: (input: Input, context: WorkflowJobContext) => Output | Promise; } @@ -78,7 +78,7 @@ interface CreateWorkflowJobConfig { * @param config - Job configuration with name and body function. * @param config.name - Unique job name across the project. * @param config.body - Function that processes the job input. - * @returns A WorkflowJob that can be triggered from other jobs. + * @returns A WorkflowJob that can be started from other jobs. * @example * // Simple job with async body: * export const fetchData = createWorkflowJob({ @@ -93,8 +93,8 @@ interface CreateWorkflowJobConfig { * export const orchestrate = createWorkflowJob({ * name: "orchestrate", * body: (input: { orderId: string }) => { - * const inventory = checkInventory.trigger({ orderId: input.orderId }); - * const payment = processPayment.trigger({ orderId: input.orderId }); + * const inventory = checkInventory.start({ orderId: input.orderId }); + * const payment = processPayment.start({ orderId: input.orderId }); * return { inventory, payment }; * }, * }); @@ -113,28 +113,28 @@ export function createWorkflowJob { throw new Error( - "This workflow job's .trigger() is rewritten at build time and is unavailable in the bundle", + "This workflow job's .start() is rewritten at build time and is unavailable in the bundle", ); } : // Preserve arity: use `arguments.length` (regular function, not arrow) so - // `.trigger(args, undefined)` is treated as "options passed" — matching + // `.start(args, undefined)` is treated as "options passed" — matching // the bundler rewrite, which forwards the literal `undefined` from the // AST as a third argument. Without this, local execution and bundled // workflows would hand mocks different call shapes. - function trigger(args?: unknown, options?: TriggerJobFunctionOptions) { + function start(args?: unknown, options?: StartJobFunctionOptions) { // oxlint-disable-next-line prefer-rest-params return ( arguments.length >= 2 - ? dispatchTriggerJob(config.name, args, options) - : dispatchTriggerJob(config.name, args) + ? dispatchStartJob(config.name, args, options) + : dispatchStartJob(config.name, args) ) as Awaited; }; return brandValue( - { name: config.name, trigger, body } as WorkflowJob>, + { name: config.name, start, body } as WorkflowJob>, "workflow-job", ); } diff --git a/packages/sdk/src/configure/services/workflow/registry.ts b/packages/sdk/src/configure/services/workflow/registry.ts index f6fbd4085..904ee4452 100644 --- a/packages/sdk/src/configure/services/workflow/registry.ts +++ b/packages/sdk/src/configure/services/workflow/registry.ts @@ -15,13 +15,7 @@ const JOB_REGISTRY_KEY: unique symbol = Symbol.for("tailor-platform/sdk:job-regi type PlatformWorkflow = { startWorkflow: (name: string, args?: unknown, options?: StartWorkflowOptions) => Promise; - triggerWorkflow: ( - name: string, - args?: unknown, - options?: StartWorkflowOptions, - ) => Promise; startJobFunction: (name: string, args?: unknown, options?: StartJobFunctionOptions) => unknown; - triggerJobFunction: (name: string, args?: unknown, options?: StartJobFunctionOptions) => unknown; }; type GlobalWithRegistry = typeof globalThis & { @@ -42,9 +36,9 @@ function jobs(): Map { /** * Register a job body keyed by job name. Called as a side effect by * `createWorkflowJob` so `runWorkflowLocally()` can execute dependent job - * bodies when `globalThis.tailor.workflow.triggerJobFunction(name, args)` is invoked. + * bodies when `globalThis.tailor.workflow.startJobFunction(name, args)` is invoked. * - * In production builds the bundler rewrites `.trigger()` calls so this registry + * In production builds the bundler rewrites `.start()` calls so this registry * is never read; the gated write is dropped as dead code. * @param name - Job name * @param body - Job body function @@ -80,14 +74,14 @@ function requirePlatformWorkflow(): PlatformWorkflow { // A valid placeholder UUID, so callers that validate the execution id behave the // same locally as against the platform. -export const TRIGGER_DEFAULT = "00000000-0000-4000-8000-000000000000"; +export const START_DEFAULT = "00000000-0000-4000-8000-000000000000"; -// `.trigger()` routes through the installed `tailor.workflow` shim. Local body +// `.start()` routes through the installed `tailor.workflow` shim. Local body // execution is intentionally available only through `runWorkflowLocally()`. // Preserve arity: the shim sees a 2-argument call when the caller supplied no // options, and a 3-argument call otherwise, mirroring the bundler rewrite so // mocks observe the same shape in local execution and in bundled workflows. -export function dispatchTriggerJob( +export function dispatchStartJob( name: string, args?: unknown, options?: StartJobFunctionOptions, @@ -95,16 +89,16 @@ export function dispatchTriggerJob( const workflow = requirePlatformWorkflow(); // oxlint-disable-next-line prefer-rest-params return arguments.length >= 3 - ? workflow.triggerJobFunction(name, args, options) - : workflow.triggerJobFunction(name, args); + ? workflow.startJobFunction(name, args, options) + : workflow.startJobFunction(name, args); } -// Accepts `unknown` because the SDK-side `.trigger()` accepts a wider options +// Accepts `unknown` because the SDK-side `.start()` accepts a wider options // shape than the platform surface (e.g. `authInvoker` may be a machine-user // name string that the bundler normalizes at build time). Local execution // forwards the value verbatim; only the bundled path enforces the platform // contract. -export function dispatchTriggerWorkflow( +export function dispatchStartWorkflow( name: string, args?: unknown, options?: { invoker?: unknown }, @@ -112,9 +106,9 @@ export function dispatchTriggerWorkflow( const workflow = requirePlatformWorkflow(); // oxlint-disable-next-line prefer-rest-params if (arguments.length < 3) { - return workflow.triggerWorkflow(name, args); + return workflow.startWorkflow(name, args); } - return workflow.triggerWorkflow( + return workflow.startWorkflow( name, args, options?.invoker === undefined diff --git a/packages/sdk/src/configure/services/workflow/workflow.ts b/packages/sdk/src/configure/services/workflow/workflow.ts index 9d8c8492b..83390c711 100644 --- a/packages/sdk/src/configure/services/workflow/workflow.ts +++ b/packages/sdk/src/configure/services/workflow/workflow.ts @@ -1,6 +1,6 @@ /* oxlint-disable typescript/no-explicit-any */ import { brandValue } from "#/utils/brand"; -import { dispatchTriggerWorkflow } from "./registry"; +import { dispatchStartWorkflow } from "./registry"; import type { MachineUserName } from "#/configure/types/machine-user"; import type { ConcurrencyPolicy, RetryPolicy } from "#/types/workflow.generated"; import type { WorkflowJob } from "./job"; @@ -21,8 +21,8 @@ export interface Workflow = WorkflowJob
[0], + start: ( + args: Parameters[0], options?: { invoker: MachineUserName }, ) => Promise; } @@ -35,8 +35,8 @@ interface WorkflowDefinition> { } /** - * Create a workflow definition that can be triggered via the Tailor SDK. - * In production, bundler transforms .trigger() calls to tailor.workflow.triggerWorkflow(). + * Create a workflow definition that can be started via the Tailor SDK. + * In production, the bundler rewrites `.start()` calls into direct platform workflow calls. * * The workflow MUST be the default export of the file. * All jobs referenced by the workflow MUST be named exports. @@ -48,7 +48,7 @@ interface WorkflowDefinition> { * export const processData = createWorkflowJob({ * name: "process-data", * body: (input: { id: string }) => { - * const data = fetchData.trigger({ id: input.id }); + * const data = fetchData.start({ id: input.id }); * return { data }; * }, * }); @@ -65,25 +65,25 @@ export function createWorkflow>( return brandValue( { ...config, - trigger: process.env.__TAILOR_PLATFORM_BUNDLE + start: process.env.__TAILOR_PLATFORM_BUNDLE ? async () => { throw new Error( - "workflow.trigger() is rewritten at build time and unavailable in the bundle", + "workflow.start() is rewritten at build time and unavailable in the bundle", ); } : // Preserve arity: use `arguments.length` (regular function, not arrow) so - // `.trigger(args, undefined)` is treated as "options passed" — matching + // `.start(args, undefined)` is treated as "options passed" — matching // the bundler rewrite, which forwards the literal `undefined` from the // AST as a third argument. Without this, local execution and bundled // workflows would hand mocks different call shapes. - async function trigger( - args: Parameters[0], + async function start( + args: Parameters[0], options?: { invoker: MachineUserName }, ) { // oxlint-disable-next-line prefer-rest-params return arguments.length >= 2 - ? await dispatchTriggerWorkflow(config.name, args, options) - : await dispatchTriggerWorkflow(config.name, args); + ? await dispatchStartWorkflow(config.name, args, options) + : await dispatchStartWorkflow(config.name, args); }, } as Workflow, "workflow", diff --git a/packages/sdk/src/parser/service/workflow/schema.ts b/packages/sdk/src/parser/service/workflow/schema.ts index e7834c044..1b643adce 100644 --- a/packages/sdk/src/parser/service/workflow/schema.ts +++ b/packages/sdk/src/parser/service/workflow/schema.ts @@ -3,7 +3,7 @@ import { functionSchema } from "../common"; export const WorkflowJobSchema = z.strictObject({ name: z.string().describe("Job name (must be unique across the project)"), - trigger: functionSchema.describe("Trigger function that initiates the job"), + start: functionSchema.describe("Start function that initiates the job"), body: functionSchema.describe("Job implementation function"), }); @@ -75,9 +75,7 @@ export const ExecutionPolicyKeySchema = z /^[a-z0-9][a-z0-9_:.-]{0,62}[a-z0-9*]$/, "Invalid execution policy key: must match [a-z0-9_:.-] (2-64 chars; must start with [a-z0-9] and end with [a-z0-9] or a trailing '*')", ) - .describe( - "Execution policy key passed to startJobFunction's (or its frozen alias triggerJobFunction's) executionPolicyKey option", - ); + .describe("Execution policy key passed to startJobFunction's executionPolicyKey option"); export const WorkflowJobFunctionExecutionPolicySchema = z.strictObject({ name: ExecutionPolicyNameSchema, diff --git a/packages/sdk/src/runtime/globals.test.ts b/packages/sdk/src/runtime/globals.test.ts index d7e7afeaa..3f3744b1e 100644 --- a/packages/sdk/src/runtime/globals.test.ts +++ b/packages/sdk/src/runtime/globals.test.ts @@ -26,24 +26,12 @@ describe("@tailor-platform/sdk/runtime/globals activates ambient globals", () => >(); }); - test("tailor.workflow.triggerWorkflow returns Promise", () => { - expectTypeOf>().toEqualTypeOf< - Promise - >(); - }); - test("tailor.workflow.startWorkflow returns Promise", () => { expectTypeOf>().toEqualTypeOf< Promise >(); }); - test("tailor.workflow.resumeWorkflow returns Promise", () => { - expectTypeOf>().toEqualTypeOf< - Promise - >(); - }); - test("tailor.workflow.resumeWorkflowExecution returns Promise", () => { expectTypeOf>().toEqualTypeOf< Promise diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index 3275186ac..a47a24bfd 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -50,7 +50,6 @@ import type { import type { Invoker as WorkflowInvoker, StartWorkflowOptions as WorkflowStartWorkflowOptions, - TriggerWorkflowOptions as WorkflowTriggerWorkflowOptions, } from "./workflow"; type TailorIdpClientConfig = IdpClientConfig; @@ -64,7 +63,6 @@ type TailorIdpUpdateUserInput = IdpUpdateUserInput; type TailorIdpUser = IdpUser; type TailorIdpUserQuery = IdpUserQuery; type TailorWorkflowInvoker = WorkflowInvoker; -type TailorWorkflowTriggerWorkflowOptions = WorkflowTriggerWorkflowOptions; type TailorWorkflowStartWorkflowOptions = WorkflowStartWorkflowOptions; declare global { @@ -98,8 +96,6 @@ declare global { namespace workflow { type Invoker = TailorWorkflowInvoker; type StartWorkflowOptions = TailorWorkflowStartWorkflowOptions; - /** @deprecated Use `StartWorkflowOptions` instead. */ - type TriggerWorkflowOptions = TailorWorkflowTriggerWorkflowOptions; } namespace context { diff --git a/packages/sdk/src/runtime/workflow.test.ts b/packages/sdk/src/runtime/workflow.test.ts index 623f0c790..824ffc973 100644 --- a/packages/sdk/src/runtime/workflow.test.ts +++ b/packages/sdk/src/runtime/workflow.test.ts @@ -2,12 +2,7 @@ * Tests for `@tailor-platform/sdk/runtime/workflow` typed wrappers. */ import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; -import { - workflow, - type ExecutionPolicyKey, - type TailorWorkflowAPI, - type TriggerWorkflowOptions, -} from "#/runtime/workflow"; +import { workflow, type ExecutionPolicyKey, type PlatformWorkflowAPI } from "#/runtime/workflow"; import { cleanupMocks, injectMocks, mockWorkflow } from "#/vitest/mock"; describe("@tailor-platform/sdk/runtime/workflow", () => { @@ -19,71 +14,68 @@ describe("@tailor-platform/sdk/runtime/workflow", () => { cleanupMocks(globalThis); }); - test("exposes the wrapper trigger options", () => { - expectTypeOf(workflow).toExtend(); - expectTypeOf[2]>().toEqualTypeOf< - TriggerWorkflowOptions | undefined - >(); + test("exposes the platform workflow API", () => { + expectTypeOf(workflow).toExtend(); }); - test("triggerWorkflow forwards args and returns Promise", async () => { + test("startWorkflow forwards args and returns Promise", async () => { using wf = mockWorkflow(); - wf.setTriggerHandler("exec-42"); + wf.setStartHandler("exec-42"); - const promise = workflow.triggerWorkflow("my-workflow", { a: 1 }); + const promise = workflow.startWorkflow("my-workflow", { a: 1 }); expectTypeOf(promise).toEqualTypeOf>(); await expect(promise).resolves.toBe("exec-42"); - expect(wf.triggerWorkflow.mock.calls).toEqual([["my-workflow", { a: 1 }]]); + expect(wf.startWorkflow.mock.calls).toEqual([["my-workflow", { a: 1 }]]); }); - test("triggerWorkflow forwards options", async () => { + test("startWorkflow forwards options", async () => { using wf = mockWorkflow(); - await workflow.triggerWorkflow( + await workflow.startWorkflow( "my-workflow", { a: 1 }, { - invoker: { namespace: "ns", machineUserName: "mu" }, + authInvoker: { namespace: "ns", machineUserName: "mu" }, }, ); - expect(wf.triggerWorkflow.mock.calls[0]?.[2]).toEqual({ + expect(wf.startWorkflow.mock.calls[0]?.[2]).toEqual({ authInvoker: { namespace: "ns", machineUserName: "mu" }, }); }); - test("resumeWorkflow forwards executionId and returns Promise", async () => { + test("resumeWorkflowExecution forwards executionId and returns Promise", async () => { using wf = mockWorkflow(); wf.setResumeHandler("exec-resumed"); - const promise = workflow.resumeWorkflow("exec-1"); + const promise = workflow.resumeWorkflowExecution("exec-1"); expectTypeOf(promise).toEqualTypeOf>(); await expect(promise).resolves.toBe("exec-resumed"); - expect(wf.resumeWorkflow.mock.calls).toEqual([["exec-1"]]); + expect(wf.resumeWorkflowExecution.mock.calls).toEqual([["exec-1"]]); }); - test("triggerJobFunction forwards and returns enqueued result", () => { + test("startJobFunction forwards and returns enqueued result", () => { using wf = mockWorkflow(); wf.enqueueResult({ ok: true }); - const result = workflow.triggerJobFunction("my-job", { id: 1 }); + const result = workflow.startJobFunction("my-job", { id: 1 }); expect(result).toEqual({ ok: true }); - expect(wf.triggeredJobs).toEqual([{ jobName: "my-job", args: { id: 1 } }]); + expect(wf.startedJobs).toEqual([{ jobName: "my-job", args: { id: 1 } }]); }); - test("triggerJobFunction forwards executionPolicyKey option", () => { + test("startJobFunction forwards executionPolicyKey option", () => { using wf = mockWorkflow(); wf.enqueueResult({ ok: true }); const policyKey = "premium" as ExecutionPolicyKey; - workflow.triggerJobFunction("my-job", { id: 1 }, { executionPolicyKey: policyKey }); + workflow.startJobFunction("my-job", { id: 1 }, { executionPolicyKey: policyKey }); - expect(wf.triggeredJobs).toEqual([ + expect(wf.startedJobs).toEqual([ { jobName: "my-job", args: { id: 1 }, options: { executionPolicyKey: "premium" } }, ]); - expect(wf.triggerJobFunction.mock.calls[0]?.[2]).toEqual({ executionPolicyKey: "premium" }); + expect(wf.startJobFunction.mock.calls[0]?.[2]).toEqual({ executionPolicyKey: "premium" }); }); test("wait records the call and returns the configured result", () => { @@ -107,54 +99,4 @@ describe("@tailor-platform/sdk/runtime/workflow", () => { expect(wf.resolve).toHaveBeenCalledTimes(1); expect(wf.resolveCalls).toEqual([{ executionId: "exec-1", key: "key-1" }]); }); - - describe("canonical aliases", () => { - test("startWorkflow behaves as an alias of triggerWorkflow", async () => { - using wf = mockWorkflow(); - wf.setTriggerHandler("exec-canonical"); - - const promise = workflow.startWorkflow("my-workflow", { a: 1 }); - - expectTypeOf(promise).toEqualTypeOf>(); - await expect(promise).resolves.toBe("exec-canonical"); - expect(wf.startWorkflow.mock.calls).toEqual([["my-workflow", { a: 1 }]]); - expect(wf.startWorkflow).toBe(wf.triggerWorkflow); - }); - - test("resumeWorkflowExecution behaves as an alias of resumeWorkflow", async () => { - using wf = mockWorkflow(); - wf.setResumeHandler("exec-canonical-resumed"); - - const promise = workflow.resumeWorkflowExecution("exec-1"); - - expectTypeOf(promise).toEqualTypeOf>(); - await expect(promise).resolves.toBe("exec-canonical-resumed"); - expect(wf.resumeWorkflowExecution.mock.calls).toEqual([["exec-1"]]); - expect(wf.resumeWorkflowExecution).toBe(wf.resumeWorkflow); - }); - - test("startJobFunction behaves as an alias of triggerJobFunction", () => { - using wf = mockWorkflow(); - wf.enqueueResult({ canonical: true }); - - const result = workflow.startJobFunction("my-job", { id: 1 }); - - expect(result).toEqual({ canonical: true }); - expect(wf.startJobFunction.mock.calls).toEqual([["my-job", { id: 1 }]]); - expect(wf.startJobFunction).toBe(wf.triggerJobFunction); - }); - - test("calls through canonical and legacy names share the same call log", () => { - using wf = mockWorkflow(); - wf.setJobHandler(() => ({ ok: true })); - - workflow.startJobFunction("job-a", { via: "canonical" }); - workflow.triggerJobFunction("job-b", { via: "legacy" }); - - expect(wf.triggeredJobs).toEqual([ - { jobName: "job-a", args: { via: "canonical" } }, - { jobName: "job-b", args: { via: "legacy" } }, - ]); - }); - }); }); diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index 46ee32394..e87fdfd14 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -4,12 +4,6 @@ * Thin typed wrapper around the platform-provided `tailor.workflow` runtime API. * At runtime this delegates to `globalThis.tailor.workflow`. Use `mockWorkflow` * from `@tailor-platform/sdk/vitest` to mock these calls in unit tests. - * - * The canonical names (`startWorkflow`, `startJobFunction`, - * `resumeWorkflowExecution`) mirror the public `tailor.v1` RPC vocabulary. - * The pre-alignment names (`triggerWorkflow`, `triggerJobFunction`, - * `resumeWorkflow`) are kept as frozen aliases that reference the same platform - * implementations, so existing code continues to work unchanged. * @example * import { workflow } from "@tailor-platform/sdk/runtime"; * @@ -35,21 +29,6 @@ export interface StartWorkflowOptions { authInvoker?: Invoker; } -/** - * Options for the legacy {@link triggerWorkflow} alias, using the SDK-facing - * `invoker` key that {@link triggerWorkflow} converts to `authInvoker` before - * calling the platform. - * @deprecated Use {@link startWorkflow} and {@link StartWorkflowOptions} instead. - */ -export interface TriggerWorkflowOptions { - /** Optional invoker to specify which machine user should execute the workflow */ - invoker?: Invoker; -} - -export interface PlatformTriggerWorkflowOptions { - authInvoker?: Invoker; -} - declare const executionPolicyKeyBrand: unique symbol; /** @@ -69,12 +48,6 @@ export interface StartJobFunctionOptions { executionPolicyKey?: ExecutionPolicyKey; } -/** - * Frozen alias for {@link StartJobFunctionOptions}. Kept for backward compatibility. - * @deprecated Use {@link StartJobFunctionOptions} instead. - */ -export type TriggerJobFunctionOptions = StartJobFunctionOptions; - /** * Platform API surface for `tailor.workflow`. Describes the shape the platform * runtime injects on `globalThis.tailor.workflow`. @@ -82,10 +55,6 @@ export type TriggerJobFunctionOptions = StartJobFunctionOptions; export interface PlatformWorkflowAPI { /** * Starts a workflow and returns its execution ID. - * - * Canonical name that mirrors the `tailor.v1` RPC vocabulary. - * {@link triggerWorkflow} is a frozen alias that resolves to the same - * platform implementation. * @param workflowName - Workflow name as defined in tailor.config * @param args - Arguments forwarded to the workflow's main job * @param options - Optional start options (e.g. `authInvoker`) @@ -93,39 +62,15 @@ export interface PlatformWorkflowAPI { */ startWorkflow(workflowName: string, args?: any, options?: StartWorkflowOptions): Promise; - /** - * Frozen alias for {@link startWorkflow}. Kept for backward compatibility. - * @deprecated Use {@link startWorkflow} instead. - */ - triggerWorkflow( - workflowName: string, - args?: any, - options?: PlatformTriggerWorkflowOptions, - ): Promise; - /** * Resumes a failed or pending-retry workflow execution and returns its execution ID. - * - * Canonical name that mirrors the `tailor.v1` RPC vocabulary. - * {@link resumeWorkflow} is a frozen alias that resolves to the same - * platform implementation. * @param executionId - The execution to resume * @returns The execution ID of the resumed workflow */ resumeWorkflowExecution(executionId: string): Promise; - /** - * Frozen alias for {@link resumeWorkflowExecution}. Kept for backward compatibility. - * @deprecated Use {@link resumeWorkflowExecution} instead. - */ - resumeWorkflow(executionId: string): Promise; - /** * Starts a job function and returns its result. - * - * Canonical name that mirrors the `tailor.v1` RPC vocabulary. - * {@link triggerJobFunction} is a frozen alias that resolves to the same - * platform implementation. * @param jobName - Job name as defined in the workflow * @param args - Arguments forwarded to the job * @param options - Optional start options (e.g. `executionPolicyKey`) @@ -133,12 +78,6 @@ export interface PlatformWorkflowAPI { */ startJobFunction(jobName: string, args?: any, options?: StartJobFunctionOptions): any; - /** - * Frozen alias for {@link startJobFunction}. Kept for backward compatibility. - * @deprecated Use {@link startJobFunction} instead. - */ - triggerJobFunction(jobName: string, args?: any, options?: TriggerJobFunctionOptions): any; - /** * Suspends the current workflow execution and waits for an external signal to resume. * @param key - Wait point key @@ -157,87 +96,33 @@ export interface PlatformWorkflowAPI { resolve(executionId: string, key: string, callback: (waitPayload: any) => any): Promise; } -/** Runtime wrapper API for workflow and job control. */ -export interface TailorWorkflowAPI extends Omit { - /** - * 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 SDK trigger options - * @returns The execution ID of the triggered workflow - */ - triggerWorkflow( - workflowName: string, - args?: any, - options?: TriggerWorkflowOptions, - ): Promise; -} - const api = (): PlatformWorkflowAPI => (globalThis as unknown as { tailor: { workflow: PlatformWorkflowAPI } }).tailor.workflow; /** - * See {@link TailorWorkflowAPI.startWorkflow}. - * @param args - Forwarded to {@link TailorWorkflowAPI.startWorkflow} + * See {@link PlatformWorkflowAPI.startWorkflow}. + * @param args - Forwarded to {@link PlatformWorkflowAPI.startWorkflow} * @returns The execution ID of the started workflow */ const startWorkflow: PlatformWorkflowAPI["startWorkflow"] = (...args) => api().startWorkflow(...args); /** - * Frozen alias for {@link startWorkflow}. Kept for backward compatibility. - * Converts the SDK-facing `invoker` option to the platform's `authInvoker`. - * @param workflowName - Workflow name as defined in tailor.config - * @param args - Arguments forwarded to the workflow's main job - * @param options - Optional SDK trigger options - * @returns The execution ID of the triggered workflow - * @deprecated Use {@link startWorkflow} instead. - */ -function triggerWorkflow( - workflowName: string, - args?: any, - options?: TriggerWorkflowOptions, -): Promise { - if (options?.invoker === undefined) { - return api().triggerWorkflow(workflowName, args); - } - return api().triggerWorkflow(workflowName, args, { authInvoker: options.invoker }); -} - -/** - * See {@link TailorWorkflowAPI.resumeWorkflowExecution}. - * @param args - Forwarded to {@link TailorWorkflowAPI.resumeWorkflowExecution} + * See {@link PlatformWorkflowAPI.resumeWorkflowExecution}. + * @param args - Forwarded to {@link PlatformWorkflowAPI.resumeWorkflowExecution} * @returns The execution ID of the resumed workflow */ const resumeWorkflowExecution: PlatformWorkflowAPI["resumeWorkflowExecution"] = (...args) => api().resumeWorkflowExecution(...args); /** - * Frozen alias for {@link resumeWorkflowExecution}. Kept for backward compatibility. - * @param args - Forwarded to {@link resumeWorkflowExecution} - * @returns The execution ID of the resumed workflow - * @deprecated Use {@link resumeWorkflowExecution} instead. - */ -const resumeWorkflow: PlatformWorkflowAPI["resumeWorkflow"] = (...args) => - api().resumeWorkflow(...args); - -/** - * See {@link TailorWorkflowAPI.startJobFunction}. - * @param args - Forwarded to {@link TailorWorkflowAPI.startJobFunction} + * See {@link PlatformWorkflowAPI.startJobFunction}. + * @param args - Forwarded to {@link PlatformWorkflowAPI.startJobFunction} * @returns The job's return value */ const startJobFunction: PlatformWorkflowAPI["startJobFunction"] = (...args) => api().startJobFunction(...args); -/** - * Frozen alias for {@link startJobFunction}. Kept for backward compatibility. - * @param args - Forwarded to {@link startJobFunction} - * @returns The job's return value - * @deprecated Use {@link startJobFunction} instead. - */ -const triggerJobFunction: PlatformWorkflowAPI["triggerJobFunction"] = (...args) => - api().triggerJobFunction(...args); - const wait: PlatformWorkflowAPI["wait"] = (...args) => api().wait(...args); const resolve: PlatformWorkflowAPI["resolve"] = (...args) => api().resolve(...args); @@ -245,11 +130,8 @@ const resolve: PlatformWorkflowAPI["resolve"] = (...args) => api().resolve(...ar /** Runtime wrapper namespace for `tailor.workflow`. */ export const workflow = { startWorkflow, - triggerWorkflow, resumeWorkflowExecution, - resumeWorkflow, startJobFunction, - triggerJobFunction, wait, resolve, -} as const satisfies TailorWorkflowAPI; +} as const satisfies PlatformWorkflowAPI; diff --git a/packages/sdk/src/types/workflow.generated.ts b/packages/sdk/src/types/workflow.generated.ts index 7c571ebb3..aeb6dfe06 100644 --- a/packages/sdk/src/types/workflow.generated.ts +++ b/packages/sdk/src/types/workflow.generated.ts @@ -3,8 +3,8 @@ export type WorkflowJob = { /** Job name (must be unique across the project) */ name: string; - /** Trigger function that initiates the job */ - trigger: Function; + /** Start function that initiates the job */ + start: Function; /** Job implementation function */ body: Function; }; @@ -35,7 +35,7 @@ export type ExecutionPolicyName = string; export type ExecutionPolicyNameInput = ExecutionPolicyName; /** - * Execution policy key passed to startJobFunction's (or its frozen alias triggerJobFunction's) executionPolicyKey option + * Execution policy key passed to startJobFunction's executionPolicyKey option */ export type ExecutionPolicyKey = string; export type ExecutionPolicyKeyInput = ExecutionPolicyKey; @@ -43,7 +43,7 @@ export type ExecutionPolicyKeyInput = ExecutionPolicyKey; export type WorkflowJobFunctionExecutionPolicy = { /** Workspace-unique execution policy name embedded in the resource TRN */ name: ExecutionPolicyName; - /** Execution policy key passed to startJobFunction's (or its frozen alias triggerJobFunction's) executionPolicyKey option */ + /** Execution policy key passed to startJobFunction's executionPolicyKey option */ key: ExecutionPolicyKey; /** Optional per-key concurrency cap for job function dispatches matching this policy */ concurrencyPolicy?: ConcurrencyPolicy; diff --git a/packages/sdk/src/utils/test/mock.ts b/packages/sdk/src/utils/test/mock.ts index 4d2233399..c8f37bee6 100644 --- a/packages/sdk/src/utils/test/mock.ts +++ b/packages/sdk/src/utils/test/mock.ts @@ -2,7 +2,7 @@ import * as path from "node:path"; import { pathToFileURL } from "node:url"; import type { ContextInvoker } from "#/runtime/context"; import type { TailorPrincipal } from "#/runtime/types"; -import type { TriggerJobFunctionOptions } from "#/runtime/workflow"; +import type { StartJobFunctionOptions } from "#/runtime/workflow"; type MainFunction = (args: Record) => unknown | Promise; type QueryResolver = (query: string, params: unknown[]) => unknown[]; @@ -27,10 +27,10 @@ interface TailordbGlobal { }; tailor?: { workflow: { - triggerJobFunction: ( + startJobFunction: ( jobName: string, args: unknown, - options?: TriggerJobFunctionOptions, + options?: StartJobFunctionOptions, ) => unknown; wait?: (key: string, payload?: unknown) => unknown; resolve?: ( @@ -54,7 +54,7 @@ interface TailorErrorsGlobal { TailorErrors?: new (errors: TailorErrorItem[]) => Error; } -const GlobalThis = globalThis as TailordbGlobal & TailorErrorsGlobal; +const GlobalThis = globalThis as unknown as TailordbGlobal & TailorErrorsGlobal; /** * Sets up a mock for `globalThis.tailordb.Client` used in bundled resolver/executor tests. @@ -99,17 +99,17 @@ export function setupTailordbMock(resolver: QueryResolver = () => []): { } /** - * Sets up a mock for `globalThis.tailor.workflow.triggerJobFunction` used in bundled workflow tests. + * Sets up a mock for `globalThis.tailor.workflow.startJobFunction` used in bundled workflow tests. * `wait`/`resolve` are stubbed to throw a helpful error directing to `mockWorkflow`, * so mistakenly calling wait without wait-point mocks produces a clear message instead of a TypeError. * @deprecated Use `mockWorkflow` from `@tailor-platform/sdk/vitest` with the `tailor-runtime` environment instead. - * @param handler - Function that handles triggered job calls and returns results. - * @returns Object containing an array of triggered jobs for assertions. + * @param handler - Function that handles started job calls and returns results. + * @returns Object containing an array of started jobs for assertions. */ export function setupWorkflowMock(handler: JobHandler): { - triggeredJobs: { jobName: string; args: unknown }[]; + startedJobs: { jobName: string; args: unknown }[]; } { - const triggeredJobs: { jobName: string; args: unknown }[] = []; + const startedJobs: { jobName: string; args: unknown }[] = []; GlobalThis.tailor = { ...GlobalThis.tailor, @@ -125,14 +125,14 @@ export function setupWorkflowMock(handler: JobHandler): { ); }, ...GlobalThis.tailor?.workflow, - triggerJobFunction: (jobName: string, args: unknown) => { - triggeredJobs.push({ jobName, args }); + startJobFunction: (jobName: string, args: unknown) => { + startedJobs.push({ jobName, args }); return handler(jobName, args); }, }, } as typeof GlobalThis.tailor; - return { triggeredJobs }; + return { startedJobs }; } /** @@ -179,8 +179,8 @@ export function setupTailorErrorsMock(): void { /** * Sets up mocks for `globalThis.tailor.workflow.wait` and `.resolve` used in bundled workflow tests. - * `triggerJobFunction` is stubbed to throw a helpful error directing to `setupWorkflowMock()`, - * so mistakenly triggering a job without job mocks produces a clear message instead of silently returning undefined. + * `startJobFunction` is stubbed to throw a helpful error directing to `setupWorkflowMock()`, + * so mistakenly starting a job without job mocks produces a clear message instead of silently returning undefined. * @deprecated Use `mockWorkflow` from `@tailor-platform/sdk/vitest` with the `tailor-runtime` environment instead. * `setWaitHandler` / `setResolveHandler` cover wait/resolve, and `waitCalls` / `resolveCalls` give the same assertion shape. * @param config - Optional handlers for wait and resolve calls. @@ -198,9 +198,9 @@ export function setupWaitPointMock(config?: { onWait?: WaitHandler; onResolve?: GlobalThis.tailor = { ...GlobalThis.tailor, workflow: { - triggerJobFunction: () => { + startJobFunction: () => { throw new Error( - "tailor.workflow.triggerJobFunction is not mocked. Use setupWorkflowMock() in tests.", + "tailor.workflow.startJobFunction is not mocked. Use setupWorkflowMock() in tests.", ); }, ...GlobalThis.tailor?.workflow, diff --git a/packages/sdk/src/vitest/globals.ts b/packages/sdk/src/vitest/globals.ts index 8af34c905..0080e807f 100644 --- a/packages/sdk/src/vitest/globals.ts +++ b/packages/sdk/src/vitest/globals.ts @@ -102,7 +102,7 @@ export function installPlatformGlobals(global: typeof globalThis): void { // Containers. Namespace mocks (secretmanager, …) are added to these by the // corresponding `xMock()` on acquisition. `workflow` carries a default runner - // so `.trigger()` runs the real job chain locally without `mockWorkflow()`; + // so `.start()` runs the real job chain locally without `mockWorkflow()`; // `mockWorkflow()` overlays and restores it. g.tailor = { context: { getInvoker: defaultGetInvoker }, diff --git a/packages/sdk/src/vitest/integration/should-pass.test.ts b/packages/sdk/src/vitest/integration/should-pass.test.ts index 8ac1c4477..3a9ec0ded 100644 --- a/packages/sdk/src/vitest/integration/should-pass.test.ts +++ b/packages/sdk/src/vitest/integration/should-pass.test.ts @@ -31,7 +31,7 @@ test("base platform globals are injected; namespace mocks install on acquire", ( expect(g.TailorDBFileError).toBeTypeOf("function"); // Namespace mocks are installed once the corresponding mock is acquired. expect(g.tailordb.Client).toBeTypeOf("function"); - expect(g.tailor.workflow.triggerJobFunction).toBeTypeOf("function"); + expect(g.tailor.workflow.startJobFunction).toBeTypeOf("function"); }); test("setup.ts removes performance global during test execution", () => { diff --git a/packages/sdk/src/vitest/mock-ergonomics.test.ts b/packages/sdk/src/vitest/mock-ergonomics.test.ts index 19e9109b5..dc853c0f3 100644 --- a/packages/sdk/src/vitest/mock-ergonomics.test.ts +++ b/packages/sdk/src/vitest/mock-ergonomics.test.ts @@ -152,11 +152,11 @@ describe("ergonomic runtime mocks", () => { job.mockResolvedValue({ customerId: "c-1", source: "mock" }); workflow.mockResolvedValue("execution-1"); - await expect(lookupCustomer.trigger({ customerId: "c-1" })).resolves.toEqual({ + await expect(lookupCustomer.start({ customerId: "c-1" })).resolves.toEqual({ customerId: "c-1", source: "mock", }); - await expect(customerWorkflow.trigger({ customerId: "c-1" })).resolves.toBe("execution-1"); + await expect(customerWorkflow.start({ customerId: "c-1" })).resolves.toBe("execution-1"); expect(job).toHaveBeenCalledWith({ customerId: "c-1" }); expect(workflow).toHaveBeenCalledWith({ customerId: "c-1" }); }); @@ -195,29 +195,25 @@ describe("ergonomic runtime mocks", () => { expect(innerWorkflow).not.toBe(outerWorkflow); expect(innerWaitPoint.wait).not.toBe(outerWaitPoint.wait); // A fresh inner scope does not inherit the outer scope's configured - // resolved value; the unconfigured trigger falls through to the real + // resolved value; the unconfigured start falls through to the real // dispatch, which throws without a low-level job handler. - expect(() => lookupCustomer.trigger({ customerId: "c-1" })).toThrow( - /No workflow job mock for/, - ); + expect(() => lookupCustomer.start({ customerId: "c-1" })).toThrow(/No workflow job mock for/); innerJob.mockResolvedValue({ customerId: "c-1", source: "inner" }); innerWorkflow.mockResolvedValue("inner-execution"); innerWaitPoint.wait.mockResolvedValue({ approved: false }); - await expect(lookupCustomer.trigger({ customerId: "c-1" })).resolves.toMatchObject({ + await expect(lookupCustomer.start({ customerId: "c-1" })).resolves.toMatchObject({ source: "inner", }); - await expect(customerWorkflow.trigger({ customerId: "c-1" })).resolves.toBe( - "inner-execution", - ); + await expect(customerWorkflow.start({ customerId: "c-1" })).resolves.toBe("inner-execution"); await expect(approval.wait({ message: "inner" })).resolves.toEqual({ approved: false }); } - await expect(lookupCustomer.trigger({ customerId: "c-1" })).resolves.toMatchObject({ + await expect(lookupCustomer.start({ customerId: "c-1" })).resolves.toMatchObject({ source: "outer", }); - await expect(customerWorkflow.trigger({ customerId: "c-1" })).resolves.toBe("outer-execution"); + await expect(customerWorkflow.start({ customerId: "c-1" })).resolves.toBe("outer-execution"); await expect(approval.wait({ message: "outer" })).resolves.toEqual({ approved: true }); }); @@ -449,7 +445,7 @@ describe("ergonomic runtime mocks", () => { wf.reset(); - expect(() => lookupCustomer.trigger({ customerId: "c-1" })).toThrow(/No workflow job mock for/); + expect(() => lookupCustomer.start({ customerId: "c-1" })).toThrow(/No workflow job mock for/); }); test("resets IdP and File mocks to their fallback behavior", async () => { diff --git a/packages/sdk/src/vitest/mock-types.test.ts b/packages/sdk/src/vitest/mock-types.test.ts index 6800e723e..a834648bf 100644 --- a/packages/sdk/src/vitest/mock-types.test.ts +++ b/packages/sdk/src/vitest/mock-types.test.ts @@ -26,24 +26,12 @@ describe("mock types match @tailor-platform/sdk/runtime/globals", () => { }); describe("tailor.workflow", () => { - test("triggerWorkflow returns Promise", () => { - expectTypeOf(() => tailor.workflow.triggerWorkflow("wf", {})).returns.toEqualTypeOf< - Promise - >(); - }); - test("startWorkflow returns Promise", () => { expectTypeOf(() => tailor.workflow.startWorkflow("wf", {})).returns.toEqualTypeOf< Promise >(); }); - test("resumeWorkflow returns Promise", () => { - expectTypeOf(() => tailor.workflow.resumeWorkflow("exec-1")).returns.toEqualTypeOf< - Promise - >(); - }); - test("resumeWorkflowExecution returns Promise", () => { expectTypeOf(() => tailor.workflow.resumeWorkflowExecution("exec-1")).returns.toEqualTypeOf< Promise diff --git a/packages/sdk/src/vitest/mock.test.ts b/packages/sdk/src/vitest/mock.test.ts index 167094086..213a7cbdf 100644 --- a/packages/sdk/src/vitest/mock.test.ts +++ b/packages/sdk/src/vitest/mock.test.ts @@ -147,10 +147,10 @@ describe("mock", () => { test("records triggered jobs even when no handler is configured", () => { using wf = mockWorkflow(); - const trigger = (globalThis as any).tailor.workflow.triggerJobFunction; + const trigger = (globalThis as any).tailor.workflow.startJobFunction; expect(() => trigger("my-job", { key: "value" })).toThrow(/No workflow job mock/); - expect(wf.triggeredJobs).toEqual([{ jobName: "my-job", args: { key: "value" } }]); + expect(wf.startedJobs).toEqual([{ jobName: "my-job", args: { key: "value" } }]); }); test("setJobHandler provides content-based responses", () => { @@ -160,7 +160,7 @@ describe("mock", () => { return null; }); - const trigger = (globalThis as any).tailor.workflow.triggerJobFunction; + const trigger = (globalThis as any).tailor.workflow.startJobFunction; const result = trigger("validate", {}); expect(result).toEqual({ valid: true }); @@ -170,7 +170,7 @@ describe("mock", () => { using wf = mockWorkflow(); wf.enqueueResults({ step: 1 }, { step: 2 }); - const trigger = (globalThis as any).tailor.workflow.triggerJobFunction; + const trigger = (globalThis as any).tailor.workflow.startJobFunction; expect(trigger("job1", {})).toEqual({ step: 1 }); expect(trigger("job2", {})).toEqual({ step: 2 }); }); @@ -180,19 +180,19 @@ describe("mock", () => { wf.setJobHandler(() => ({ fallback: true })); wf.enqueueResult({ queued: true }); - const trigger = (globalThis as any).tailor.workflow.triggerJobFunction; + const trigger = (globalThis as any).tailor.workflow.startJobFunction; expect(trigger("job1", {})).toEqual({ queued: true }); expect(trigger("job2", {})).toEqual({ fallback: true }); }); test("reset clears all state", () => { using wf = mockWorkflow(); - const trigger = (globalThis as any).tailor.workflow.triggerJobFunction; + const trigger = (globalThis as any).tailor.workflow.startJobFunction; expect(() => trigger("job", {})).toThrow(/No workflow job mock/); wf.reset(); - expect(wf.triggeredJobs).toHaveLength(0); + expect(wf.startedJobs).toHaveLength(0); }); test("setEnv exposes env to job bodies via runWorkflowLocally()", async () => { @@ -274,16 +274,16 @@ describe("mock", () => { }); describe("default workflow runtime (no mockWorkflow needed)", () => { - test("a job's .trigger() requires a configured workflow mock", () => { + test("a job's .start() requires a configured workflow mock", () => { const double = createWorkflowJob({ name: "default-runtime-double", body: (input: { n: number }) => ({ doubled: input.n * 2 }), }); - expect(() => double.trigger({ n: 21 })).toThrow(/No workflow job mock/); + expect(() => double.start({ n: 21 })).toThrow(/No workflow job mock/); }); - test("workflow.trigger() returns an execution id without running the main job", async () => { + test("workflow.start() returns an execution id without running the main job", async () => { let bodyRan = false; const main = createWorkflowJob({ name: "default-runtime-main-trigger", @@ -294,22 +294,18 @@ describe("mock", () => { }); const workflow = createWorkflow({ name: "default-runtime-trigger-wf", mainJob: main }); - await expect(workflow.trigger(undefined)).resolves.toBe( - "00000000-0000-4000-8000-000000000000", - ); + await expect(workflow.start(undefined)).resolves.toBe("00000000-0000-4000-8000-000000000000"); expect(bodyRan).toBe(false); }); - test("workflow.trigger() validates args in the default runtime", async () => { + test("workflow.start() validates args in the default runtime", async () => { const main = createWorkflowJob({ name: "default-runtime-trigger-args", body: (input: { when: string }) => ({ when: input.when }), }); const workflow = createWorkflow({ name: "default-runtime-trigger-args-wf", mainJob: main }); - await expect(workflow.trigger({ when: new Date() } as never)).rejects.toThrow( - /Date instance/, - ); + await expect(workflow.start({ when: new Date() } as never)).rejects.toThrow(/Date instance/); }); test("runWorkflowLocally() runs the whole chain", async () => { @@ -320,8 +316,8 @@ describe("mock", () => { const main = createWorkflowJob({ name: "default-runtime-main", body: async (input: { n: number }) => { - const a = inner.trigger({ n: input.n }); - const b = inner.trigger({ n: a.n }); + const a = inner.start({ n: input.n }); + const b = inner.start({ n: a.n }); return { total: b.n }; }, }); @@ -342,9 +338,9 @@ describe("mock", () => { const main = createWorkflowJob({ name: "default-runtime-mutation-main", body: () => { - const result = mutable.trigger(); + const result = mutable.start(); result.items.push("x"); - step.trigger(); + step.start(); return result; }, }); @@ -365,7 +361,7 @@ describe("mock", () => { name: "default-runtime-caught-main", body: () => { try { - return inner.trigger(); + return inner.start(); } catch { return { ok: false }; } @@ -392,7 +388,7 @@ describe("mock", () => { name: "default-runtime-failing-main", body: () => { try { - inner.trigger(); + inner.start(); return { handled: false, message: "" }; } catch (cause) { return { handled: true, message: (cause as Error).message }; @@ -423,7 +419,7 @@ describe("mock", () => { const main = createWorkflowJob({ name: "default-runtime-nested-workflow-main", body: async () => { - await innerWorkflow.trigger({ when: new Date() } as never); + await innerWorkflow.start({ when: new Date() } as never); return { ok: true }; }, }); @@ -833,19 +829,19 @@ describe("mock", () => { }); describe("mockWorkflow extended", () => { - test("records triggerWorkflow calls", async () => { + test("records startWorkflow calls", async () => { using wf = mockWorkflow(); - await (globalThis as any).tailor.workflow.triggerWorkflow("wf-1", { key: "val" }); - expect(wf.triggerWorkflow.mock.calls).toEqual([["wf-1", { key: "val" }]]); + await (globalThis as any).tailor.workflow.startWorkflow("wf-1", { key: "val" }); + expect(wf.startWorkflow.mock.calls).toEqual([["wf-1", { key: "val" }]]); }); test("preserves a forwarded third options arg even when undefined", async () => { using wf = mockWorkflow(); - await (globalThis as any).tailor.workflow.triggerWorkflow("wf-1", { key: "val" }, undefined); - expect(wf.triggerWorkflow.mock.calls).toEqual([["wf-1", { key: "val" }, undefined]]); + await (globalThis as any).tailor.workflow.startWorkflow("wf-1", { key: "val" }, undefined); + expect(wf.startWorkflow.mock.calls).toEqual([["wf-1", { key: "val" }, undefined]]); }); - test("createWorkflow().trigger(args) without options records a 2-arg call", async () => { + test("createWorkflow().start(args) without options records a 2-arg call", async () => { using wf = mockWorkflow(); const arityJob = createWorkflowJob({ name: "arity-workflow-main", @@ -853,12 +849,12 @@ describe("mock", () => { }); const arityWorkflow = createWorkflow({ name: "arity-workflow", mainJob: arityJob }); - await arityWorkflow.trigger(undefined); + await arityWorkflow.start(undefined); - expect(wf.triggerWorkflow.mock.calls).toEqual([["arity-workflow", undefined]]); + expect(wf.startWorkflow.mock.calls).toEqual([["arity-workflow", undefined]]); }); - test("createWorkflow().trigger(args, undefined) records a 3-arg call", async () => { + test("createWorkflow().start(args, undefined) records a 3-arg call", async () => { using wf = mockWorkflow(); const arityJob = createWorkflowJob({ name: "arity-workflow-main-3arg", @@ -866,28 +862,26 @@ describe("mock", () => { }); const arityWorkflow = createWorkflow({ name: "arity-workflow-3arg", mainJob: arityJob }); - await arityWorkflow.trigger(undefined, undefined); + await arityWorkflow.start(undefined, undefined); - expect(wf.triggerWorkflow.mock.calls).toEqual([ - ["arity-workflow-3arg", undefined, undefined], - ]); + expect(wf.startWorkflow.mock.calls).toEqual([["arity-workflow-3arg", undefined, undefined]]); }); - test("setTriggerHandler with string controls triggerWorkflow response", async () => { + test("setStartHandler with string controls startWorkflow response", async () => { using wf = mockWorkflow(); - wf.setTriggerHandler("exec-123"); - const result = await (globalThis as any).tailor.workflow.triggerWorkflow("wf"); + wf.setStartHandler("exec-123"); + const result = await (globalThis as any).tailor.workflow.startWorkflow("wf"); expect(result).toBe("exec-123"); }); - test("setTriggerHandler with function receives name/args/options", async () => { + test("setStartHandler with function receives name/args/options", async () => { using wf = mockWorkflow(); const seen: unknown[] = []; - wf.setTriggerHandler((name, args, options) => { + wf.setStartHandler((name, args, options) => { seen.push({ name, args, options }); return `exec-${name}`; }); - const result = await (globalThis as any).tailor.workflow.triggerWorkflow( + const result = await (globalThis as any).tailor.workflow.startWorkflow( "wf", { key: "val" }, { authInvoker: { namespace: "ns", machineUserName: "mu" } }, @@ -902,17 +896,19 @@ describe("mock", () => { ]); }); - test("resumeWorkflow echoes the executionId by default", async () => { + test("resumeWorkflowExecution echoes the executionId by default", async () => { using wf = mockWorkflow(); - const result = await (globalThis as any).tailor.workflow.resumeWorkflow("exec-42"); + const result = await (globalThis as any).tailor.workflow.resumeWorkflowExecution("exec-42"); expect(result).toBe("exec-42"); - expect(wf.resumeWorkflow.mock.calls).toEqual([["exec-42"]]); + expect(wf.resumeWorkflowExecution.mock.calls).toEqual([["exec-42"]]); }); - test("setResumeHandler with string controls resumeWorkflow response", async () => { + test("setResumeHandler with string controls resumeWorkflowExecution response", async () => { using wf = mockWorkflow(); wf.setResumeHandler("exec-resumed"); - const result = await (globalThis as any).tailor.workflow.resumeWorkflow("exec-original"); + const result = await (globalThis as any).tailor.workflow.resumeWorkflowExecution( + "exec-original", + ); expect(result).toBe("exec-resumed"); }); @@ -923,7 +919,7 @@ describe("mock", () => { seen.push(executionId); return `resumed:${executionId}`; }); - const result = await (globalThis as any).tailor.workflow.resumeWorkflow("exec-1"); + const result = await (globalThis as any).tailor.workflow.resumeWorkflowExecution("exec-1"); expect(result).toBe("resumed:exec-1"); expect(seen).toEqual(["exec-1"]); }); @@ -979,42 +975,6 @@ describe("mock", () => { expect(callbackRan).toBe(false); expect(wf.resolveCalls).toEqual([{ executionId: "exec-1", key: "approval" }]); }); - - describe("canonical aliases", () => { - test("startJobFunction and triggerJobFunction share the same call log", () => { - using wf = mockWorkflow(); - wf.setJobHandler(() => undefined); - const start = (globalThis as any).tailor.workflow.startJobFunction; - const trigger = (globalThis as any).tailor.workflow.triggerJobFunction; - - start("job-a", { via: "canonical" }); - trigger("job-b", { via: "legacy" }); - - expect(wf.triggeredJobs).toEqual([ - { jobName: "job-a", args: { via: "canonical" } }, - { jobName: "job-b", args: { via: "legacy" } }, - ]); - expect(wf.startJobFunction).toBe(wf.triggerJobFunction); - }); - - test("startWorkflow honors setTriggerHandler", async () => { - using wf = mockWorkflow(); - wf.setTriggerHandler("exec-canonical"); - const result = await (globalThis as any).tailor.workflow.startWorkflow("wf", {}); - expect(result).toBe("exec-canonical"); - expect(wf.startWorkflow.mock.calls).toEqual([["wf", {}]]); - expect(wf.startWorkflow).toBe(wf.triggerWorkflow); - }); - - test("resumeWorkflowExecution honors setResumeHandler", async () => { - using wf = mockWorkflow(); - wf.setResumeHandler("exec-canonical-resumed"); - const result = await (globalThis as any).tailor.workflow.resumeWorkflowExecution("exec-1"); - expect(result).toBe("exec-canonical-resumed"); - expect(wf.resumeWorkflowExecution.mock.calls).toEqual([["exec-1"]]); - expect(wf.resumeWorkflowExecution).toBe(wf.resumeWorkflow); - }); - }); }); describe("injectMocks / cleanupMocks (base platform globals)", () => { @@ -1038,11 +998,11 @@ describe("mock", () => { test("a mock overlays the default workflow runner and dispose restores it", () => { const base = (globalThis as any).tailor.workflow; - expect(base.triggerJobFunction).toBeTypeOf("function"); + expect(base.startJobFunction).toBeTypeOf("function"); { using _wf = mockWorkflow(); expect((globalThis as any).tailor.workflow).not.toBe(base); - expect((globalThis as any).tailor.workflow.triggerJobFunction).toBeTypeOf("function"); + expect((globalThis as any).tailor.workflow.startJobFunction).toBeTypeOf("function"); } expect((globalThis as any).tailor.workflow).toBe(base); }); diff --git a/packages/sdk/src/vitest/mocks/workflow.ts b/packages/sdk/src/vitest/mocks/workflow.ts index 7498b2cc8..6b8f07f14 100644 --- a/packages/sdk/src/vitest/mocks/workflow.ts +++ b/packages/sdk/src/vitest/mocks/workflow.ts @@ -1,5 +1,5 @@ import { type Mock, vi } from "vitest"; -import { TRIGGER_DEFAULT } from "#/configure/services/workflow/registry"; +import { START_DEFAULT } from "#/configure/services/workflow/registry"; import { platformSerialize } from "#/utils/test/platform-serialize"; import { clearWorkflowTestEnv, @@ -15,7 +15,7 @@ import type { TailorEnv } from "../../runtime/types"; type JobHandler = (jobName: string, args: unknown, options?: StartJobFunctionOptions) => unknown; -type TriggerHandlerFn = ( +type StartHandlerFn = ( workflowName: string, args: unknown, options?: StartWorkflowOptions, @@ -37,7 +37,7 @@ type SetWaitHandler = { (handler: unknown): void; }; -interface TriggeredJob { +interface StartedJob { jobName: string; args: unknown; options?: StartJobFunctionOptions; @@ -50,13 +50,13 @@ interface ScopedMock { } type WaitPayload = [Payload] extends [undefined] ? undefined : Payload; -type TriggerProcedure = (...args: never[]) => unknown; +type ProcedureFn = (...args: never[]) => unknown; // `vi.spyOn` reuses an existing spy for the same property. Keep the base // procedure separately so nested mockWorkflow scopes get independent mocks // while disposal can still restore the immediately preceding scope. -const originalProcedures = new WeakMap(); +const originalProcedures = new WeakMap(); -function replaceProcedure( +function replaceProcedure( target: object, key: string, current: Procedure, @@ -69,7 +69,7 @@ function replaceProcedure( return Reflect.apply(original, this, args) as ReturnType; }; const mock = vi.fn(defaultImplementation) as unknown as Mock; - originalProcedures.set(mock as TriggerProcedure, original); + originalProcedures.set(mock as ProcedureFn, original); const record = target as Record; record[key] = mock; @@ -86,10 +86,10 @@ function replaceProcedure( }; } -function replaceTrigger(definition: { - trigger: Trigger; -}): { mock: Mock; scoped: ScopedMock } { - return replaceProcedure(definition, "trigger", definition.trigger); +function replaceStart(definition: { + start: Start; +}): { mock: Mock; scoped: ScopedMock } { + return replaceProcedure(definition, "start", definition.start); } // --------------------------------------------------------------------------- @@ -99,17 +99,12 @@ function replaceTrigger(definition: { /** * Acquire a disposable mock for workflow operations (`tailor.workflow`). * Restored on dispose. - * - * Canonical names (`startWorkflow`, `startJobFunction`, `resumeWorkflowExecution`) - * and their frozen aliases (`triggerWorkflow`, `triggerJobFunction`, `resumeWorkflow`) - * share the same underlying `vi.fn`, so calls through either name are recorded - * once and handlers configured on either name apply to both. * @returns Disposable workflow mock control object * @example * ```typescript * import { mockWorkflow } from "@tailor-platform/sdk/vitest"; * - * test("job trigger", async () => { + * test("job start", async () => { * using wf = mockWorkflow(); * const job = wf.job(validateOrder); * job.mockResolvedValue({ valid: true }); @@ -127,7 +122,7 @@ export function mockWorkflow() { const waitPointMocks = new Map(); const scopedMocks = new Set(); - const defaultTriggerJob = ( + const defaultStartJob = ( jobName: string, _args?: unknown, _options?: StartJobFunctionOptions, @@ -136,20 +131,21 @@ export function mockWorkflow() { `No workflow job mock for "${jobName}". Call mockWorkflow().setJobHandler(...) or enqueueResult(...), or use runWorkflowLocally() for local workflow execution.`, ); }; - const defaultTriggerWorkflow = async ( + const defaultStartWorkflow = async ( _workflowName: string, _args?: unknown, _options?: StartWorkflowOptions, ): Promise => { - return TRIGGER_DEFAULT; + return START_DEFAULT; }; - const defaultResumeWorkflow = async (executionId: string): Promise => executionId; + const defaultResumeWorkflowExecution = async (executionId: string): Promise => + executionId; // Inner vi.fns hold the overridable behavior + call recording; the installed // shims below cross the platform JSON boundary (serialize args + results) once. - const triggerJobFunction = vi.fn(defaultTriggerJob); - const triggerWorkflow = vi.fn(defaultTriggerWorkflow); - const resumeWorkflow = vi.fn(defaultResumeWorkflow); + const startJobFunction = vi.fn(defaultStartJob); + const startWorkflow = vi.fn(defaultStartWorkflow); + const resumeWorkflowExecution = vi.fn(defaultResumeWorkflowExecution); const wait = vi.fn((_key: string, _payload?: unknown): unknown => null); const resolve = vi.fn( async ( @@ -160,30 +156,25 @@ export function mockWorkflow() { ); // Preserve arity: recording `undefined` as the third element only when the - // caller supplied it, mirroring `.triggerJobFunction(name, args, options)`. + // caller supplied it, mirroring `.startJobFunction(name, args, options)`. const jobFunctionShim = (...call: [string, unknown?, StartJobFunctionOptions?]) => { const out = call.length >= 3 - ? triggerJobFunction(call[0], platformSerialize(call[1]), call[2]) - : triggerJobFunction(call[0], platformSerialize(call[1])); + ? startJobFunction(call[0], platformSerialize(call[1]), call[2]) + : startJobFunction(call[0], platformSerialize(call[1])); return out instanceof Promise ? out.then((v) => platformSerialize(v)) : platformSerialize(out); }; // Preserve arity so a forwarded third `options` arg — even `undefined` — is - // recorded, matching the real `.trigger(args, options)` call shape. + // recorded, matching the real `.start(args, options)` call shape. const workflowShim = (...call: [string, unknown?, StartWorkflowOptions?]) => call.length >= 3 - ? triggerWorkflow(call[0], platformSerialize(call[1]), call[2]) - : triggerWorkflow(call[0], platformSerialize(call[1])); - const resumeShim = (executionId: string) => resumeWorkflow(executionId); + ? startWorkflow(call[0], platformSerialize(call[1]), call[2]) + : startWorkflow(call[0], platformSerialize(call[1])); + const resumeShim = (executionId: string) => resumeWorkflowExecution(executionId); root.workflow = { - // Canonical and frozen alias names share a single shim so calls through - // either name are recorded on the same underlying vi.fn. startJobFunction: jobFunctionShim, - triggerJobFunction: jobFunctionShim, startWorkflow: workflowShim, - triggerWorkflow: workflowShim, resumeWorkflowExecution: resumeShim, - resumeWorkflow: resumeShim, wait: (key: string, payload?: unknown) => wait(key, platformSerialize(payload)), resolve: (executionId: string, key: string, callback: (payload: unknown) => unknown) => resolve(executionId, key, (payload: unknown) => { @@ -196,63 +187,47 @@ export function mockWorkflow() { const facade = { /** The `startJobFunction` `vi.fn`. */ - startJobFunction: triggerJobFunction, - /** - * Frozen alias of `startJobFunction` (same `vi.fn` reference). - * @deprecated Use `startJobFunction` instead. - */ - triggerJobFunction, + startJobFunction, /** The `startWorkflow` `vi.fn`. */ - startWorkflow: triggerWorkflow, - /** - * Frozen alias of `startWorkflow` (same `vi.fn` reference). - * @deprecated Use `startWorkflow` instead. - */ - triggerWorkflow, + startWorkflow, /** The `resumeWorkflowExecution` `vi.fn`. */ - resumeWorkflowExecution: resumeWorkflow, - /** - * Frozen alias of `resumeWorkflowExecution` (same `vi.fn` reference). - * @deprecated Use `resumeWorkflowExecution` instead. - */ - resumeWorkflow, + resumeWorkflowExecution, /** The `wait` `vi.fn`. */ wait, /** The `resolve` `vi.fn`. */ resolve, /** - * Get a stable, typed mock for a workflow job's `trigger` method. - * The real trigger behavior is used until an implementation or result is configured. + * Get a stable, typed mock for a workflow job's `start` method. + * The real start behavior is used until an implementation or result is configured. * @param definition - Workflow job definition to mock - * @returns Typed `trigger` mock for the definition + * @returns Typed `start` mock for the definition */ job( definition: WorkflowJob, - ): Mock["trigger"]> { + ): Mock["start"]> { const existing = jobSpies.get(definition); if (existing) { - return existing as Mock["trigger"]>; + return existing as Mock["start"]>; } - const { mock, scoped } = - replaceTrigger["trigger"]>(definition); + const { mock, scoped } = replaceStart["start"]>(definition); scopedMocks.add(scoped); jobSpies.set(definition, mock); return mock; }, /** - * Get a stable, typed mock for a workflow definition's `trigger` method. - * The real trigger behavior is used until an implementation or result is configured. + * Get a stable, typed mock for a workflow definition's `start` method. + * The real start behavior is used until an implementation or result is configured. * @param definition - Workflow definition to mock - * @returns Typed `trigger` mock for the definition + * @returns Typed `start` mock for the definition */ - workflow(definition: Definition): Mock { + workflow(definition: Definition): Mock { const existing = workflowSpies.get(definition); - if (existing) return existing as Mock; + if (existing) return existing as Mock; - const { mock, scoped } = replaceTrigger(definition); + const { mock, scoped } = replaceStart(definition); workflowSpies.set(definition, mock); scopedMocks.add(scoped); return mock; @@ -305,34 +280,34 @@ export function mockWorkflow() { * @param handler - Function returning a result for a job name, args, and options */ setJobHandler(handler: JobHandler): void { - triggerJobFunction.mockImplementation((name, args, options) => handler(name, args, options)); + startJobFunction.mockImplementation((name, args, options) => handler(name, args, options)); }, /** - * Enqueue a single result for the next `triggerJobFunction` call (FIFO; + * Enqueue a single result for the next `startJobFunction` call (FIFO; * takes priority over `setJobHandler`). * @param result - Result to return from the next call */ enqueueResult(result: unknown): void { - triggerJobFunction.mockImplementationOnce(() => result); + startJobFunction.mockImplementationOnce(() => result); }, /** - * Enqueue results for multiple subsequent `triggerJobFunction` calls (FIFO). + * Enqueue results for multiple subsequent `startJobFunction` calls (FIFO). * @param results - Results to enqueue, one per upcoming call */ enqueueResults(...results: unknown[]): void { for (const result of results) { - triggerJobFunction.mockImplementationOnce(() => result); + startJobFunction.mockImplementationOnce(() => result); } }, /** - * All jobs triggered via `triggerJobFunction`, in order. - * @returns Triggered jobs array + * All jobs started via `startJobFunction`, in order. + * @returns Started jobs array */ - get triggeredJobs(): TriggeredJob[] { - return triggerJobFunction.mock.calls.map(([jobName, args, options]) => ({ + get startedJobs(): StartedJob[] { + return startJobFunction.mock.calls.map(([jobName, args, options]) => ({ jobName: jobName as string, args, ...(options !== undefined && { options: options as StartJobFunctionOptions }), @@ -340,12 +315,12 @@ export function mockWorkflow() { }, /** - * Configure what `triggerWorkflow` returns. Pass a string (same id every + * Configure what `startWorkflow` returns. Pass a string (same id every * call) or `(name, args, options) => string`. Default: a placeholder UUID. * @param handler - Static execution ID or a function returning one */ - setTriggerHandler(handler: string | TriggerHandlerFn): void { - triggerWorkflow.mockImplementation( + setStartHandler(handler: string | StartHandlerFn): void { + startWorkflow.mockImplementation( typeof handler === "function" ? async (name, args, options) => handler(name, args, options) : async () => handler, @@ -353,12 +328,12 @@ export function mockWorkflow() { }, /** - * Configure what `resumeWorkflow` returns. Pass a string (same id every - * call) or `(executionId) => string`. Default: echoes the input executionId. + * Configure what `resumeWorkflowExecution` returns. Pass a string (same id + * every call) or `(executionId) => string`. Default: echoes the input executionId. * @param handler - Static execution ID or a function returning one */ setResumeHandler(handler: string | ResumeHandlerFn): void { - resumeWorkflow.mockImplementation( + resumeWorkflowExecution.mockImplementation( typeof handler === "function" ? async (executionId) => handler(executionId) : async () => handler, @@ -379,7 +354,7 @@ export function mockWorkflow() { }) as SetWaitHandler, /** - * Set the `env` passed to job bodies invoked via `createWorkflowJob().trigger()`. + * Set the `env` passed to job bodies invoked via `createWorkflowJob().start()`. * Cleared on dispose / reset. * @param env - Env passed to job bodies. */ @@ -419,9 +394,9 @@ export function mockWorkflow() { /** Clear recorded calls while preserving configured responses. */ clear(): void { - triggerJobFunction.mockClear(); - triggerWorkflow.mockClear(); - resumeWorkflow.mockClear(); + startJobFunction.mockClear(); + startWorkflow.mockClear(); + resumeWorkflowExecution.mockClear(); wait.mockClear(); resolve.mockClear(); for (const mock of scopedMocks) mock.mockClear(); @@ -429,12 +404,12 @@ export function mockWorkflow() { /** Reset all workflow responses and recorded calls (keeps the mock installed). */ reset(): void { - triggerJobFunction.mockReset(); - triggerJobFunction.mockImplementation(defaultTriggerJob); - triggerWorkflow.mockReset(); - triggerWorkflow.mockImplementation(defaultTriggerWorkflow); - resumeWorkflow.mockReset(); - resumeWorkflow.mockImplementation(defaultResumeWorkflow); + startJobFunction.mockReset(); + startJobFunction.mockImplementation(defaultStartJob); + startWorkflow.mockReset(); + startWorkflow.mockImplementation(defaultStartWorkflow); + resumeWorkflowExecution.mockReset(); + resumeWorkflowExecution.mockImplementation(defaultResumeWorkflowExecution); wait.mockReset(); wait.mockImplementation(() => null); resolve.mockReset(); diff --git a/packages/sdk/src/vitest/workflow-local.ts b/packages/sdk/src/vitest/workflow-local.ts index 5658e6851..d2ba58164 100644 --- a/packages/sdk/src/vitest/workflow-local.ts +++ b/packages/sdk/src/vitest/workflow-local.ts @@ -1,5 +1,5 @@ /* oxlint-disable typescript/no-explicit-any */ -import { TRIGGER_DEFAULT, getRegisteredJob } from "../configure/services/workflow/registry"; +import { START_DEFAULT, getRegisteredJob } from "../configure/services/workflow/registry"; import { buildJobContext, clearWorkflowTestEnv, @@ -25,7 +25,7 @@ type GlobalWithTailor = { }; }; -type TriggerRecord = { +type StartRecord = { jobName: string; args: unknown; } & ( @@ -40,12 +40,12 @@ type TriggerRecord = { ); interface LocalExecution { - records: TriggerRecord[]; + records: StartRecord[]; cursor: number; - pending?: PendingTrigger; + pending?: PendingStart; } -class PendingTrigger { +class PendingStart { constructor( readonly jobName: string, readonly args: unknown, @@ -58,9 +58,9 @@ export interface RunWorkflowLocallyOptions { } /** - * Run a workflow's main job and dependent job triggers locally with real job bodies. + * Run a workflow's main job and dependent job starts locally with real job bodies. * - * Use this for local full-chain workflow tests. Regular `.trigger()` calls + * Use this for local full-chain workflow tests. Regular `.start()` calls * delegate to the platform workflow runtime and should be mocked with * `mockWorkflow()` when you are not intentionally running the local chain. * @param workflow - Workflow definition to run @@ -110,7 +110,7 @@ export async function runWorkflowLocally( root.tailor = { ...previousTailor, - workflow: createLocalWorkflowRuntime(previousTailor?.workflow, runner.triggerJobFunction), + workflow: createLocalWorkflowRuntime(previousTailor?.workflow, runner.startJobFunction), }; try { @@ -134,14 +134,14 @@ export async function runWorkflowLocally( function createLocalJobRunner(): { runJob: (name: string, args?: unknown) => Promise; - triggerJobFunction: (name: string, args?: unknown) => unknown; + startJobFunction: (name: string, args?: unknown) => unknown; } { let activeExecution: LocalExecution | undefined; - const triggerJobFunction = (jobName: string, args?: unknown): unknown => { + const startJobFunction = (jobName: string, args?: unknown): unknown => { if (!activeExecution) { throw new Error( - `Cannot trigger workflow job "${jobName}" outside runWorkflowLocally() job execution.`, + `Cannot start workflow job "${jobName}" outside runWorkflowLocally() job execution.`, ); } if (activeExecution.pending) { @@ -154,14 +154,14 @@ function createLocalJobRunner(): { const cached = activeExecution.records[index]; if (cached) { - assertSameTrigger(cached, jobName, serializedArgs); + assertSameStart(cached, jobName, serializedArgs); if (cached.status === "rejected") { throw cached.error; } return platformSerialize(cached.result); } - const pending = new PendingTrigger(jobName, serializedArgs); + const pending = new PendingStart(jobName, serializedArgs); activeExecution.pending = pending; throw pending; }; @@ -172,7 +172,7 @@ function createLocalJobRunner(): { return null; } - const records: TriggerRecord[] = []; + const records: StartRecord[] = []; for (;;) { const execution: LocalExecution = { records, cursor: 0 }; @@ -182,19 +182,19 @@ function createLocalJobRunner(): { try { const out = await body(platformSerialize(args), buildJobContext()); if (execution.pending) { - await settlePendingTrigger(records, execution.pending, runJob); + await settlePendingStart(records, execution.pending, runJob); continue; } if (execution.cursor !== records.length) { throw new Error( - `Workflow job trigger sequence changed while replaying "${name}". Expected ${records.length} trigger(s), but replay reached ${execution.cursor}.`, + `Workflow job start sequence changed while replaying "${name}". Expected ${records.length} start(s), but replay reached ${execution.cursor}.`, ); } return platformSerialize(out); } catch (cause) { - const pending = cause instanceof PendingTrigger ? cause : execution.pending; + const pending = cause instanceof PendingStart ? cause : execution.pending; if (pending) { - await settlePendingTrigger(records, pending, runJob); + await settlePendingStart(records, pending, runJob); continue; } throw cause; @@ -204,12 +204,12 @@ function createLocalJobRunner(): { } }; - return { runJob, triggerJobFunction }; + return { runJob, startJobFunction }; } -async function settlePendingTrigger( - records: TriggerRecord[], - pending: PendingTrigger, +async function settlePendingStart( + records: StartRecord[], + pending: PendingStart, runJob: (name: string, args?: unknown) => Promise, ): Promise { try { @@ -229,26 +229,26 @@ async function settlePendingTrigger( } } -function assertSameTrigger(record: TriggerRecord, jobName: string, args: unknown): void { +function assertSameStart(record: StartRecord, jobName: string, args: unknown): void { if (record.jobName === jobName && JSON.stringify(record.args) === JSON.stringify(args)) { return; } throw new Error( - `Workflow job trigger sequence changed while replaying. Expected ${record.jobName}(${JSON.stringify(record.args)}), but got ${jobName}(${JSON.stringify(args)}).`, + `Workflow job start sequence changed while replaying. Expected ${record.jobName}(${JSON.stringify(record.args)}), but got ${jobName}(${JSON.stringify(args)}).`, ); } function createLocalWorkflowRuntime( previous: PlatformWorkflowAPI | undefined, - triggerJobFunction: (name: string, args?: unknown) => unknown, + startJobFunction: (name: string, args?: unknown) => unknown, ): PlatformWorkflowAPI { const startWorkflow: PlatformWorkflowAPI["startWorkflow"] = async (name, args, options) => { if (previous) { return await previous.startWorkflow(name, args, options); } platformSerialize(args); - return TRIGGER_DEFAULT; + return START_DEFAULT; }; const resumeWorkflowExecution: PlatformWorkflowAPI["resumeWorkflowExecution"] = async ( executionId, @@ -260,22 +260,9 @@ function createLocalWorkflowRuntime( }; return { - triggerJobFunction, - startJobFunction: triggerJobFunction, + startJobFunction, startWorkflow, - triggerWorkflow: async (name, args, options) => { - if (previous) { - return await previous.triggerWorkflow(name, args, options); - } - return await startWorkflow(name, args); - }, resumeWorkflowExecution, - resumeWorkflow: async (executionId) => { - if (previous) { - return await previous.resumeWorkflow(executionId); - } - return await resumeWorkflowExecution(executionId); - }, wait: (key, payload) => { if (previous) { return previous.wait(key, payload); diff --git a/packages/sdk/src/vitest/workflow-runtime.ts b/packages/sdk/src/vitest/workflow-runtime.ts index e79caef61..28ef891a9 100644 --- a/packages/sdk/src/vitest/workflow-runtime.ts +++ b/packages/sdk/src/vitest/workflow-runtime.ts @@ -1,21 +1,14 @@ // Default `tailor.workflow` runner installed by the `tailor-runtime` environment. // Must stay free of `vitest` (`vi`): it loads via `./globals` in the environment // realm where `vi` is unavailable, hence relative imports only (no `@/` alias). -import { TRIGGER_DEFAULT } from "../configure/services/workflow/registry"; +import { START_DEFAULT } from "../configure/services/workflow/registry"; import { platformSerialize } from "../utils/test/platform-serialize"; import type { StartJobFunctionOptions, StartWorkflowOptions } from "../runtime/workflow"; export interface DefaultWorkflowRuntime { startJobFunction: (name: string, args?: unknown, options?: StartJobFunctionOptions) => unknown; - triggerJobFunction: (name: string, args?: unknown, options?: StartJobFunctionOptions) => unknown; startWorkflow: (name: string, args?: unknown, options?: StartWorkflowOptions) => Promise; - triggerWorkflow: ( - name: string, - args?: unknown, - options?: StartWorkflowOptions, - ) => Promise; resumeWorkflowExecution: (executionId: string) => Promise; - resumeWorkflow: (executionId: string) => Promise; wait: (key: string, payload?: unknown) => unknown; resolve: ( executionId: string, @@ -32,18 +25,15 @@ export function createDefaultWorkflowRuntime(): DefaultWorkflowRuntime { }; const startWorkflow: DefaultWorkflowRuntime["startWorkflow"] = async (_name, args) => { platformSerialize(args); - return TRIGGER_DEFAULT; + return START_DEFAULT; }; const resumeWorkflowExecution: DefaultWorkflowRuntime["resumeWorkflowExecution"] = async ( executionId, ) => executionId; return { startJobFunction, - triggerJobFunction: startJobFunction, startWorkflow, - triggerWorkflow: startWorkflow, resumeWorkflowExecution, - resumeWorkflow: resumeWorkflowExecution, wait: (key: string): unknown => { throw new Error( `No wait handler for "${key}". Acquire mockWorkflow() and call setWaitHandler(...).`, From 6bc5b578be441c7cf4e44d33dcf1a00371b811c2 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 15 Jul 2026 23:01:18 +0900 Subject: [PATCH 568/618] ci(link-check): exclude the not-yet-merged startWorkflow.ts example link `docs/services/workflow.md` links to `example/resolvers/startWorkflow.ts` on `main`, but that file only exists on the `v2` branch until it merges. Exclude the URL from lychee's link check so this known, temporary, non-blocking gap doesn't fail CI; it self-resolves once `v2` lands on `main`. --- .github/workflows/link-check.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml index 35f83e231..58c522747 100644 --- a/.github/workflows/link-check.yml +++ b/.github/workflows/link-check.yml @@ -34,6 +34,7 @@ jobs: --no-progress --include-fragments --exclude 'mailto:' + --exclude 'github\.com/tailor-platform/sdk/blob/main/example/resolvers/startWorkflow\.ts' packages/sdk/docs/**/*.md fail: true From d5bdf4e4a36962ec0f9f9fe56a90cae404e393aa Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 15 Jul 2026 23:17:42 +0900 Subject: [PATCH 569/618] fix(workflow): address Copilot review findings on the start() rename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename the internal `getTriggerCallInfo`/`TriggerCallInfo` AST helper to `getStartCallInfo`/`StartCallInfo`: it now detects `.start(...)` calls, and its JSDoc already said so, so the old name was misleading next to it. - Fix `packages/sdk/src/vitest/globals.ts`'s comment on the default workflow runtime: it doesn't run the real job chain locally (that's `runWorkflowLocally()`) — job/wait/resolve calls throw a helpful error by default, and workflow starts return a placeholder execution id. - Fix "in separated job" -> "in a separate job" in docs/services/workflow.md. --- packages/sdk/docs/services/workflow.md | 4 ++-- packages/sdk/src/cli/services/workflow/ast-utils.ts | 6 +++--- .../sdk/src/cli/services/workflow/trigger-transformer.ts | 8 ++++---- packages/sdk/src/vitest/globals.ts | 7 ++++--- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/sdk/docs/services/workflow.md b/packages/sdk/docs/services/workflow.md index 466150ffc..1760a4f50 100644 --- a/packages/sdk/docs/services/workflow.md +++ b/packages/sdk/docs/services/workflow.md @@ -139,7 +139,7 @@ for (const region of regions) { // ❌ Bad: non-deterministic — argument changes between executions processJob.start({ timestamp: Date.now() }); -// ✅ OK: call Date.now() in separated job +// ✅ OK: call Date.now() in a separate job const timestamp = timestampJob.start(); processJob.start({ timestamp }); ``` @@ -151,7 +151,7 @@ for (const item of items) { processItem.start({ id: item.id }); } -// ✅ OK: call fetch("https://api.example.com/items").then((r) => r.json()); in separated job +// ✅ OK: call fetch("https://api.example.com/items").then((r) => r.json()); in a separate job const items = fetchItemsJob.start(); for (const item of items) { processItem.start({ id: item.id }); diff --git a/packages/sdk/src/cli/services/workflow/ast-utils.ts b/packages/sdk/src/cli/services/workflow/ast-utils.ts index 771b772ce..591eb3ba0 100644 --- a/packages/sdk/src/cli/services/workflow/ast-utils.ts +++ b/packages/sdk/src/cli/services/workflow/ast-utils.ts @@ -21,7 +21,7 @@ export interface Replacement { text: string; } -export interface TriggerCallInfo { +export interface StartCallInfo { identifierName: string; callRange: { start: number; end: number }; argsText: string; @@ -105,10 +105,10 @@ function argumentSourceText(arg: unknown, sourceText: string): string | undefine * @param sourceText - Source code text * @returns Start call metadata, or null when the node is not a start call */ -export function getTriggerCallInfo( +export function getStartCallInfo( node: ASTNode | null | undefined, sourceText: string, -): TriggerCallInfo | null { +): StartCallInfo | null { if (!node || typeof node !== "object" || node.type !== "CallExpression") { return null; } diff --git a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts b/packages/sdk/src/cli/services/workflow/trigger-transformer.ts index a77e4166c..40849c68c 100644 --- a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts +++ b/packages/sdk/src/cli/services/workflow/trigger-transformer.ts @@ -10,15 +10,15 @@ import { import { type ASTNode, type Replacement, - type TriggerCallInfo, + type StartCallInfo, applyReplacements, getModuleExportName, - getTriggerCallInfo, + getStartCallInfo, } from "./ast-utils"; import type { Program } from "@oxc-project/types"; import type { Plugin } from "rolldown"; -export interface ResolvedTriggerCall extends TriggerCallInfo { +export interface ResolvedTriggerCall extends StartCallInfo { kind: "job" | "workflow"; targetName: string; } @@ -283,7 +283,7 @@ function detectTriggerCallsWithTargets( const targetNames = new Set(targets.keys()); walkBindingAware(program, targetNames, (node, shadowedNames) => { - const triggerCall = getTriggerCallInfo(node, sourceText); + const triggerCall = getStartCallInfo(node, sourceText); if (!triggerCall || shadowedNames.has(triggerCall.identifierName)) return; const target = targets.get(triggerCall.identifierName); if (target) { diff --git a/packages/sdk/src/vitest/globals.ts b/packages/sdk/src/vitest/globals.ts index 0080e807f..6ee283cd4 100644 --- a/packages/sdk/src/vitest/globals.ts +++ b/packages/sdk/src/vitest/globals.ts @@ -101,9 +101,10 @@ export function installPlatformGlobals(global: typeof globalThis): void { g[RUNTIME_FLAG_KEY] = true; // Containers. Namespace mocks (secretmanager, …) are added to these by the - // corresponding `xMock()` on acquisition. `workflow` carries a default runner - // so `.start()` runs the real job chain locally without `mockWorkflow()`; - // `mockWorkflow()` overlays and restores it. + // corresponding `xMock()` on acquisition. `workflow` carries a default + // runner: job/wait/resolve calls throw a helpful error, and workflow starts + // return a placeholder execution id, unless overlaid by `mockWorkflow()` or + // `runWorkflowLocally()`. g.tailor = { context: { getInvoker: defaultGetInvoker }, workflow: createDefaultWorkflowRuntime(), From fb43e4f3a4880918bf681d1e74ff5ff765717214 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 23:21:56 +0900 Subject: [PATCH 570/618] docs(sdk): place plugin arg-scan JSDoc above the functions they document --- packages/sdk/src/cli/shared/plugin.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/sdk/src/cli/shared/plugin.ts b/packages/sdk/src/cli/shared/plugin.ts index dad50bbbf..ac2d74683 100644 --- a/packages/sdk/src/cli/shared/plugin.ts +++ b/packages/sdk/src/cli/shared/plugin.ts @@ -363,13 +363,6 @@ function buildSpawnTarget( return { command: sh, args: ["-c", 'command "$@"', "--", pluginPath, ...args], shell: false }; } -/** - * Extract an explicit `--profile`/`-p` value from args forwarded to a plugin, - * so the injected platform context matches the profile the plugin will use. - * Scanning stops at a `--` terminator; the last occurrence wins. - * @param args - Args forwarded to the plugin - * @returns The explicit profile value, or undefined when not present - */ /** * Check whether args forwarded to a plugin carry an explicit env-file flag. * Scanning stops at a `--` terminator. @@ -393,6 +386,13 @@ export function hasEnvFileFlag(args: readonly string[]): boolean { return false; } +/** + * Extract an explicit `--profile`/`-p` value from args forwarded to a plugin, + * so the injected platform context matches the profile the plugin will use. + * Scanning stops at a `--` terminator; the last occurrence wins. + * @param args - Args forwarded to the plugin + * @returns The explicit profile value, or undefined when not present + */ export function explicitProfileFromArgs(args: readonly string[]): string | undefined { let profile: string | undefined; for (let i = 0; i < args.length; i++) { From 74a145f321fc4cfe4ce7c6c5608c4f95299f19c3 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 23:22:37 +0900 Subject: [PATCH 571/618] test(sdk): pin platform-context skip for a missing --env-file-if-exists file --- packages/sdk/src/cli/shared/plugin.test.ts | 21 +++++++++++++++++++++ packages/sdk/src/cli/shared/plugin.ts | 4 +++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/cli/shared/plugin.test.ts b/packages/sdk/src/cli/shared/plugin.test.ts index 1145d08c9..254eff269 100644 --- a/packages/sdk/src/cli/shared/plugin.test.ts +++ b/packages/sdk/src/cli/shared/plugin.test.ts @@ -315,6 +315,27 @@ describe("dispatchPlugin", () => { expect(env.TAILOR_CONFIG_PATH).toBe("/proj/tailor.config.ts"); }); + test("skips platform context even when the --env-file-if-exists file is missing", async () => { + const project = path.join(tempDir, "project"); + writeCapturePlugin(path.join(project, "node_modules", ".bin"), `${CLI}-hello`, outFile); + process.chdir(project); + process.env.PATH = ""; + + const code = await dispatchPlugin({ + name: "hello", + args: ["deploy", "--env-file-if-exists", ".env.missing"], + cliName: CLI, + }); + + // The flag check is presence-only: with the file absent, neither the + // injected context nor the plugin's env-file loader supplies platform vars. + expect(code).toBe(0); + const { env } = readCapture(); + expect(env.TAILOR_PLATFORM_TOKEN).toBeUndefined(); + expect(env.TAILOR_PLATFORM_WORKSPACE_ID).toBeUndefined(); + expect(env.TAILOR_PLATFORM_URL).toBeUndefined(); + }); + test("omits best-effort context when it cannot be resolved but still dispatches", async () => { contextMocks.loadAccessToken.mockRejectedValue(new Error("not logged in")); contextMocks.loadWorkspaceId.mockRejectedValue(new Error("no workspace")); diff --git a/packages/sdk/src/cli/shared/plugin.ts b/packages/sdk/src/cli/shared/plugin.ts index ac2d74683..544d8e080 100644 --- a/packages/sdk/src/cli/shared/plugin.ts +++ b/packages/sdk/src/cli/shared/plugin.ts @@ -365,7 +365,9 @@ function buildSpawnTarget( /** * Check whether args forwarded to a plugin carry an explicit env-file flag. - * Scanning stops at a `--` terminator. + * Scanning stops at a `--` terminator. The check is presence-only: + * `--env-file-if-exists` counts even when the named file does not exist, so + * no platform context is injected in that case either. * @param args - Args forwarded to the plugin * @returns Whether an `--env-file`/`-e`/`--env-file-if-exists` flag is present */ From 6e28e06c5a30ca7d9f4aabfa9dbf87e77340971a Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 23:24:04 +0900 Subject: [PATCH 572/618] refactor(sdk): extract the plugin install hint into a testable dispatch helper Share the plugin slug computation between dispatch and the install hint, and move the hint branch out of runMain so it can be unit tested. --- packages/sdk/src/cli/index.ts | 21 ++----- packages/sdk/src/cli/shared/plugin.test.ts | 61 ++++++++++++++++++++ packages/sdk/src/cli/shared/plugin.ts | 67 +++++++++++++++++----- 3 files changed, 119 insertions(+), 30 deletions(-) diff --git a/packages/sdk/src/cli/index.ts b/packages/sdk/src/cli/index.ts index 6ef2bec52..1190c47f8 100644 --- a/packages/sdk/src/cli/index.ts +++ b/packages/sdk/src/cli/index.ts @@ -39,7 +39,7 @@ import { commonArgs, isVerbose } from "./shared/args"; import { isCLIError } from "./shared/errors"; import { logger } from "./shared/logger"; import { readPackageJson } from "./shared/package-json"; -import { dispatchPlugin, KNOWN_PLUGIN_PACKAGES } from "./shared/plugin"; +import { dispatchPluginWithInstallHint } from "./shared/plugin"; import { registerTsHook } from "./shared/register-ts-hook"; await registerTsHook(new URL("./ts-hook.mjs", import.meta.url)); @@ -158,27 +158,14 @@ runMain(mainCommand, { displayErrors: false, // CLI plugin dispatch: an unknown subcommand at any level execs the external // `tailor--` binary, forwarding args and injecting context. - onUnknownSubcommand: async ({ commandPath, name, args }) => { - const exitCode = await dispatchPlugin({ + onUnknownSubcommand: ({ commandPath, name, args }) => + dispatchPluginWithInstallHint({ commandPath, name, args, cliName, profile: process.env.TAILOR_PLATFORM_PROFILE, - }); - if (exitCode !== undefined) { - return exitCode; - } - const knownPackage = KNOWN_PLUGIN_PACKAGES[[...commandPath, name].join("-")]; - if (!knownPackage) { - return undefined; - } - logger.error( - `"${cliName} ${[...commandPath, name].join(" ")}" is provided by the ${knownPackage} CLI plugin, which is not installed.`, - ); - logger.info(`Install it with: npm install -D ${knownPackage}`); - return 1; - }, + }), cleanup: async ({ error }) => { if (error) { if (isCLIError(error)) { diff --git a/packages/sdk/src/cli/shared/plugin.test.ts b/packages/sdk/src/cli/shared/plugin.test.ts index 254eff269..3e0159dfb 100644 --- a/packages/sdk/src/cli/shared/plugin.test.ts +++ b/packages/sdk/src/cli/shared/plugin.test.ts @@ -3,8 +3,10 @@ import * as os from "node:os"; import * as path from "pathe"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { getOAuth2ClientId, getPlatformBaseUrl } from "./client"; +import { logger } from "./logger"; import { dispatchPlugin, + dispatchPluginWithInstallHint, explicitProfileFromArgs, hasEnvFileFlag, listPlugins, @@ -397,6 +399,65 @@ describe("dispatchPlugin", () => { }); }); +describe("dispatchPluginWithInstallHint", () => { + let tempDir: string; + let originalCwd: string; + let originalPath: string | undefined; + + beforeEach(() => { + tempDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "tailor-hint-"))); + originalCwd = process.cwd(); + originalPath = process.env.PATH; + const project = path.join(tempDir, "project"); + fs.mkdirSync(project, { recursive: true }); + process.chdir(project); + process.env.PATH = ""; + }); + + afterEach(() => { + vi.restoreAllMocks(); + process.chdir(originalCwd); + process.env.PATH = originalPath; + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + test("prints an install hint when a known plugin package is not installed", async () => { + const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => {}); + const infoSpy = vi.spyOn(logger, "info").mockImplementation(() => {}); + + const code = await dispatchPluginWithInstallHint({ + commandPath: ["tailordb"], + name: "erd", + args: ["export"], + cliName: CLI, + }); + + expect(code).toBe(1); + expect(errorSpy).toHaveBeenCalledWith( + `"${CLI} tailordb erd" is provided by the @tailor-platform/sdk-tailordb-erd-plugin CLI plugin, which is not installed.`, + ); + expect(infoSpy).toHaveBeenCalledWith( + "Install it with: npm install -D @tailor-platform/sdk-tailordb-erd-plugin", + ); + }); + + test("returns undefined for an unknown subcommand with no known package", async () => { + const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => {}); + const infoSpy = vi.spyOn(logger, "info").mockImplementation(() => {}); + + const code = await dispatchPluginWithInstallHint({ + commandPath: [], + name: "missing", + args: [], + cliName: CLI, + }); + + expect(code).toBeUndefined(); + expect(errorSpy).not.toHaveBeenCalled(); + expect(infoSpy).not.toHaveBeenCalled(); + }); +}); + describe("explicitProfileFromArgs", () => { test.each([ [["deploy", "--profile", "staging"], "staging"], diff --git a/packages/sdk/src/cli/shared/plugin.ts b/packages/sdk/src/cli/shared/plugin.ts index 544d8e080..b6757b81c 100644 --- a/packages/sdk/src/cli/shared/plugin.ts +++ b/packages/sdk/src/cli/shared/plugin.ts @@ -18,7 +18,7 @@ import { readPackageJson } from "./package-json"; * dispatcher resolves (command path joined with `-`). Used to suggest an * install command when the plugin executable is not found. */ -export const KNOWN_PLUGIN_PACKAGES: Record = { +const KNOWN_PLUGIN_PACKAGES: Record = { "tailordb-erd": "@tailor-platform/sdk-tailordb-erd-plugin", }; @@ -418,24 +418,39 @@ export function explicitProfileFromArgs(args: readonly string[]): string | undef } /** - * Resolve and execute a plugin, forwarding stdio and propagating its exit code. - * @param params - Dispatch parameters - * @param params.name - Plugin name (without the `-` prefix) - * @param params.args - Args to forward to the plugin - * @param params.cliName - The host CLI name - * @param params.profile - Active profile name, if any - * @returns The plugin's exit code, or undefined when no matching plugin exists + * Parameters for {@link dispatchPlugin}. */ -export async function dispatchPlugin(params: { +interface DispatchPluginParams { + /** Plugin name (the part after the `-` prefix). */ name: string; + /** Args to forward to the plugin. */ args: readonly string[]; + /** The host CLI name (e.g. `tailor`). */ cliName: string; + /** Parent command path preceding the unknown subcommand, if any. */ commandPath?: readonly string[] | undefined; + /** Active profile name, if any. */ profile?: string | undefined; -}): Promise { - // Build the plugin slug from the known command path plus the unknown name, - // so `tailor tailordb erd` resolves `tailor-tailordb-erd`. - const slug = [...(params.commandPath ?? []), params.name].join("-"); +} + +/** + * Build the plugin slug from the known command path plus the unknown name, + * so `tailor tailordb erd` resolves `tailor-tailordb-erd`. + * @param commandPath - Parent command path preceding the unknown subcommand + * @param name - Unknown subcommand name + * @returns The plugin slug + */ +function pluginSlug(commandPath: readonly string[] | undefined, name: string): string { + return [...(commandPath ?? []), name].join("-"); +} + +/** + * Resolve and execute a plugin, forwarding stdio and propagating its exit code. + * @param params - Dispatch parameters + * @returns The plugin's exit code, or undefined when no matching plugin exists + */ +export async function dispatchPlugin(params: DispatchPluginParams): Promise { + const slug = pluginSlug(params.commandPath, params.name); const plugin = resolvePlugin(slug, params.cliName); if (!plugin) { return undefined; @@ -463,3 +478,29 @@ export async function dispatchPlugin(params: { }); }); } + +/** + * Dispatch a plugin, printing an install hint when the subcommand maps to a + * known plugin package that is not installed. + * @param params - Dispatch parameters + * @returns The plugin's exit code (1 after an install hint), or undefined when + * the subcommand matches no plugin and no known package + */ +export async function dispatchPluginWithInstallHint( + params: DispatchPluginParams, +): Promise { + const exitCode = await dispatchPlugin(params); + if (exitCode !== undefined) { + return exitCode; + } + const knownPackage = KNOWN_PLUGIN_PACKAGES[pluginSlug(params.commandPath, params.name)]; + if (!knownPackage) { + return undefined; + } + const fullCommand = [params.cliName, ...(params.commandPath ?? []), params.name].join(" "); + logger.error( + `"${fullCommand}" is provided by the ${knownPackage} CLI plugin, which is not installed.`, + ); + logger.info(`Install it with: npm install -D ${knownPackage}`); + return 1; +} From c1fc67c16277bbcd06da9d84d90560903163bd96 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 23:24:31 +0900 Subject: [PATCH 573/618] fix(sdk): list only missing namespaces in the not-found error --- packages/sdk/src/cli/shared/tailordb-namespaces.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/cli/shared/tailordb-namespaces.ts b/packages/sdk/src/cli/shared/tailordb-namespaces.ts index 34ea4845c..c919cf987 100644 --- a/packages/sdk/src/cli/shared/tailordb-namespaces.ts +++ b/packages/sdk/src/cli/shared/tailordb-namespaces.ts @@ -58,10 +58,11 @@ export async function loadTailorDBNamespaces( : application.tailorDBServices; if (namespaceFilter && services.length !== namespaceFilter.size) { + const found = new Set(services.map((db) => db.namespace)); + const missing = [...namespaceFilter].filter((ns) => !found.has(ns)).join(", "); const available = application.tailorDBServices.map((db) => db.namespace).join(", "); - const requested = [...namespaceFilter].join(", "); throw new Error( - `TailorDB namespace "${requested}" not found in local config.db.` + + `TailorDB namespace "${missing}" not found in local config.db.` + (available ? ` Available owned namespaces: ${available}` : ""), ); } From ef71728d4b610786b46f3ea097dbc9036f7b5e40 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 23:24:48 +0900 Subject: [PATCH 574/618] docs: point ERD plugin installs at the next dist-tag The v2 branch releases under the npm next dist-tag while changesets pre-release mode is active; a plain install would resolve to the 0.0.0 bootstrap version on latest. --- packages/sdk-tailordb-erd-plugin/README.md | 2 +- packages/sdk/docs/cli/tailordb.md | 2 +- packages/sdk/docs/cli/tailordb.template.md | 2 +- packages/sdk/docs/github-actions.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/sdk-tailordb-erd-plugin/README.md b/packages/sdk-tailordb-erd-plugin/README.md index b74d7f1d3..64f03e47c 100644 --- a/packages/sdk-tailordb-erd-plugin/README.md +++ b/packages/sdk-tailordb-erd-plugin/README.md @@ -10,7 +10,7 @@ Tailor CLI plugin that provides the `tailor tailordb erd` commands: export, diff Install it next to `@tailor-platform/sdk` in your project: ```bash -npm install -D @tailor-platform/sdk-tailordb-erd-plugin +npm install -D @tailor-platform/sdk-tailordb-erd-plugin@next ``` The Tailor CLI discovers the plugin automatically from `node_modules/.bin` (or your `PATH`). Run `tailor plugin list` to confirm it resolves. diff --git a/packages/sdk/docs/cli/tailordb.md b/packages/sdk/docs/cli/tailordb.md index f10fd0d52..37b7c12f6 100644 --- a/packages/sdk/docs/cli/tailordb.md +++ b/packages/sdk/docs/cli/tailordb.md @@ -234,7 +234,7 @@ See [Global Options](../cli-reference.md#global-options) for options available t The `tailordb erd` commands (export, diff, serve, deploy) are provided by the `@tailor-platform/sdk-tailordb-erd-plugin` CLI plugin. Install it next to the SDK and keep running `tailor tailordb erd ` as before: ```bash -npm install -D @tailor-platform/sdk-tailordb-erd-plugin +npm install -D @tailor-platform/sdk-tailordb-erd-plugin@next tailor tailordb erd export --namespace myNamespace ``` diff --git a/packages/sdk/docs/cli/tailordb.template.md b/packages/sdk/docs/cli/tailordb.template.md index b159143b0..be5bc0a6a 100644 --- a/packages/sdk/docs/cli/tailordb.template.md +++ b/packages/sdk/docs/cli/tailordb.template.md @@ -75,7 +75,7 @@ Note: Migration scripts are automatically executed during `tailor deploy`. See [ The `tailordb erd` commands (export, diff, serve, deploy) are provided by the `@tailor-platform/sdk-tailordb-erd-plugin` CLI plugin. Install it next to the SDK and keep running `tailor tailordb erd ` as before: ```bash -npm install -D @tailor-platform/sdk-tailordb-erd-plugin +npm install -D @tailor-platform/sdk-tailordb-erd-plugin@next tailor tailordb erd export --namespace myNamespace ``` diff --git a/packages/sdk/docs/github-actions.md b/packages/sdk/docs/github-actions.md index ffe694ec4..69f73bd0f 100644 --- a/packages/sdk/docs/github-actions.md +++ b/packages/sdk/docs/github-actions.md @@ -77,7 +77,7 @@ The generated workflow runs `tailor tailordb erd`, which is provided by the dev-dependency in your project: ```bash -npm install -D @tailor-platform/sdk-tailordb-erd-plugin +npm install -D @tailor-platform/sdk-tailordb-erd-plugin@next ``` The generated workflow builds one self-contained ERD viewer HTML file for each From f4df095c6b6ed57116cc57a4bb9c41fdeb934bee Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 23:24:49 +0900 Subject: [PATCH 575/618] ci: publish sdk-tailordb-erd-plugin previews to pkg.pr.new --- .github/workflows/pkg-pr-new.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml index 913e93725..f00d2c35e 100644 --- a/.github/workflows/pkg-pr-new.yml +++ b/.github/workflows/pkg-pr-new.yml @@ -70,7 +70,7 @@ jobs: pnpm --config.verify-deps-before-run=false --filter @tailor-platform/create-sdk build - name: Publish to pkg.pr.new - run: pnpm --config.verify-deps-before-run=false exec pkg-pr-new publish --pnpm --compact --commentWithSha --packageManager=pnpm "packages/sdk" "packages/create-sdk" # zizmor: ignore[use-trusted-publishing] + run: pnpm --config.verify-deps-before-run=false exec pkg-pr-new publish --pnpm --compact --commentWithSha --packageManager=pnpm "packages/sdk" "packages/create-sdk" "packages/sdk-tailordb-erd-plugin" # zizmor: ignore[use-trusted-publishing] init-smoke: needs: [publish] From 5da2674552f116a4826f90a49f77ae6f658d16bd Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 15 Jul 2026 23:30:27 +0900 Subject: [PATCH 576/618] fix(workflow): whitespace-tolerant start-call detection, stale comments - The bundler's and trigger-transformer's `.start(` pre-checks used a plain substring match, which misses whitespace/newline-formatted calls (e.g. `.start (` or `.start\n(`) and would silently skip the AST transform, leaving an untransformed `.start(...)` call to throw at runtime in the bundle. Add a shared `hasStartCall()` regex helper and use it in all three call sites (trigger-transformer.ts, bundler.ts x2). - Fix stale "trigger"/"triggerJobFunction" wording in ast-transformer.test.ts comments and titles that no longer match the `startJobFunction` assertions they describe. --- .../cli/services/workflow/ast-transformer.test.ts | 10 +++++----- packages/sdk/src/cli/services/workflow/bundler.ts | 10 +++++++--- .../cli/services/workflow/trigger-transformer.ts | 15 ++++++++++++++- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts b/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts index 56df32eab..ff7c6910a 100644 --- a/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts +++ b/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts @@ -379,7 +379,7 @@ const job = createWorkflowJob({ describe("AST Transformer - transformation logic", () => { describe("transformWorkflowSource", () => { - test("transforms trigger calls to triggerJobFunction", () => { + test("transforms start calls to startJobFunction", () => { const source = ` import { createWorkflowJob } from "@tailor-platform/sdk"; @@ -413,7 +413,7 @@ const mainJob = createWorkflowJob({ expect(result).not.toContain("const fetchData"); }); - test("forwards a second options argument to triggerJobFunction", () => { + test("forwards a second options argument to startJobFunction", () => { const source = ` import { createWorkflowJob } from "@tailor-platform/sdk"; @@ -484,7 +484,7 @@ const mainJob = createWorkflowJob({ expect(result).not.toContain("getDB"); // mainJob body is preserved expect(result).toContain('result: "main"'); - // trigger is transformed (job name appears in triggerJobFunction call) + // start is transformed (job name appears in startJobFunction call) expect(result).toContain('tailor.workflow.startJobFunction("heavy-job", undefined)'); }); @@ -531,7 +531,7 @@ const mainJob = createWorkflowJob({ expect(result).toContain('"main"'); // heavy code is removed (part of job1/job2 body) expect(result).not.toContain("heavy code"); - // triggers are transformed (job names appear in triggerJobFunction calls) + // starts are transformed (job names appear in startJobFunction calls) expect(result).toContain('tailor.workflow.startJobFunction("job-one", undefined)'); expect(result).toContain('tailor.workflow.startJobFunction("job-two", undefined)'); }); @@ -996,7 +996,7 @@ const result = await simpleJob.start(); expect(result).toContain('tailor.workflow.startJobFunction("simple-job", undefined)'); }); - test("forwards a second options argument to triggerJobFunction", () => { + test("forwards a second options argument to startJobFunction", () => { const source = ` const result = await fetchCustomer.start({ id: "123" }, { executionPolicyKey: "premium" }); `; diff --git a/packages/sdk/src/cli/services/workflow/bundler.ts b/packages/sdk/src/cli/services/workflow/bundler.ts index 583c79478..1c08ad4a9 100644 --- a/packages/sdk/src/cli/services/workflow/bundler.ts +++ b/packages/sdk/src/cli/services/workflow/bundler.ts @@ -15,7 +15,11 @@ import { serializeTriggerContext, type TriggerContext } from "#/cli/shared/trigg import ml from "#/utils/multiline"; import { findAllJobs } from "./job-detector"; import { transformWorkflowSource } from "./source-transformer"; -import { detectResolvedTriggerCalls, transformFunctionTriggers } from "./trigger-transformer"; +import { + detectResolvedTriggerCalls, + hasStartCall, + transformFunctionTriggers, +} from "./trigger-transformer"; import type { LogLevel } from "#/configure/config/types"; function safeRealpath(p: string): string { @@ -338,7 +342,7 @@ async function bundleSingleJob( if ( !code.includes("createWorkflowJob") && !code.includes("createWorkflow") && - !code.includes(".start(") + !hasStartCall(code) ) { return null; } @@ -359,7 +363,7 @@ async function bundleSingleJob( } // Apply workflow.start / job.start transformation. - if (transformed.includes(".start(")) { + if (hasStartCall(transformed)) { transformed = transformFunctionTriggers(transformed, triggerContext, id); } diff --git a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts b/packages/sdk/src/cli/services/workflow/trigger-transformer.ts index 40849c68c..d2c6d4491 100644 --- a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts +++ b/packages/sdk/src/cli/services/workflow/trigger-transformer.ts @@ -23,6 +23,19 @@ export interface ResolvedTriggerCall extends StartCallInfo { targetName: string; } +const START_CALL_RE = /\.start\s*\(/; + +/** + * Fast pre-check for whether `code` contains a `.start(` call, tolerating + * whitespace/newlines between `.start` and `(` that a plain substring check + * would miss. + * @param code - Source text to scan + * @returns Whether `code` contains a `.start(` call + */ +export function hasStartCall(code: string): boolean { + return START_CALL_RE.test(code); +} + const NORMALIZER_IDENTIFIER = "__tailor_normalizeTriggerOptions"; /** @@ -380,7 +393,7 @@ export function createTriggerTransformPlugin( transform: { filter: { id: { include: [/\.(ts|mts|cts|js|mjs|cjs)$/] } }, handler(code, id) { - if (!code.includes(".start(")) return null; + if (!hasStartCall(code)) return null; return { code: transformFunctionTriggers(code, triggerContext, id) }; }, }, From e81a5715a7d085c749b5f423277dcec7ef842fc7 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 23:36:01 +0900 Subject: [PATCH 577/618] fix(deps): align the ERD plugin's tsdown with the workspace The v2 merge bumped tsdown to 0.22.5 everywhere else, dropping the 0.22.4 lockfile entry the plugin still referenced and breaking frozen-lockfile installs. --- packages/sdk-tailordb-erd-plugin/package.json | 2 +- pnpm-lock.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sdk-tailordb-erd-plugin/package.json b/packages/sdk-tailordb-erd-plugin/package.json index 69615cbf3..da55d620f 100644 --- a/packages/sdk-tailordb-erd-plugin/package.json +++ b/packages/sdk-tailordb-erd-plugin/package.json @@ -42,7 +42,7 @@ "@types/node": "24.13.3", "eslint-plugin-zod": "4.7.0", "oxlint": "1.73.0", - "tsdown": "0.22.4", + "tsdown": "0.22.5", "typescript": "6.0.3", "vitest": "4.1.10" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b3643d70..2a8ac8f9c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -701,8 +701,8 @@ importers: specifier: 1.73.0 version: 1.73.0(oxlint-tsgolint@0.24.0) tsdown: - specifier: 0.22.4 - version: 0.22.4(@typescript/native-preview@7.0.0-dev.20260707.2)(oxc-resolver@11.21.3)(publint@0.3.21)(tsx@4.23.0)(typescript@6.0.3) + specifier: 0.22.5 + version: 0.22.5(@typescript/native-preview@7.0.0-dev.20260707.2)(oxc-resolver@11.21.3)(publint@0.3.21)(tsx@4.23.0)(typescript@6.0.3) typescript: specifier: 6.0.3 version: 6.0.3 From 106fbbfe86a76f440ba0d394a89aeddbea91a06c Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 15 Jul 2026 23:46:33 +0900 Subject: [PATCH 578/618] ci: stub the ERD plugin bin target before install install-deps now consumes the pending-rebuild state while the plugin's dist is still missing, so the later rebuild --pending no longer relinks the skipped bin. Linking at install time against a stub removes the relink dependency entirely. --- .github/workflows/erd-schema.yml | 34 +++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/.github/workflows/erd-schema.yml b/.github/workflows/erd-schema.yml index f0ec2f536..2c885f092 100644 --- a/.github/workflows/erd-schema.yml +++ b/.github/workflows/erd-schema.yml @@ -42,16 +42,21 @@ jobs: with: persist-credentials: false + # pnpm links a package's bin only when its target file exists at install + # time, and neither a same-lockfile re-install nor `rebuild --pending` + # retries a skipped link. Stub the plugin's dist entry before install; + # the build below replaces it. + - name: Stub ERD plugin bin target + run: | + mkdir -p packages/sdk-tailordb-erd-plugin/dist + printf '#!/usr/bin/env node\n' > packages/sdk-tailordb-erd-plugin/dist/index.js + - uses: ./.github/actions/install-deps # install-deps only builds the SDK; `tailor tailordb erd` dispatches to - # this plugin's bin, which must also point at a real dist file. Relink - # bins afterwards — pnpm skips bin links whose target is missing at - # install time. + # this plugin's bin. - name: Build ERD plugin - run: | - pnpm --filter @tailor-platform/sdk-tailordb-erd-plugin run build - pnpm rebuild --pending + run: pnpm --filter @tailor-platform/sdk-tailordb-erd-plugin run build - uses: tailor-platform/actions/erd-schema-export@0bd69a0fc9aab3f818b636aa111995441a2c1a03 # v2 branch (unreleased) with: @@ -87,18 +92,23 @@ jobs: with: persist-credentials: false + # pnpm links a package's bin only when its target file exists at install + # time, and neither a same-lockfile re-install nor `rebuild --pending` + # retries a skipped link. Stub the plugin's dist entry before install; + # the build below replaces it. + - name: Stub ERD plugin bin target + run: | + mkdir -p packages/sdk-tailordb-erd-plugin/dist + printf '#!/usr/bin/env node\n' > packages/sdk-tailordb-erd-plugin/dist/index.js + # install-deps builds the SDK once, so the `tailor-sdk` CLI bin resolves. - name: Install deps uses: ./.github/actions/install-deps # install-deps only builds the SDK; `tailor tailordb erd` dispatches to - # this plugin's bin, which must also point at a real dist file. Relink - # bins afterwards — pnpm skips bin links whose target is missing at - # install time. + # this plugin's bin. - name: Build ERD plugin - run: | - pnpm --filter @tailor-platform/sdk-tailordb-erd-plugin run build - pnpm rebuild --pending + run: pnpm --filter @tailor-platform/sdk-tailordb-erd-plugin run build - uses: tailor-platform/actions/erd-schema-preview@0bd69a0fc9aab3f818b636aa111995441a2c1a03 # v2 branch (unreleased) with: From 815e468bc864d4364ed990e477a436278d4d3117 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 15 Jul 2026 23:47:05 +0900 Subject: [PATCH 579/618] fix(workflow): tolerate comments between .start and ( in start-call detection --- .../services/workflow/ast-transformer.test.ts | 27 ++++++++++++++++++- .../services/workflow/trigger-transformer.ts | 6 ++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts b/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts index ff7c6910a..b7f3d4f57 100644 --- a/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts +++ b/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts @@ -10,7 +10,10 @@ import { } from "#/cli/shared/trigger-context"; import { findAllJobs } from "./job-detector"; import { transformWorkflowSource } from "./source-transformer"; -import { transformFunctionTriggers as transformFunctionTriggersWithContext } from "./trigger-transformer"; +import { + hasStartCall, + transformFunctionTriggers as transformFunctionTriggersWithContext, +} from "./trigger-transformer"; import { findAllWorkflows } from "./workflow-detector"; function parseProgram(source: string) { @@ -1244,3 +1247,25 @@ unknown.start(fetchCustomer.start({ customerId: "123" })); }); }); }); + +describe("AST Transformer - hasStartCall", () => { + test("detects a plain .start( call", () => { + expect(hasStartCall("job.start({ id: 1 });")).toBe(true); + }); + + test("detects .start( split across whitespace/newlines", () => { + expect(hasStartCall("job.start\n (\n { id: 1 }\n );")).toBe(true); + }); + + test("detects .start( with a block comment before the parens", () => { + expect(hasStartCall("job.start/* why */({ id: 1 });")).toBe(true); + }); + + test("detects .start( with a line comment before the parens", () => { + expect(hasStartCall("job.start // why\n({ id: 1 });")).toBe(true); + }); + + test("returns false when there is no .start( call", () => { + expect(hasStartCall("job.stop({ id: 1 });")).toBe(false); + }); +}); diff --git a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts b/packages/sdk/src/cli/services/workflow/trigger-transformer.ts index d2c6d4491..192c7cbdf 100644 --- a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts +++ b/packages/sdk/src/cli/services/workflow/trigger-transformer.ts @@ -23,12 +23,12 @@ export interface ResolvedTriggerCall extends StartCallInfo { targetName: string; } -const START_CALL_RE = /\.start\s*\(/; +const START_CALL_RE = /\.start(?:\s|\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)*\(/; /** * Fast pre-check for whether `code` contains a `.start(` call, tolerating - * whitespace/newlines between `.start` and `(` that a plain substring check - * would miss. + * whitespace, newlines, and comments between `.start` and `(` that a plain + * substring check would miss. * @param code - Source text to scan * @returns Whether `code` contains a `.start(` call */ From 3bdf3136f2ca33b8488ba4a6e9e37cf9fcd4fe2d Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 09:03:42 +0900 Subject: [PATCH 580/618] refactor(workflow): rename internal Trigger* naming to Start* to match the public API rename Rename trigger-context.ts/trigger-transformer.ts and their exported identifiers (TriggerContext, TriggerTarget, TriggerModuleBindings, ResolvedTriggerCall, transformFunctionTriggers, createTriggerTransformPlugin, detectResolvedTriggerCalls, etc.) to their Start* equivalents, aligning internal AST/bundler naming with the .start() rename. Unrelated Trigger usages (executor/IdP event triggers) are untouched. --- .../sdk/src/cli/cache/bundle-cache.test.ts | 4 +- packages/sdk/src/cli/cache/bundle-cache.ts | 16 +-- packages/sdk/src/cli/services/application.ts | 14 +- packages/sdk/src/cli/services/auth/bundler.ts | 18 +-- .../sdk/src/cli/services/executor/bundler.ts | 22 ++-- .../src/cli/services/http-adapter/bundler.ts | 2 +- .../sdk/src/cli/services/resolver/bundler.ts | 20 +-- .../services/workflow/ast-transformer.test.ts | 124 +++++++++--------- .../src/cli/services/workflow/bundler.test.ts | 24 ++-- .../sdk/src/cli/services/workflow/bundler.ts | 53 ++++---- ...er-transformer.ts => start-transformer.ts} | 82 ++++++------ ...-context.test.ts => start-context.test.ts} | 42 +++--- .../{trigger-context.ts => start-context.ts} | 36 ++--- 13 files changed, 221 insertions(+), 236 deletions(-) rename packages/sdk/src/cli/services/workflow/{trigger-transformer.ts => start-transformer.ts} (84%) rename packages/sdk/src/cli/shared/{trigger-context.test.ts => start-context.test.ts} (86%) rename packages/sdk/src/cli/shared/{trigger-context.ts => start-context.ts} (81%) diff --git a/packages/sdk/src/cli/cache/bundle-cache.test.ts b/packages/sdk/src/cli/cache/bundle-cache.test.ts index 8327c2f7d..0e1fbe68a 100644 --- a/packages/sdk/src/cli/cache/bundle-cache.test.ts +++ b/packages/sdk/src/cli/cache/bundle-cache.test.ts @@ -427,7 +427,7 @@ describe("withCache", () => { describe("computeBundlerContextHash", () => { const baseParams = { sourceFile: "/tmp/src/resolver.ts", - serializedTriggerContext: "ctx", + serializedStartContext: "ctx", }; test("returns the same hash for identical inputs", () => { @@ -440,7 +440,7 @@ describe("computeBundlerContextHash", () => { test.each([ ["sourceFile", {}, { sourceFile: "/tmp/src/executor.ts" }], - ["serializedTriggerContext", {}, { serializedTriggerContext: "other" }], + ["serializedStartContext", {}, { serializedStartContext: "other" }], ["prefix", { prefix: "ENV_A=1" }, { prefix: "ENV_B=2" }], ["bundleLogLevel", { bundleLogLevel: "DEBUG" }, { bundleLogLevel: "WARN" }], ])("returns different hash when %s differs", (_label, overrideA, overrideB) => { diff --git a/packages/sdk/src/cli/cache/bundle-cache.ts b/packages/sdk/src/cli/cache/bundle-cache.ts index 3b958daf7..b7f55ebae 100644 --- a/packages/sdk/src/cli/cache/bundle-cache.ts +++ b/packages/sdk/src/cli/cache/bundle-cache.ts @@ -54,7 +54,7 @@ function combineHash(fileHash: string, contextHash?: string): string { type ComputeBundlerContextHashParams = { sourceFile: string; - serializedTriggerContext: string; + serializedStartContext: string; tsconfig?: string; inlineSourcemap?: boolean; bundleLogLevel?: string; @@ -64,25 +64,19 @@ type ComputeBundlerContextHashParams = { /** * Compute a context hash for cache invalidation across bundlers. * - * Combines the source file path, serialized trigger context, tsconfig hash, + * Combines the source file path, serialized start context, tsconfig hash, * sourcemap mode, bundle log level, and an optional prefix (e.g., serialized * env variables) into a single SHA-256 hash. * @param params - Context hash computation parameters * @returns SHA-256 hex digest of the combined context */ function computeBundlerContextHash(params: ComputeBundlerContextHashParams): string { - const { - sourceFile, - serializedTriggerContext, - tsconfig, - inlineSourcemap, - bundleLogLevel, - prefix, - } = params; + const { sourceFile, serializedStartContext, tsconfig, inlineSourcemap, bundleLogLevel, prefix } = + params; return hashContent( (prefix ?? "") + path.resolve(sourceFile) + - serializedTriggerContext + + serializedStartContext + (tsconfig ? hashFile(tsconfig) : "") + String(inlineSourcemap ?? false) + (bundleLogLevel ?? ""), diff --git a/packages/sdk/src/cli/services/application.ts b/packages/sdk/src/cli/services/application.ts index 9e1ba5f5f..f6700d4f9 100644 --- a/packages/sdk/src/cli/services/application.ts +++ b/packages/sdk/src/cli/services/application.ts @@ -26,7 +26,7 @@ import { getDistDir } from "#/cli/shared/dist-dir"; import { resolveInlineSourcemap } from "#/cli/shared/inline-sourcemap"; import { logger } from "#/cli/shared/logger"; import { resolverBundleKey } from "#/cli/shared/resolver-bundle-key"; -import { buildTriggerContext } from "#/cli/shared/trigger-context"; +import { buildStartContext } from "#/cli/shared/start-context"; import { type AppConfig, type ExecutorServiceInput, @@ -526,8 +526,8 @@ export async function loadApplication( await httpAdapterService.loadAdapters(); } - // 7. Build trigger context for workflow/job trigger transformation - const triggerContext = await buildTriggerContext( + // 7. Build start context for workflow/job start transformation + const startContext = await buildStartContext( config.workflow, getApplicationAuthNamespace({ authService: authResult.authService, config }), ); @@ -549,7 +549,7 @@ export async function loadApplication( const resolverBundles = await bundleResolvers( pipeline.namespace, pipeline.config, - triggerContext, + startContext, bundleCache, inlineSourcemap, bundleLogLevel, @@ -563,7 +563,7 @@ export async function loadApplication( if (executorService) { bundledScripts.executors = await bundleExecutors({ config: executorService.config, - triggerContext, + startContext, additionalFiles: [...pluginExecutorFiles], cache: bundleCache, inlineSourcemap, @@ -579,7 +579,7 @@ export async function loadApplication( workflowService.jobs, mainJobNames, config.env ?? {}, - triggerContext, + startContext, bundleCache, inlineSourcemap, bundleLogLevel, @@ -610,7 +610,7 @@ export async function loadApplication( authName, handlerAccessPath: `auth.hooks.beforeLogin.handler`, env: config.env ?? {}, - triggerContext, + startContext, cache: bundleCache, inlineSourcemap, bundleLogLevel, diff --git a/packages/sdk/src/cli/services/auth/bundler.ts b/packages/sdk/src/cli/services/auth/bundler.ts index 545062fd3..34a707695 100644 --- a/packages/sdk/src/cli/services/auth/bundler.ts +++ b/packages/sdk/src/cli/services/auth/bundler.ts @@ -4,13 +4,13 @@ import { resolveTSConfig } from "pkg-types"; import * as rolldown from "rolldown"; import { computeBundlerContextHash, withCache, type BundleCache } from "#/cli/cache/bundle-cache"; import { removeStaleEntryFiles } from "#/cli/services/stale-cleanup"; -import { createTriggerTransformPlugin } from "#/cli/services/workflow/trigger-transformer"; +import { createStartTransformPlugin } from "#/cli/services/workflow/start-transformer"; import { createLogLevelTreeshakeOptions } from "#/cli/shared/bundle-log-level"; import { getDistDir } from "#/cli/shared/dist-dir"; import { composeFunctionTreeshakeOptions } from "#/cli/shared/function-treeshake"; import { logger, styles } from "#/cli/shared/logger"; import { platformBundleDefinePlugin } from "#/cli/shared/platform-bundle-plugin"; -import { serializeTriggerContext, type TriggerContext } from "#/cli/shared/trigger-context"; +import { serializeStartContext, type StartContext } from "#/cli/shared/start-context"; import ml from "#/utils/multiline"; import type { LogLevel } from "#/configure/config/types"; @@ -26,8 +26,8 @@ export interface BundleAuthHooksOptions { handlerAccessPath: string; /** Environment variables to inject into the hook args */ env?: Record; - /** Trigger context for workflow/job transformations */ - triggerContext?: TriggerContext; + /** Start context for workflow/job transformations */ + startContext?: StartContext; /** Optional bundle cache for skipping unchanged builds */ cache?: BundleCache; /** Whether to enable inline sourcemaps */ @@ -53,7 +53,7 @@ export async function bundleAuthHooks( authName, handlerAccessPath, env = {}, - triggerContext, + startContext, cache, inlineSourcemap, bundleLogLevel = "DEBUG", @@ -77,7 +77,7 @@ export async function bundleAuthHooks( const functionName = `auth-hook--${authName}--before-login`; const absoluteConfigPath = path.resolve(configPath); - const serializedTriggerContext = serializeTriggerContext(triggerContext); + const serializedStartContext = serializeStartContext(startContext); // Include sorted env variables as a prefix so that env changes invalidate the cache const sortedEnvPrefix = JSON.stringify( @@ -85,7 +85,7 @@ export async function bundleAuthHooks( ); const contextHash = computeBundlerContextHash({ sourceFile: absoluteConfigPath, - serializedTriggerContext, + serializedStartContext, tsconfig, inlineSourcemap, bundleLogLevel, @@ -111,8 +111,8 @@ export async function bundleAuthHooks( `; fs.writeFileSync(entryPath, entryContent); - const triggerPlugin = createTriggerTransformPlugin(triggerContext); - const plugins: rolldown.Plugin[] = triggerPlugin ? [triggerPlugin] : []; + const startPlugin = createStartTransformPlugin(startContext); + const plugins: rolldown.Plugin[] = startPlugin ? [startPlugin] : []; plugins.push(platformBundleDefinePlugin, ...cachePlugins); const result = await rolldown.build({ diff --git a/packages/sdk/src/cli/services/executor/bundler.ts b/packages/sdk/src/cli/services/executor/bundler.ts index 8a629ac10..08b426caa 100644 --- a/packages/sdk/src/cli/services/executor/bundler.ts +++ b/packages/sdk/src/cli/services/executor/bundler.ts @@ -5,7 +5,7 @@ import * as rolldown from "rolldown"; import { computeBundlerContextHash, withCache, type BundleCache } from "#/cli/cache/bundle-cache"; import { loadFilesWithIgnores, type FileLoadConfig } from "#/cli/services/file-loader"; import { removeStaleEntryFiles } from "#/cli/services/stale-cleanup"; -import { createTriggerTransformPlugin } from "#/cli/services/workflow/trigger-transformer"; +import { createStartTransformPlugin } from "#/cli/services/workflow/start-transformer"; import { withBundleConcurrency } from "#/cli/shared/bundle-concurrency"; import { createLogLevelTreeshakeOptions } from "#/cli/shared/bundle-log-level"; import { getDistDir } from "#/cli/shared/dist-dir"; @@ -13,7 +13,7 @@ import { composeFunctionTreeshakeOptions } from "#/cli/shared/function-treeshake import { logger, styles } from "#/cli/shared/logger"; import { platformBundleDefinePlugin } from "#/cli/shared/platform-bundle-plugin"; import { INVOKER_EXPR } from "#/cli/shared/runtime-exprs"; -import { serializeTriggerContext, type TriggerContext } from "#/cli/shared/trigger-context"; +import { serializeStartContext, type StartContext } from "#/cli/shared/start-context"; import ml from "#/utils/multiline"; import { loadExecutor } from "./loader"; import type { LogLevel } from "#/configure/config/types"; @@ -29,8 +29,8 @@ interface ExecutorInfo { export interface BundleExecutorsOptions { /** Executor file loading configuration */ config: FileLoadConfig; - /** Trigger context for workflow/job transformations */ - triggerContext?: TriggerContext; + /** Start context for workflow/job transformations */ + startContext?: StartContext; /** Additional files to bundle (e.g., plugin-generated executors) */ additionalFiles?: string[]; /** Optional bundle cache for skipping unchanged builds */ @@ -56,7 +56,7 @@ export async function bundleExecutors( const bundledCode = new Map(); const { config, - triggerContext, + startContext, additionalFiles = [], cache, inlineSourcemap, @@ -123,7 +123,7 @@ export async function bundleExecutors( executor, outputDir, tsconfig, - triggerContext, + startContext, cache, inlineSourcemap, bundleLogLevel, @@ -143,16 +143,16 @@ async function bundleSingleExecutor( executor: ExecutorInfo, outputDir: string, tsconfig: string | undefined, - triggerContext?: TriggerContext, + startContext?: StartContext, cache?: BundleCache, inlineSourcemap?: boolean, bundleLogLevel: LogLevel = "DEBUG", ): Promise<[string, string]> { - const serializedTriggerContext = serializeTriggerContext(triggerContext); + const serializedStartContext = serializeStartContext(startContext); const contextHash = computeBundlerContextHash({ sourceFile: executor.sourceFile, - serializedTriggerContext, + serializedStartContext, tsconfig, inlineSourcemap, bundleLogLevel, @@ -182,8 +182,8 @@ async function bundleSingleExecutor( fs.writeFileSync(entryPath, entryContent); // Step 2: Bundle with tree-shaking (write: false to avoid unnecessary disk I/O) - const triggerPlugin = createTriggerTransformPlugin(triggerContext); - const plugins: rolldown.Plugin[] = triggerPlugin ? [triggerPlugin] : []; + const startPlugin = createStartTransformPlugin(startContext); + const plugins: rolldown.Plugin[] = startPlugin ? [startPlugin] : []; plugins.push(platformBundleDefinePlugin, ...cachePlugins); const result = await rolldown.build({ diff --git a/packages/sdk/src/cli/services/http-adapter/bundler.ts b/packages/sdk/src/cli/services/http-adapter/bundler.ts index 3f3c39b76..4958b978c 100644 --- a/packages/sdk/src/cli/services/http-adapter/bundler.ts +++ b/packages/sdk/src/cli/services/http-adapter/bundler.ts @@ -100,7 +100,7 @@ async function bundleAdapterScript( ): Promise<[string, "input" | "output", string]> { const contextHash = computeBundlerContextHash({ sourceFile: adapter.sourceFile, - serializedTriggerContext: kind === "input" ? adapter.methods.join(",") : "", + serializedStartContext: kind === "input" ? adapter.methods.join(",") : "", tsconfig, inlineSourcemap: false, bundleLogLevel, diff --git a/packages/sdk/src/cli/services/resolver/bundler.ts b/packages/sdk/src/cli/services/resolver/bundler.ts index ebd402b48..fc33eb4f4 100644 --- a/packages/sdk/src/cli/services/resolver/bundler.ts +++ b/packages/sdk/src/cli/services/resolver/bundler.ts @@ -5,7 +5,7 @@ import * as rolldown from "rolldown"; import { type BundleCache, computeBundlerContextHash, withCache } from "#/cli/cache/bundle-cache"; import { type FileLoadConfig, loadFilesWithIgnores } from "#/cli/services/file-loader"; import { removeStaleEntryFiles } from "#/cli/services/stale-cleanup"; -import { createTriggerTransformPlugin } from "#/cli/services/workflow/trigger-transformer"; +import { createStartTransformPlugin } from "#/cli/services/workflow/start-transformer"; import { withBundleConcurrency } from "#/cli/shared/bundle-concurrency"; import { createLogLevelTreeshakeOptions } from "#/cli/shared/bundle-log-level"; import { getDistDir } from "#/cli/shared/dist-dir"; @@ -13,7 +13,7 @@ import { composeFunctionTreeshakeOptions } from "#/cli/shared/function-treeshake import { logger, styles } from "#/cli/shared/logger"; import { platformBundleDefinePlugin } from "#/cli/shared/platform-bundle-plugin"; import { INVOKER_EXPR } from "#/cli/shared/runtime-exprs"; -import { serializeTriggerContext, type TriggerContext } from "#/cli/shared/trigger-context"; +import { serializeStartContext, type StartContext } from "#/cli/shared/start-context"; import ml from "#/utils/multiline"; import { loadResolver } from "./loader"; import type { LogLevel } from "#/configure/config/types"; @@ -32,7 +32,7 @@ interface ResolverInfo { * 3. Bundles in a single step with tree-shaking * @param namespace - Resolver namespace name * @param config - Resolver file loading configuration - * @param triggerContext - Trigger context for workflow/job transformations + * @param startContext - Start context for workflow/job transformations * @param cache - Optional bundle cache for skipping unchanged builds * @param inlineSourcemap - Whether to enable inline sourcemaps * @param bundleLogLevel - Controls which console calls are kept in bundled code @@ -41,7 +41,7 @@ interface ResolverInfo { export async function bundleResolvers( namespace: string, config: FileLoadConfig, - triggerContext?: TriggerContext, + startContext?: StartContext, cache?: BundleCache, inlineSourcemap?: boolean, bundleLogLevel: LogLevel = "DEBUG", @@ -98,7 +98,7 @@ export async function bundleResolvers( resolver, outputDir, tsconfig, - triggerContext, + startContext, cache, inlineSourcemap, bundleLogLevel, @@ -119,16 +119,16 @@ async function bundleSingleResolver( resolver: ResolverInfo, outputDir: string, tsconfig: string | undefined, - triggerContext?: TriggerContext, + startContext?: StartContext, cache?: BundleCache, inlineSourcemap?: boolean, bundleLogLevel: LogLevel = "DEBUG", ): Promise<[string, string]> { - const serializedTriggerContext = serializeTriggerContext(triggerContext); + const serializedStartContext = serializeStartContext(startContext); const contextHash = computeBundlerContextHash({ sourceFile: resolver.sourceFile, - serializedTriggerContext, + serializedStartContext, tsconfig, inlineSourcemap, bundleLogLevel, @@ -175,8 +175,8 @@ async function bundleSingleResolver( fs.writeFileSync(entryPath, entryContent); // Step 2: Bundle with tree-shaking (write: false to avoid unnecessary disk I/O) - const triggerPlugin = createTriggerTransformPlugin(triggerContext); - const plugins: rolldown.Plugin[] = triggerPlugin ? [triggerPlugin] : []; + const startPlugin = createStartTransformPlugin(startContext); + const plugins: rolldown.Plugin[] = startPlugin ? [startPlugin] : []; plugins.push(platformBundleDefinePlugin, ...cachePlugins); const result = await rolldown.build({ diff --git a/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts b/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts index b7f3d4f57..c302b207a 100644 --- a/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts +++ b/packages/sdk/src/cli/services/workflow/ast-transformer.test.ts @@ -4,16 +4,16 @@ import { describe, expect, test, vi } from "vitest"; import { logger } from "#/cli/shared/logger"; import { normalizeFilePath, - type TriggerContext, - type TriggerModuleBindings, - type TriggerTarget, -} from "#/cli/shared/trigger-context"; + type StartContext, + type StartModuleBindings, + type StartTarget, +} from "#/cli/shared/start-context"; import { findAllJobs } from "./job-detector"; import { transformWorkflowSource } from "./source-transformer"; import { hasStartCall, - transformFunctionTriggers as transformFunctionTriggersWithContext, -} from "./trigger-transformer"; + transformStartCalls as transformStartCallsWithContext, +} from "./start-transformer"; import { findAllWorkflows } from "./workflow-detector"; function parseProgram(source: string) { @@ -28,7 +28,7 @@ function findWorkflows(source: string) { return findAllWorkflows(parseProgram(source), source); } -function transformFunctionTriggers( +function transformStartCalls( source: string, workflowNameMap: Map, jobNameMap: Map, @@ -36,7 +36,7 @@ function transformFunctionTriggers( currentFilePath = path.resolve("test.ts"), authNamespace?: string, ) { - const localBindings = new Map(); + const localBindings = new Map(); for (const [name, jobName] of jobNameMap) { localBindings.set(name, { kind: "job", name: jobName }); } @@ -44,7 +44,7 @@ function transformFunctionTriggers( localBindings.set(name, { kind: "workflow", name: workflowName }); } - const modules = new Map([ + const modules = new Map([ [normalizeFilePath(currentFilePath), { localBindings, exports: new Map(localBindings) }], ]); for (const [file, workflowName] of workflowFileMap ?? []) { @@ -54,8 +54,8 @@ function transformFunctionTriggers( }); } - const context: TriggerContext = { modules, authNamespace }; - return transformFunctionTriggersWithContext(source, context, currentFilePath); + const context: StartContext = { modules, authNamespace }; + return transformStartCallsWithContext(source, context, currentFilePath); } function transformWorkflowJobSource( @@ -71,7 +71,7 @@ function transformWorkflowJobSource( targetJobExportName, otherJobExportNames, ); - return transformFunctionTriggers(stripped, new Map(), jobNameMap); + return transformStartCalls(stripped, new Map(), jobNameMap); } describe("AST Transformer - createWorkflowJob call detection", () => { @@ -539,7 +539,7 @@ const mainJob = createWorkflowJob({ expect(result).toContain('tailor.workflow.startJobFunction("job-two", undefined)'); }); - test("does not transform trigger calls inside fallback-removed job bodies", () => { + test("does not transform start calls inside fallback-removed job bodies", () => { const source = ` import { createWorkflowJob } from "@tailor-platform/sdk"; @@ -568,7 +568,7 @@ const mainJob = createWorkflowJob({ expect(result).not.toContain("startJobFunction"); }); - test("does not modify jobs without trigger calls", () => { + test("does not modify jobs without start calls", () => { const source = ` import { createWorkflowJob } from "@tailor-platform/sdk"; @@ -758,8 +758,8 @@ const workflow2 = createWorkflow({ name: "workflow-two", mainJob: job2 }); }); }); -describe("AST Transformer - transformFunctionTriggers", () => { - describe("workflow trigger transformation", () => { +describe("AST Transformer - transformStartCalls", () => { + describe("workflow start transformation", () => { test("transforms workflow.start() calls to tailor.workflow.startWorkflow()", () => { const source = ` const workflowRunId = await orderWorkflow.start( @@ -770,7 +770,7 @@ const workflowRunId = await orderWorkflow.start( const workflowNameMap = new Map([["orderWorkflow", "order-processing"]]); const jobNameMap = new Map(); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain('tailor.workflow.startWorkflow("order-processing"'); expect(result).toContain('{ orderId: "123", customerId: "456" }'); @@ -785,7 +785,7 @@ const result = await myWorkflow.start({ id: 1 }, { invoker }); const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain('tailor.workflow.startWorkflow("my-workflow"'); expect(result).toContain("{ invoker }"); @@ -798,7 +798,7 @@ const result = await myWorkflow.start({ id: 1 }); const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); - const result = transformFunctionTriggers( + const result = transformStartCalls( source, workflowNameMap, jobNameMap, @@ -808,7 +808,7 @@ const result = await myWorkflow.start({ id: 1 }); ); expect(result).toContain('tailor.workflow.startWorkflow("my-workflow", { id: 1 })'); - expect(result).not.toContain("__tailor_normalizeTriggerOptions"); + expect(result).not.toContain("__tailor_normalizeStartOptions"); }); test("wraps a string-literal invoker with the runtime normalizer when authNamespace is provided", () => { @@ -818,7 +818,7 @@ const result = await myWorkflow.start({ id: 1 }, { invoker: "kiosk" }); const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); - const result = transformFunctionTriggers( + const result = transformStartCalls( source, workflowNameMap, jobNameMap, @@ -828,10 +828,10 @@ const result = await myWorkflow.start({ id: 1 }, { invoker: "kiosk" }); ); expect(result).toContain('tailor.workflow.startWorkflow("my-workflow"'); - expect(result).toContain('__tailor_normalizeTriggerOptions({ invoker: "kiosk" })'); + expect(result).toContain('__tailor_normalizeStartOptions({ invoker: "kiosk" })'); // Helper injected at the top of the file with the namespace baked in expect(result).toContain( - 'const __tailor_normalizeTriggerOptions = (o) => { if (!o) return o; const { invoker, ...rest } = o; return typeof invoker === "string" ? { ...rest, authInvoker: { namespace: "my-auth", machineUserName: invoker } } : typeof invoker === "object" ? { ...rest, authInvoker: invoker } : o; };', + 'const __tailor_normalizeStartOptions = (o) => { if (!o) return o; const { invoker, ...rest } = o; return typeof invoker === "string" ? { ...rest, authInvoker: { namespace: "my-auth", machineUserName: invoker } } : typeof invoker === "object" ? { ...rest, authInvoker: invoker } : o; };', ); }); @@ -843,7 +843,7 @@ const result = await myWorkflow.start({ id: 1 }, { invoker: machineUser }); const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); - const result = transformFunctionTriggers( + const result = transformStartCalls( source, workflowNameMap, jobNameMap, @@ -852,7 +852,7 @@ const result = await myWorkflow.start({ id: 1 }, { invoker: machineUser }); "my-auth", ); - expect(result).toContain("__tailor_normalizeTriggerOptions({ invoker: machineUser })"); + expect(result).toContain("__tailor_normalizeStartOptions({ invoker: machineUser })"); }); test("wraps a shorthand invoker with the runtime normalizer", () => { @@ -863,7 +863,7 @@ const result = await myWorkflow.start({ id: 1 }, { invoker }); const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); - const result = transformFunctionTriggers( + const result = transformStartCalls( source, workflowNameMap, jobNameMap, @@ -872,7 +872,7 @@ const result = await myWorkflow.start({ id: 1 }, { invoker }); "my-auth", ); - expect(result).toContain("__tailor_normalizeTriggerOptions({ invoker })"); + expect(result).toContain("__tailor_normalizeStartOptions({ invoker })"); }); test("wraps a variable options argument with the runtime normalizer", () => { @@ -883,7 +883,7 @@ const result = await myWorkflow.start({ id: 1 }, opts); const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); - const result = transformFunctionTriggers( + const result = transformStartCalls( source, workflowNameMap, jobNameMap, @@ -893,7 +893,7 @@ const result = await myWorkflow.start({ id: 1 }, opts); ); expect(result).toContain( - 'tailor.workflow.startWorkflow("my-workflow", { id: 1 }, __tailor_normalizeTriggerOptions(opts))', + 'tailor.workflow.startWorkflow("my-workflow", { id: 1 }, __tailor_normalizeStartOptions(opts))', ); }); @@ -904,7 +904,7 @@ const result = await myWorkflow.start({ id: 1 }, { ...base }); const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); - const result = transformFunctionTriggers( + const result = transformStartCalls( source, workflowNameMap, jobNameMap, @@ -913,7 +913,7 @@ const result = await myWorkflow.start({ id: 1 }, { ...base }); "my-auth", ); - expect(result).toContain("__tailor_normalizeTriggerOptions({ ...base })"); + expect(result).toContain("__tailor_normalizeStartOptions({ ...base })"); }); test("transforms workflow.start() with an empty options object", () => { @@ -923,7 +923,7 @@ const result = await myWorkflow.start({ id: 1 }, {}); const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); - const result = transformFunctionTriggers( + const result = transformStartCalls( source, workflowNameMap, jobNameMap, @@ -933,11 +933,11 @@ const result = await myWorkflow.start({ id: 1 }, {}); ); expect(result).toContain( - 'tailor.workflow.startWorkflow("my-workflow", { id: 1 }, __tailor_normalizeTriggerOptions({}))', + 'tailor.workflow.startWorkflow("my-workflow", { id: 1 }, __tailor_normalizeStartOptions({}))', ); }); - test("injects the normalizer helper only once per file even for multiple trigger calls", () => { + test("injects the normalizer helper only once per file even for multiple start calls", () => { const source = ` await myWorkflow.start({ id: 1 }, { invoker: "kiosk" }); await myWorkflow.start({ id: 2 }, { invoker: "batch" }); @@ -945,7 +945,7 @@ await myWorkflow.start({ id: 2 }, { invoker: "batch" }); const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); - const result = transformFunctionTriggers( + const result = transformStartCalls( source, workflowNameMap, jobNameMap, @@ -954,7 +954,7 @@ await myWorkflow.start({ id: 2 }, { invoker: "batch" }); "my-auth", ); - const matches = result.match(/const __tailor_normalizeTriggerOptions =/g); + const matches = result.match(/const __tailor_normalizeStartOptions =/g); expect(matches).toHaveLength(1); }); @@ -965,15 +965,15 @@ const result = await myWorkflow.start({ id: 1 }, { invoker: "kiosk" }); const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain( 'tailor.workflow.startWorkflow("my-workflow", { id: 1 }, { invoker: "kiosk" })', ); - expect(result).not.toContain("__tailor_normalizeTriggerOptions"); + expect(result).not.toContain("__tailor_normalizeStartOptions"); }); }); - describe("job trigger transformation", () => { + describe("job start transformation", () => { test("transforms job.start() calls to tailor.workflow.startJobFunction()", () => { const source = ` const result = await fetchCustomer.start({ customerId: "123" }); @@ -981,7 +981,7 @@ const result = await fetchCustomer.start({ customerId: "123" }); const workflowNameMap = new Map(); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain('tailor.workflow.startJobFunction("fetch-customer"'); expect(result).toContain('{ customerId: "123" }'); @@ -994,7 +994,7 @@ const result = await simpleJob.start(); const workflowNameMap = new Map(); const jobNameMap = new Map([["simpleJob", "simple-job"]]); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain('tailor.workflow.startJobFunction("simple-job", undefined)'); }); @@ -1006,7 +1006,7 @@ const result = await fetchCustomer.start({ id: "123" }, { executionPolicyKey: "p const workflowNameMap = new Map(); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain( 'tailor.workflow.startJobFunction("fetch-customer", { id: "123" }, { executionPolicyKey: "premium" })', @@ -1020,7 +1020,7 @@ const result = await fetchCustomer.start({ id: "123" }); const workflowNameMap = new Map(); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); // Args only, no trailing options argument. expect(result).toContain('tailor.workflow.startJobFunction("fetch-customer", { id: "123" })'); @@ -1040,7 +1040,7 @@ const event = button.start("click"); const workflowNameMap = new Map(); const jobNameMap = new Map(); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); // Should remain unchanged expect(result).toContain('someRandomObject.start({ data: "test" })'); @@ -1048,7 +1048,7 @@ const event = button.start("click"); expect(result).not.toContain("tailor.workflow"); }); - test("only transforms trigger calls for known workflows and jobs", () => { + test("only transforms start calls for known workflows and jobs", () => { const source = ` // Known workflow - should be transformed const wfResult = await orderWorkflow.start({ id: 1 }, { invoker: "admin" }); @@ -1062,7 +1062,7 @@ const unknown = await randomThing.start({ id: 3 }); const workflowNameMap = new Map([["orderWorkflow", "order-processing"]]); const jobNameMap = new Map([["fetchData", "fetch-data"]]); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); // Known workflow transformed expect(result).toContain('tailor.workflow.startWorkflow("order-processing"'); @@ -1079,21 +1079,21 @@ const result = await myWorkflow.start({ id: 1 }); const workflowNameMap = new Map([["myWorkflow", "my-workflow"]]); const jobNameMap = new Map(); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain('tailor.workflow.startWorkflow("my-workflow", { id: 1 })'); expect(result).not.toContain("myWorkflow.start"); }); }); - describe("mixed workflow and job triggers", () => { - test("transforms both workflow and job triggers in the same source", () => { + describe("mixed workflow and job starts", () => { + test("transforms both workflow and job starts in the same source", () => { const source = ` async function processOrder(orderId: string) { - // Trigger a job to fetch data + // Start a job to fetch data const data = await fetchCustomer.start({ id: orderId }); - // Then trigger a workflow for processing + // Then start a workflow for processing const workflowRunId = await orderWorkflow.start( { orderId, data }, { invoker: "system" } @@ -1105,14 +1105,14 @@ async function processOrder(orderId: string) { const workflowNameMap = new Map([["orderWorkflow", "order-processing"]]); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain('tailor.workflow.startJobFunction("fetch-customer"'); expect(result).toContain('tailor.workflow.startWorkflow("order-processing"'); }); }); - describe("direct job trigger transformation", () => { + describe("direct job start transformation", () => { test("replaces job.start() with startJobFunction() and preserves await", () => { const source = ` const customer = await fetchCustomer.start({ customerId: "123" }); @@ -1121,7 +1121,7 @@ console.log(customer); const workflowNameMap = new Map(); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain( 'const customer = await tailor.workflow.startJobFunction("fetch-customer", { customerId: "123" })', @@ -1139,7 +1139,7 @@ const notification = await sendNotification.start({ message: "Hello" }); ["sendNotification", "send-notification"], ]); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain('tailor.workflow.startJobFunction("fetch-customer"'); expect(result).toContain('tailor.workflow.startJobFunction("send-notification"'); @@ -1152,7 +1152,7 @@ const executionId = await orderWorkflow.start({ orderId: "123" }, { invoker }); const workflowNameMap = new Map([["orderWorkflow", "order-processing"]]); const jobNameMap = new Map(); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain('await tailor.workflow.startWorkflow("order-processing"'); expect(result).not.toContain("(async () => tailor.workflow.startWorkflow"); @@ -1165,7 +1165,7 @@ const customerPromise = fetchCustomer.start({ customerId: "123" }); const workflowNameMap = new Map(); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain( 'const customerPromise = tailor.workflow.startJobFunction("fetch-customer", { customerId: "123" })', @@ -1186,7 +1186,7 @@ const [customer, notification] = await Promise.all([ ["sendNotification", "send-notification"], ]); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain( 'tailor.workflow.startJobFunction("fetch-customer", { customerId: "123" })', @@ -1206,14 +1206,14 @@ fetchCustomer.start({ customerId: "123" }).then((customer) => { const workflowNameMap = new Map(); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain( 'tailor.workflow.startJobFunction("fetch-customer", { customerId: "123" }).then(', ); }); - test("transforms only the outer call when a known trigger is nested inside another", () => { + test("transforms only the outer call when a known start call is nested inside another", () => { const source = ` await myWorkflow.start(fetchCustomer.start({ customerId: "123" })); `; @@ -1221,7 +1221,7 @@ await myWorkflow.start(fetchCustomer.start({ customerId: "123" })); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); using warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain( 'await tailor.workflow.startWorkflow("my-workflow", fetchCustomer.start({ customerId: "123" }));', @@ -1238,7 +1238,7 @@ unknown.start(fetchCustomer.start({ customerId: "123" })); const workflowNameMap = new Map(); const jobNameMap = new Map([["fetchCustomer", "fetch-customer"]]); - const result = transformFunctionTriggers(source, workflowNameMap, jobNameMap); + const result = transformStartCalls(source, workflowNameMap, jobNameMap); expect(result).toContain( 'tailor.workflow.startJobFunction("fetch-customer", { customerId: "123" })', diff --git a/packages/sdk/src/cli/services/workflow/bundler.test.ts b/packages/sdk/src/cli/services/workflow/bundler.test.ts index ab061897b..7e1c9e47d 100644 --- a/packages/sdk/src/cli/services/workflow/bundler.test.ts +++ b/packages/sdk/src/cli/services/workflow/bundler.test.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "pathe"; import { afterEach, describe, expect, test } from "vitest"; -import { buildTriggerContext, normalizeFilePath } from "#/cli/shared/trigger-context"; +import { buildStartContext, normalizeFilePath } from "#/cli/shared/start-context"; import { bundleWorkflowJobs } from "./bundler"; describe("bundleWorkflowJobs", () => { @@ -14,7 +14,7 @@ describe("bundleWorkflowJobs", () => { }); }); - describe("job trigger binding resolution", () => { + describe("job start binding resolution", () => { let tmpDir: string | undefined; afterEach(() => { @@ -59,7 +59,7 @@ export const mainB = createWorkflowJob({ export default createWorkflow({ name: "workflow-b", mainJob: mainB }); `, ); - const context = await buildTriggerContext({ files: [firstFile, secondFile] }); + const context = await buildStartContext({ files: [firstFile, secondFile] }); const result = await bundleWorkflowJobs( [ @@ -103,7 +103,7 @@ export const mainJob = createWorkflowJob({ export default createWorkflow({ name: "workflow", mainJob }); `, ); - const context = await buildTriggerContext({ files: [jobsFile, callerFile] }); + const context = await buildStartContext({ files: [jobsFile, callerFile] }); const result = await bundleWorkflowJobs( [ @@ -136,7 +136,7 @@ export const mainJob = createWorkflowJob({ export default createWorkflow({ name: "workflow", mainJob }); `, ); - const context = await buildTriggerContext({ files: [workflowFile] }); + const context = await buildStartContext({ files: [workflowFile] }); const result = await bundleWorkflowJobs( [ @@ -160,7 +160,7 @@ export default createWorkflow({ name: "workflow", mainJob }); type BuildBundleFixtureOptions = { ext: string; importPath: string; - triggerArgs?: string; + startArgs?: string; }; afterEach(() => { @@ -171,7 +171,7 @@ export default createWorkflow({ name: "workflow", mainJob }); }); const buildBundleFixture = (options: BuildBundleFixtureOptions) => { - const { ext, importPath, triggerArgs = `{ input: 0 }, { invoker: "admin" }` } = options; + const { ext, importPath, startArgs = `{ input: 0 }, { invoker: "admin" }` } = options; // Use realpathSync to avoid macOS symlink mismatch (/var -> /private/var) tmpDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "bundler-test-"))); @@ -206,7 +206,7 @@ import simpleWorkflow from "${importPath}"; export const callerJob = createWorkflowJob({ name: "caller-job", body: async () => { - const executionId = await simpleWorkflow.start(${triggerArgs}); + const executionId = await simpleWorkflow.start(${startArgs}); return { executionId }; }, }); @@ -224,7 +224,7 @@ export default createWorkflow({ ]; const mainJobNames = ["caller-job"]; - const triggerContext = { + const startContext = { modules: new Map([ [ normalizeFilePath(simpleFile), @@ -250,7 +250,7 @@ export default createWorkflow({ authNamespace: "default", }; - return bundleWorkflowJobs(allJobs, mainJobNames, {}, triggerContext); + return bundleWorkflowJobs(allJobs, mainJobNames, {}, startContext); }; test.each([ @@ -263,7 +263,7 @@ export default createWorkflow({ expect(result.bundledCode.has("caller-job")).toBe(true); const callerCode = result.bundledCode.get("caller-job")!; - // The trigger call should be transformed to startWorkflow + // The start call should be transformed to startWorkflow expect(callerCode).toContain("startWorkflow"); // The raw simpleWorkflow.start() should NOT remain in the bundle expect(callerCode).not.toContain("simpleWorkflow.start"); @@ -288,7 +288,7 @@ export default createWorkflow({ const result = await buildBundleFixture({ ext: "ts", importPath: "./simple", - triggerArgs: "{ input: 0 }", + startArgs: "{ input: 0 }", }); expect(result.bundledCode.has("caller-job")).toBe(true); diff --git a/packages/sdk/src/cli/services/workflow/bundler.ts b/packages/sdk/src/cli/services/workflow/bundler.ts index 1c08ad4a9..00cf6a1bf 100644 --- a/packages/sdk/src/cli/services/workflow/bundler.ts +++ b/packages/sdk/src/cli/services/workflow/bundler.ts @@ -11,15 +11,11 @@ import { composeFunctionTreeshakeOptions } from "#/cli/shared/function-treeshake import { logger, styles } from "#/cli/shared/logger"; import { platformBundleDefinePlugin } from "#/cli/shared/platform-bundle-plugin"; import { INVOKER_EXPR } from "#/cli/shared/runtime-exprs"; -import { serializeTriggerContext, type TriggerContext } from "#/cli/shared/trigger-context"; +import { serializeStartContext, type StartContext } from "#/cli/shared/start-context"; import ml from "#/utils/multiline"; import { findAllJobs } from "./job-detector"; import { transformWorkflowSource } from "./source-transformer"; -import { - detectResolvedTriggerCalls, - hasStartCall, - transformFunctionTriggers, -} from "./trigger-transformer"; +import { detectResolvedStartCalls, hasStartCall, transformStartCalls } from "./start-transformer"; import type { LogLevel } from "#/configure/config/types"; function safeRealpath(p: string): string { @@ -52,14 +48,14 @@ export interface BundleWorkflowJobsResult { * * This function: * 1. Detects which jobs are actually used (mainJobs + their dependencies) - * 2. Uses a transform plugin to transform trigger calls during bundling + * 2. Uses a transform plugin to transform start calls during bundling * 3. Creates entry file and bundles with tree-shaking * * Returns metadata about which jobs each workflow uses. * @param allJobs - All available job infos * @param mainJobNames - Names of main jobs * @param env - Environment variables to inject - * @param triggerContext - Trigger context for transformations + * @param startContext - Start context for transformations * @param cache - Optional bundle cache for skipping unchanged builds * @param inlineSourcemap - Whether to enable inline sourcemaps * @param bundleLogLevel - Controls which console calls are kept in bundled code @@ -69,7 +65,7 @@ export async function bundleWorkflowJobs( allJobs: JobInfo[], mainJobNames: string[], env: Record = {}, - triggerContext: TriggerContext, + startContext: StartContext, cache?: BundleCache, inlineSourcemap?: boolean, bundleLogLevel: LogLevel = "DEBUG", @@ -80,7 +76,7 @@ export async function bundleWorkflowJobs( } // Filter to only used jobs and get per-mainJob dependencies - const { usedJobs, mainJobDeps } = await filterUsedJobs(allJobs, mainJobNames, triggerContext); + const { usedJobs, mainJobDeps } = await filterUsedJobs(allJobs, mainJobNames, startContext); logger.newline(); logger.log( @@ -118,7 +114,7 @@ export async function bundleWorkflowJobs( outputDir, tsconfig, env, - triggerContext, + startContext, cache, inlineSourcemap, bundleLogLevel, @@ -153,13 +149,13 @@ interface FilterUsedJobsResult { * Also returns a map of mainJob -> all jobs it depends on (for metadata). * @param allJobs - All available job infos * @param mainJobNames - Names of main jobs - * @param triggerContext - Module binding metadata for resolving trigger targets + * @param startContext - Module binding metadata for resolving start targets * @returns Used jobs and main job dependency map */ async function filterUsedJobs( allJobs: JobInfo[], mainJobNames: string[], - triggerContext: TriggerContext, + startContext: StartContext, ): Promise { if (allJobs.length === 0 || mainJobNames.length === 0) { return { usedJobs: [], mainJobDeps: {} }; @@ -173,8 +169,8 @@ async function filterUsedJobs( jobsBySourceFile.set(job.sourceFile, existing); } - // Detect trigger calls and build dependency graph - // Maps job name -> set of job names it triggers + // Detect start calls and build dependency graph + // Maps job name -> set of job names it starts const dependencies = new Map>(); // Process all source files in parallel @@ -186,14 +182,9 @@ async function filterUsedJobs( // Find all jobs in this file to get body ranges const detectedJobs = findAllJobs(program, source); - const triggerCalls = detectResolvedTriggerCalls( - program, - source, - triggerContext, - sourceFile, - ); - - // For each job in this file, find which triggers are inside its body + const startCalls = detectResolvedStartCalls(program, source, startContext, sourceFile); + + // For each job in this file, find which start calls are inside its body const jobDependencies: Array<{ jobName: string; deps: Set }> = []; for (const job of jobs) { @@ -202,8 +193,8 @@ async function filterUsedJobs( const jobDeps = new Set(); - for (const call of triggerCalls) { - // Check if this trigger call is inside the job's body + for (const call of startCalls) { + // Check if this start call is inside the job's body if ( call.kind === "job" && call.callRange.start >= detectedJob.bodyValueRange.start && @@ -273,12 +264,12 @@ async function bundleSingleJob( outputDir: string, tsconfig: string | undefined, env: Record, - triggerContext: TriggerContext, + startContext: StartContext, cache?: BundleCache, inlineSourcemap?: boolean, bundleLogLevel: LogLevel = "DEBUG", ): Promise<[string, string]> { - const serializedTriggerContext = serializeTriggerContext(triggerContext); + const serializedStartContext = serializeStartContext(startContext); // Include sorted env variables as a prefix so that env changes invalidate the cache const sortedEnvPrefix = JSON.stringify( @@ -286,7 +277,7 @@ async function bundleSingleJob( ); const contextHash = computeBundlerContextHash({ sourceFile: job.sourceFile, - serializedTriggerContext, + serializedStartContext, tsconfig, inlineSourcemap, bundleLogLevel, @@ -318,7 +309,7 @@ async function bundleSingleJob( // Pre-compute once to avoid redundant realpathSync calls per module const resolvedSourceFile = safeRealpath(job.sourceFile); - // Step 2: Bundle with a transform plugin that transforms trigger calls + // Step 2: Bundle with a transform plugin that transforms start calls // Collect export names for enhanced AST removal (catches jobs missed by AST detection) const otherJobExportNames = allJobs .filter( @@ -328,7 +319,7 @@ async function bundleSingleJob( ) .map((j) => j.exportName); - // Create transform plugin to transform trigger calls and remove other job declarations + // Create transform plugin to transform start calls and remove other job declarations const transformPlugin: rolldown.Plugin = { name: "workflow-transform", transform: { @@ -364,7 +355,7 @@ async function bundleSingleJob( // Apply workflow.start / job.start transformation. if (hasStartCall(transformed)) { - transformed = transformFunctionTriggers(transformed, triggerContext, id); + transformed = transformStartCalls(transformed, startContext, id); } if (transformed === code) return null; diff --git a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts b/packages/sdk/src/cli/services/workflow/start-transformer.ts similarity index 84% rename from packages/sdk/src/cli/services/workflow/trigger-transformer.ts rename to packages/sdk/src/cli/services/workflow/start-transformer.ts index 192c7cbdf..2fa42cd27 100644 --- a/packages/sdk/src/cli/services/workflow/trigger-transformer.ts +++ b/packages/sdk/src/cli/services/workflow/start-transformer.ts @@ -3,10 +3,10 @@ import * as path from "pathe"; import { logger } from "#/cli/shared/logger"; import { normalizeFilePath, - type TriggerContext, - type TriggerModuleBindings, - type TriggerTarget, -} from "#/cli/shared/trigger-context"; + type StartContext, + type StartModuleBindings, + type StartTarget, +} from "#/cli/shared/start-context"; import { type ASTNode, type Replacement, @@ -18,7 +18,7 @@ import { import type { Program } from "@oxc-project/types"; import type { Plugin } from "rolldown"; -export interface ResolvedTriggerCall extends StartCallInfo { +export interface ResolvedStartCall extends StartCallInfo { kind: "job" | "workflow"; targetName: string; } @@ -36,12 +36,12 @@ export function hasStartCall(code: string): boolean { return START_CALL_RE.test(code); } -const NORMALIZER_IDENTIFIER = "__tailor_normalizeTriggerOptions"; +const NORMALIZER_IDENTIFIER = "__tailor_normalizeStartOptions"; /** * Build the source text of the injected normalizer helper. * - * Renames an `invoker` in the trigger options to the `authInvoker` form the + * Renames an `invoker` in the start options to the `authInvoker` form the * platform RPC expects: a plain string (machine user name) becomes * `{ namespace, machineUserName }`, while an object form passes through * as-is. Any other options value is unchanged. The auth namespace is baked @@ -239,10 +239,10 @@ function walkBindingAware( } function resolveRelativeImport( - context: TriggerContext, + context: StartContext, currentFilePath: string, importSource: string, -): TriggerModuleBindings | undefined { +): StartModuleBindings | undefined { if (!importSource.startsWith(".")) return undefined; const currentDirectory = path.dirname(currentFilePath.replace(/[?#].*$/, "")); const modulePath = normalizeFilePath(path.resolve(currentDirectory, importSource)); @@ -251,10 +251,10 @@ function resolveRelativeImport( function collectLocalTargets( program: Program, - context: TriggerContext, + context: StartContext, currentFilePath: string, -): Map { - const targets = new Map(); +): Map { + const targets = new Map(); const currentModule = context.modules.get(normalizeFilePath(currentFilePath)); if (currentModule) { for (const [localName, target] of currentModule.localBindings) { @@ -287,73 +287,73 @@ function collectLocalTargets( return targets; } -function detectTriggerCallsWithTargets( +function detectStartCallsWithTargets( program: Program, sourceText: string, - targets: Map, -): ResolvedTriggerCall[] { - const calls: ResolvedTriggerCall[] = []; + targets: Map, +): ResolvedStartCall[] { + const calls: ResolvedStartCall[] = []; const targetNames = new Set(targets.keys()); walkBindingAware(program, targetNames, (node, shadowedNames) => { - const triggerCall = getStartCallInfo(node, sourceText); - if (!triggerCall || shadowedNames.has(triggerCall.identifierName)) return; - const target = targets.get(triggerCall.identifierName); + const startCall = getStartCallInfo(node, sourceText); + if (!startCall || shadowedNames.has(startCall.identifierName)) return; + const target = targets.get(startCall.identifierName); if (target) { - calls.push({ ...triggerCall, kind: target.kind, targetName: target.name }); + calls.push({ ...startCall, kind: target.kind, targetName: target.name }); } }); return calls; } -export function detectResolvedTriggerCalls( +export function detectResolvedStartCalls( program: Program, sourceText: string, - context: TriggerContext, + context: StartContext, currentFilePath: string, -): ResolvedTriggerCall[] { - return detectTriggerCallsWithTargets( +): ResolvedStartCall[] { + return detectStartCallsWithTargets( program, sourceText, collectLocalTargets(program, context, currentFilePath), ); } -export function transformFunctionTriggers( +export function transformStartCalls( source: string, - triggerContext: TriggerContext, + startContext: StartContext, currentFilePath: string, ): string { const { program } = parseSync("input.ts", source); - const localTargets = collectLocalTargets(program, triggerContext, currentFilePath); - const { authNamespace } = triggerContext; - const allTriggerCalls = detectTriggerCallsWithTargets(program, source, localTargets); - const nestedTriggerCalls: Array<{ call: ResolvedTriggerCall; parent: ResolvedTriggerCall }> = []; - const triggerCalls = allTriggerCalls.filter((call) => { - const parent = allTriggerCalls.find( + const localTargets = collectLocalTargets(program, startContext, currentFilePath); + const { authNamespace } = startContext; + const allStartCalls = detectStartCallsWithTargets(program, source, localTargets); + const nestedStartCalls: Array<{ call: ResolvedStartCall; parent: ResolvedStartCall }> = []; + const startCalls = allStartCalls.filter((call) => { + const parent = allStartCalls.find( (other) => other !== call && other.callRange.start <= call.callRange.start && call.callRange.end <= other.callRange.end, ); if (!parent) return true; - nestedTriggerCalls.push({ call, parent }); + nestedStartCalls.push({ call, parent }); return false; }); - for (const { call, parent } of nestedTriggerCalls) { + for (const { call, parent } of nestedStartCalls) { logger.warn( `Nested start call "${call.identifierName}.start(...)" inside "${parent.identifierName}.start(...)" cannot be converted. Move it to a separate statement and pass the result instead.`, ); } const replacements: Replacement[] = []; - // Whether any workflow trigger invoker was wrapped with the runtime + // Whether any workflow start invoker was wrapped with the runtime // normalizer. Used to decide whether to inject the helper at the top. let needsNormalizerHelper = false; - for (const call of triggerCalls) { + for (const call of startCalls) { let transformedCall: string; if (call.kind === "workflow") { let optionsPart = ""; @@ -383,18 +383,18 @@ export function transformFunctionTriggers( : transformed; } -export function createTriggerTransformPlugin( - triggerContext: TriggerContext | undefined, +export function createStartTransformPlugin( + startContext: StartContext | undefined, ): Plugin | undefined { - if (!triggerContext) return undefined; + if (!startContext) return undefined; return { - name: "trigger-transform", + name: "start-transform", transform: { filter: { id: { include: [/\.(ts|mts|cts|js|mjs|cjs)$/] } }, handler(code, id) { if (!hasStartCall(code)) return null; - return { code: transformFunctionTriggers(code, triggerContext, id) }; + return { code: transformStartCalls(code, startContext, id) }; }, }, }; diff --git a/packages/sdk/src/cli/shared/trigger-context.test.ts b/packages/sdk/src/cli/shared/start-context.test.ts similarity index 86% rename from packages/sdk/src/cli/shared/trigger-context.test.ts rename to packages/sdk/src/cli/shared/start-context.test.ts index de78b85c1..d0b41870f 100644 --- a/packages/sdk/src/cli/shared/trigger-context.test.ts +++ b/packages/sdk/src/cli/shared/start-context.test.ts @@ -2,30 +2,30 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import * as path from "pathe"; import { afterEach, describe, expect, test } from "vitest"; -import { transformFunctionTriggers } from "#/cli/services/workflow/trigger-transformer"; +import { transformStartCalls } from "#/cli/services/workflow/start-transformer"; import { - buildTriggerContext, + buildStartContext, normalizeFilePath, - serializeTriggerContext, - type TriggerContext, - type TriggerModuleBindings, -} from "./trigger-context"; + serializeStartContext, + type StartContext, + type StartModuleBindings, +} from "./start-context"; -describe("serializeTriggerContext", () => { - function emptyContext(): TriggerContext { +describe("serializeStartContext", () => { + function emptyContext(): StartContext { return { modules: new Map() }; } - function bindings(localBindings: TriggerModuleBindings["localBindings"]) { + function bindings(localBindings: StartModuleBindings["localBindings"]) { return { localBindings, exports: new Map(localBindings) }; } test("returns empty string for undefined", () => { - expect(serializeTriggerContext(undefined)).toBe(""); + expect(serializeStartContext(undefined)).toBe(""); }); test("returns deterministic output for empty maps", () => { - expect(serializeTriggerContext(emptyContext())).toBe("[]"); + expect(serializeStartContext(emptyContext())).toBe("[]"); }); test("returns same output regardless of map insertion order", () => { @@ -49,7 +49,7 @@ describe("serializeTriggerContext", () => { bindings(new Map([["workflow", { kind: "workflow", name: "WorkflowB" }]])), ); - expect(serializeTriggerContext(first)).toBe(serializeTriggerContext(second)); + expect(serializeStartContext(first)).toBe(serializeStartContext(second)); }); test("returns different output when map content differs", () => { @@ -61,7 +61,7 @@ describe("serializeTriggerContext", () => { bindings(new Map([["job", { kind: "job", name: "ProcessPayment" }]])), ); - expect(serializeTriggerContext(first)).not.toBe(serializeTriggerContext(second)); + expect(serializeStartContext(first)).not.toBe(serializeStartContext(second)); }); test("distinguishes entries in different maps", () => { @@ -73,11 +73,11 @@ describe("serializeTriggerContext", () => { const second = emptyContext(); second.modules.set("/module", bindings(new Map([["target", { kind: "job", name: "Name" }]]))); - expect(serializeTriggerContext(first)).not.toBe(serializeTriggerContext(second)); + expect(serializeStartContext(first)).not.toBe(serializeStartContext(second)); }); }); -describe("buildTriggerContext", () => { +describe("buildStartContext", () => { const tempDirs: string[] = []; afterEach(() => { @@ -87,7 +87,7 @@ describe("buildTriggerContext", () => { }); async function createDuplicateExportContext() { - const tempDir = mkdtempSync(path.join(tmpdir(), "trigger-context-")); + const tempDir = mkdtempSync(path.join(tmpdir(), "start-context-")); tempDirs.push(tempDir); const firstPath = path.join(tempDir, "first.ts"); const secondPath = path.join(tempDir, "second.ts"); @@ -101,12 +101,12 @@ export const step = createWorkflowJob({ name: "step-b", body: async () => "b" }) `; writeFileSync(firstPath, firstSource); writeFileSync(secondPath, secondSource); - const context = await buildTriggerContext({ files: [firstPath, secondPath] }); + const context = await buildStartContext({ files: [firstPath, secondPath] }); return { context, firstPath, firstSource, tempDir }; } - function transform(source: string, currentFilePath: string, context: TriggerContext) { - return transformFunctionTriggers(source, context, currentFilePath); + function transform(source: string, currentFilePath: string, context: StartContext) { + return transformStartCalls(source, context, currentFilePath); } test("resolves duplicate local export names from the current module", async () => { @@ -142,7 +142,7 @@ const mainJob = createWorkflowJob({ name: "main-job", body: async () => "done" } export default createWorkflow({ name: "workflow-a", mainJob }); `, ); - const context = await buildTriggerContext({ files: [workflowPath] }); + const context = await buildStartContext({ files: [workflowPath] }); const source = ` import workflow from "./workflow"; await workflow.start(); @@ -165,7 +165,7 @@ const mainJob = createWorkflowJob({ name: "main-job", body: async () => "done" } export default createWorkflow({ name: "workflow-a", mainJob }); `, ); - const context = await buildTriggerContext({ files: [workflowPath] }); + const context = await buildStartContext({ files: [workflowPath] }); const source = ` import workflow, * as helpers from "./workflow"; console.log(helpers); diff --git a/packages/sdk/src/cli/shared/trigger-context.ts b/packages/sdk/src/cli/shared/start-context.ts similarity index 81% rename from packages/sdk/src/cli/shared/trigger-context.ts rename to packages/sdk/src/cli/shared/start-context.ts index 9a6dbfac6..f341d990c 100644 --- a/packages/sdk/src/cli/shared/trigger-context.ts +++ b/packages/sdk/src/cli/shared/start-context.ts @@ -7,23 +7,23 @@ import { findAllJobs } from "#/cli/services/workflow/job-detector"; import { findAllWorkflows } from "#/cli/services/workflow/workflow-detector"; import { logger } from "#/cli/shared/logger"; -export interface TriggerTarget { +export interface StartTarget { kind: "job" | "workflow"; name: string; } -export interface TriggerModuleBindings { - localBindings: Map; - exports: Map; +export interface StartModuleBindings { + localBindings: Map; + exports: Map; } -export interface TriggerContext { - modules: Map; +export interface StartContext { + modules: Map; authNamespace?: string; } /** - * Normalize a source module path for trigger binding lookup. + * Normalize a source module path for start-call binding lookup. * @param filePath - Source file path or extensionless relative import path * @returns Absolute path without a JavaScript or TypeScript extension */ @@ -32,8 +32,8 @@ export function normalizeFilePath(filePath: string): string { } function createModuleBindings(program: ReturnType["program"], source: string) { - const localBindings = new Map(); - const exports = new Map(); + const localBindings = new Map(); + const exports = new Map(); for (const workflow of findAllWorkflows(program, source)) { const target = { kind: "workflow", name: workflow.name } as const; @@ -79,20 +79,20 @@ function createModuleBindings(program: ReturnType["program"], } } - return { localBindings, exports } satisfies TriggerModuleBindings; + return { localBindings, exports } satisfies StartModuleBindings; } /** - * Build trigger context from configured workflow source files. + * Build start-call context from configured workflow source files. * @param workflowConfig - Workflow file loading configuration * @param authNamespace - Auth service namespace (optional, used for string-literal invoker expansion) * @returns Module-local workflow and job binding metadata */ -export async function buildTriggerContext( +export async function buildStartContext( workflowConfig: FileLoadConfig | undefined, authNamespace?: string, -): Promise { - const modules = new Map(); +): Promise { + const modules = new Map(); if (!workflowConfig) return { modules, authNamespace }; for (const file of loadFilesWithIgnores(workflowConfig)) { @@ -111,18 +111,18 @@ export async function buildTriggerContext( return { modules, authNamespace }; } -function sortedTargets(bindings: Map) { +function sortedTargets(bindings: Map) { return [...bindings] .toSorted(([a], [b]) => a.localeCompare(b)) .map(([binding, target]) => [binding, target.kind, target.name]); } /** - * Serialize trigger context to a deterministic cache input. - * @param context - Trigger context to serialize + * Serialize start-call context to a deterministic cache input. + * @param context - Start-call context to serialize * @returns Deterministic string, or an empty string when context is absent */ -export function serializeTriggerContext(context: TriggerContext | undefined): string { +export function serializeStartContext(context: StartContext | undefined): string { if (!context) return ""; const modules = [...context.modules] .toSorted(([a], [b]) => a.localeCompare(b)) From 05f188c51268086ff804f41e9231ae07594112ab Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 09:57:38 +0900 Subject: [PATCH 581/618] fix(sdk-codemod): drop unreachable .start( suspiciousPattern from workflow-trigger-dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sdk-codemod's CLI applies all applicable codemods in a single --from/--to pass. v2/workflow-start-rename has no scriptPath, so it never auto-transforms .trigger() to .start() within that pass, and pre-migration v1 code cannot contain .start( at all (the method did not exist under that name). The scenario this pattern guarded against — a file already renamed to .start( before workflow-trigger-dispatch's suspiciousPatterns are checked — is unreachable in the tool's actual single-shot usage. --- packages/sdk-codemod/src/registry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index e2bbebe39..ffd07b7a7 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -865,7 +865,7 @@ export const allCodemods: CodemodPackage[] = [ since: "1.0.0", until: "2.0.0", prereleaseUntil: V2_NEXT_1, - suspiciousPatterns: [".trigger(", ".start("], + suspiciousPatterns: [".trigger("], examples: [ { caption: "Tests must mock the workflow runtime instead of running bodies locally:", From 898d0b0f809d15ea883a32a78a247eda4ca7caa7 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 15:21:22 +0900 Subject: [PATCH 582/618] fix(codemod): align v2 prerelease codemod boundaries with actual release versions `v2/db-type-to-table` and `v2/runtime-subpath-namespace` declared `prereleaseUntil: 2.0.0-next.3`, but their breaking changes shipped in `2.0.0-next.4`. `v2/forward-relation-name`, `v2/tailordb-validate-simplify`, and `v2/tailordb-hook-redesign` declared `2.0.0-next.4`, but shipped in `2.0.0-next.5`. Because getApplicableCodemods() requires fromVersion < boundary <= toVersion, upgrading exactly across the real release boundary (e.g. --from 2.0.0-next.4 on a 2.0.0-next.5 project) matched none of these codemods. --- .changeset/fix-next5-codemod-boundaries.md | 5 +++++ packages/sdk-codemod/src/registry.test.ts | 6 +++--- packages/sdk-codemod/src/registry.ts | 12 ++++++------ 3 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 .changeset/fix-next5-codemod-boundaries.md diff --git a/.changeset/fix-next5-codemod-boundaries.md b/.changeset/fix-next5-codemod-boundaries.md new file mode 100644 index 000000000..9d0d9b68d --- /dev/null +++ b/.changeset/fix-next5-codemod-boundaries.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Fix `tailor upgrade` reporting zero codemods across several v2 prerelease boundaries. `v2/db-type-to-table` and `v2/runtime-subpath-namespace` now trigger at `2.0.0-next.4` (where they actually shipped) instead of `2.0.0-next.3`, and `v2/forward-relation-name`, `v2/tailordb-validate-simplify`, and `v2/tailordb-hook-redesign` now trigger at `2.0.0-next.5` instead of `2.0.0-next.4`. diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 74d5cd53e..0e7fb7b95 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -48,7 +48,7 @@ describe("getApplicableCodemods", () => { }); test("returns db.type to db.table codemod for the prerelease that removes db.type", () => { - const prereleaseIds = getApplicableCodemods("1.67.1", "2.0.0-next.3").map( + const prereleaseIds = getApplicableCodemods("1.67.1", "2.0.0-next.4").map( (codemod) => codemod.id, ); @@ -56,10 +56,10 @@ describe("getApplicableCodemods", () => { }); test("reviews forward relation names at the prerelease that changes their defaults", () => { - const previousIds = getApplicableCodemods("1.67.1", "2.0.0-next.3").map( + const previousIds = getApplicableCodemods("1.67.1", "2.0.0-next.4").map( (codemod) => codemod.id, ); - const codemod = getApplicableCodemods("2.0.0-next.3", "2.0.0-next.4").find( + const codemod = getApplicableCodemods("2.0.0-next.4", "2.0.0-next.5").find( (entry) => entry.id === "v2/forward-relation-name", ); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index ad4e3df88..a6afe677d 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -96,8 +96,8 @@ const RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN = new RegExp( ); const V2_NEXT_1 = "2.0.0-next.1"; const V2_NEXT_2 = "2.0.0-next.2"; -const V2_NEXT_3 = "2.0.0-next.3"; const V2_NEXT_4 = "2.0.0-next.4"; +const V2_NEXT_5 = "2.0.0-next.5"; /** All registered codemods, in registration order. */ export const allCodemods: CodemodPackage[] = [ @@ -509,7 +509,7 @@ export const allCodemods: CodemodPackage[] = [ "Rewrite `@tailor-platform/sdk/runtime/*` namespace-star and flat value imports to self-named namespace imports, and aggregate `file.deleteFile` calls to `file.delete`. `TailorContextAPI` and `TailorWorkflowAPI` now describe SDK wrappers; direct platform globals use `PlatformContextAPI` and `PlatformWorkflowAPI`.", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_3, + prereleaseUntil: V2_NEXT_4, scriptPath: "v2/runtime-subpath-namespace/scripts/transform.js", filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], legacyPatterns: [ @@ -592,7 +592,7 @@ export const allCodemods: CodemodPackage[] = [ "Rename TailorDB schema builder calls from `db.type()` to `db.table()`. TailorDB schema definitions now use table terminology in SDK projects.", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_3, + prereleaseUntil: V2_NEXT_4, scriptPath: "v2/db-type-to-table/scripts/transform.js", filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], legacyPatterns: ["db.type"], @@ -623,7 +623,7 @@ export const allCodemods: CodemodPackage[] = [ "Review TailorDB relations that omit `toward.as`. Their forward GraphQL field names now derive from the relation field name with a trailing `ID`, `Id`, or `id` removed, instead of from the target table name.", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_4, + prereleaseUntil: V2_NEXT_5, scriptPath: "v2/forward-relation-name/scripts/transform.js", filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], suspiciousPatterns: [ @@ -956,7 +956,7 @@ export const allCodemods: CodemodPackage[] = [ "Field-level `ValidateFn` is simplified from `(args: { value, data, invoker }) => boolean` to `(args: { value }) => string | void` — the function now returns the error message directly instead of a separate `[fn, message]` tuple. The `ValidateConfig` tuple form and `Validators` record syntax on `db.type().validate()` are removed. Type-level validation uses `db.type().validate((args, issues) => void)` with `{ newRecord, oldRecord, invoker }` args and an `issues(field, message)` callback for cross-field rules.", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_4, + prereleaseUntil: V2_NEXT_5, suspiciousPatterns: ["ValidateConfig", "Validators<", "ValidatorsBase", ".validate("], examples: [ { @@ -1007,7 +1007,7 @@ export const allCodemods: CodemodPackage[] = [ "Field-level `HookFn` args change from `{ value, data, invoker }` to create `{ input, invoker, now }` / update `{ input, oldValue, invoker, now }` — `value` is renamed to `input`, matching the `input` arg on type-level hooks (same pre-hook data, narrowed to one field); `data` (the full record) is removed; `oldValue` (previous field value) is added for update hooks only; `now` (operation timestamp) is shared across all hooks. Type-level hooks on `db.type().hooks()` change from per-field mapping `{ fieldName: { create, update } }` (`Hooks`) to a single `{ create, update }` object (`TypeHook`) — create hooks take `{ input, invoker, now }`, update hooks take `{ input, oldRecord, invoker, now }` (oldRecord is always non-null). Both return partial field overrides.", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_4, + prereleaseUntil: V2_NEXT_5, suspiciousPatterns: ["Hooks<", "HookFn<", "Hook<", ".hooks("], examples: [ { From 56d776ed1fa16c9c0b04995d8b27f365400cda09 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 15:53:05 +0900 Subject: [PATCH 583/618] refactor(cli): remove legacy bundle artifact cleanup shim removeLegacyBundleFiles (packages/sdk/src/cli/services/stale-cleanup.ts) was a backward-compat shim that cleaned up on-disk bundle artifacts written by pre-virtual-entry SDK versions. Bundlers no longer write those files, so the shim is now dead code. - Delete stale-cleanup.ts - Remove the removeLegacyBundleFiles call and import in deploy.ts - Remove the legacy-file generation in __test_fixtures__/prepare.ts and the corresponding assertion in integration.test.ts --- .../__test_fixtures__/integration.test.ts | 3 -- .../deploy/__test_fixtures__/prepare.ts | 16 ------ .../sdk/src/cli/commands/deploy/deploy.ts | 2 - .../sdk/src/cli/services/stale-cleanup.ts | 50 ------------------- 4 files changed, 71 deletions(-) delete mode 100644 packages/sdk/src/cli/services/stale-cleanup.ts diff --git a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/integration.test.ts b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/integration.test.ts index ba9561bb6..4bcbc8e94 100644 --- a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/integration.test.ts +++ b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/integration.test.ts @@ -79,9 +79,6 @@ describe("deploy command integration tests", () => { ); expect(entryFiles).toEqual([]); - // Unrelated files in former bundle directories should be preserved - expect(actualFiles).toContain("resolvers/keep.txt"); - // Bundle output files should NOT exist on disk (in-memory only) const bundleOutputFiles = actualFiles.filter( (f) => f.endsWith(".js") && !f.endsWith(".entry.js"), diff --git a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/prepare.ts b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/prepare.ts index 677412a16..e115a54a0 100644 --- a/packages/sdk/src/cli/commands/deploy/__test_fixtures__/prepare.ts +++ b/packages/sdk/src/cli/commands/deploy/__test_fixtures__/prepare.ts @@ -23,22 +23,6 @@ export async function prepareFixtures(): Promise<{ fs.rmSync(outputDir, { recursive: true }); } - for (const file of [ - "resolvers/legacy.entry.js", - "executors/legacy.entry.js", - "workflow-jobs/legacy.entry.js", - "workflow-jobs/removed-job.js", - "workflow-jobs/removed-job.js.map", - "auth-hooks/legacy.entry.js", - "http-adapters/legacy.input.entry.js", - "hooks-validate-scripts/LegacyType/tailordb-script-0.entry.ts", - "resolvers/keep.txt", - ]) { - const filePath = path.join(outputDir, file); - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, "legacy bundle artifact"); - } - const configPath = path.join(fixtureDir, "tailor.config.ts"); process.env.TAILOR_BUILD_OUTPUT_DIR = outputDir; diff --git a/packages/sdk/src/cli/commands/deploy/deploy.ts b/packages/sdk/src/cli/commands/deploy/deploy.ts index b6c1b5269..ad74f3a77 100644 --- a/packages/sdk/src/cli/commands/deploy/deploy.ts +++ b/packages/sdk/src/cli/commands/deploy/deploy.ts @@ -4,7 +4,6 @@ import * as path from "pathe"; import { hashFile } from "#/cli/cache/hasher"; import { createCacheManager } from "#/cli/cache/manager"; import { loadApplication, type Application } from "#/cli/services/application"; -import { removeLegacyBundleFiles } from "#/cli/services/stale-cleanup"; import { assertUniqueTailorDBTypeNamesWithExternal } from "#/cli/services/tailordb/type-name-validation"; import { getOrNull, type OperatorClient } from "#/cli/shared/client"; import { loadConfig } from "#/cli/shared/config-loader"; @@ -950,7 +949,6 @@ export async function buildDeploymentTargets( buildTarget = buildDeploymentTarget, ...targetParams } = params; - await removeLegacyBundleFiles(path.resolve(getDistDir())); return Promise.all( configPaths.map((configPath, index) => diff --git a/packages/sdk/src/cli/services/stale-cleanup.ts b/packages/sdk/src/cli/services/stale-cleanup.ts deleted file mode 100644 index 66717e81c..000000000 --- a/packages/sdk/src/cli/services/stale-cleanup.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as fs from "node:fs/promises"; -import * as path from "pathe"; - -const legacyBundleDirectories: ReadonlyArray<{ - name: string; - suffixes: readonly string[]; -}> = [ - { name: "resolvers", suffixes: [".entry.js"] }, - { name: "executors", suffixes: [".entry.js"] }, - { name: "workflow-jobs", suffixes: [".js", ".js.map"] }, - { name: "auth-hooks", suffixes: [".entry.js"] }, - { name: "http-adapters", suffixes: [".entry.js"] }, -]; - -/** - * Remove bundle artifacts created by SDK versions that used disk-backed entries. - * - * Concurrent callers are safe because current bundlers no longer create files - * in these directories and each removal uses `force: true`. - * @param outputRoot - SDK output directory - */ -export async function removeLegacyBundleFiles(outputRoot: string): Promise { - await Promise.all([ - ...legacyBundleDirectories.map(({ name, suffixes }) => - removeMatchingFiles(path.join(outputRoot, name), suffixes), - ), - fs.rm(path.join(outputRoot, "hooks-validate-scripts"), { - recursive: true, - force: true, - }), - ]); -} - -async function removeMatchingFiles(outputDir: string, suffixes: readonly string[]): Promise { - let files: string[]; - try { - files = await fs.readdir(outputDir); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") { - return; - } - throw error; - } - - for (const file of files) { - if (suffixes.some((suffix) => file.endsWith(suffix))) { - await fs.rm(path.join(outputDir, file), { force: true }); - } - } -} From da7d0c49322deebc9343dee88652152620a7cef9 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 15:57:35 +0900 Subject: [PATCH 584/618] chore: add changeset --- .changeset/remove-legacy-bundle-cleanup.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/remove-legacy-bundle-cleanup.md diff --git a/.changeset/remove-legacy-bundle-cleanup.md b/.changeset/remove-legacy-bundle-cleanup.md new file mode 100644 index 000000000..07011a671 --- /dev/null +++ b/.changeset/remove-legacy-bundle-cleanup.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": patch +--- + +Remove the internal cleanup step that deleted on-disk bundle artifacts left by SDK versions predating the in-memory bundling approach From 6653afd5a0be52f8c9a1dc6fa50e445f0f678dc0 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 16:04:39 +0900 Subject: [PATCH 585/618] feat(codemod): auto-resolve pending prerelease codemod boundaries in CI Add a V2_NEXT_PENDING sentinel prereleaseUntil for a codemod whose exact 2.0.0-next.N release boundary is not known yet at implementation time, since that number is only decided once the release PR bumps the version. A codemod pinned to V2_NEXT_PENDING never applies via getApplicableCodemods() until resolved. `pnpm codemod:resolve-pending` rewrites it to the concrete V2_NEXT_N constant (reusing an existing one when its value already matches) once the real version is known. Wire this into release.yml: right after changesets/action creates or updates the release PR, checkout that PR branch, run the resolver, and push any fixup through the GitHub Contents API (like ensure-github-releases.sh) rather than a local git commit, since this token has no configured git identity or signing key. --- .../resolve-pending-codemod-boundaries.md | 5 ++ .../resolve-pending-codemod-boundaries.sh | 36 +++++++++ .github/workflows/release.yml | 10 +++ package.json | 1 + .../scripts/resolve-pending-boundaries.ts | 23 ++++++ packages/sdk-codemod/src/registry.ts | 15 +++- .../src/resolve-pending-boundaries.test.ts | 81 +++++++++++++++++++ .../src/resolve-pending-boundaries.ts | 65 +++++++++++++++ 8 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 .changeset/resolve-pending-codemod-boundaries.md create mode 100644 .github/scripts/resolve-pending-codemod-boundaries.sh create mode 100644 packages/sdk-codemod/scripts/resolve-pending-boundaries.ts create mode 100644 packages/sdk-codemod/src/resolve-pending-boundaries.test.ts create mode 100644 packages/sdk-codemod/src/resolve-pending-boundaries.ts diff --git a/.changeset/resolve-pending-codemod-boundaries.md b/.changeset/resolve-pending-codemod-boundaries.md new file mode 100644 index 000000000..7a885d407 --- /dev/null +++ b/.changeset/resolve-pending-codemod-boundaries.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Add a `V2_NEXT_PENDING` placeholder `prereleaseUntil` for codemods whose exact `2.0.0-next.N` release boundary isn't known yet at implementation time, plus a `pnpm codemod:resolve-pending` step (wired into the release workflow) that resolves it to the real version constant once the release PR bumps `@tailor-platform/sdk`'s version. Prevents codemod boundaries from drifting out of sync with the version they actually ship in, which previously required a manual follow-up fix after each release. diff --git a/.github/scripts/resolve-pending-codemod-boundaries.sh b/.github/scripts/resolve-pending-codemod-boundaries.sh new file mode 100644 index 000000000..d6d33072e --- /dev/null +++ b/.github/scripts/resolve-pending-codemod-boundaries.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Resolves `prereleaseUntil: V2_NEXT_PENDING` codemod boundaries (see +# packages/sdk-codemod/src/registry.ts) against the version the release PR +# just bumped `@tailor-platform/sdk` to. A codemod's exact `2.0.0-next.N` +# boundary is only known once changesets/action resolves the real next +# version here, so codemods added while that version is still unknown are +# registered against the V2_NEXT_PENDING sentinel and fixed up in this step. +# +# Pushes the fixup straight to the release PR branch through the GitHub +# Contents API (like ensure-github-releases.sh) rather than a local git +# commit/push, since this token has no configured git identity or signing key. +set -euo pipefail + +PR_BRANCH="$(gh pr view "$PR_NUMBER" --json headRefName -q .headRefName)" +gh pr checkout "$PR_NUMBER" + +pnpm codemod:resolve-pending + +registry_path="packages/sdk-codemod/src/registry.ts" +if git diff --quiet -- "$registry_path"; then + exit 0 +fi + +pnpm exec oxfmt --write "$registry_path" + +content_b64="$(base64 -w0 "$registry_path")" +sha="$(gh api "repos/${GITHUB_REPOSITORY}/contents/${registry_path}?ref=${PR_BRANCH}" -q .sha)" + +gh api "repos/${GITHUB_REPOSITORY}/contents/${registry_path}" \ + -X PUT \ + -f message="chore(codemod): resolve pending prerelease boundary" \ + -f content="${content_b64}" \ + -f sha="${sha}" \ + -f branch="${PR_BRANCH}" + +echo "Resolved pending codemod boundaries on ${PR_BRANCH}." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 98eef125b..d9c4efb6b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,6 +67,16 @@ jobs: env: GITHUB_TOKEN: ${{ steps.get_access_token.outputs.token }} + # Only fires when changesets/action created/updated a release PR (as + # opposed to publishing). See resolve-pending-codemod-boundaries.sh. + - name: Resolve pending codemod version boundaries + if: steps.changesets.outputs.pullRequestNumber + env: + GH_TOKEN: ${{ steps.get_access_token.outputs.token }} + PR_NUMBER: ${{ steps.changesets.outputs.pullRequestNumber }} + shell: bash + run: bash ./.github/scripts/resolve-pending-codemod-boundaries.sh + # Idempotent: creates a release (and its tag, via `gh release create # --target`) for every package version that doesn't have one yet, and # fails the job if creation fails — so a broken release step is caught diff --git a/package.json b/package.json index 01e71dc00..37bd43180 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "agent:rules:check": "node scripts/sync-agent-rules.mjs --check", "codemod:docs:update": "tsx packages/sdk-codemod/scripts/sync-codemod-docs.ts --write", "codemod:docs:check": "tsx packages/sdk-codemod/scripts/sync-codemod-docs.ts --check", + "codemod:resolve-pending": "tsx packages/sdk-codemod/scripts/resolve-pending-boundaries.ts", "check": "pnpm run generate && pnpm run format && pnpm run \"/^check:/\"", "check:agent-rules": "pnpm run agent:rules:check", "check:codemod-docs": "pnpm run codemod:docs:check", diff --git a/packages/sdk-codemod/scripts/resolve-pending-boundaries.ts b/packages/sdk-codemod/scripts/resolve-pending-boundaries.ts new file mode 100644 index 000000000..46f758149 --- /dev/null +++ b/packages/sdk-codemod/scripts/resolve-pending-boundaries.ts @@ -0,0 +1,23 @@ +#!/usr/bin/env tsx +import { readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { resolvePendingBoundaries } from "../src/resolve-pending-boundaries"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const registryPath = resolve(scriptDir, "../src/registry.ts"); +const sdkPackageJsonPath = resolve(scriptDir, "../../sdk/package.json"); + +const sdkPackageJson = JSON.parse(await readFile(sdkPackageJsonPath, "utf-8")); +const source = await readFile(registryPath, "utf-8"); +const result = resolvePendingBoundaries(source, sdkPackageJson.version); + +if (!result.changed) { + process.stderr.write("No pending codemod boundaries to resolve.\n"); + process.exit(0); +} + +await writeFile(registryPath, result.source, "utf-8"); +process.stderr.write( + `Resolved V2_NEXT_PENDING to ${result.constantName} (${sdkPackageJson.version}).\n`, +); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index a6afe677d..11cbc782f 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -98,6 +98,13 @@ const V2_NEXT_1 = "2.0.0-next.1"; const V2_NEXT_2 = "2.0.0-next.2"; const V2_NEXT_4 = "2.0.0-next.4"; const V2_NEXT_5 = "2.0.0-next.5"; +/** + * Sentinel `prereleaseUntil` for a codemod whose exact `2.0.0-next.N` release is not + * known yet. `pnpm codemod:resolve-pending`, run in CI against the release PR, replaces + * it with the resolved `V2_NEXT_N` constant once the version is bumped. Exported so it + * stays lint-clean while no codemod currently references it. + */ +export const V2_NEXT_PENDING = "pending"; /** All registered codemods, in registration order. */ export const allCodemods: CodemodPackage[] = [ @@ -1075,6 +1082,9 @@ export function resolveCodemodScript(scriptPath: string): string { } function reachesCodemodBoundary(toVersion: string, codemod: CodemodPackage): boolean { + if (codemod.prereleaseUntil === V2_NEXT_PENDING) { + return false; + } if (gte(toVersion, codemod.until)) { return true; } @@ -1094,6 +1104,9 @@ function reachesCodemodBoundary(toVersion: string, codemod: CodemodPackage): boo } function effectiveCodemodBoundary(codemod: CodemodPackage): string { + if (codemod.prereleaseUntil === V2_NEXT_PENDING) { + return codemod.until; + } return codemod.prereleaseUntil ?? codemod.until; } @@ -1108,7 +1121,7 @@ function assertCodemodBoundaries(codemods: CodemodPackage[]): void { if (boundary.prerelease.length > 0) { throw new Error(`Codemod ${codemod.id} until must be a stable version: ${codemod.until}`); } - if (codemod.prereleaseUntil === undefined) { + if (codemod.prereleaseUntil === undefined || codemod.prereleaseUntil === V2_NEXT_PENDING) { continue; } diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts new file mode 100644 index 000000000..b5eb1f515 --- /dev/null +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from "vitest"; +import { resolvePendingBoundaries } from "./resolve-pending-boundaries"; + +const V2_NEXT_4_DECL = 'const V2_NEXT_4 = "2.0.0-next.4";'; +const PENDING_DECL = 'export const V2_NEXT_PENDING = "pending";'; + +function registrySource(...extraLines: string[]): string { + return [V2_NEXT_4_DECL, PENDING_DECL, ...extraLines].join("\n"); +} + +describe("resolvePendingBoundaries", () => { + test("is a no-op when no codemod references V2_NEXT_PENDING", () => { + const source = registrySource(" prereleaseUntil: V2_NEXT_4,"); + const result = resolvePendingBoundaries(source, "2.0.0-next.5"); + + expect(result).toEqual({ changed: false, source }); + }); + + test("inserts the resolved constant and rewrites usages", () => { + const source = registrySource(" prereleaseUntil: V2_NEXT_PENDING,"); + const result = resolvePendingBoundaries(source, "2.0.0-next.5"); + + expect(result.changed).toBe(true); + expect(result.constantName).toBe("V2_NEXT_5"); + expect(result.source).toContain('const V2_NEXT_5 = "2.0.0-next.5";'); + expect(result.source).toContain(PENDING_DECL); + expect(result.source).toContain("prereleaseUntil: V2_NEXT_5,"); + expect(result.source).not.toContain("V2_NEXT_PENDING,"); + }); + + test("rewrites every pending usage in the same resolution", () => { + const source = registrySource( + " prereleaseUntil: V2_NEXT_PENDING,", + " prereleaseUntil: V2_NEXT_PENDING,", + ); + const result = resolvePendingBoundaries(source, "2.0.0-next.5"); + + expect(result.source.match(/prereleaseUntil: V2_NEXT_5,/g)).toHaveLength(2); + expect(result.source.match(/const V2_NEXT_5 = /g)).toHaveLength(1); + }); + + test("reuses an already-declared constant instead of duplicating it", () => { + const source = [ + 'const V2_NEXT_5 = "2.0.0-next.5";', + PENDING_DECL, + " prereleaseUntil: V2_NEXT_PENDING,", + ].join("\n"); + const result = resolvePendingBoundaries(source, "2.0.0-next.5"); + + expect(result.source.match(/const V2_NEXT_5 = /g)).toHaveLength(1); + expect(result.source).toContain("prereleaseUntil: V2_NEXT_5,"); + }); + + test("throws when the existing constant points at a different version", () => { + const source = [ + 'const V2_NEXT_5 = "2.0.0-next.99";', + PENDING_DECL, + " prereleaseUntil: V2_NEXT_PENDING,", + ].join("\n"); + + expect(() => resolvePendingBoundaries(source, "2.0.0-next.5")).toThrow( + "V2_NEXT_5 is already declared as 2.0.0-next.99", + ); + }); + + test("throws when the resolved version is not a next.N prerelease", () => { + const source = registrySource(" prereleaseUntil: V2_NEXT_PENDING,"); + + expect(() => resolvePendingBoundaries(source, "2.0.0")).toThrow( + 'resolvedVersion must be a "next.N" prerelease', + ); + }); + + test("throws when the resolved version is not valid semver", () => { + const source = registrySource(" prereleaseUntil: V2_NEXT_PENDING,"); + + expect(() => resolvePendingBoundaries(source, "not-a-version")).toThrow( + "resolvedVersion must be a valid semver version", + ); + }); +}); diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.ts new file mode 100644 index 000000000..9c426b089 --- /dev/null +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.ts @@ -0,0 +1,65 @@ +import { parse } from "semver"; + +const PENDING_USAGE_PATTERN = /prereleaseUntil: V2_NEXT_PENDING,/g; +const PENDING_DECLARATION_PATTERN = /^export const V2_NEXT_PENDING = "pending";$/m; + +export interface ResolvePendingBoundariesResult { + /** Whether any `V2_NEXT_PENDING` usage was found and rewritten. */ + changed: boolean; + /** The constant name usages were rewritten to, when `changed` is true. */ + constantName?: string; + /** The rewritten registry.ts source. Identical to the input when `changed` is false. */ + source: string; +} + +/** + * Rewrite `prereleaseUntil: V2_NEXT_PENDING` usages in a registry.ts source to the + * concrete `V_NEXT_` constant for a resolved release version, inserting that + * constant's declaration if it doesn't already exist. A no-op when no usage is present. + * @param source - Current contents of registry.ts + * @param resolvedVersion - The version the release PR bumped `@tailor-platform/sdk` to (e.g. "2.0.0-next.5") + * @returns The (possibly) rewritten source and whether it changed + */ +export function resolvePendingBoundaries( + source: string, + resolvedVersion: string, +): ResolvePendingBoundariesResult { + if (!PENDING_USAGE_PATTERN.test(source)) { + return { changed: false, source }; + } + PENDING_USAGE_PATTERN.lastIndex = 0; + + const parsed = parse(resolvedVersion); + if (parsed === null) { + throw new Error(`resolvedVersion must be a valid semver version: ${resolvedVersion}`); + } + if (parsed.prerelease.length !== 2 || parsed.prerelease[0] !== "next") { + throw new Error( + `resolvedVersion must be a "next.N" prerelease to resolve V2_NEXT_PENDING: ${resolvedVersion}`, + ); + } + const constantName = `V${parsed.major}_NEXT_${parsed.prerelease[1]}`; + + let updated = source; + const existingDeclaration = new RegExp(`^const ${constantName} = "([^"]+)";$`, "m").exec(source); + if (existingDeclaration) { + if (existingDeclaration[1] !== resolvedVersion) { + throw new Error( + `${constantName} is already declared as ${existingDeclaration[1]}, which does not match the resolved version ${resolvedVersion}`, + ); + } + } else { + const declarationMatch = PENDING_DECLARATION_PATTERN.exec(updated); + if (declarationMatch === null) { + throw new Error("Could not find the V2_NEXT_PENDING declaration in registry.ts"); + } + updated = updated.replace( + declarationMatch[0], + `const ${constantName} = "${resolvedVersion}";\n${declarationMatch[0]}`, + ); + } + + updated = updated.replace(PENDING_USAGE_PATTERN, `prereleaseUntil: ${constantName},`); + + return { changed: true, constantName, source: updated }; +} From 36ad006f9eb2d23b21b2028741dbce1d6ba9f91b Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 16:11:59 +0900 Subject: [PATCH 586/618] feat(sdk)!: remove generate --watch flag Drop the --watch/-W flag, the chokidar/madge-based dependency watcher, and the self-restart-on-change logic from the generate command. Users should re-run tailor generate after making changes instead. --- .changeset/remove-generate-watch.md | 5 + packages/sdk/docs/cli/application.md | 7 +- packages/sdk/package.json | 3 - .../sdk/src/cli/commands/generate/index.ts | 5 - .../sdk/src/cli/commands/generate/options.ts | 1 - .../src/cli/commands/generate/service.test.ts | 57 +- .../sdk/src/cli/commands/generate/service.ts | 131 +--- .../cli/commands/generate/watch/index.test.ts | 249 ------ .../src/cli/commands/generate/watch/index.ts | 711 ------------------ .../src/cli/commands/generate/watch/types.ts | 6 - packages/sdk/src/cli/shared/config-loader.ts | 10 - pnpm-lock.yaml | 16 - 12 files changed, 25 insertions(+), 1176 deletions(-) create mode 100644 .changeset/remove-generate-watch.md delete mode 100644 packages/sdk/src/cli/commands/generate/watch/index.test.ts delete mode 100644 packages/sdk/src/cli/commands/generate/watch/index.ts delete mode 100644 packages/sdk/src/cli/commands/generate/watch/types.ts diff --git a/.changeset/remove-generate-watch.md b/.changeset/remove-generate-watch.md new file mode 100644 index 000000000..44cd15ba8 --- /dev/null +++ b/.changeset/remove-generate-watch.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": major +--- + +Remove the `generate --watch` (`-W`) flag. `tailor generate` now always runs a single generation pass; re-run the command after making changes instead of relying on the watcher to regenerate automatically. diff --git a/packages/sdk/docs/cli/application.md b/packages/sdk/docs/cli/application.md index b7fbf3bbc..d89a7f9f6 100644 --- a/packages/sdk/docs/cli/application.md +++ b/packages/sdk/docs/cli/application.md @@ -38,10 +38,9 @@ tailor generate [options] **Options** -| Option | Alias | Description | Required | Default | -| ------------------- | ----- | ---------------------------------------------- | -------- | -------------------- | -| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | -| `--watch` | `-W` | Watch for type/resolver changes and regenerate | No | `false` | +| Option | Alias | Description | Required | Default | +| ------------------- | ----- | ----------------------- | -------- | -------------------- | +| `--config ` | `-c` | Path to SDK config file | No | `"tailor.config.ts"` | See [Global Options](../cli-reference.md#global-options) for options available to all commands. diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 955e301cd..feaa556a3 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -190,7 +190,6 @@ "@urql/core": "6.0.3", "amaro": "1.1.10", "chalk": "5.6.2", - "chokidar": "5.0.0", "confbox": "0.2.4", "date-fns": "4.4.0", "es-toolkit": "1.49.0", @@ -199,7 +198,6 @@ "graphql": "17.0.2", "inflection": "3.0.2", "kysely": "0.29.3", - "madge": "8.0.0", "mime-types": "3.0.2", "open": "11.0.0", "oxc-parser": "0.139.0", @@ -222,7 +220,6 @@ "devDependencies": { "@opentelemetry/sdk-trace-base": "2.9.0", "@tailor-platform/tailor-proto": "workspace:^", - "@types/madge": "5.0.3", "@types/mime-types": "3.0.1", "@types/node": "24.13.3", "@types/semver": "7.7.1", diff --git a/packages/sdk/src/cli/commands/generate/index.ts b/packages/sdk/src/cli/commands/generate/index.ts index 358a711af..94d2ae33a 100644 --- a/packages/sdk/src/cli/commands/generate/index.ts +++ b/packages/sdk/src/cli/commands/generate/index.ts @@ -11,17 +11,12 @@ export const generateCommand = defineAppCommand({ alias: "c", description: "Path to SDK config file", }), - watch: arg(z.boolean().default(false), { - alias: "W", - description: "Watch for type/resolver changes and regenerate", - }), }), run: async (args) => { const { initTelemetry } = await import("#/cli/telemetry/index"); await initTelemetry(); await generate({ configPath: args.config, - watch: args.watch, }); }, }); diff --git a/packages/sdk/src/cli/commands/generate/options.ts b/packages/sdk/src/cli/commands/generate/options.ts index 8b332d56c..59fb9c6e9 100644 --- a/packages/sdk/src/cli/commands/generate/options.ts +++ b/packages/sdk/src/cli/commands/generate/options.ts @@ -1,4 +1,3 @@ export type GenerateOptions = { configPath?: string; - watch?: boolean; }; diff --git a/packages/sdk/src/cli/commands/generate/service.test.ts b/packages/sdk/src/cli/commands/generate/service.test.ts index 38f412e17..3c03b3fa9 100644 --- a/packages/sdk/src/cli/commands/generate/service.test.ts +++ b/packages/sdk/src/cli/commands/generate/service.test.ts @@ -145,7 +145,7 @@ describe("GenerationManager", () => { config: mockConfig, }); - await manager.generate(false); + await manager.generate(); expect(manager.services).toBeDefined(); }); @@ -169,7 +169,7 @@ describe("GenerationManager", () => { pluginManager, }); - await manager.generate(false); + await manager.generate(); expect(onTailorDBReady).toHaveBeenCalledWith( expect.objectContaining({ @@ -200,60 +200,9 @@ describe("GenerationManager", () => { config: mockConfig, }); - await expect(duplicateManager.generate(false)).rejects.toThrow( + await expect(duplicateManager.generate()).rejects.toThrow( /Duplicate TailorDB type names detected/, ); }); - - test("does not exit watch mode for duplicate TailorDB type names", async () => { - const duplicateApp = applicationWithTailorDBServices(mockConfig, [ - loadedTailorDBService("main", ["User"]), - loadedTailorDBService("analytics", ["User"]), - ]); - const duplicateManager = createGenerationManager({ - application: duplicateApp, - config: mockConfig, - }); - - await expect(duplicateManager.generate(true)).resolves.not.toThrow(); - }); - }); - - describe("watch", () => { - test("watch method exists", () => { - const application = defineApplication({ config: mockConfig }); - const manager = createGenerationManager({ - application, - config: mockConfig, - }); - - expect(typeof manager.watch).toBe("function"); - }); - - test("application has tailorDBServices for watch", () => { - const application = defineApplication({ config: mockConfig }); - const manager = createGenerationManager({ - application, - config: mockConfig, - }); - - expect(manager.application.tailorDBServices).toBeDefined(); - expect(manager.application.tailorDBServices.length).toBeGreaterThan(0); - expect(manager.application.tailorDBServices[0]?.namespace).toBe("main"); - expect(manager.application.tailorDBServices[0]?.config.files).toEqual(["src/types/*.ts"]); - }); - - test("application has resolverServices for watch", () => { - const application = defineApplication({ config: mockConfig }); - const manager = createGenerationManager({ - application, - config: mockConfig, - }); - - expect(manager.application.resolverServices).toBeDefined(); - expect(manager.application.resolverServices.length).toBeGreaterThan(0); - expect(manager.application.resolverServices[0]?.namespace).toBe("main"); - expect(manager.application.resolverServices[0]?.config.files).toEqual(["src/resolvers/*.ts"]); - }); }); }); diff --git a/packages/sdk/src/cli/commands/generate/service.ts b/packages/sdk/src/cli/commands/generate/service.ts index 3a1768db9..750007bdc 100644 --- a/packages/sdk/src/cli/commands/generate/service.ts +++ b/packages/sdk/src/cli/commands/generate/service.ts @@ -1,4 +1,3 @@ -import { spawn } from "node:child_process"; import * as fs from "node:fs"; import * as path from "pathe"; import { @@ -15,7 +14,6 @@ import { generateUserTypes } from "#/cli/shared/type-generator"; import { withSpan } from "#/cli/telemetry/index"; import { PluginManager } from "#/plugin/manager"; import { assertDefined } from "#/utils/assert"; -import { createDependencyWatcher, type DependencyWatcher } from "./watch"; import type { TypeSourceInfo, TailorDBType } from "#/parser/service/tailordb/types"; import type { GeneratorAuthInput, @@ -46,8 +44,7 @@ export type GenerationManager = { resolver: Record>; executor: Record; }; - generate: (watch: boolean) => Promise; - watch: () => Promise; + generate: () => Promise; }; /** @@ -73,8 +70,6 @@ export function createGenerationManager(params: { executor: Record; } = { tailordb: {}, resolver: {}, executor: {} }; - let watcher: DependencyWatcher | null = null; - // Get plugins that have generation hooks const generationPlugins = pluginManager?.getPluginsWithGenerationHooks() ?? []; @@ -196,11 +191,9 @@ export function createGenerationManager(params: { * Each hook runs at its natural pipeline phase, ensuring outputs from earlier * phases are available when later phases load resolvers/executors. * @param hookName - Name of the hook to call - * @param watch - Whether running in watch mode (suppresses throws) */ async function runPluginHook( hookName: "onTailorDBReady" | "onResolverReady" | "onExecutorReady", - watch: boolean, ): Promise { const plugins = generationPlugins.filter((p) => p[hookName] != null); if (plugins.length === 0) return; @@ -211,17 +204,13 @@ export function createGenerationManager(params: { } catch (error) { logger.error(`Error processing plugin ${styles.bold(plugin.id)} (${hookName})`); logger.error(String(error)); - if (!watch) { - throw error; - } + throw error; } }), ); - if (!watch) { - const failures = results.filter((r): r is PromiseRejectedResult => r.status === "rejected"); - if (failures.length > 0) { - throw new AggregateError(failures.map((f) => f.reason)); - } + const failures = results.filter((r): r is PromiseRejectedResult => r.status === "rejected"); + if (failures.length > 0) { + throw new AggregateError(failures.map((f) => f.reason)); } } @@ -278,59 +267,12 @@ export function createGenerationManager(params: { ); } - async function restartWatchProcess(): Promise { - logger.newline(); - logger.info("Restarting watch process to clear module cache...", { - mode: "stream", - }); - logger.newline(); - - // Clean up watcher first - if (watcher) { - await watcher.stop(); - } - - // Spawn a new process with the same arguments - const args = process.argv.slice(2); - const env = { - ...process.env, - __TAILOR_WATCH_GENERATION: ( - parseInt(process.env.__TAILOR_WATCH_GENERATION || "0", 10) + 1 - ).toString(), - }; - - const child = spawn( - assertDefined(process.argv[0], "argv[0] missing"), - [assertDefined(process.argv[1], "argv[1] missing"), ...args], - { - stdio: "inherit", - env, - detached: false, - }, - ); - - // Forward signals to child - const forwardSignal = (signal: NodeJS.Signals) => { - child.kill(signal); - }; - - process.on("SIGINT", forwardSignal); - process.on("SIGTERM", forwardSignal); - - // Wait for child to exit, then exit parent - child.on("exit", (code) => { - process.exit(code || 0); - }); - - // Don't exit immediately - let child handle everything - } - return { application, baseDir, services, - async generate(watch: boolean): Promise { + async generate(): Promise { logger.newline(); logger.log(`Generation for application: ${styles.highlight(application.config.name)}`); @@ -357,9 +299,7 @@ export function createGenerationManager(params: { } catch (error) { logger.error(`Error loading types for TailorDB service ${styles.bold(namespace)}`); logger.error(String(error)); - if (!watch) { - throw error; - } + throw error; } }); } @@ -370,9 +310,7 @@ export function createGenerationManager(params: { } catch (error) { logger.error("Error validating TailorDB type names"); logger.error(String(error)); - if (!watch) { - throw error; - } + throw error; } }); @@ -412,7 +350,7 @@ export function createGenerationManager(params: { const hasOnTailorDBReady = generationPlugins.some((p) => p.onTailorDBReady != null); if (hasOnTailorDBReady) { await withSpan("generate.onTailorDBReady", async () => { - await runPluginHook("onTailorDBReady", watch); + await runPluginHook("onTailorDBReady"); }); logger.newline(); } @@ -434,9 +372,7 @@ export function createGenerationManager(params: { `Error loading resolvers for Resolver service ${styles.bold(namespace)}`, ); logger.error(String(error)); - if (!watch) { - throw error; - } + throw error; } }); } @@ -446,7 +382,7 @@ export function createGenerationManager(params: { const hasOnResolverReady = generationPlugins.some((p) => p.onResolverReady != null); if (hasOnResolverReady) { await withSpan("generate.onResolversReady", async () => { - await runPluginHook("onResolverReady", watch); + await runPluginHook("onResolverReady"); }); logger.newline(); } @@ -471,51 +407,18 @@ export function createGenerationManager(params: { const hasOnExecutorReady = generationPlugins.some((p) => p.onExecutorReady != null); if (hasOnExecutorReady) { await withSpan("generate.onExecutorsReady", async () => { - await runPluginHook("onExecutorReady", watch); + await runPluginHook("onExecutorReady"); }); logger.newline(); } }, - - async watch(): Promise { - watcher = createDependencyWatcher(); - - // Set up restart callback - watcher.setRestartCallback(() => { - restartWatchProcess(); - }); - - // Watch config file - await watcher.addWatchGroup("Config", [config.path]); - - // Watch application services - const app = application; - - // Watch TailorDB services - for (const db of app.tailorDBServices) { - const dbNamespace = db.namespace; - await watcher.addWatchGroup(`TailorDB/${dbNamespace}`, db.config.files); - } - - // Watch Resolver services - for (const resolverService of app.resolverServices) { - const resolverNamespace = resolverService.namespace; - await watcher.addWatchGroup( - `Resolver/${resolverNamespace}`, - resolverService["config"].files, - ); - } - - // Keep the process running - await new Promise(() => {}); - }, }; } /** * Run code generation using the Tailor configuration. * @param options - Generation options - * @returns Promise that resolves when generation (and watch, if enabled) completes + * @returns Promise that resolves when generation completes */ export async function generate(options?: GenerateOptions) { return withSpan("generate", async (rootSpan) => { @@ -523,9 +426,6 @@ export async function generate(options?: GenerateOptions) { const { config, plugins } = await withSpan("generate.loadConfig", async () => { return loadConfig(options?.configPath); }); - const watch = options?.watch ?? false; - - rootSpan.setAttribute("generate.watch", watch); // Generate user types from loaded config await withSpan("generate.generateUserTypes", async () => @@ -544,9 +444,6 @@ export async function generate(options?: GenerateOptions) { rootSpan.setAttribute("app.name", application.config.name); const manager = createGenerationManager({ application, config, pluginManager }); - await manager.generate(watch); - if (watch) { - await manager.watch(); - } + await manager.generate(); }); } diff --git a/packages/sdk/src/cli/commands/generate/watch/index.test.ts b/packages/sdk/src/cli/commands/generate/watch/index.test.ts deleted file mode 100644 index a1b58c2f2..000000000 --- a/packages/sdk/src/cli/commands/generate/watch/index.test.ts +++ /dev/null @@ -1,249 +0,0 @@ -/* eslint-disable import-x/order */ -import * as fs from "node:fs/promises"; -import * as os from "node:os"; -import * as path from "pathe"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; - -const madgeMock = vi.hoisted(() => - vi.fn(async () => ({ - obj: () => ({}), - circular: () => [], - })), -); - -vi.mock("madge", () => ({ - default: madgeMock, -})); - -import { - createDependencyGraphManager, - createDependencyWatcher, - type DependencyGraphManager, - type DependencyWatcher, - WatcherError, - WatcherErrorCode, -} from "./index"; - -let manager: DependencyGraphManager; - -beforeEach(() => { - madgeMock.mockReset(); - madgeMock.mockImplementation(async () => ({ - obj: () => ({}), - circular: () => [], - })); - manager = createDependencyGraphManager(); -}); - -async function createTempDir(): Promise { - return await fs.mkdtemp(path.join(os.tmpdir(), "dependency-watcher-test-")); -} - -async function createTestFile(filePath: string, content: string): Promise { - await fs.mkdir(path.dirname(filePath), { recursive: true }); - await fs.writeFile(filePath, content); -} - -describe("DependencyWatcher", () => { - let tempDir: string; - let watcher: DependencyWatcher; - - beforeEach(async () => { - tempDir = await createTempDir(); - watcher = createDependencyWatcher({ - debounceTime: 10, - detectCircularDependencies: true, - }); - }); - - afterEach(async () => { - await watcher.stop(); - await fs.rm(tempDir, { recursive: true, force: true }); - }); - - describe("initialization", () => { - test("can initialize correctly", async () => { - await watcher.initialize(); - const status = watcher.getWatchStatus(); - expect(status.isWatching).toBe(true); - expect(status.groupCount).toBe(0); - expect(status.fileCount).toBe(0); - }); - }); - - describe("watch group management", () => { - test("can add watch group", async () => { - const testFile = path.join(tempDir, "test.ts"); - await createTestFile(testFile, 'export const test = "hello";'); - - await watcher.addWatchGroup("test-group", [testFile]); - - const status = watcher.getWatchStatus(); - expect(status.groupCount).toBe(1); - expect(status.fileCount).toBe(1); - }); - - test("can watch multiple files with glob pattern", async () => { - const testFile1 = path.join(tempDir, "file1.ts"); - const testFile2 = path.join(tempDir, "file2.ts"); - await createTestFile(testFile1, 'export const file1 = "hello";'); - await createTestFile(testFile2, 'export const file2 = "world";'); - - const pattern = path.join(tempDir, "*.ts"); - await watcher.addWatchGroup("test-group", [pattern]); - - const status = watcher.getWatchStatus(); - expect(status.groupCount).toBe(1); - expect(status.fileCount).toBe(2); - }); - - test("can remove watch group", async () => { - const testFile = path.join(tempDir, "test.ts"); - await createTestFile(testFile, 'export const test = "hello";'); - - await watcher.addWatchGroup("test-group", [testFile]); - await watcher.removeWatchGroup("test-group"); - - const status = watcher.getWatchStatus(); - expect(status.groupCount).toBe(0); - expect(status.fileCount).toBe(0); - }); - - test("duplicate group ID causes error", async () => { - const testFile = path.join(tempDir, "test.ts"); - await createTestFile(testFile, 'export const test = "hello";'); - - await watcher.addWatchGroup("test-group", [testFile]); - - await expect(watcher.addWatchGroup("test-group", [testFile])).rejects.toThrow(WatcherError); - }); - }); - - describe("validation", () => { - test.each([ - ["invalid group ID", "", ["test.ts"]], - ["empty pattern array", "test-group", []], - ])("%s causes error", async (_, groupId, patterns) => { - await expect(watcher.addWatchGroup(groupId, patterns)).rejects.toThrow(WatcherError); - }); - }); - - describe("impact scope calculation", () => { - test("impact scope is empty for files without dependencies", async () => { - const testFile = path.join(tempDir, "test.ts"); - await createTestFile(testFile, 'export const test = "hello";'); - - await watcher.addWatchGroup("test-group", [testFile]); - - const impact = watcher.calculateImpact(testFile); - expect(impact.changedFile).toBe(testFile); - expect(impact.affectedFiles).toEqual([testFile]); - expect(impact.affectedGroups).toEqual(["test-group"]); - }); - }); - - describe("error handling", () => { - test("can set error callback", () => { - const errorCallback = vi.fn(); - watcher.onError(errorCallback); - - expect(errorCallback).not.toHaveBeenCalled(); - }); - - test("WatcherError is created correctly", () => { - const error = new WatcherError( - "Test error", - WatcherErrorCode.INVALID_WATCH_GROUP, - "/test/file.ts", - ); - - expect(error.message).toBe("Test error"); - expect(error.code).toBe(WatcherErrorCode.INVALID_WATCH_GROUP); - expect(error.filePath).toBe("/test/file.ts"); - expect(error.name).toBe("WatcherError"); - }); - }); - - describe("watch status", () => { - test("can get watch status correctly", async () => { - const testFile1 = path.join(tempDir, "file1.ts"); - const testFile2 = path.join(tempDir, "file2.ts"); - await createTestFile(testFile1, 'export const file1 = "hello";'); - await createTestFile(testFile2, 'export const file2 = "world";'); - - await watcher.addWatchGroup("group1", [testFile1]); - await watcher.addWatchGroup("group2", [testFile2]); - - const status = watcher.getWatchStatus(); - expect(status.isWatching).toBe(true); - expect(status.groupCount).toBe(2); - expect(status.fileCount).toBe(2); - expect(status.dependencyNodeCount).toBeGreaterThanOrEqual(0); - }); - }); - - describe("circular dependency detection", () => { - test("can detect circular dependencies", async () => { - const circular = watcher.detectCircularDependencies(); - expect(Array.isArray(circular)).toBe(true); - }); - }); -}); - -describe("DependencyGraphManager", () => { - let tempDir: string; - - beforeEach(async () => { - tempDir = await createTempDir(); - }); - - afterEach(async () => { - await fs.rm(tempDir, { recursive: true, force: true }); - }); - - test("can build graph with empty file array", async () => { - await expect(manager.buildGraph([])).resolves.not.toThrow(); - }); - - test("calls madge to build dependency graph", async () => { - const testFile = path.join(tempDir, "sample.ts"); - await createTestFile(testFile, "export const value = 1;"); - - await manager.buildGraph([testFile]); - - expect(madgeMock).toHaveBeenCalledWith( - [testFile], - expect.objectContaining({ - fileExtensions: expect.arrayContaining(["ts", "js"]), - }), - ); - }); - - test("detects error when madge does not provide function", async () => { - const mockedMadge = await import("madge"); - const originalDefault = (mockedMadge as { default?: unknown }).default; - (mockedMadge as { default?: unknown }).default = undefined; - - const localManager = createDependencyGraphManager(); - - try { - await expect( - localManager.buildGraph([path.join(tempDir, "broken.ts")]), - ).rejects.toMatchObject({ - code: WatcherErrorCode.MADGE_INITIALIZATION_FAILED, - }); - } finally { - (mockedMadge as { default?: unknown }).default = originalDefault; - } - }); -}); - -describe("WatcherErrorCode", () => { - test("all error codes are defined", () => { - expect(WatcherErrorCode.DEPENDENCY_ANALYSIS_FAILED).toBe("DEPENDENCY_ANALYSIS_FAILED"); - expect(WatcherErrorCode.FILE_WATCH_FAILED).toBe("FILE_WATCH_FAILED"); - expect(WatcherErrorCode.CIRCULAR_DEPENDENCY_DETECTED).toBe("CIRCULAR_DEPENDENCY_DETECTED"); - expect(WatcherErrorCode.INVALID_WATCH_GROUP).toBe("INVALID_WATCH_GROUP"); - expect(WatcherErrorCode.MADGE_INITIALIZATION_FAILED).toBe("MADGE_INITIALIZATION_FAILED"); - }); -}); diff --git a/packages/sdk/src/cli/commands/generate/watch/index.ts b/packages/sdk/src/cli/commands/generate/watch/index.ts deleted file mode 100644 index ea4987ad4..000000000 --- a/packages/sdk/src/cli/commands/generate/watch/index.ts +++ /dev/null @@ -1,711 +0,0 @@ -import { glob } from "node:fs/promises"; -import { watch } from "chokidar"; -import * as madgeModule from "madge"; -import * as path from "pathe"; -import { logger, styles } from "#/cli/shared/logger"; -import type { MadgeLoader } from "./types"; - -/** - * Types of file change events. - */ -type FileChangeEvent = "add" | "change" | "unlink"; - -/** - * Definition of a watch group. - */ -interface WatchGroup { - /** Unique identifier of the group. */ - id: string; - /** File patterns to watch (glob format). */ - patterns: string[]; - /** List of absolute file paths in the group. */ - files: Set; -} - -/** - * Node in the dependency graph. - */ -interface DependencyNode { - /** Absolute path of the file. */ - filePath: string; - /** List of files this file depends on. */ - dependencies: Set; - /** List of files that depend on this file. */ - dependents: Set; -} - -/** - * Impact analysis result. - */ -interface ImpactAnalysisResult { - /** Changed file. */ - changedFile: string; - /** List of affected files (all files depending on the changed file). */ - affectedFiles: string[]; - /** List of affected watch groups. */ - affectedGroups: string[]; -} - -/** - * Type of the error handling callback. - */ -type ErrorCallback = (error: WatcherError) => void; - -/** - * Options for the watcher system. - */ -interface WatcherOptions { - /** Options for chokidar. */ - chokidarOptions?: Parameters[1]; - /** Options for madge. */ - madgeOptions?: Parameters[1]; - /** Update interval for the dependency graph (milliseconds). */ - dependencyUpdateInterval?: number; - /** Debounce duration (milliseconds). */ - debounceTime?: number; - /** Whether to enable circular dependency detection. */ - detectCircularDependencies?: boolean; -} - -/** - * Watcher status. - */ -interface WatchStatus { - /** Whether watching is active. */ - isWatching: boolean; - /** Number of watch groups. */ - groupCount: number; - /** Number of watched files. */ - fileCount: number; - /** Number of nodes in the dependency graph. */ - dependencyNodeCount: number; -} - -/** - * Graph statistics. - */ -interface GraphStats { - /** Number of nodes. */ - nodeCount: number; - /** Number of edges. */ - edgeCount: number; - /** Number of circular dependencies. */ - circularDependencyCount: number; -} - -/** - * Error codes. - */ -const WatcherErrorCode = { - DEPENDENCY_ANALYSIS_FAILED: "DEPENDENCY_ANALYSIS_FAILED", - FILE_WATCH_FAILED: "FILE_WATCH_FAILED", - CIRCULAR_DEPENDENCY_DETECTED: "CIRCULAR_DEPENDENCY_DETECTED", - INVALID_WATCH_GROUP: "INVALID_WATCH_GROUP", - MADGE_INITIALIZATION_FAILED: "MADGE_INITIALIZATION_FAILED", -} as const; -type WatcherErrorCode = (typeof WatcherErrorCode)[keyof typeof WatcherErrorCode]; - -/** - * Watcher-specific error. - */ -export class WatcherError extends Error { - constructor( - message: string, - public readonly code: WatcherErrorCode, - public readonly filePath?: string, - public readonly originalError?: Error, - ) { - super(message); - this.name = "WatcherError"; - } -} - -/** - * Dependency graph manager type. - */ -export type DependencyGraphManager = { - buildGraph: (filePaths: string[]) => Promise; - getDependents: (filePath: string) => string[]; - getDependencies: (filePath: string) => string[]; - findCircularDependencies: () => string[][]; - addNode: (filePath: string) => void; - removeNode: (filePath: string) => void; - getGraphStats: () => GraphStats; -}; - -/** - * Creates a dependency graph manager. - * @param options - Options for madge - * @returns DependencyGraphManager instance - */ -export function createDependencyGraphManager( - options: Parameters[1] = {}, -): DependencyGraphManager { - const graph: Map = new Map(); - let madgeInstance: Awaited> | null = null; - let madgeLoader: MadgeLoader | null = null; - - function getMadgeLoader(): MadgeLoader { - if (madgeLoader) { - return madgeLoader; - } - - const defaultExport = (madgeModule as { default?: unknown }).default; - if (typeof defaultExport === "function") { - madgeLoader = defaultExport as MadgeLoader; - return madgeLoader; - } - - if (typeof (madgeModule as unknown) === "function") { - madgeLoader = madgeModule as unknown as MadgeLoader; - return madgeLoader; - } - - throw new WatcherError( - "Failed to initialize madge analyzer: module did not export a callable function.", - WatcherErrorCode.MADGE_INITIALIZATION_FAILED, - ); - } - - function traverseDependents(filePath: string, visited: Set): string[] { - if (visited.has(filePath)) return []; - visited.add(filePath); - - const node = graph.get(filePath); - if (!node) return []; - - const result: string[] = []; - for (const dependent of node.dependents) { - result.push(dependent); - result.push(...traverseDependents(dependent, visited)); - } - - return result; - } - - function traverseDependencies(filePath: string, visited: Set): string[] { - if (visited.has(filePath)) return []; - visited.add(filePath); - - const node = graph.get(filePath); - if (!node) return []; - - const result: string[] = []; - for (const dependency of node.dependencies) { - result.push(dependency); - result.push(...traverseDependencies(dependency, visited)); - } - - return result; - } - - function addNode(filePath: string): void { - const absolutePath = path.resolve(filePath); - if (!graph.has(absolutePath)) { - graph.set(absolutePath, { - filePath: absolutePath, - dependencies: new Set(), - dependents: new Set(), - }); - } - } - - function removeNode(filePath: string): void { - const absolutePath = path.resolve(filePath); - const node = graph.get(absolutePath); - if (!node) return; - - for (const dep of node.dependencies) { - const depNode = graph.get(dep); - if (depNode) { - depNode.dependents.delete(absolutePath); - } - } - - for (const dependent of node.dependents) { - const dependentNode = graph.get(dependent); - if (dependentNode) { - dependentNode.dependencies.delete(absolutePath); - } - } - - graph.delete(absolutePath); - } - - function findCircularDependencies(): string[][] { - if (!madgeInstance) return []; - try { - return madgeInstance.circular(); - } catch (error) { - logger.warn(`Failed to detect circular dependencies: ${String(error)}`); - return []; - } - } - - return { - async buildGraph(filePaths: string[]): Promise { - try { - if (filePaths.length === 0) return; - - const madge = getMadgeLoader(); - - madgeInstance = await madge(filePaths, { - fileExtensions: ["ts", "js"], - excludeRegExp: [/node_modules/], - baseDir: ".", - ...options, - }); - - const dependencyObj = madgeInstance.obj() as Record; - graph.clear(); - - for (const filePath of filePaths) { - addNode(filePath); - } - - for (const [filePath, dependencies] of Object.entries(dependencyObj)) { - const absoluteFilePath = path.resolve(".", filePath); - const node = graph.get(absoluteFilePath); - if (!node) continue; - - for (const dep of dependencies) { - const absoluteDepPath = path.resolve(".", dep); - node.dependencies.add(absoluteDepPath); - - const depNode = graph.get(absoluteDepPath); - if (depNode) { - depNode.dependents.add(absoluteFilePath); - } - } - } - } catch (error) { - if (error instanceof WatcherError) { - throw error; - } - throw new WatcherError( - `Failed to build dependency graph: ${error instanceof Error ? error.message : String(error)}`, - WatcherErrorCode.DEPENDENCY_ANALYSIS_FAILED, - undefined, - error instanceof Error ? error : undefined, - ); - } - }, - - getDependents(filePath: string): string[] { - const visited = new Set(); - return traverseDependents(path.resolve(filePath), visited); - }, - - getDependencies(filePath: string): string[] { - const visited = new Set(); - return traverseDependencies(path.resolve(filePath), visited); - }, - - findCircularDependencies, - addNode, - removeNode, - - getGraphStats(): GraphStats { - let edgeCount = 0; - for (const node of graph.values()) { - edgeCount += node.dependencies.size; - } - - return { - nodeCount: graph.size, - edgeCount, - circularDependencyCount: findCircularDependencies().length, - }; - }, - }; -} - -/** - * Dependency watcher type. - */ -export type DependencyWatcher = { - initialize: () => Promise; - addWatchGroup: (groupId: string, patterns: string[]) => Promise; - removeWatchGroup: (groupId: string) => Promise; - start: () => Promise; - stop: () => Promise; - onError: (callback: ErrorCallback) => void; - updateDependencyGraph: () => Promise; - calculateImpact: (filePath: string) => ImpactAnalysisResult; - detectCircularDependencies: () => string[][]; - getWatchStatus: () => WatchStatus; - setRestartCallback: (callback: () => void) => void; -}; - -/** - * Creates a dependency watcher. - * @param options - Watcher options - * @returns DependencyWatcher instance - */ -export function createDependencyWatcher(options: WatcherOptions = {}): DependencyWatcher { - let chokidarWatcher: ReturnType | null = null; - const watchGroups: Map = new Map(); - const dependencyGraphManager = createDependencyGraphManager(options.madgeOptions); - let errorCallback: ErrorCallback | null = null; - const debounceTimers: Map = new Map(); - let isInitialized = false; - const dependencyCache: Map = new Map(); - const maxCacheSize = 1000; - let signalHandlersRegistered = false; - let restartCallback: (() => void) | null = null; - - function validateWatchGroup(groupId: string, patterns: string[]): void { - if (!groupId || typeof groupId !== "string") { - throw new WatcherError( - "Group ID must be a non-empty string", - WatcherErrorCode.INVALID_WATCH_GROUP, - ); - } - - if (!Array.isArray(patterns) || patterns.length === 0) { - throw new WatcherError( - "Patterns must be a non-empty array", - WatcherErrorCode.INVALID_WATCH_GROUP, - ); - } - - if (watchGroups.has(groupId)) { - throw new WatcherError( - `Watch group with ID '${groupId}' already exists`, - WatcherErrorCode.INVALID_WATCH_GROUP, - ); - } - } - - function handleError(error: WatcherError): void { - logger.error( - `[DependencyWatcher] ${error.message} (code: ${error.code}, filePath: ${error.filePath})`, - ); - - if (errorCallback) { - errorCallback(error); - } - } - - function setCacheValue(key: string, value: string[]): void { - if (dependencyCache.size >= maxCacheSize) { - const firstKey = dependencyCache.keys().next().value; - if (firstKey) { - dependencyCache.delete(firstKey); - } - } - dependencyCache.set(key, value); - } - - function findAffectedFiles(changedFile: string): string[] { - return dependencyGraphManager.getDependents(changedFile); - } - - function findAffectedGroups(affectedFiles: string[]): string[] { - logger.debug(`Finding affected groups for files: ${affectedFiles.join(", ")}`); - const affectedGroupsSet = new Set(); - - for (const [groupId, group] of watchGroups) { - for (const affectedFile of affectedFiles) { - if (group.files.has(affectedFile)) { - logger.debug(`Group ${groupId} is affected by file: ${affectedFile}`); - affectedGroupsSet.add(groupId); - break; - } - } - } - - return Array.from(affectedGroupsSet); - } - - function calculateImpact(filePath: string): ImpactAnalysisResult { - const cacheKey = `impact:${filePath}`; - let affectedFiles = dependencyCache.get(cacheKey); - - if (!affectedFiles) { - affectedFiles = findAffectedFiles(filePath); - setCacheValue(cacheKey, affectedFiles); - } - - // Include the changed file itself in the affected files - const allAffectedFiles = [filePath, ...affectedFiles]; - const affectedGroups = findAffectedGroups(allAffectedFiles); - - return { - changedFile: filePath, - affectedFiles: allAffectedFiles, - affectedGroups, - }; - } - - async function updateDependencyGraph(): Promise { - const allFiles: string[] = []; - for (const group of watchGroups.values()) { - allFiles.push(...Array.from(group.files)); - } - - await dependencyGraphManager.buildGraph(allFiles); - dependencyCache.clear(); - - if (options.detectCircularDependencies) { - const circularDeps = dependencyGraphManager.findCircularDependencies(); - if (circularDeps.length > 0) { - logger.warn(`Circular dependencies detected: ${JSON.stringify(circularDeps)}`); - } - } - } - - async function handleFileChange(event: FileChangeEvent, filePath: string): Promise { - try { - const absolutePath = path.resolve(filePath); - - if (event === "unlink") { - dependencyGraphManager.removeNode(absolutePath); - } else { - dependencyGraphManager.addNode(absolutePath); - if (event === "change") { - await updateDependencyGraph(); - } - } - - dependencyCache.clear(); - - const impactResult = calculateImpact(absolutePath); - - // If any groups are affected, trigger restart instead of calling callbacks - if (impactResult.affectedGroups.length > 0) { - logger.info("File change detected, restarting watch process...", { - mode: "stream", - }); - logger.info(`Changed file: ${absolutePath}`, { mode: "stream" }); - logger.info(`Affected groups: ${impactResult.affectedGroups.join(", ")}`, { - mode: "stream", - }); - - if (restartCallback) { - restartCallback(); - } - } else { - logger.debug(`No affected groups found for file: ${absolutePath}`); - } - } catch (error) { - handleError( - new WatcherError( - `Failed to handle file change: ${error instanceof Error ? error.message : String(error)}`, - WatcherErrorCode.DEPENDENCY_ANALYSIS_FAILED, - filePath, - error instanceof Error ? error : undefined, - ), - ); - } - } - - function debounceFileChange(event: FileChangeEvent, filePath: string): void { - const key = `${event}:${filePath}`; - - if (debounceTimers.has(key)) { - clearTimeout(debounceTimers.get(key)); - } - - const timer = setTimeout(() => { - handleFileChange(event, filePath); - debounceTimers.delete(key); - }, options.debounceTime || 100); - - debounceTimers.set(key, timer); - } - - async function stop(): Promise { - if (chokidarWatcher) { - await chokidarWatcher.close(); - chokidarWatcher = null; - } - - for (const timer of debounceTimers.values()) { - clearTimeout(timer); - } - debounceTimers.clear(); - - removeSignalHandlers(); - isInitialized = false; - } - - function setupSignalHandlers(): void { - if (signalHandlersRegistered) return; - - const handleSignal = async () => { - try { - await stop(); - logger.info("Watcher stopped successfully"); - process.exit(0); - } catch (error) { - logger.error(`Error during shutdown: ${String(error)}`); - process.exit(0); - } - }; - - process.on("SIGINT", () => handleSignal()); - process.on("SIGTERM", () => handleSignal()); - signalHandlersRegistered = true; - } - - function removeSignalHandlers(): void { - if (!signalHandlersRegistered) return; - - process.removeAllListeners("SIGINT"); - process.removeAllListeners("SIGTERM"); - signalHandlersRegistered = false; - } - - async function initialize(): Promise { - if (isInitialized) return; - - try { - chokidarWatcher = watch([], { - ignored: /node_modules/, - persistent: true, - ignoreInitial: true, - usePolling: false, - awaitWriteFinish: { - stabilityThreshold: 100, - pollInterval: 100, - }, - ...options.chokidarOptions, - }); - - chokidarWatcher.on("add", (filePath: string) => { - logger.debug(`File added: ${filePath}`); - debounceFileChange("add", filePath); - }); - - chokidarWatcher.on("change", (filePath: string) => { - logger.debug(`File changed: ${filePath}`); - debounceFileChange("change", filePath); - }); - - chokidarWatcher.on("unlink", (filePath: string) => { - logger.debug(`File removed: ${filePath}`); - debounceFileChange("unlink", filePath); - }); - - chokidarWatcher.on("error", (error: unknown) => { - logger.error(`Watcher error: ${error instanceof Error ? error.message : String(error)}`, { - mode: "stream", - }); - handleError( - new WatcherError( - `File watcher error: ${error instanceof Error ? error.message : String(error)}`, - WatcherErrorCode.FILE_WATCH_FAILED, - undefined, - error instanceof Error ? error : undefined, - ), - ); - }); - - setupSignalHandlers(); - isInitialized = true; - } catch (error) { - throw new WatcherError( - `Failed to initialize watcher: ${error instanceof Error ? error.message : String(error)}`, - WatcherErrorCode.FILE_WATCH_FAILED, - undefined, - error instanceof Error ? error : undefined, - ); - } - } - - return { - initialize, - - async addWatchGroup(groupId: string, patterns: string[]): Promise { - validateWatchGroup(groupId, patterns); - - if (!isInitialized) { - await initialize(); - } - - const files = new Set(); - for (const pattern of patterns) { - logger.log( - `${styles.dim(`Watch pattern for`)} ${styles.dim(groupId + ":")} ${path.relative(process.cwd(), pattern)}`, - ); - for await (const file of glob(pattern)) { - files.add(path.resolve(file)); - } - } - - const watchGroup: WatchGroup = { - id: groupId, - patterns, - files, - }; - - watchGroups.set(groupId, watchGroup); - - if (chokidarWatcher) { - const filePaths = Array.from(files); - chokidarWatcher.add(filePaths); - } - - await updateDependencyGraph(); - }, - - async removeWatchGroup(groupId: string): Promise { - const watchGroup = watchGroups.get(groupId); - if (!watchGroup) return; - - if (chokidarWatcher) { - chokidarWatcher.unwatch(watchGroup.patterns); - } - - for (const filePath of watchGroup.files) { - dependencyGraphManager.removeNode(filePath); - } - - watchGroups.delete(groupId); - dependencyCache.clear(); - }, - - async start(): Promise { - if (!isInitialized) { - await initialize(); - } - await updateDependencyGraph(); - }, - - stop, - - onError(callback: ErrorCallback): void { - errorCallback = callback; - }, - - updateDependencyGraph, - calculateImpact, - - detectCircularDependencies(): string[][] { - return dependencyGraphManager.findCircularDependencies(); - }, - - getWatchStatus(): WatchStatus { - let fileCount = 0; - for (const group of watchGroups.values()) { - fileCount += group.files.size; - } - - const stats = dependencyGraphManager.getGraphStats(); - - return { - isWatching: isInitialized && chokidarWatcher !== null, - groupCount: watchGroups.size, - fileCount, - dependencyNodeCount: stats.nodeCount, - }; - }, - - setRestartCallback(callback: () => void): void { - restartCallback = callback; - }, - }; -} - -export { WatcherErrorCode }; diff --git a/packages/sdk/src/cli/commands/generate/watch/types.ts b/packages/sdk/src/cli/commands/generate/watch/types.ts deleted file mode 100644 index 0213937b3..000000000 --- a/packages/sdk/src/cli/commands/generate/watch/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type * as madgeTypes from "madge"; - -export type MadgeLoader = ( - path: madgeTypes.MadgePath, - config?: madgeTypes.MadgeConfig, -) => Promise; diff --git a/packages/sdk/src/cli/shared/config-loader.ts b/packages/sdk/src/cli/shared/config-loader.ts index 1f71486cb..36d8d82f6 100644 --- a/packages/sdk/src/cli/shared/config-loader.ts +++ b/packages/sdk/src/cli/shared/config-loader.ts @@ -13,20 +13,13 @@ import type { Plugin } from "#/plugin/types"; */ export type LoadedConfig = AppConfig & { path: string }; -export interface LoadConfigOptions { - /** Import cache-busting value for watch-mode reloads. */ - importNonce?: string; -} - /** * Load Tailor configuration file and associated plugins. * @param configPath - Optional explicit config path - * @param options - Optional module import behavior. * @returns Loaded config, plugins, and config path */ export async function loadConfig( configPath?: string, - options: LoadConfigOptions = {}, ): Promise<{ config: LoadedConfig; plugins: Plugin[] }> { installCliTailordbStub(); const foundPath = loadConfigPath(configPath); @@ -42,9 +35,6 @@ export async function loadConfig( } const configUrl = pathToFileURL(resolvedPath); - if (options.importNonce) { - configUrl.searchParams.set("tailorImportNonce", options.importNonce); - } const configModule = await import(configUrl.href); if (!configModule || !configModule.default) { throw new Error("Invalid Tailor config module: default export not found"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a8ac8f9c..243709c10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -463,9 +463,6 @@ importers: chalk: specifier: 5.6.2 version: 5.6.2 - chokidar: - specifier: 5.0.0 - version: 5.0.0 confbox: specifier: 0.2.4 version: 0.2.4 @@ -490,9 +487,6 @@ importers: kysely: specifier: 0.29.3 version: 0.29.3 - madge: - specifier: 8.0.0 - version: 8.0.0(typescript@6.0.3) mime-types: specifier: 3.0.2 version: 3.0.2 @@ -557,9 +551,6 @@ importers: '@tailor-platform/tailor-proto': specifier: workspace:^ version: link:../tailor-proto - '@types/madge': - specifier: 5.0.3 - version: 5.0.3 '@types/mime-types': specifier: 3.0.1 version: 3.0.1 @@ -2401,9 +2392,6 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/madge@5.0.3': - resolution: {integrity: sha512-NlQJd0qRAoyu+pawTDhLxkW940QT2dqASfwd2g/xEZu2F4Xjwa7TVRSPdbmZwUF1ygvAh0/nepeN7JjwEuOXCA==} - '@types/mime-types@3.0.1': resolution: {integrity: sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==} @@ -5516,10 +5504,6 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/madge@5.0.3': - dependencies: - '@types/node': 24.13.3 - '@types/mime-types@3.0.1': {} '@types/node@24.13.3': From 88110dcaf9fb7f33d36284d61f541d8f01f840e4 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 16:21:32 +0900 Subject: [PATCH 587/618] fix(codemod): address Copilot review on the pending-boundary resolver - Insert the resolved constant above any JSDoc block preceding V2_NEXT_PENDING's declaration, not between them, so the comment stays attached to the sentinel instead of the new constant. - Reject a non-numeric next.N identifier (e.g. next.foo) instead of producing an unpredictable constant name from it. - Let a codemod pinned to V2_NEXT_PENDING apply once the target reaches the stable boundary, so it isn't permanently unselectable if the CI fixup step never runs. It still never matches an intermediate prerelease, since the exact next.N it needs is unknown. - Restore the original HEAD after gh pr checkout in resolve-pending-codemod-boundaries.sh (via a trap, covering the early-exit path too), so later release.yml steps that read HEAD don't operate on the release PR's unpublished commit. --- .../resolve-pending-codemod-boundaries.sh | 8 +++++ packages/sdk-codemod/src/registry.test.ts | 31 ++++++++++++++++++- packages/sdk-codemod/src/registry.ts | 9 +++--- .../src/resolve-pending-boundaries.test.ts | 29 +++++++++++++++++ .../src/resolve-pending-boundaries.ts | 11 +++++-- 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/.github/scripts/resolve-pending-codemod-boundaries.sh b/.github/scripts/resolve-pending-codemod-boundaries.sh index d6d33072e..c82f98d5c 100644 --- a/.github/scripts/resolve-pending-codemod-boundaries.sh +++ b/.github/scripts/resolve-pending-codemod-boundaries.sh @@ -9,8 +9,16 @@ # Pushes the fixup straight to the release PR branch through the GitHub # Contents API (like ensure-github-releases.sh) rather than a local git # commit/push, since this token has no configured git identity or signing key. +# +# `gh pr checkout` moves the working tree onto the release PR branch, but the +# steps after this one (notably ensure-github-releases.sh) assume HEAD is +# still the commit that triggered the workflow. Restore it on every exit path +# so an unpublished release-PR commit is never mistaken for that trigger. set -euo pipefail +original_ref="$(git rev-parse HEAD)" +trap 'git checkout --quiet "$original_ref"' EXIT + PR_BRANCH="$(gh pr view "$PR_NUMBER" --json headRefName -q .headRefName)" gh pr checkout "$PR_NUMBER" diff --git a/packages/sdk-codemod/src/registry.test.ts b/packages/sdk-codemod/src/registry.test.ts index 0e7fb7b95..cb571654a 100644 --- a/packages/sdk-codemod/src/registry.test.ts +++ b/packages/sdk-codemod/src/registry.test.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs"; import * as path from "pathe"; import picomatch from "picomatch"; import { describe, expect, test } from "vitest"; -import { allCodemods, getApplicableCodemods } from "./registry"; +import { V2_NEXT_PENDING, allCodemods, getApplicableCodemods } from "./registry"; describe("getApplicableCodemods", () => { test("returns codemods when upgrading across their version boundary", () => { @@ -125,6 +125,35 @@ describe("getApplicableCodemods", () => { expect(getApplicableCodemods("2.0.0-next.2", "2.0.0-next.2")).toEqual([]); }); + describe("a codemod pinned to V2_NEXT_PENDING", () => { + const pending = { + id: "v2/pending-boundary-test", + name: "Pending boundary test", + description: "Pending boundary test", + since: "1.0.0", + until: "2.0.0", + prereleaseUntil: V2_NEXT_PENDING, + }; + + test("never applies while upgrading between prereleases", () => { + allCodemods.push(pending); + try { + expect(getApplicableCodemods("2.0.0-next.4", "2.0.0-next.5")).not.toContain(pending); + } finally { + allCodemods.pop(); + } + }); + + test("still applies once the target reaches the stable boundary, as a fallback", () => { + allCodemods.push(pending); + try { + expect(getApplicableCodemods("2.0.0-next.4", "2.0.0")).toContain(pending); + } finally { + allCodemods.pop(); + } + }); + }); + test("runs stable-only codemods when upgrading from a prerelease to stable", () => { const ids = getApplicableCodemods("2.0.0-next.2", "2.0.0").map((codemod) => codemod.id); diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 11cbc782f..95a693f8c 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -1082,13 +1082,14 @@ export function resolveCodemodScript(scriptPath: string): string { } function reachesCodemodBoundary(toVersion: string, codemod: CodemodPackage): boolean { - if (codemod.prereleaseUntil === V2_NEXT_PENDING) { - return false; - } if (gte(toVersion, codemod.until)) { return true; } - if (codemod.prereleaseUntil === undefined || !gte(toVersion, codemod.prereleaseUntil)) { + if ( + codemod.prereleaseUntil === undefined || + codemod.prereleaseUntil === V2_NEXT_PENDING || + !gte(toVersion, codemod.prereleaseUntil) + ) { return false; } diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts index b5eb1f515..cd9f55c11 100644 --- a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts @@ -71,6 +71,14 @@ describe("resolvePendingBoundaries", () => { ); }); + test("throws when the next identifier is not numeric", () => { + const source = registrySource(" prereleaseUntil: V2_NEXT_PENDING,"); + + expect(() => resolvePendingBoundaries(source, "2.0.0-next.foo")).toThrow( + 'resolvedVersion must be a "next.N" prerelease', + ); + }); + test("throws when the resolved version is not valid semver", () => { const source = registrySource(" prereleaseUntil: V2_NEXT_PENDING,"); @@ -78,4 +86,25 @@ describe("resolvePendingBoundaries", () => { "resolvedVersion must be a valid semver version", ); }); + + test("inserts the new constant above a JSDoc block preceding V2_NEXT_PENDING", () => { + const source = [ + V2_NEXT_4_DECL, + "/**", + " * Sentinel for pending codemods.", + " */", + PENDING_DECL, + " prereleaseUntil: V2_NEXT_PENDING,", + ].join("\n"); + const result = resolvePendingBoundaries(source, "2.0.0-next.5"); + + const lines = result.source.split("\n"); + const constIndex = lines.indexOf('const V2_NEXT_5 = "2.0.0-next.5";'); + const jsdocIndex = lines.indexOf("/**"); + expect(constIndex).toBeGreaterThanOrEqual(0); + expect(constIndex).toBeLessThan(jsdocIndex); + expect(result.source).toContain( + ["/**", " * Sentinel for pending codemods.", " */", PENDING_DECL].join("\n"), + ); + }); }); diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.ts index 9c426b089..5308510ab 100644 --- a/packages/sdk-codemod/src/resolve-pending-boundaries.ts +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.ts @@ -1,7 +1,10 @@ import { parse } from "semver"; const PENDING_USAGE_PATTERN = /prereleaseUntil: V2_NEXT_PENDING,/g; -const PENDING_DECLARATION_PATTERN = /^export const V2_NEXT_PENDING = "pending";$/m; +// Includes a preceding JSDoc block (if any) so a new constant is inserted above it, +// not between the comment and `V2_NEXT_PENDING` where it would attach to the wrong export. +const PENDING_DECLARATION_PATTERN = + /(?:^\/\*\*[\s\S]*?\*\/\n)?^export const V2_NEXT_PENDING = "pending";$/m; export interface ResolvePendingBoundariesResult { /** Whether any `V2_NEXT_PENDING` usage was found and rewritten. */ @@ -33,7 +36,11 @@ export function resolvePendingBoundaries( if (parsed === null) { throw new Error(`resolvedVersion must be a valid semver version: ${resolvedVersion}`); } - if (parsed.prerelease.length !== 2 || parsed.prerelease[0] !== "next") { + if ( + parsed.prerelease.length !== 2 || + parsed.prerelease[0] !== "next" || + typeof parsed.prerelease[1] !== "number" + ) { throw new Error( `resolvedVersion must be a "next.N" prerelease to resolve V2_NEXT_PENDING: ${resolvedVersion}`, ); From c7f32565f0b15974f66321c8c44b1d33892ebc2c Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 16:28:55 +0900 Subject: [PATCH 588/618] docs(codemod): explain why V2_NEXT_PENDING must stay export const The comment only mentioned the lint-clean rationale; the resolver script's regex also matches this exact line to find the insertion point, so an edit that dropped the export would silently break codemod:resolve-pending. --- packages/sdk-codemod/src/registry.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 95a693f8c..c29b324f7 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -101,8 +101,10 @@ const V2_NEXT_5 = "2.0.0-next.5"; /** * Sentinel `prereleaseUntil` for a codemod whose exact `2.0.0-next.N` release is not * known yet. `pnpm codemod:resolve-pending`, run in CI against the release PR, replaces - * it with the resolved `V2_NEXT_N` constant once the version is bumped. Exported so it - * stays lint-clean while no codemod currently references it. + * it with the resolved `V2_NEXT_N` constant once the version is bumped. Keep this + * `export const` declaration exactly as-is: resolve-pending-boundaries.ts matches this + * exact line to find where to insert that constant, and exporting also keeps it + * lint-clean between resolutions, when no codemod currently references it. */ export const V2_NEXT_PENDING = "pending"; From e4db171e2a0138ea1a0ba1a972bf895fd0616a28 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 16:32:51 +0900 Subject: [PATCH 589/618] chore(sdk): remove stale generate:watch script and add v2 codemod Delete the now-broken example/package.json generate:watch script and its README section left over from the --watch removal. Add a suspicious-pattern-only (manual review) v2 codemod entry that flags tailor generate --watch/-W invocations and programmatic generate({ watch }) usage. --- .changeset/generate-watch-codemod.md | 5 ++++ example/README.md | 3 -- example/package.json | 1 - packages/sdk-codemod/src/registry.ts | 43 ++++++++++++++++++++++++++++ packages/sdk/docs/migration/v2.md | 41 ++++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 .changeset/generate-watch-codemod.md diff --git a/.changeset/generate-watch-codemod.md b/.changeset/generate-watch-codemod.md new file mode 100644 index 000000000..2603c0ef6 --- /dev/null +++ b/.changeset/generate-watch-codemod.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Flag `tailor generate --watch` / `-W` invocations and programmatic `generate({ watch })` usage for manual review as part of the v2 migration (the flag and its dependency watcher are removed in `@tailor-platform/sdk` v2). diff --git a/example/README.md b/example/README.md index 535996e36..8c3e9024e 100644 --- a/example/README.md +++ b/example/README.md @@ -16,9 +16,6 @@ This example is used for: # Generate code and types pnpm generate -# Watch mode -pnpm generate:watch - # Deploy to Tailor Platform pnpm deploy diff --git a/example/package.json b/example/package.json index a78be44d7..501889116 100644 --- a/example/package.json +++ b/example/package.json @@ -7,7 +7,6 @@ "type": "module", "scripts": { "generate": "tailor generate -c tailor.config.ts", - "generate:watch": "tailor generate -c tailor.config.ts --watch", "deploy": "tailor deploy -c tailor.config.ts", "test": "pnpm test:generator", "test:all": "pnpm test:generator:prepare && vitest", diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index ad4e3df88..6146a3594 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -98,6 +98,7 @@ const V2_NEXT_1 = "2.0.0-next.1"; const V2_NEXT_2 = "2.0.0-next.2"; const V2_NEXT_3 = "2.0.0-next.3"; const V2_NEXT_4 = "2.0.0-next.4"; +const V2_NEXT_6 = "2.0.0-next.6"; /** All registered codemods, in registration order. */ export const allCodemods: CodemodPackage[] = [ @@ -1054,6 +1055,48 @@ export const allCodemods: CodemodPackage[] = [ "3. Remove unused `Hooks` / `HookFn<>` type imports", ].join("\n"), }, + { + id: "v2/generate-watch-flag", + name: "generate --watch flag removed", + description: + "Review and remove `tailor generate --watch` / `-W` invocations and the `watch` option on `GenerateOptions`. The flag, its dependency watcher, and the self-restart-on-change logic are removed; `generate` now always performs a single generation pass.", + since: "1.0.0", + until: "2.0.0", + prereleaseUntil: V2_NEXT_6, + filePatterns: [ + "**/package.json", + "**/*.{sh,bash,zsh,yml,yaml}", + "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}", + "**/*.md", + ], + suspiciousPatterns: [ + /\bgenerate\b[^\n]*(?:--watch\b|\s-W\b)/, + [/\bgenerate\s*\(/, /\bwatch\s*:/], + ], + examples: [ + { + lang: "sh", + caption: "The --watch/-W flag no longer exists; re-run generate after each change:", + before: "tailor generate --watch", + after: "tailor generate", + }, + ], + prompt: [ + "Tailor SDK v2 removes the `generate --watch` (`-W`) flag along with the", + "dependency watcher and self-restart logic that powered it. `tailor generate`", + "now always runs a single generation pass and exits.", + "", + "For each flagged `tailor generate ... --watch` / `-W` invocation (package.json", + "scripts, shell scripts, CI configs, or docs), drop the flag and re-run", + "`tailor generate` after each change instead. If automatic regeneration on file", + "change is still needed, wrap the command with a general-purpose file watcher", + "(e.g. `chokidar-cli`, `nodemon`) at the project level.", + "", + "For programmatic use of `generate()` from `@tailor-platform/sdk/cli`, remove the", + "`watch` field from the `GenerateOptions` argument — the function now performs a", + "single generation pass and resolves once it completes.", + ].join("\n"), + }, { id: "v2/node-minimum-22-15-0", name: "Node.js minimum version raised to 22.15.0", diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index d49ede61e..d7dfa2996 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -1031,6 +1031,47 @@ Migration steps for each `.hooks()` call on a `db.type()`: +## generate --watch flag removed + +**Migration:** Manual + +Review and remove `tailor generate --watch` / `-W` invocations and the `watch` option on `GenerateOptions`. The flag, its dependency watcher, and the self-restart-on-change logic are removed; `generate` now always performs a single generation pass. + +The --watch/-W flag no longer exists; re-run generate after each change: + +Before: + +```sh +tailor generate --watch +``` + +After: + +```sh +tailor generate +``` + +
+Prompt for an AI agent (to perform this migration) + +```text +Tailor SDK v2 removes the `generate --watch` (`-W`) flag along with the +dependency watcher and self-restart logic that powered it. `tailor generate` +now always runs a single generation pass and exits. + +For each flagged `tailor generate ... --watch` / `-W` invocation (package.json +scripts, shell scripts, CI configs, or docs), drop the flag and re-run +`tailor generate` after each change instead. If automatic regeneration on file +change is still needed, wrap the command with a general-purpose file watcher +(e.g. `chokidar-cli`, `nodemon`) at the project level. + +For programmatic use of `generate()` from `@tailor-platform/sdk/cli`, remove the +`watch` field from the `GenerateOptions` argument — the function now performs a +single generation pass and resolves once it completes. +``` + +
+ ## Behavioral changes (no migration required) These v2 changes alter runtime or CLI behavior; no source change is needed. From 1a1f944fb4cbe898a80a423454d87bfeaea7cf44 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 16:42:21 +0900 Subject: [PATCH 590/618] fix(codemod): use a portable base64 invocation in the pending-boundary script `base64 -w0 ` is GNU-coreutils-only: macOS's BSD base64 rejects both the -w flag and a positional filename argument (it only reads a file via -i). Piping the file through stdin (`base64 Date: Thu, 16 Jul 2026 16:43:38 +0900 Subject: [PATCH 591/618] fix(sdk): restore loadConfig importNonce option The generate --watch removal dropped LoadConfigOptions/importNonce from loadConfig() assuming it was dead code, but sdk-tailordb-erd-plugin's serve command still relies on it to cache-bust config reloads for its own (unrelated) ERD watch/reload feature. Restore the option to fix that package's typecheck. --- packages/sdk/src/cli/shared/config-loader.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/sdk/src/cli/shared/config-loader.ts b/packages/sdk/src/cli/shared/config-loader.ts index 36d8d82f6..3f85e9c9e 100644 --- a/packages/sdk/src/cli/shared/config-loader.ts +++ b/packages/sdk/src/cli/shared/config-loader.ts @@ -13,13 +13,20 @@ import type { Plugin } from "#/plugin/types"; */ export type LoadedConfig = AppConfig & { path: string }; +export interface LoadConfigOptions { + /** Import cache-busting value for callers that reload the config module after a rebuild. */ + importNonce?: string; +} + /** * Load Tailor configuration file and associated plugins. * @param configPath - Optional explicit config path + * @param options - Optional module import behavior. * @returns Loaded config, plugins, and config path */ export async function loadConfig( configPath?: string, + options: LoadConfigOptions = {}, ): Promise<{ config: LoadedConfig; plugins: Plugin[] }> { installCliTailordbStub(); const foundPath = loadConfigPath(configPath); @@ -35,6 +42,9 @@ export async function loadConfig( } const configUrl = pathToFileURL(resolvedPath); + if (options.importNonce) { + configUrl.searchParams.set("tailorImportNonce", options.importNonce); + } const configModule = await import(configUrl.href); if (!configModule || !configModule.default) { throw new Error("Invalid Tailor config module: default export not found"); From 97ced1d4cf681321c6e83aa43aa26d0ef9f8d8b9 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 17:01:59 +0900 Subject: [PATCH 592/618] fix(codemod): harden the pending-boundary resolver against dirty checkouts and off-line versions - The EXIT trap restored the original commit with a plain `git checkout`, which refuses to switch commits while registry.ts still has the uncommitted edits codemod:resolve-pending and oxfmt just made, leaving the runner stuck on the release PR's commit for later steps. Reproduced locally against two distinct real commits (a plain checkout with a dirty tracked file does fail this way) and confirmed `git reset --hard` restores cleanly regardless. Use that in the trap instead. - resolvePendingBoundaries() only validated the next.N prerelease identifier, not that resolvedVersion is actually on the 2.0.0 line. A version like 2.1.0-next.1 would silently generate a V2_NEXT_1 constant indistinguishable from (and colliding with) the real 2.0.0-next.1 one, and later fail assertCodemodBoundaries() at the next getApplicableCodemods() call. Reject major/minor/patch != 2.0.0 explicitly. --- .../scripts/resolve-pending-codemod-boundaries.sh | 4 +++- .../src/resolve-pending-boundaries.test.ts | 15 +++++++++++++-- .../sdk-codemod/src/resolve-pending-boundaries.ts | 5 ++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/scripts/resolve-pending-codemod-boundaries.sh b/.github/scripts/resolve-pending-codemod-boundaries.sh index 0d7163303..0c150070f 100644 --- a/.github/scripts/resolve-pending-codemod-boundaries.sh +++ b/.github/scripts/resolve-pending-codemod-boundaries.sh @@ -17,7 +17,9 @@ set -euo pipefail original_ref="$(git rev-parse HEAD)" -trap 'git checkout --quiet "$original_ref"' EXIT +# --hard: codemod:resolve-pending and oxfmt below leave registry.ts modified, and a +# plain `git checkout` refuses to switch away from a dirty tracked file. +trap 'git reset --hard --quiet "$original_ref"' EXIT PR_BRANCH="$(gh pr view "$PR_NUMBER" --json headRefName -q .headRefName)" gh pr checkout "$PR_NUMBER" diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts index cd9f55c11..2e7f828c3 100644 --- a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts @@ -67,7 +67,7 @@ describe("resolvePendingBoundaries", () => { const source = registrySource(" prereleaseUntil: V2_NEXT_PENDING,"); expect(() => resolvePendingBoundaries(source, "2.0.0")).toThrow( - 'resolvedVersion must be a "next.N" prerelease', + 'resolvedVersion must be a "2.0.0-next.N" prerelease', ); }); @@ -75,7 +75,18 @@ describe("resolvePendingBoundaries", () => { const source = registrySource(" prereleaseUntil: V2_NEXT_PENDING,"); expect(() => resolvePendingBoundaries(source, "2.0.0-next.foo")).toThrow( - 'resolvedVersion must be a "next.N" prerelease', + 'resolvedVersion must be a "2.0.0-next.N" prerelease', + ); + }); + + test("throws when the resolved version is not on the 2.0.0 line", () => { + const source = registrySource(" prereleaseUntil: V2_NEXT_PENDING,"); + + expect(() => resolvePendingBoundaries(source, "2.1.0-next.1")).toThrow( + 'resolvedVersion must be a "2.0.0-next.N" prerelease', + ); + expect(() => resolvePendingBoundaries(source, "3.0.0-next.1")).toThrow( + 'resolvedVersion must be a "2.0.0-next.N" prerelease', ); }); diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.ts index 5308510ab..8a1f97b6b 100644 --- a/packages/sdk-codemod/src/resolve-pending-boundaries.ts +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.ts @@ -37,12 +37,15 @@ export function resolvePendingBoundaries( throw new Error(`resolvedVersion must be a valid semver version: ${resolvedVersion}`); } if ( + parsed.major !== 2 || + parsed.minor !== 0 || + parsed.patch !== 0 || parsed.prerelease.length !== 2 || parsed.prerelease[0] !== "next" || typeof parsed.prerelease[1] !== "number" ) { throw new Error( - `resolvedVersion must be a "next.N" prerelease to resolve V2_NEXT_PENDING: ${resolvedVersion}`, + `resolvedVersion must be a "2.0.0-next.N" prerelease to resolve V2_NEXT_PENDING: ${resolvedVersion}`, ); } const constantName = `V${parsed.major}_NEXT_${parsed.prerelease[1]}`; From 6da187a9b41c84dd8a72ae0c51c4d1fe43be78ef Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 17:21:20 +0900 Subject: [PATCH 593/618] docs(codemod): document legacy bundle cleanup removal as a v2 notice Register the removeLegacyBundleFiles removal as an informational (notice: true) v2 migration entry, following the existing pattern for runtime/CLI behavior changes with no source to migrate (e.g. v2/cli-token-keyring-storage, v2/node-minimum-22-15-0). Regenerate packages/sdk/docs/migration/v2.md from the registry. --- packages/sdk-codemod/src/registry.ts | 9 +++++++++ packages/sdk/docs/migration/v2.md | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index ad4e3df88..373a2f0ae 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -1063,6 +1063,15 @@ export const allCodemods: CodemodPackage[] = [ until: "2.0.0", notice: true, }, + { + id: "v2/remove-legacy-bundle-cleanup", + name: "Legacy bundle artifact cleanup removed from deploy", + description: + "`tailor deploy` no longer deletes on-disk bundle artifacts (`.entry.js` files, workflow-job bundles, and the `hooks-validate-scripts/` directory) left in the SDK output directory (`.tailor` by default) by SDK versions that predate the current in-memory bundling approach. Current bundlers no longer write these files. No source change is required; if such stale files remain from a very old SDK version, delete the output directory manually (it is regenerated automatically).", + since: "1.0.0", + until: "2.0.0", + notice: true, + }, ]; /** diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index d49ede61e..21f2a9386 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -1050,3 +1050,7 @@ The CLI stores human users by their stable subject ID instead of email (email is ### Node.js minimum version raised to 22.15.0 v2 requires Node.js **22.15.0** or later. This is the first version that includes `module.registerHooks()`, which the SDK uses to register its TypeScript loader hook synchronously in the main thread. No source change is required; ensure your environment runs Node.js 22.15.0+. + +### Legacy bundle artifact cleanup removed from deploy + +`tailor deploy` no longer deletes on-disk bundle artifacts (`.entry.js` files, workflow-job bundles, and the `hooks-validate-scripts/` directory) left in the SDK output directory (`.tailor` by default) by SDK versions that predate the current in-memory bundling approach. Current bundlers no longer write these files. No source change is required; if such stale files remain from a very old SDK version, delete the output directory manually (it is regenerated automatically). From 077578aa07e6bc2778e0381a8c1022044e6814a3 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 17:30:11 +0900 Subject: [PATCH 594/618] fix(codemod): tolerate non-canonical formatting and avoid mutating a branch ref MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The V2_NEXT_PENDING usage/declaration regexes required exact single- space, LF-only formatting. Relax them (\s* around the value/comma, \r?\n for the JSDoc boundary) so a manual edit or formatter change doesn't stop codemod:resolve-pending from matching. - After `gh pr checkout`, the trap's `git reset --hard` moves whatever ref is currently checked out — the local branch gh pr checkout just created, not just HEAD. Detach immediately after checkout so the reset only repoints HEAD, leaving that branch (and anything else gh pr checkout set up) untouched. --- .../resolve-pending-codemod-boundaries.sh | 2 ++ .../src/resolve-pending-boundaries.test.ts | 22 +++++++++++++++++++ .../src/resolve-pending-boundaries.ts | 6 +++-- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/scripts/resolve-pending-codemod-boundaries.sh b/.github/scripts/resolve-pending-codemod-boundaries.sh index 0c150070f..1ab6e575a 100644 --- a/.github/scripts/resolve-pending-codemod-boundaries.sh +++ b/.github/scripts/resolve-pending-codemod-boundaries.sh @@ -23,6 +23,8 @@ trap 'git reset --hard --quiet "$original_ref"' EXIT PR_BRANCH="$(gh pr view "$PR_NUMBER" --json headRefName -q .headRefName)" gh pr checkout "$PR_NUMBER" +# Detach so the trap's reset only moves HEAD, not the local branch gh pr checkout made. +git checkout --quiet --detach pnpm codemod:resolve-pending diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts index 2e7f828c3..37e82a28f 100644 --- a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts @@ -118,4 +118,26 @@ describe("resolvePendingBoundaries", () => { ["/**", " * Sentinel for pending codemods.", " */", PENDING_DECL].join("\n"), ); }); + + test("tolerates non-canonical spacing around the usage and declaration", () => { + const source = [ + V2_NEXT_4_DECL, + 'export const V2_NEXT_PENDING="pending";', + " prereleaseUntil:V2_NEXT_PENDING ,", + ].join("\n"); + const result = resolvePendingBoundaries(source, "2.0.0-next.5"); + + expect(result.changed).toBe(true); + expect(result.source).toContain("prereleaseUntil: V2_NEXT_5,"); + }); + + test("tolerates CRLF line endings before the declaration", () => { + const source = [V2_NEXT_4_DECL, PENDING_DECL, " prereleaseUntil: V2_NEXT_PENDING,"].join( + "\r\n", + ); + const result = resolvePendingBoundaries(source, "2.0.0-next.5"); + + expect(result.changed).toBe(true); + expect(result.source).toContain('const V2_NEXT_5 = "2.0.0-next.5";'); + }); }); diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.ts index 8a1f97b6b..fe20b8eff 100644 --- a/packages/sdk-codemod/src/resolve-pending-boundaries.ts +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.ts @@ -1,10 +1,12 @@ import { parse } from "semver"; -const PENDING_USAGE_PATTERN = /prereleaseUntil: V2_NEXT_PENDING,/g; +// Tolerant of whitespace and CRLF, not just the exact oxfmt-formatted spacing this file +// happens to use today, so a manual edit or formatter change doesn't stop this from matching. +const PENDING_USAGE_PATTERN = /prereleaseUntil:\s*V2_NEXT_PENDING\s*,/g; // Includes a preceding JSDoc block (if any) so a new constant is inserted above it, // not between the comment and `V2_NEXT_PENDING` where it would attach to the wrong export. const PENDING_DECLARATION_PATTERN = - /(?:^\/\*\*[\s\S]*?\*\/\n)?^export const V2_NEXT_PENDING = "pending";$/m; + /(?:^\/\*\*[\s\S]*?\*\/\r?\n)?^export const\s+V2_NEXT_PENDING\s*=\s*"pending";$/m; export interface ResolvePendingBoundariesResult { /** Whether any `V2_NEXT_PENDING` usage was found and rewritten. */ From 02c1f625b9b01065a2b514b00a1eb984c708b618 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 17:36:48 +0900 Subject: [PATCH 595/618] fix(workflow): genericize the bundle-cache extra-context field name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the shared computeBundlerContextHash helper's serializedStartContext field (and its call sites in the workflow, resolver, executor, auth, and http-adapter bundlers) to extraContext. The helper is used by every bundler, not only workflow-related ones — the HTTP adapter bundler passes a serialized method list, not a workflow start context — so the previous name and JSDoc leaked workflow-specific wording into a generic helper. --- packages/sdk/src/cli/cache/bundle-cache.test.ts | 4 ++-- packages/sdk/src/cli/cache/bundle-cache.ts | 14 +++++++------- packages/sdk/src/cli/services/auth/bundler.ts | 2 +- packages/sdk/src/cli/services/executor/bundler.ts | 2 +- .../sdk/src/cli/services/http-adapter/bundler.ts | 2 +- packages/sdk/src/cli/services/resolver/bundler.ts | 2 +- packages/sdk/src/cli/services/workflow/bundler.ts | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/sdk/src/cli/cache/bundle-cache.test.ts b/packages/sdk/src/cli/cache/bundle-cache.test.ts index 0e1fbe68a..6cfcfab1b 100644 --- a/packages/sdk/src/cli/cache/bundle-cache.test.ts +++ b/packages/sdk/src/cli/cache/bundle-cache.test.ts @@ -427,7 +427,7 @@ describe("withCache", () => { describe("computeBundlerContextHash", () => { const baseParams = { sourceFile: "/tmp/src/resolver.ts", - serializedStartContext: "ctx", + extraContext: "ctx", }; test("returns the same hash for identical inputs", () => { @@ -440,7 +440,7 @@ describe("computeBundlerContextHash", () => { test.each([ ["sourceFile", {}, { sourceFile: "/tmp/src/executor.ts" }], - ["serializedStartContext", {}, { serializedStartContext: "other" }], + ["extraContext", {}, { extraContext: "other" }], ["prefix", { prefix: "ENV_A=1" }, { prefix: "ENV_B=2" }], ["bundleLogLevel", { bundleLogLevel: "DEBUG" }, { bundleLogLevel: "WARN" }], ])("returns different hash when %s differs", (_label, overrideA, overrideB) => { diff --git a/packages/sdk/src/cli/cache/bundle-cache.ts b/packages/sdk/src/cli/cache/bundle-cache.ts index b7f55ebae..3561e252a 100644 --- a/packages/sdk/src/cli/cache/bundle-cache.ts +++ b/packages/sdk/src/cli/cache/bundle-cache.ts @@ -54,7 +54,7 @@ function combineHash(fileHash: string, contextHash?: string): string { type ComputeBundlerContextHashParams = { sourceFile: string; - serializedStartContext: string; + extraContext: string; tsconfig?: string; inlineSourcemap?: boolean; bundleLogLevel?: string; @@ -64,19 +64,19 @@ type ComputeBundlerContextHashParams = { /** * Compute a context hash for cache invalidation across bundlers. * - * Combines the source file path, serialized start context, tsconfig hash, - * sourcemap mode, bundle log level, and an optional prefix (e.g., serialized - * env variables) into a single SHA-256 hash. + * Combines the source file path, a caller-supplied extra context string + * (e.g. serialized workflow start-call bindings, or an HTTP adapter's method + * list), tsconfig hash, sourcemap mode, bundle log level, and an optional + * prefix (e.g., serialized env variables) into a single SHA-256 hash. * @param params - Context hash computation parameters * @returns SHA-256 hex digest of the combined context */ function computeBundlerContextHash(params: ComputeBundlerContextHashParams): string { - const { sourceFile, serializedStartContext, tsconfig, inlineSourcemap, bundleLogLevel, prefix } = - params; + const { sourceFile, extraContext, tsconfig, inlineSourcemap, bundleLogLevel, prefix } = params; return hashContent( (prefix ?? "") + path.resolve(sourceFile) + - serializedStartContext + + extraContext + (tsconfig ? hashFile(tsconfig) : "") + String(inlineSourcemap ?? false) + (bundleLogLevel ?? ""), diff --git a/packages/sdk/src/cli/services/auth/bundler.ts b/packages/sdk/src/cli/services/auth/bundler.ts index 34a707695..9f3f7b3dc 100644 --- a/packages/sdk/src/cli/services/auth/bundler.ts +++ b/packages/sdk/src/cli/services/auth/bundler.ts @@ -85,7 +85,7 @@ export async function bundleAuthHooks( ); const contextHash = computeBundlerContextHash({ sourceFile: absoluteConfigPath, - serializedStartContext, + extraContext: serializedStartContext, tsconfig, inlineSourcemap, bundleLogLevel, diff --git a/packages/sdk/src/cli/services/executor/bundler.ts b/packages/sdk/src/cli/services/executor/bundler.ts index 08b426caa..7a26be151 100644 --- a/packages/sdk/src/cli/services/executor/bundler.ts +++ b/packages/sdk/src/cli/services/executor/bundler.ts @@ -152,7 +152,7 @@ async function bundleSingleExecutor( const contextHash = computeBundlerContextHash({ sourceFile: executor.sourceFile, - serializedStartContext, + extraContext: serializedStartContext, tsconfig, inlineSourcemap, bundleLogLevel, diff --git a/packages/sdk/src/cli/services/http-adapter/bundler.ts b/packages/sdk/src/cli/services/http-adapter/bundler.ts index 4958b978c..57dac1de7 100644 --- a/packages/sdk/src/cli/services/http-adapter/bundler.ts +++ b/packages/sdk/src/cli/services/http-adapter/bundler.ts @@ -100,7 +100,7 @@ async function bundleAdapterScript( ): Promise<[string, "input" | "output", string]> { const contextHash = computeBundlerContextHash({ sourceFile: adapter.sourceFile, - serializedStartContext: kind === "input" ? adapter.methods.join(",") : "", + extraContext: kind === "input" ? adapter.methods.join(",") : "", tsconfig, inlineSourcemap: false, bundleLogLevel, diff --git a/packages/sdk/src/cli/services/resolver/bundler.ts b/packages/sdk/src/cli/services/resolver/bundler.ts index fc33eb4f4..bc3f77599 100644 --- a/packages/sdk/src/cli/services/resolver/bundler.ts +++ b/packages/sdk/src/cli/services/resolver/bundler.ts @@ -128,7 +128,7 @@ async function bundleSingleResolver( const contextHash = computeBundlerContextHash({ sourceFile: resolver.sourceFile, - serializedStartContext, + extraContext: serializedStartContext, tsconfig, inlineSourcemap, bundleLogLevel, diff --git a/packages/sdk/src/cli/services/workflow/bundler.ts b/packages/sdk/src/cli/services/workflow/bundler.ts index 00cf6a1bf..38dd078e1 100644 --- a/packages/sdk/src/cli/services/workflow/bundler.ts +++ b/packages/sdk/src/cli/services/workflow/bundler.ts @@ -277,7 +277,7 @@ async function bundleSingleJob( ); const contextHash = computeBundlerContextHash({ sourceFile: job.sourceFile, - serializedStartContext, + extraContext: serializedStartContext, tsconfig, inlineSourcemap, bundleLogLevel, From 28bf5087da8ec074dde13644a1c7edaf263acedf 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, 16 Jul 2026 08:37:34 +0000 Subject: [PATCH 596/618] Version Packages (next) --- .changeset/pre.json | 13 ++++++++-- packages/create-sdk/CHANGELOG.md | 2 ++ packages/create-sdk/package.json | 2 +- packages/sdk-codemod/CHANGELOG.md | 6 +++++ packages/sdk-codemod/package.json | 2 +- packages/sdk-tailordb-erd-plugin/CHANGELOG.md | 12 +++++++++ packages/sdk-tailordb-erd-plugin/package.json | 2 +- packages/sdk/CHANGELOG.md | 26 +++++++++++++++++++ packages/sdk/package.json | 2 +- 9 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 packages/sdk-tailordb-erd-plugin/CHANGELOG.md diff --git a/.changeset/pre.json b/.changeset/pre.json index 24c5500d9..b9ecfaa00 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -2,9 +2,10 @@ "mode": "pre", "tag": "next", "changesets": [ + "add-erd-plugin-package", "apply-deploy-source-strings", "auth-attributes-rename", - "calm-types-rest", + "cli-plugin-schema-api", "cli-plugins", "codemod-llm-review", "codemod-migration-docs", @@ -13,14 +14,18 @@ "db-table-builder", "execute-script-json-arg", "execute-script-review-scope", - "fix-execution-policy-match-semantics-docs", + "extract-erd-plugin", + "field-parse-runtime-layer", "fix-mockfile-open-download-stream", "fix-rename-bin-source-files", "fix-ts-hook-dotted-basename", "fix-ts-hook-paths-without-baseurl", "fix-v2-prerelease-codemods", + "fix-validate-issues-generic-field", "forward-relation-name", + "generate-watch-codemod", "invoker-option-rename", + "isolate-concurrent-deployment-bundles", "keyring-default-storage", "kysely-date-timestamp", "open-download-review-scope", @@ -35,6 +40,7 @@ "remove-auth-invoker-helper", "remove-define-generators", "remove-function-test-run-input-wrapper", + "remove-generate-watch", "remove-open-download-stream", "remove-runtime-globals-compatibility", "remove-tailor-sdk-skills-shim", @@ -45,6 +51,7 @@ "rename-bin-command", "rename-tailor-cli-env", "require-function-log-content-hash", + "require-tailordb-permission-config", "revert-strict-scalar-strings", "runtime-global-source-strings", "runtime-globals-import", @@ -59,6 +66,8 @@ "user-profile-type-schema", "v2-baseline", "wait-point-rename", + "workflow-canonical-names", + "workflow-executor-args-contract", "workflow-trigger-dispatch" ] } diff --git a/packages/create-sdk/CHANGELOG.md b/packages/create-sdk/CHANGELOG.md index b7e3b2c0b..1811e3891 100644 --- a/packages/create-sdk/CHANGELOG.md +++ b/packages/create-sdk/CHANGELOG.md @@ -1,5 +1,7 @@ # @tailor-platform/create-sdk +## 2.0.0-next.6 + ## 2.0.0-next.5 ### Patch Changes diff --git a/packages/create-sdk/package.json b/packages/create-sdk/package.json index 32ea3584b..2bcf36f4b 100644 --- a/packages/create-sdk/package.json +++ b/packages/create-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/create-sdk", - "version": "2.0.0-next.5", + "version": "2.0.0-next.6", "description": "A CLI tool to quickly create a new Tailor Platform SDK project", "license": "MIT", "repository": { diff --git a/packages/sdk-codemod/CHANGELOG.md b/packages/sdk-codemod/CHANGELOG.md index 3b85e2019..0901a0135 100644 --- a/packages/sdk-codemod/CHANGELOG.md +++ b/packages/sdk-codemod/CHANGELOG.md @@ -1,5 +1,11 @@ # @tailor-platform/sdk-codemod +## 0.3.0-next.6 + +### Patch Changes + +- [#1789](https://github.com/tailor-platform/sdk/pull/1789) [`e4db171`](https://github.com/tailor-platform/sdk/commit/e4db171e2a0138ea1a0ba1a972bf895fd0616a28) Thanks [@toiroakr](https://github.com/toiroakr)! - Flag `tailor generate --watch` / `-W` invocations and programmatic `generate({ watch })` usage for manual review as part of the v2 migration (the flag and its dependency watcher are removed in `@tailor-platform/sdk` v2). + ## 0.3.0-next.5 ### Patch Changes diff --git a/packages/sdk-codemod/package.json b/packages/sdk-codemod/package.json index f29422dda..72c0fbfc8 100644 --- a/packages/sdk-codemod/package.json +++ b/packages/sdk-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk-codemod", - "version": "0.3.0-next.5", + "version": "0.3.0-next.6", "description": "Codemod runner for Tailor Platform SDK upgrades", "license": "MIT", "repository": { diff --git a/packages/sdk-tailordb-erd-plugin/CHANGELOG.md b/packages/sdk-tailordb-erd-plugin/CHANGELOG.md new file mode 100644 index 000000000..761e68493 --- /dev/null +++ b/packages/sdk-tailordb-erd-plugin/CHANGELOG.md @@ -0,0 +1,12 @@ +# @tailor-platform/sdk-tailordb-erd-plugin + +## 0.1.0-next.0 + +### Minor Changes + +- [#1776](https://github.com/tailor-platform/sdk/pull/1776) [`7338457`](https://github.com/tailor-platform/sdk/commit/733845732fabff504c32b725f6919be6167bc7a1) Thanks [@dqn](https://github.com/dqn)! - New Tailor CLI plugin providing the `tailor tailordb erd` commands (export, diff, serve, deploy), extracted from `@tailor-platform/sdk`. + +### Patch Changes + +- Updated dependencies [[`7338457`](https://github.com/tailor-platform/sdk/commit/733845732fabff504c32b725f6919be6167bc7a1), [`7338457`](https://github.com/tailor-platform/sdk/commit/733845732fabff504c32b725f6919be6167bc7a1), [`9837182`](https://github.com/tailor-platform/sdk/commit/983718288eab23a4c15762c179a67569dd78a287), [`1316447`](https://github.com/tailor-platform/sdk/commit/13164471cc934da03dd64640b2fb2457c79b4413), [`000db7e`](https://github.com/tailor-platform/sdk/commit/000db7ef91b699918a8da600faa183ebcb40ba7c), [`36ad006`](https://github.com/tailor-platform/sdk/commit/36ad006f9eb2d23b21b2028741dbce1d6ba9f91b), [`322b69c`](https://github.com/tailor-platform/sdk/commit/322b69c843953551ccb4b32d7cbd528ae2b0e10c), [`813f86e`](https://github.com/tailor-platform/sdk/commit/813f86eb0b0df1a768db5de6a39a550d6633749a), [`09f5691`](https://github.com/tailor-platform/sdk/commit/09f5691ef5a76761812f039d125d33eb3211994a)]: + - @tailor-platform/sdk@2.0.0-next.6 diff --git a/packages/sdk-tailordb-erd-plugin/package.json b/packages/sdk-tailordb-erd-plugin/package.json index da55d620f..8ecf6886a 100644 --- a/packages/sdk-tailordb-erd-plugin/package.json +++ b/packages/sdk-tailordb-erd-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk-tailordb-erd-plugin", - "version": "0.0.0", + "version": "0.1.0-next.0", "description": "Tailor CLI plugin providing the `tailor tailordb erd` commands (TailorDB ERD viewer)", "license": "MIT", "repository": { diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 50ad40476..c672fb6d4 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,5 +1,31 @@ # @tailor-platform/sdk +## 2.0.0-next.6 + +### Major Changes + +- [#1776](https://github.com/tailor-platform/sdk/pull/1776) [`7338457`](https://github.com/tailor-platform/sdk/commit/733845732fabff504c32b725f6919be6167bc7a1) Thanks [@dqn](https://github.com/dqn)! - Remove the built-in `tailordb erd` commands. They are now provided by the `@tailor-platform/sdk-tailordb-erd-plugin` CLI plugin: install it with `npm install -D @tailor-platform/sdk-tailordb-erd-plugin` and keep running `tailor tailordb erd ` as before — the CLI dispatches to the plugin automatically and suggests the install command when the plugin is missing. The `erdSite` TailorDB setting is unchanged. + +- [#1789](https://github.com/tailor-platform/sdk/pull/1789) [`36ad006`](https://github.com/tailor-platform/sdk/commit/36ad006f9eb2d23b21b2028741dbce1d6ba9f91b) Thanks [@toiroakr](https://github.com/toiroakr)! - Remove the `generate --watch` (`-W`) flag. `tailor generate` now always runs a single generation pass; re-run the command after making changes instead of relying on the watcher to regenerate automatically. + +- [#1733](https://github.com/tailor-platform/sdk/pull/1733) [`09f5691`](https://github.com/tailor-platform/sdk/commit/09f5691ef5a76761812f039d125d33eb3211994a) Thanks [@dqn](https://github.com/dqn)! - Require workflow executor `args` when the target workflow input is required, and accept primitive and array static JSON inputs during configuration parsing. Existing workflow executor configurations that omit required input must add `args`. + +### Minor Changes + +- [#1776](https://github.com/tailor-platform/sdk/pull/1776) [`7338457`](https://github.com/tailor-platform/sdk/commit/733845732fabff504c32b725f6919be6167bc7a1) Thanks [@dqn](https://github.com/dqn)! - Add `loadTailorDBNamespaces`, `deployStaticWebsite`, `assertWritable`, and `isPluginGeneratedType` (with their related types) to `@tailor-platform/sdk/cli`, so CLI plugins and scripts can load local TailorDB schema data and deploy static websites through the public API. + +- [#1768](https://github.com/tailor-platform/sdk/pull/1768) [`813f86e`](https://github.com/tailor-platform/sdk/commit/813f86eb0b0df1a768db5de6a39a550d6633749a) Thanks [@k1LoW](https://github.com/k1LoW)! - Add canonical `tailor.workflow.*` names that mirror the public `tailor.v1` RPC vocabulary. The `startWorkflow`, `startJobFunction`, and `resumeWorkflowExecution` methods are now available on `tailor.workflow`, alongside `workflow.startWorkflow` / `workflow.startJobFunction` / `workflow.resumeWorkflowExecution` from `@tailor-platform/sdk/runtime`. The pre-alignment names (`triggerWorkflow`, `triggerJobFunction`, `resumeWorkflow`) continue to work as frozen aliases, but are now marked `@deprecated` so IDEs surface a hint to migrate to the canonical names. + +### Patch Changes + +- [#1769](https://github.com/tailor-platform/sdk/pull/1769) [`9837182`](https://github.com/tailor-platform/sdk/commit/983718288eab23a4c15762c179a67569dd78a287) Thanks [@dqn](https://github.com/dqn)! - Fix `tailor function test-run` crashing with `TypeError: Cannot convert undefined or null to object` when `--arg` is a non-object JSON value such as `null`. The argument is now forwarded to the server, which reports the validation error. Local input validation also runs the same logic as deployed resolvers. + +- [#1780](https://github.com/tailor-platform/sdk/pull/1780) [`1316447`](https://github.com/tailor-platform/sdk/commit/13164471cc934da03dd64640b2fb2457c79b4413) Thanks [@toiroakr](https://github.com/toiroakr)! - Fix a spurious `TS2719` type error when a `db.table()` type built with custom fields is passed across module boundaries (e.g. into another package's factory function) and its `.validate()` callback's `issues()` field parameter is compared for assignability. The `issues()` field argument type no longer uses a self-generic parameter, so structurally-equal `TailorDBType` instances derived through different generic instantiation paths are recognized as compatible. + +- [#1781](https://github.com/tailor-platform/sdk/pull/1781) [`000db7e`](https://github.com/tailor-platform/sdk/commit/000db7ef91b699918a8da600faa183ebcb40ba7c) Thanks [@dqn](https://github.com/dqn)! - Prevent concurrent multi-config deployments from mixing resolver, executor, workflow, Auth hook, HTTP adapter, and TailorDB hook or validator bundles + +- [#1777](https://github.com/tailor-platform/sdk/pull/1777) [`322b69c`](https://github.com/tailor-platform/sdk/commit/322b69c843953551ccb4b32d7cbd528ae2b0e10c) Thanks [@toiroakr](https://github.com/toiroakr)! - Fail fast at `generate`/`deploy` time when a TailorDB type is missing `.permission()`, or missing `.gqlPermission()` while GraphQL operations are enabled for it (`.gqlPermission()` is not required when GraphQL exposure is fully disabled via `gqlOperations`). Previously these omissions deployed silently and only surfaced later as an opaque `internal error` on insert. + ## 2.0.0-next.5 ### Major Changes diff --git a/packages/sdk/package.json b/packages/sdk/package.json index feaa556a3..b1026d311 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk", - "version": "2.0.0-next.5", + "version": "2.0.0-next.6", "description": "Tailor Platform SDK - The SDK to work with Tailor Platform", "license": "MIT", "repository": { From 8b6398018de40e50d2a4946a91786ebafe589723 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 17:38:10 +0900 Subject: [PATCH 597/618] docs(sdk-codemod): explain why workflow-start-rename stays manual Replace the workflow-start-rename codemod's claim that resolving a .trigger() receiver back to createWorkflow()/createWorkflowJob() is "not reliably decidable from a single file's syntax" with the actual reasoning: the SDK's own CLI bundler already does this cross-file resolution for build-time rewriting, but replicating it in a standalone script is a nontrivial lift, and a codemod false positive would silently mis-rewrite unrelated code (unlike the bundler, which fails loudly). Manual review is the safer trade-off for the low call-site volume this rename involves. Regenerated docs/migration/v2.md via pnpm codemod:docs:update. --- packages/sdk-codemod/src/registry.ts | 2 +- packages/sdk/docs/migration/v2.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index ffd07b7a7..9ab98c4a9 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -886,7 +886,7 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/workflow-start-rename", name: "Workflow.trigger()/WorkflowJob.trigger() → .start()", description: - "Rename `Workflow.trigger()` (returned by `createWorkflow()`) and `WorkflowJob.trigger()` (returned by `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary. No codemod ships for this rename: distinguishing a workflow/job `.trigger()` call from an unrelated object's own `.trigger()` method requires resolving the receiver back to a `createWorkflow`/`createWorkflowJob` result, which is not reliably decidable from a single file's syntax.", + "Rename `Workflow.trigger()` (returned by `createWorkflow()`) and `WorkflowJob.trigger()` (returned by `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary. No codemod ships for this rename: distinguishing a workflow/job `.trigger()` call from an unrelated object's own `.trigger()` method requires resolving the receiver back to a `createWorkflow`/`createWorkflowJob` result across files, which the SDK's own CLI bundler already does for build-time rewriting. Reusing that logic in a standalone script is a nontrivial lift, and — unlike the bundler, which fails loudly when it cannot rewrite a call — a codemod false positive would silently rewrite an unrelated `.trigger()` call with no error. For the call-site volume this rename typically involves, manual review guided by the prompt below is the safer trade-off.", since: "1.0.0", until: "2.0.0", suspiciousPatterns: [".trigger("], diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 4c4f0343b..9e6022277 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -873,7 +873,7 @@ start result as the job output directly (no Promise wrapper to unwrap). **Migration:** Manual -Rename `Workflow.trigger()` (returned by `createWorkflow()`) and `WorkflowJob.trigger()` (returned by `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary. No codemod ships for this rename: distinguishing a workflow/job `.trigger()` call from an unrelated object's own `.trigger()` method requires resolving the receiver back to a `createWorkflow`/`createWorkflowJob` result, which is not reliably decidable from a single file's syntax. +Rename `Workflow.trigger()` (returned by `createWorkflow()`) and `WorkflowJob.trigger()` (returned by `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary. No codemod ships for this rename: distinguishing a workflow/job `.trigger()` call from an unrelated object's own `.trigger()` method requires resolving the receiver back to a `createWorkflow`/`createWorkflowJob` result across files, which the SDK's own CLI bundler already does for build-time rewriting. Reusing that logic in a standalone script is a nontrivial lift, and — unlike the bundler, which fails loudly when it cannot rewrite a call — a codemod false positive would silently rewrite an unrelated `.trigger()` call with no error. For the call-site volume this rename typically involves, manual review guided by the prompt below is the safer trade-off. Before: From 0a76d020c95d2988b368fea51cbc71f82246a70b Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 18:14:57 +0900 Subject: [PATCH 598/618] fix(sdk-codemod): rename invoker to authInvoker on renamed triggerWorkflow calls startWorkflow's StartWorkflowOptions expects authInvoker directly, but the removed triggerWorkflow wrapper accepted invoker and converted it to authInvoker internally. Mechanically renaming a triggerWorkflow call that passed { invoker: ... } to startWorkflow left the options object in the wrong shape. Extend the workflow-trigger-rename codemod to also rename a literal invoker key (including shorthand { invoker }) to authInvoker in the third argument of a renamed triggerWorkflow call. Options passed via a variable or spread are left for manual review, matching this codemod's existing scope for other residual cases. Add a test fixture covering both the explicit-key and shorthand forms, update the registry description/examples/prompt, and regenerate docs/migration/v2.md. --- .../scripts/transform.ts | 41 +++++++++++++++++++ .../tests/invoker-option-rename/expected.ts | 15 +++++++ .../tests/invoker-option-rename/input.ts | 15 +++++++ packages/sdk-codemod/src/registry.ts | 17 +++++++- packages/sdk/docs/migration/v2.md | 24 ++++++++++- 5 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/invoker-option-rename/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/invoker-option-rename/input.ts diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/scripts/transform.ts index 4e707498e..f6859e299 100644 --- a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/scripts/transform.ts @@ -61,6 +61,43 @@ function isAmbientTailorWorkflow(object: SgNode | null): boolean { ); } +function expressionArguments(args: SgNode): SgNode[] { + return args.children().filter((child) => !["(", ")", ","].includes(child.kind() as string)); +} + +/** + * `triggerWorkflow`'s removed SDK wrapper converted an `invoker` option to + * the platform's `authInvoker` shape before calling through; `startWorkflow` + * expects `authInvoker` directly. Build an edit renaming a literal `invoker` + * key (or shorthand) in the third argument of a `triggerWorkflow(...)` call + * being renamed to `startWorkflow`, so the option keeps working. Options + * passed via a variable/spread are left for manual review — only literal + * object arguments are inspected here. + * @param member - The `.triggerWorkflow` member-expression node being renamed + * @returns An edit renaming the `invoker` key, or null when not applicable + */ +function findInvokerOptionEdit(member: SgNode): Edit | null { + const call = member.parent(); + if (call?.kind() !== "call_expression") return null; + const args = call.field("arguments"); + if (!args) return null; + + const optionsArg = expressionArguments(args)[2]; + if (!optionsArg || optionsArg.kind() !== "object") return null; + + for (const child of optionsArg.children()) { + if (child.kind() === "pair") { + const key = child.field("key"); + if (key?.kind() === "property_identifier" && key.text() === "invoker") { + return key.replace("authInvoker"); + } + } else if (child.kind() === "shorthand_property_identifier" && child.text() === "invoker") { + return child.replace("authInvoker: invoker"); + } + } + return null; +} + /** * Rewrite `.triggerWorkflow(`, `.triggerJobFunction(`, and `.resumeWorkflow(` * member accesses to their canonical `start*`/`resumeWorkflowExecution` names, @@ -94,6 +131,10 @@ export default function transform(source: string, filePath?: string): string | n if (!isWorkflowImportReceiver && !isAmbientReceiver) continue; edits.push(property.replace(newName)); + if (newName === "startWorkflow") { + const invokerEdit = findInvokerOptionEdit(member); + if (invokerEdit) edits.push(invokerEdit); + } } if (edits.length === 0) return null; diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/invoker-option-rename/expected.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/invoker-option-rename/expected.ts new file mode 100644 index 000000000..7d141c131 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/invoker-option-rename/expected.ts @@ -0,0 +1,15 @@ +import { workflow } from "@tailor-platform/sdk/runtime"; + +const invoker = { namespace: "my-auth", machineUserName: "admin" }; + +export async function runShorthand(): Promise { + return await workflow.startWorkflow("myWorkflow", { data: "value" }, { authInvoker: invoker }); +} + +export async function runExplicitKey(): Promise { + return await workflow.startWorkflow( + "myWorkflow", + { data: "value" }, + { authInvoker: { namespace: "my-auth", machineUserName: "admin" } }, + ); +} diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/invoker-option-rename/input.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/invoker-option-rename/input.ts new file mode 100644 index 000000000..36c4b4f07 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/invoker-option-rename/input.ts @@ -0,0 +1,15 @@ +import { workflow } from "@tailor-platform/sdk/runtime"; + +const invoker = { namespace: "my-auth", machineUserName: "admin" }; + +export async function runShorthand(): Promise { + return await workflow.triggerWorkflow("myWorkflow", { data: "value" }, { invoker }); +} + +export async function runExplicitKey(): Promise { + return await workflow.triggerWorkflow( + "myWorkflow", + { data: "value" }, + { invoker: { namespace: "my-auth", machineUserName: "admin" } }, + ); +} diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 445959536..6aeb7a78c 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -722,7 +722,7 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/workflow-trigger-rename", name: "workflow.triggerWorkflow/triggerJobFunction/resumeWorkflow → startWorkflow/startJobFunction/resumeWorkflowExecution", description: - "Rename tailor.workflow call sites from the pre-alignment triggerWorkflow/triggerJobFunction/resumeWorkflow names to the canonical startWorkflow/startJobFunction/resumeWorkflowExecution names, on both the ambient tailor.workflow global and a workflow value imported from @tailor-platform/sdk/runtime(/workflow).", + "Rename tailor.workflow call sites from the pre-alignment triggerWorkflow/triggerJobFunction/resumeWorkflow names to the canonical startWorkflow/startJobFunction/resumeWorkflowExecution names, on both the ambient tailor.workflow global and a workflow value imported from @tailor-platform/sdk/runtime(/workflow). For a renamed triggerWorkflow call, also renames a literal `invoker` option key to `authInvoker` — startWorkflow's options expect the platform shape directly, unlike the removed triggerWorkflow wrapper, which converted invoker to authInvoker internally.", since: "1.0.0", until: "2.0.0", scriptPath: "v2/workflow-trigger-rename/scripts/transform.js", @@ -734,6 +734,13 @@ export const allCodemods: CodemodPackage[] = [ after: 'import { workflow } from "@tailor-platform/sdk/runtime";\n\nawait workflow.startWorkflow("myWorkflow", { data: "value" });', }, + { + caption: "A literal invoker option is renamed to authInvoker:", + before: + 'await workflow.triggerWorkflow("myWorkflow", { data: "value" }, { invoker: myInvoker });', + after: + 'await workflow.startWorkflow("myWorkflow", { data: "value" }, { authInvoker: myInvoker });', + }, ], prompt: [ "The pre-alignment tailor.workflow names triggerWorkflow, triggerJobFunction, and", @@ -746,11 +753,19 @@ export const allCodemods: CodemodPackage[] = [ "ambient tailor name, to avoid rewriting an unrelated same-named value — review", "those manually.", "", + "For a renamed triggerWorkflow call, the codemod also renames a literal invoker", + "option key (including shorthand { invoker }) to authInvoker, since startWorkflow", + "expects the platform's authInvoker shape directly while triggerWorkflow's removed", + "wrapper converted invoker to authInvoker internally.", + "", "Also review, and migrate by hand:", "- Destructured references (e.g. const { triggerWorkflow } = workflow) — the", " codemod only rewrites direct member-access calls.", "- Imported TriggerWorkflowOptions / TriggerJobFunctionOptions types — rename", " them to StartWorkflowOptions / StartJobFunctionOptions.", + "- An invoker option passed via a variable or spread (not a literal object) —", + " the codemod only inspects literal object arguments; rename the invoker key", + " to authInvoker in the options object's own definition.", ].join("\n"), }, { diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 26e7cf970..c2fa18fc0 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -698,7 +698,7 @@ export const { approval } = createWaitPoints((define) => ({ **Migration:** Partially automatic -Rename tailor.workflow call sites from the pre-alignment triggerWorkflow/triggerJobFunction/resumeWorkflow names to the canonical startWorkflow/startJobFunction/resumeWorkflowExecution names, on both the ambient tailor.workflow global and a workflow value imported from @tailor-platform/sdk/runtime(/workflow). +Rename tailor.workflow call sites from the pre-alignment triggerWorkflow/triggerJobFunction/resumeWorkflow names to the canonical startWorkflow/startJobFunction/resumeWorkflowExecution names, on both the ambient tailor.workflow global and a workflow value imported from @tailor-platform/sdk/runtime(/workflow). For a renamed triggerWorkflow call, also renames a literal `invoker` option key to `authInvoker` — startWorkflow's options expect the platform shape directly, unlike the removed triggerWorkflow wrapper, which converted invoker to authInvoker internally. Before: @@ -716,6 +716,20 @@ import { workflow } from "@tailor-platform/sdk/runtime"; await workflow.startWorkflow("myWorkflow", { data: "value" }); ``` +A literal invoker option is renamed to authInvoker: + +Before: + +```ts +await workflow.triggerWorkflow("myWorkflow", { data: "value" }, { invoker: myInvoker }); +``` + +After: + +```ts +await workflow.startWorkflow("myWorkflow", { data: "value" }, { authInvoker: myInvoker }); +``` +
Prompt for an AI agent (to finish the cases the codemod could not migrate) @@ -730,11 +744,19 @@ file entirely when a local declaration shadows the workflow import or the ambient tailor name, to avoid rewriting an unrelated same-named value — review those manually. +For a renamed triggerWorkflow call, the codemod also renames a literal invoker +option key (including shorthand { invoker }) to authInvoker, since startWorkflow +expects the platform's authInvoker shape directly while triggerWorkflow's removed +wrapper converted invoker to authInvoker internally. + Also review, and migrate by hand: - Destructured references (e.g. const { triggerWorkflow } = workflow) — the codemod only rewrites direct member-access calls. - Imported TriggerWorkflowOptions / TriggerJobFunctionOptions types — rename them to StartWorkflowOptions / StartJobFunctionOptions. +- An invoker option passed via a variable or spread (not a literal object) — + the codemod only inspects literal object arguments; rename the invoker key + to authInvoker in the options object's own definition. ```
From 44d537e5a9fc9c00c82f1bf8703fe27072f1f2e4 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 18:15:53 +0900 Subject: [PATCH 599/618] test(workflow): rename stale trigger wording in mock.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A few test titles, local variable names, and fixture name strings still said trigger/triggered while their bodies already used .start() / startJobFunction — a leftover from earlier passes of the .trigger() -> .start() rename that a plain call-site search missed. --- packages/sdk/src/vitest/mock.test.ts | 42 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/sdk/src/vitest/mock.test.ts b/packages/sdk/src/vitest/mock.test.ts index 213a7cbdf..a30e61d7a 100644 --- a/packages/sdk/src/vitest/mock.test.ts +++ b/packages/sdk/src/vitest/mock.test.ts @@ -145,10 +145,10 @@ describe("mock", () => { vi.unstubAllEnvs(); }); - test("records triggered jobs even when no handler is configured", () => { + test("records started jobs even when no handler is configured", () => { using wf = mockWorkflow(); - const trigger = (globalThis as any).tailor.workflow.startJobFunction; - expect(() => trigger("my-job", { key: "value" })).toThrow(/No workflow job mock/); + const start = (globalThis as any).tailor.workflow.startJobFunction; + expect(() => start("my-job", { key: "value" })).toThrow(/No workflow job mock/); expect(wf.startedJobs).toEqual([{ jobName: "my-job", args: { key: "value" } }]); }); @@ -160,8 +160,8 @@ describe("mock", () => { return null; }); - const trigger = (globalThis as any).tailor.workflow.startJobFunction; - const result = trigger("validate", {}); + const start = (globalThis as any).tailor.workflow.startJobFunction; + const result = start("validate", {}); expect(result).toEqual({ valid: true }); }); @@ -170,9 +170,9 @@ describe("mock", () => { using wf = mockWorkflow(); wf.enqueueResults({ step: 1 }, { step: 2 }); - const trigger = (globalThis as any).tailor.workflow.startJobFunction; - expect(trigger("job1", {})).toEqual({ step: 1 }); - expect(trigger("job2", {})).toEqual({ step: 2 }); + const start = (globalThis as any).tailor.workflow.startJobFunction; + expect(start("job1", {})).toEqual({ step: 1 }); + expect(start("job2", {})).toEqual({ step: 2 }); }); test("enqueueResult takes priority over jobHandler", () => { @@ -180,15 +180,15 @@ describe("mock", () => { wf.setJobHandler(() => ({ fallback: true })); wf.enqueueResult({ queued: true }); - const trigger = (globalThis as any).tailor.workflow.startJobFunction; - expect(trigger("job1", {})).toEqual({ queued: true }); - expect(trigger("job2", {})).toEqual({ fallback: true }); + const start = (globalThis as any).tailor.workflow.startJobFunction; + expect(start("job1", {})).toEqual({ queued: true }); + expect(start("job2", {})).toEqual({ fallback: true }); }); test("reset clears all state", () => { using wf = mockWorkflow(); - const trigger = (globalThis as any).tailor.workflow.startJobFunction; - expect(() => trigger("job", {})).toThrow(/No workflow job mock/); + const start = (globalThis as any).tailor.workflow.startJobFunction; + expect(() => start("job", {})).toThrow(/No workflow job mock/); wf.reset(); @@ -286,13 +286,13 @@ describe("mock", () => { test("workflow.start() returns an execution id without running the main job", async () => { let bodyRan = false; const main = createWorkflowJob({ - name: "default-runtime-main-trigger", + name: "default-runtime-main-start", body: () => { bodyRan = true; return { ok: true }; }, }); - const workflow = createWorkflow({ name: "default-runtime-trigger-wf", mainJob: main }); + const workflow = createWorkflow({ name: "default-runtime-start-wf", mainJob: main }); await expect(workflow.start(undefined)).resolves.toBe("00000000-0000-4000-8000-000000000000"); expect(bodyRan).toBe(false); @@ -300,10 +300,10 @@ describe("mock", () => { test("workflow.start() validates args in the default runtime", async () => { const main = createWorkflowJob({ - name: "default-runtime-trigger-args", + name: "default-runtime-start-args", body: (input: { when: string }) => ({ when: input.when }), }); - const workflow = createWorkflow({ name: "default-runtime-trigger-args-wf", mainJob: main }); + const workflow = createWorkflow({ name: "default-runtime-start-args-wf", mainJob: main }); await expect(workflow.start({ when: new Date() } as never)).rejects.toThrow(/Date instance/); }); @@ -326,7 +326,7 @@ describe("mock", () => { expect(await runWorkflowLocally(workflow, { n: 0 })).toEqual({ total: 2 }); }); - test("runWorkflowLocally() clones cached trigger results on replay", async () => { + test("runWorkflowLocally() clones cached start results on replay", async () => { const mutable = createWorkflowJob({ name: "default-runtime-mutable", body: () => ({ items: [] as string[] }), @@ -375,7 +375,7 @@ describe("mock", () => { expect(await runWorkflowLocally(workflow)).toEqual({ ok: true }); }); - test("runWorkflowLocally() replays failed trigger errors into user catch blocks once", async () => { + test("runWorkflowLocally() replays failed start errors into user catch blocks once", async () => { let attempts = 0; const inner = createWorkflowJob({ name: "default-runtime-failing-inner", @@ -407,7 +407,7 @@ describe("mock", () => { expect(attempts).toBe(1); }); - test("runWorkflowLocally() validates nested workflow trigger args", async () => { + test("runWorkflowLocally() validates nested workflow start args", async () => { const innerMain = createWorkflowJob({ name: "default-runtime-nested-workflow-inner", body: (_input: { when: string }) => ({ ok: true }), @@ -431,7 +431,7 @@ describe("mock", () => { await expect(runWorkflowLocally(workflow)).rejects.toThrow(/Date instance/); }); - test("runWorkflowLocally() rejects a non-serializable trigger result", async () => { + test("runWorkflowLocally() rejects a non-serializable start result", async () => { const bad = createWorkflowJob({ name: "default-runtime-bad", body: () => ({ when: new Date() }) as never, From 054d4c8b245f512c31e7b7c521252537665d498c Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 21:35:10 +0900 Subject: [PATCH 600/618] docs: rewrite changeset from a user-facing perspective Address Copilot review feedback: describe the deploy behavior change directly instead of naming the removed internal helper. --- .changeset/remove-legacy-bundle-cleanup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/remove-legacy-bundle-cleanup.md b/.changeset/remove-legacy-bundle-cleanup.md index 07011a671..19e49d376 100644 --- a/.changeset/remove-legacy-bundle-cleanup.md +++ b/.changeset/remove-legacy-bundle-cleanup.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk": patch --- -Remove the internal cleanup step that deleted on-disk bundle artifacts left by SDK versions predating the in-memory bundling approach +`tailor deploy` no longer automatically deletes on-disk bundle artifacts left by SDK versions predating the in-memory bundling approach; delete your output directory manually if stale files remain from a very old SDK version From a32ae975c2b4a5b9972346d20a46987bf48aafd9 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 21:46:44 +0900 Subject: [PATCH 601/618] fix(codemod): tolerate whitespace in export/const spacing and existing-constant lookup - PENDING_DECLARATION_PATTERN required exactly one space between `export` and `const`; relax it to \s+ so `export const V2_NEXT_PENDING` still matches. - The existing-constant lookup (`^const ${name} = "...";$`) had no whitespace tolerance at all. If a previously-inserted V2_NEXT_N declaration's spacing ever differed, this would silently insert a duplicate const declaration instead of reusing it. Relax it the same way. - Update the V2_NEXT_PENDING JSDoc to describe the actual (tolerant) matching contract instead of claiming an exact-line match. --- packages/sdk-codemod/src/registry.ts | 7 +++--- .../src/resolve-pending-boundaries.test.ts | 24 +++++++++++++++++++ .../src/resolve-pending-boundaries.ts | 6 +++-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 2eae62ebf..c4ec94e13 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -102,9 +102,10 @@ const V2_NEXT_6 = "2.0.0-next.6"; /** * Sentinel `prereleaseUntil` for a codemod whose exact `2.0.0-next.N` release is not * known yet. `pnpm codemod:resolve-pending`, run in CI against the release PR, replaces - * it with the resolved `V2_NEXT_N` constant once the version is bumped. Keep this - * `export const` declaration exactly as-is: resolve-pending-boundaries.ts matches this - * exact line to find where to insert that constant, and exporting also keeps it + * it with the resolved `V2_NEXT_N` constant once the version is bumped. Keep this an + * `export const V2_NEXT_PENDING = "pending";` declaration: resolve-pending-boundaries.ts + * matches it (tolerating whitespace changes and an optional preceding JSDoc block like + * this one) to find where to insert that constant, and exporting also keeps it * lint-clean between resolutions, when no codemod currently references it. */ export const V2_NEXT_PENDING = "pending"; diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts index 37e82a28f..ba2f82d88 100644 --- a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts @@ -131,6 +131,30 @@ describe("resolvePendingBoundaries", () => { expect(result.source).toContain("prereleaseUntil: V2_NEXT_5,"); }); + test("tolerates extra whitespace between export and const", () => { + const source = [ + V2_NEXT_4_DECL, + 'export const V2_NEXT_PENDING = "pending";', + " prereleaseUntil: V2_NEXT_PENDING,", + ].join("\n"); + const result = resolvePendingBoundaries(source, "2.0.0-next.5"); + + expect(result.changed).toBe(true); + expect(result.source).toContain('const V2_NEXT_5 = "2.0.0-next.5";'); + }); + + test("reuses an existing constant even when its spacing is non-canonical", () => { + const source = [ + 'const V2_NEXT_5="2.0.0-next.5";', + PENDING_DECL, + " prereleaseUntil: V2_NEXT_PENDING,", + ].join("\n"); + const result = resolvePendingBoundaries(source, "2.0.0-next.5"); + + expect(result.source.match(/V2_NEXT_5\s*=/g)).toHaveLength(1); + expect(result.source).toContain("prereleaseUntil: V2_NEXT_5,"); + }); + test("tolerates CRLF line endings before the declaration", () => { const source = [V2_NEXT_4_DECL, PENDING_DECL, " prereleaseUntil: V2_NEXT_PENDING,"].join( "\r\n", diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.ts index fe20b8eff..339b4f344 100644 --- a/packages/sdk-codemod/src/resolve-pending-boundaries.ts +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.ts @@ -6,7 +6,7 @@ const PENDING_USAGE_PATTERN = /prereleaseUntil:\s*V2_NEXT_PENDING\s*,/g; // Includes a preceding JSDoc block (if any) so a new constant is inserted above it, // not between the comment and `V2_NEXT_PENDING` where it would attach to the wrong export. const PENDING_DECLARATION_PATTERN = - /(?:^\/\*\*[\s\S]*?\*\/\r?\n)?^export const\s+V2_NEXT_PENDING\s*=\s*"pending";$/m; + /(?:^\/\*\*[\s\S]*?\*\/\r?\n)?^export\s+const\s+V2_NEXT_PENDING\s*=\s*"pending";$/m; export interface ResolvePendingBoundariesResult { /** Whether any `V2_NEXT_PENDING` usage was found and rewritten. */ @@ -53,7 +53,9 @@ export function resolvePendingBoundaries( const constantName = `V${parsed.major}_NEXT_${parsed.prerelease[1]}`; let updated = source; - const existingDeclaration = new RegExp(`^const ${constantName} = "([^"]+)";$`, "m").exec(source); + const existingDeclaration = new RegExp(`^const\\s+${constantName}\\s*=\\s*"([^"]+)";$`, "m").exec( + source, + ); if (existingDeclaration) { if (existingDeclaration[1] !== resolvedVersion) { throw new Error( From c56c35bcf738f88c3b1d630295ee33180677e5df Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 21:59:55 +0900 Subject: [PATCH 602/618] fix(codemod): preserve the source's EOL style when inserting a new constant The inserted `const V2_NEXT_N = ...;` line was joined with a hard-coded \n even when the surrounding file used CRLF, producing mixed line endings. Detect the source's EOL from its own content and use that instead. --- .../sdk-codemod/src/resolve-pending-boundaries.test.ts | 10 ++++++++++ packages/sdk-codemod/src/resolve-pending-boundaries.ts | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts index ba2f82d88..dae935a87 100644 --- a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts @@ -164,4 +164,14 @@ describe("resolvePendingBoundaries", () => { expect(result.changed).toBe(true); expect(result.source).toContain('const V2_NEXT_5 = "2.0.0-next.5";'); }); + + test("inserts the new constant using CRLF when the source uses CRLF throughout", () => { + const source = [V2_NEXT_4_DECL, PENDING_DECL, " prereleaseUntil: V2_NEXT_PENDING,"].join( + "\r\n", + ); + const result = resolvePendingBoundaries(source, "2.0.0-next.5"); + + expect(result.source).toContain('const V2_NEXT_5 = "2.0.0-next.5";\r\n'); + expect(result.source).not.toMatch(/[^\r]\n/); + }); }); diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.ts index 339b4f344..cd300e4fc 100644 --- a/packages/sdk-codemod/src/resolve-pending-boundaries.ts +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.ts @@ -67,9 +67,10 @@ export function resolvePendingBoundaries( if (declarationMatch === null) { throw new Error("Could not find the V2_NEXT_PENDING declaration in registry.ts"); } + const eol = source.includes("\r\n") ? "\r\n" : "\n"; updated = updated.replace( declarationMatch[0], - `const ${constantName} = "${resolvedVersion}";\n${declarationMatch[0]}`, + `const ${constantName} = "${resolvedVersion}";${eol}${declarationMatch[0]}`, ); } From 32f6711ab35b84bfffc21e86b33c35d51cf973b4 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 16 Jul 2026 22:11:33 +0900 Subject: [PATCH 603/618] fix(codemod): tolerate whitespace before the colon in the pending usage pattern PENDING_USAGE_PATTERN only allowed \s* after the colon in `prereleaseUntil: V2_NEXT_PENDING,`, not before it, so a manual edit like `prereleaseUntil : V2_NEXT_PENDING,` would fail to match. --- .../sdk-codemod/src/resolve-pending-boundaries.test.ts | 8 ++++++++ packages/sdk-codemod/src/resolve-pending-boundaries.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts index dae935a87..65590d6b2 100644 --- a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts @@ -131,6 +131,14 @@ describe("resolvePendingBoundaries", () => { expect(result.source).toContain("prereleaseUntil: V2_NEXT_5,"); }); + test("tolerates a space before the colon in the usage", () => { + const source = registrySource(" prereleaseUntil : V2_NEXT_PENDING,"); + const result = resolvePendingBoundaries(source, "2.0.0-next.5"); + + expect(result.changed).toBe(true); + expect(result.source).toContain("prereleaseUntil: V2_NEXT_5,"); + }); + test("tolerates extra whitespace between export and const", () => { const source = [ V2_NEXT_4_DECL, diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.ts index cd300e4fc..c4d8ebd30 100644 --- a/packages/sdk-codemod/src/resolve-pending-boundaries.ts +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.ts @@ -2,7 +2,7 @@ import { parse } from "semver"; // Tolerant of whitespace and CRLF, not just the exact oxfmt-formatted spacing this file // happens to use today, so a manual edit or formatter change doesn't stop this from matching. -const PENDING_USAGE_PATTERN = /prereleaseUntil:\s*V2_NEXT_PENDING\s*,/g; +const PENDING_USAGE_PATTERN = /prereleaseUntil\s*:\s*V2_NEXT_PENDING\s*,/g; // Includes a preceding JSDoc block (if any) so a new constant is inserted above it, // not between the comment and `V2_NEXT_PENDING` where it would attach to the wrong export. const PENDING_DECLARATION_PATTERN = From ff2dfdfa840a8095c4414d711a697b2b309787ac Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 17 Jul 2026 09:12:57 +0900 Subject: [PATCH 604/618] fix(codemod): scope the legacy cleanup guidance to specific files Address review feedback: deleting the whole SDK output directory also removes deploy state (secrets-state/, *.context.json) that existing secrets and Auth Connections depend on. Narrow the notice and changeset to recommend deleting only the listed legacy artifacts. --- .changeset/remove-legacy-bundle-cleanup.md | 2 +- packages/sdk-codemod/src/registry.ts | 2 +- packages/sdk/docs/migration/v2.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/remove-legacy-bundle-cleanup.md b/.changeset/remove-legacy-bundle-cleanup.md index 19e49d376..ab4c145f9 100644 --- a/.changeset/remove-legacy-bundle-cleanup.md +++ b/.changeset/remove-legacy-bundle-cleanup.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk": patch --- -`tailor deploy` no longer automatically deletes on-disk bundle artifacts left by SDK versions predating the in-memory bundling approach; delete your output directory manually if stale files remain from a very old SDK version +`tailor deploy` no longer automatically deletes on-disk bundle artifacts left by SDK versions predating the in-memory bundling approach; delete those specific stale files manually if any remain from a very old SDK version (do not delete the whole output directory, which also holds deploy state such as secrets and Auth Connection records) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 373a2f0ae..f44fff760 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -1067,7 +1067,7 @@ export const allCodemods: CodemodPackage[] = [ id: "v2/remove-legacy-bundle-cleanup", name: "Legacy bundle artifact cleanup removed from deploy", description: - "`tailor deploy` no longer deletes on-disk bundle artifacts (`.entry.js` files, workflow-job bundles, and the `hooks-validate-scripts/` directory) left in the SDK output directory (`.tailor` by default) by SDK versions that predate the current in-memory bundling approach. Current bundlers no longer write these files. No source change is required; if such stale files remain from a very old SDK version, delete the output directory manually (it is regenerated automatically).", + "`tailor deploy` no longer deletes on-disk bundle artifacts (`.entry.js` files, workflow-job bundles, and the `hooks-validate-scripts/` directory) left in the SDK output directory (`.tailor` by default) by SDK versions that predate the current in-memory bundling approach. Current bundlers no longer write these files. No source change is required; if such stale files remain from a very old SDK version, delete only those specific files/directories manually — do not delete the output directory itself, since it also holds deploy state (e.g. `secrets-state/`, `*.context.json`) that existing secrets and Auth Connections depend on.", since: "1.0.0", until: "2.0.0", notice: true, diff --git a/packages/sdk/docs/migration/v2.md b/packages/sdk/docs/migration/v2.md index 21f2a9386..938cbd703 100644 --- a/packages/sdk/docs/migration/v2.md +++ b/packages/sdk/docs/migration/v2.md @@ -1053,4 +1053,4 @@ v2 requires Node.js **22.15.0** or later. This is the first version that include ### Legacy bundle artifact cleanup removed from deploy -`tailor deploy` no longer deletes on-disk bundle artifacts (`.entry.js` files, workflow-job bundles, and the `hooks-validate-scripts/` directory) left in the SDK output directory (`.tailor` by default) by SDK versions that predate the current in-memory bundling approach. Current bundlers no longer write these files. No source change is required; if such stale files remain from a very old SDK version, delete the output directory manually (it is regenerated automatically). +`tailor deploy` no longer deletes on-disk bundle artifacts (`.entry.js` files, workflow-job bundles, and the `hooks-validate-scripts/` directory) left in the SDK output directory (`.tailor` by default) by SDK versions that predate the current in-memory bundling approach. Current bundlers no longer write these files. No source change is required; if such stale files remain from a very old SDK version, delete only those specific files/directories manually — do not delete the output directory itself, since it also holds deploy state (e.g. `secrets-state/`, `*.context.json`) that existing secrets and Auth Connections depend on. From 4b07bc04464f47c2c74be31609a926f438a4ac4d Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 17 Jul 2026 09:15:56 +0900 Subject: [PATCH 605/618] fix(codemod): discard changesets/action's uncommitted worktree edits before checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With commitMode: "github-api", changesets/action's pushChanges() calls commitChangesSinceBase() to push the version-bump diff to the remote release PR branch via the API, but never commits or cleans up the local worktree — changeset version's file edits (package.json bumps, CHANGELOG.md, consumed .changeset/*.md deletions) are left dirty locally. gh pr checkout then fails with "local changes would be overwritten" on every run that actually creates or updates a release PR, regardless of whether there's a pending codemod to resolve. Reproduced locally: dirtying a tracked file and checking out a different commit fails the same way; running git reset --hard --quiet first (discarding the leftover diff, since the same content already lives on the remote PR branch we're about to check out) lets it succeed. Reported by dqn in review. --- .github/scripts/resolve-pending-codemod-boundaries.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/scripts/resolve-pending-codemod-boundaries.sh b/.github/scripts/resolve-pending-codemod-boundaries.sh index 1ab6e575a..8a2f9bc4d 100644 --- a/.github/scripts/resolve-pending-codemod-boundaries.sh +++ b/.github/scripts/resolve-pending-codemod-boundaries.sh @@ -21,6 +21,14 @@ original_ref="$(git rev-parse HEAD)" # plain `git checkout` refuses to switch away from a dirty tracked file. trap 'git reset --hard --quiet "$original_ref"' EXIT +# changesets/action's github-api commit mode applies `changeset version`'s file +# edits (package.json bumps, CHANGELOG.md, consumed .changeset/*.md deletions) +# straight to this worktree and pushes them to the release PR branch through the +# API, but never commits them locally — leaving this checkout dirty. Without +# this, `gh pr checkout` below fails with "local changes would be overwritten" +# on every run that actually creates or updates a release PR. +git reset --hard --quiet + PR_BRANCH="$(gh pr view "$PR_NUMBER" --json headRefName -q .headRefName)" gh pr checkout "$PR_NUMBER" # Detach so the trap's reset only moves HEAD, not the local branch gh pr checkout made. From 59894e85d0b787cfad9cfa9f17d63802f7be2c82 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 17 Jul 2026 09:24:04 +0900 Subject: [PATCH 606/618] docs(codemod): fix the V2_NEXT_PENDING rationale in its JSDoc The lint-clean rationale was wrong: reachesCodemodBoundary, effectiveCodemodBoundary, and assertCodemodBoundaries already reference V2_NEXT_PENDING unconditionally, so it's never actually unused regardless of whether a codemod entry sets it. The real reason it's exported is so registry.test.ts can reference the sentinel directly. --- packages/sdk-codemod/src/registry.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index c4ec94e13..5af0eea20 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -102,11 +102,11 @@ const V2_NEXT_6 = "2.0.0-next.6"; /** * Sentinel `prereleaseUntil` for a codemod whose exact `2.0.0-next.N` release is not * known yet. `pnpm codemod:resolve-pending`, run in CI against the release PR, replaces - * it with the resolved `V2_NEXT_N` constant once the version is bumped. Keep this an + * it with the resolved `V2_NEXT_N` constant once the version is bumped. Keep this as an * `export const V2_NEXT_PENDING = "pending";` declaration: resolve-pending-boundaries.ts * matches it (tolerating whitespace changes and an optional preceding JSDoc block like - * this one) to find where to insert that constant, and exporting also keeps it - * lint-clean between resolutions, when no codemod currently references it. + * this one) to find where to insert that constant, and it's exported so registry.test.ts + * can reference the sentinel directly. */ export const V2_NEXT_PENDING = "pending"; From 6445dce2265b81288458ae8ba116c15f8912026d Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 17 Jul 2026 09:34:47 +0900 Subject: [PATCH 607/618] fix(codemod): skip (not throw) when a release jumps straight to stable resolvePendingBoundaries() required resolvedVersion to be a 2.0.0-next.N prerelease whenever a pending boundary exists, so a release that skips straight from prerelease to stable 2.0.0 would throw and fail the release workflow step. That's a valid outcome though: reachesCodemodBoundary() in registry.ts already treats a pending codemod as reached once the target hits the stable until boundary, so no concrete V2_NEXT_N constant is needed for it to keep working. Treat it as a no-op instead. Also allow the V2_NEXT_PENDING declaration (and its preceding JSDoc) to be indented, not just column-0, for the same formatting-robustness reason as the earlier whitespace tolerance fixes. --- .../src/resolve-pending-boundaries.test.ts | 24 ++++++++++++++++++- .../src/resolve-pending-boundaries.ts | 14 ++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts index 65590d6b2..f102df512 100644 --- a/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.test.ts @@ -63,10 +63,17 @@ describe("resolvePendingBoundaries", () => { ); }); + test("is a no-op when the release skips straight to stable 2.0.0", () => { + const source = registrySource(" prereleaseUntil: V2_NEXT_PENDING,"); + const result = resolvePendingBoundaries(source, "2.0.0"); + + expect(result).toEqual({ changed: false, source }); + }); + test("throws when the resolved version is not a next.N prerelease", () => { const source = registrySource(" prereleaseUntil: V2_NEXT_PENDING,"); - expect(() => resolvePendingBoundaries(source, "2.0.0")).toThrow( + expect(() => resolvePendingBoundaries(source, "2.0.0-beta.1")).toThrow( 'resolvedVersion must be a "2.0.0-next.N" prerelease', ); }); @@ -151,6 +158,21 @@ describe("resolvePendingBoundaries", () => { expect(result.source).toContain('const V2_NEXT_5 = "2.0.0-next.5";'); }); + test("tolerates an indented declaration and its JSDoc block", () => { + const source = [ + V2_NEXT_4_DECL, + " /**", + " * Sentinel for pending codemods.", + " */", + ' export const V2_NEXT_PENDING = "pending";', + " prereleaseUntil: V2_NEXT_PENDING,", + ].join("\n"); + const result = resolvePendingBoundaries(source, "2.0.0-next.5"); + + expect(result.changed).toBe(true); + expect(result.source).toContain('const V2_NEXT_5 = "2.0.0-next.5";'); + }); + test("reuses an existing constant even when its spacing is non-canonical", () => { const source = [ 'const V2_NEXT_5="2.0.0-next.5";', diff --git a/packages/sdk-codemod/src/resolve-pending-boundaries.ts b/packages/sdk-codemod/src/resolve-pending-boundaries.ts index c4d8ebd30..bae03d96f 100644 --- a/packages/sdk-codemod/src/resolve-pending-boundaries.ts +++ b/packages/sdk-codemod/src/resolve-pending-boundaries.ts @@ -6,7 +6,7 @@ const PENDING_USAGE_PATTERN = /prereleaseUntil\s*:\s*V2_NEXT_PENDING\s*,/g; // Includes a preceding JSDoc block (if any) so a new constant is inserted above it, // not between the comment and `V2_NEXT_PENDING` where it would attach to the wrong export. const PENDING_DECLARATION_PATTERN = - /(?:^\/\*\*[\s\S]*?\*\/\r?\n)?^export\s+const\s+V2_NEXT_PENDING\s*=\s*"pending";$/m; + /(?:^[ \t]*\/\*\*[\s\S]*?\*\/\r?\n)?^[ \t]*export\s+const\s+V2_NEXT_PENDING\s*=\s*"pending";$/m; export interface ResolvePendingBoundariesResult { /** Whether any `V2_NEXT_PENDING` usage was found and rewritten. */ @@ -38,6 +38,18 @@ export function resolvePendingBoundaries( if (parsed === null) { throw new Error(`resolvedVersion must be a valid semver version: ${resolvedVersion}`); } + if ( + parsed.major === 2 && + parsed.minor === 0 && + parsed.patch === 0 && + parsed.prerelease.length === 0 + ) { + // The release skipped straight from a prerelease to stable 2.0.0 without ever + // resolving this sentinel. That's fine: reachesCodemodBoundary() in registry.ts + // already treats a pending codemod as reached once the target hits the stable + // `until` boundary, so no concrete V2_NEXT_N constant is needed for it to work. + return { changed: false, source }; + } if ( parsed.major !== 2 || parsed.minor !== 0 || From b1d19313237e21590413c4fe84dc4514a5e35d51 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 17 Jul 2026 09:35:38 +0900 Subject: [PATCH 608/618] fix(workflow): allow Workflow.start() with no args when input is undefined Workflow.start required an explicit undefined argument when the main job's input type was undefined, inconsistent with WorkflowJob.start (where the input parameter becomes optional in that case) and unable to type-check workflow.start() with zero arguments. Mirror WorkflowJob's conditional-optional signature so Workflow.start follows the same rule. Add a regression test verifying workflow.start() type-checks and runs when the main job takes no input. --- .../sdk/src/configure/services/workflow/job.test.ts | 11 +++++++++++ .../sdk/src/configure/services/workflow/workflow.ts | 10 ++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/sdk/src/configure/services/workflow/job.test.ts b/packages/sdk/src/configure/services/workflow/job.test.ts index cb8d8e0a7..ec29e202a 100644 --- a/packages/sdk/src/configure/services/workflow/job.test.ts +++ b/packages/sdk/src/configure/services/workflow/job.test.ts @@ -601,4 +601,15 @@ describe("start without tailor.workflow", () => { await expect(workflow.start({ n: 0 })).rejects.toThrow(/tailor\.workflow is not available/); }); + + test("workflow start takes no arguments when the main job's input is undefined", async () => { + const main = createWorkflowJob({ + name: "fallback-main-no-input", + body: () => ({ ok: true }), + }); + const workflow = createWorkflow({ name: "fallback-wf-no-input", mainJob: main }); + + // Must type-check with zero arguments, matching WorkflowJob.start. + await expect(workflow.start()).rejects.toThrow(/tailor\.workflow is not available/); + }); }); diff --git a/packages/sdk/src/configure/services/workflow/workflow.ts b/packages/sdk/src/configure/services/workflow/workflow.ts index 83390c711..8165a452c 100644 --- a/packages/sdk/src/configure/services/workflow/workflow.ts +++ b/packages/sdk/src/configure/services/workflow/workflow.ts @@ -21,10 +21,12 @@ export interface Workflow = WorkflowJob
[0], - options?: { invoker: MachineUserName }, - ) => Promise; + start: [Parameters[0]] extends [undefined] + ? (args?: undefined, options?: { invoker: MachineUserName }) => Promise + : ( + args: Parameters[0], + options?: { invoker: MachineUserName }, + ) => Promise; } interface WorkflowDefinition> { From eb0bfe65217fa6835dc2cd4fa015801bd0b74e78 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 17 Jul 2026 09:37:40 +0900 Subject: [PATCH 609/618] fix(sdk-codemod): scope new workflow rename entries to the shipping prerelease Neither v2/workflow-trigger-rename nor v2/workflow-start-rename declared prereleaseUntil, so a migration run targeting the actual prerelease this change ships in (2.0.0-next.6) would select neither entry at all until the final stable 2.0.0 release. Add prereleaseUntil: V2_NEXT_6, matching the sibling v2/generate-watch-flag entry landing in the same release window. Both entries also fell back to the default TS-only filePatterns, so .js/.mjs call sites were never scanned. Override with the JS-inclusive glob used by other runtime codemods. --- packages/sdk-codemod/src/registry.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 6aeb7a78c..a4a134913 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -725,7 +725,9 @@ export const allCodemods: CodemodPackage[] = [ "Rename tailor.workflow call sites from the pre-alignment triggerWorkflow/triggerJobFunction/resumeWorkflow names to the canonical startWorkflow/startJobFunction/resumeWorkflowExecution names, on both the ambient tailor.workflow global and a workflow value imported from @tailor-platform/sdk/runtime(/workflow). For a renamed triggerWorkflow call, also renames a literal `invoker` option key to `authInvoker` — startWorkflow's options expect the platform shape directly, unlike the removed triggerWorkflow wrapper, which converted invoker to authInvoker internally.", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_6, scriptPath: "v2/workflow-trigger-rename/scripts/transform.js", + filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], legacyPatterns: ["triggerWorkflow", "triggerJobFunction", "resumeWorkflow"], examples: [ { @@ -905,6 +907,8 @@ export const allCodemods: CodemodPackage[] = [ "Rename `Workflow.trigger()` (returned by `createWorkflow()`) and `WorkflowJob.trigger()` (returned by `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary. No codemod ships for this rename: distinguishing a workflow/job `.trigger()` call from an unrelated object's own `.trigger()` method requires resolving the receiver back to a `createWorkflow`/`createWorkflowJob` result across files, which the SDK's own CLI bundler already does for build-time rewriting. Reusing that logic in a standalone script is a nontrivial lift, and — unlike the bundler, which fails loudly when it cannot rewrite a call — a codemod false positive would silently rewrite an unrelated `.trigger()` call with no error. For the call-site volume this rename typically involves, manual review guided by the prompt below is the safer trade-off.", since: "1.0.0", until: "2.0.0", + prereleaseUntil: V2_NEXT_6, + filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], suspiciousPatterns: [".trigger("], examples: [ { From 01f6dafd8e6561fa792e9f42a7add3de9da7e101 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 17 Jul 2026 09:43:18 +0900 Subject: [PATCH 610/618] fix(sdk-codemod): skip renaming triggerWorkflow calls with non-literal options When a triggerWorkflow(...) call's third argument was a variable or an object literal spreading one, the codemod still renamed the method call. This silently erased the triggerWorkflow token that legacyPatterns relies on to flag the call for manual review, while an invoker key hidden in that argument would now be silently ignored at runtime instead of renamed to authInvoker. Skip the rename entirely for such calls, leaving triggerWorkflow in place so the call fails loudly at compile time (the method no longer exists) and remains flaggable by the residual-pattern scan. Add a fixture covering both the variable and spread cases. --- .../scripts/transform.ts | 42 ++++++++++++++----- .../tests/invoker-option-non-literal/input.ts | 12 ++++++ 2 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/invoker-option-non-literal/input.ts diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/scripts/transform.ts index f6859e299..8a3baa6af 100644 --- a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/scripts/transform.ts @@ -65,24 +65,43 @@ function expressionArguments(args: SgNode): SgNode[] { return args.children().filter((child) => !["(", ")", ","].includes(child.kind() as string)); } +function thirdArgument(member: SgNode): SgNode | null { + const call = member.parent(); + if (call?.kind() !== "call_expression") return null; + const args = call.field("arguments"); + if (!args) return null; + return expressionArguments(args)[2] ?? null; +} + +/** + * A `triggerWorkflow(name, args, options)` call whose third argument is not a + * literal object (e.g. a variable), or is an object literal that spreads one + * (`{ ...rest }`), cannot be safely renamed: we cannot tell whether it carries + * an `invoker` key that also needs renaming to `authInvoker`, and renaming + * just the method name would erase the `triggerWorkflow` token that flags the + * call for manual review, while the option silently stops working at + * runtime. Such calls are left entirely unrenamed. + * @param member - The `.triggerWorkflow` member-expression node being considered + * @returns Whether the call must be skipped + */ +function hasUnsafeThirdArgument(member: SgNode): boolean { + const optionsArg = thirdArgument(member); + if (optionsArg === null) return false; + if (optionsArg.kind() !== "object") return true; + return optionsArg.children().some((child) => child.kind() === "spread_element"); +} + /** * `triggerWorkflow`'s removed SDK wrapper converted an `invoker` option to * the platform's `authInvoker` shape before calling through; `startWorkflow` * expects `authInvoker` directly. Build an edit renaming a literal `invoker` * key (or shorthand) in the third argument of a `triggerWorkflow(...)` call - * being renamed to `startWorkflow`, so the option keeps working. Options - * passed via a variable/spread are left for manual review — only literal - * object arguments are inspected here. + * being renamed to `startWorkflow`, so the option keeps working. * @param member - The `.triggerWorkflow` member-expression node being renamed * @returns An edit renaming the `invoker` key, or null when not applicable */ function findInvokerOptionEdit(member: SgNode): Edit | null { - const call = member.parent(); - if (call?.kind() !== "call_expression") return null; - const args = call.field("arguments"); - if (!args) return null; - - const optionsArg = expressionArguments(args)[2]; + const optionsArg = thirdArgument(member); if (!optionsArg || optionsArg.kind() !== "object") return null; for (const child of optionsArg.children()) { @@ -130,10 +149,13 @@ export default function transform(source: string, filePath?: string): string | n const isAmbientReceiver = tailorIsAmbient && isAmbientTailorWorkflow(object); if (!isWorkflowImportReceiver && !isAmbientReceiver) continue; - edits.push(property.replace(newName)); if (newName === "startWorkflow") { + if (hasUnsafeThirdArgument(member)) continue; + edits.push(property.replace(newName)); const invokerEdit = findInvokerOptionEdit(member); if (invokerEdit) edits.push(invokerEdit); + } else { + edits.push(property.replace(newName)); } } diff --git a/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/invoker-option-non-literal/input.ts b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/invoker-option-non-literal/input.ts new file mode 100644 index 000000000..18f91ae6b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/workflow-trigger-rename/tests/invoker-option-non-literal/input.ts @@ -0,0 +1,12 @@ +import { workflow } from "@tailor-platform/sdk/runtime"; + +const options = { invoker: { namespace: "my-auth", machineUserName: "admin" } }; + +export async function runWithVariable(): Promise { + return await workflow.triggerWorkflow("myWorkflow", { data: "value" }, options); +} + +export async function runWithSpread(): Promise { + const extra = { invoker: { namespace: "my-auth", machineUserName: "admin" } }; + return await workflow.triggerWorkflow("myWorkflow", { data: "value" }, { ...extra }); +} From 613df9f4bd5ee09c7bfcd7e91236a5838dbb7425 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 17 Jul 2026 10:48:11 +0900 Subject: [PATCH 611/618] chore: rename sdk-tailordb-erd-plugin package to sdk-plugin-tailordb-erd Rename @tailor-platform/sdk-tailordb-erd-plugin to @tailor-platform/sdk-plugin-tailordb-erd (eslint-plugin-* naming convention), including the package directory, workspace/lockfile references, CLI plugin-resolution messages, docs, changesets, and CI workflow paths. The published bin name (tailor-tailordb-erd) is unaffected. --- .changeset/add-erd-plugin-package.md | 2 +- .changeset/extract-erd-plugin.md | 2 +- .github/workflows/changeset-check.yml | 2 +- .github/workflows/erd-schema.yml | 18 +++++++++--------- .github/workflows/pkg-pr-new.yml | 2 +- example/package.json | 2 +- .../.oxlintrc.json | 0 .../CHANGELOG.md | 0 .../README.md | 4 ++-- .../package.json | 4 ++-- .../src/deploy.ts | 0 .../src/diff-command.test.ts | 0 .../src/diff-command.ts | 0 .../src/diff.test.ts | 0 .../src/diff.ts | 0 .../src/export.ts | 0 .../src/index.ts | 0 .../src/local-schema.test.ts | 0 .../src/local-schema.ts | 0 .../src/schema.test.ts | 0 .../src/schema.ts | 0 .../src/serve.test.ts | 0 .../src/serve.ts | 0 .../src/shared/args.ts | 0 .../src/shared/command.ts | 0 .../src/shared/logger.ts | 0 .../src/types.ts | 0 .../src/utils.ts | 0 .../src/viewer-assets/app.js | 0 .../src/viewer-assets/index.html | 0 .../src/viewer-assets/serve.json | 0 .../src/viewer-assets/styles.css | 0 .../src/viewer.test.ts | 0 .../src/viewer.ts | 0 .../tsconfig.json | 0 .../tsdown.config.ts | 0 .../vitest.config.ts | 0 packages/sdk/docs/cli-reference.md | 2 +- packages/sdk/docs/cli-reference.template.md | 2 +- packages/sdk/docs/cli/tailordb.md | 4 ++-- packages/sdk/docs/cli/tailordb.template.md | 4 ++-- packages/sdk/docs/github-actions.md | 4 ++-- .../cli/commands/setup/workflow-lint.test.ts | 2 +- .../sdk/src/cli/commands/tailordb/index.ts | 2 +- packages/sdk/src/cli/shared/plugin.test.ts | 4 ++-- packages/sdk/src/cli/shared/plugin.ts | 2 +- pnpm-lock.yaml | 6 +++--- 47 files changed, 34 insertions(+), 34 deletions(-) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/.oxlintrc.json (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/CHANGELOG.md (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/README.md (93%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/package.json (91%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/deploy.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/diff-command.test.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/diff-command.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/diff.test.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/diff.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/export.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/index.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/local-schema.test.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/local-schema.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/schema.test.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/schema.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/serve.test.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/serve.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/shared/args.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/shared/command.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/shared/logger.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/types.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/utils.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/viewer-assets/app.js (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/viewer-assets/index.html (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/viewer-assets/serve.json (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/viewer-assets/styles.css (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/viewer.test.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/src/viewer.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/tsconfig.json (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/tsdown.config.ts (100%) rename packages/{sdk-tailordb-erd-plugin => sdk-plugin-tailordb-erd}/vitest.config.ts (100%) diff --git a/.changeset/add-erd-plugin-package.md b/.changeset/add-erd-plugin-package.md index dcac7863b..7e6c85d63 100644 --- a/.changeset/add-erd-plugin-package.md +++ b/.changeset/add-erd-plugin-package.md @@ -1,5 +1,5 @@ --- -"@tailor-platform/sdk-tailordb-erd-plugin": minor +"@tailor-platform/sdk-plugin-tailordb-erd": minor --- New Tailor CLI plugin providing the `tailor tailordb erd` commands (export, diff, serve, deploy), extracted from `@tailor-platform/sdk`. diff --git a/.changeset/extract-erd-plugin.md b/.changeset/extract-erd-plugin.md index 02f5e6fbd..1775b3359 100644 --- a/.changeset/extract-erd-plugin.md +++ b/.changeset/extract-erd-plugin.md @@ -2,4 +2,4 @@ "@tailor-platform/sdk": major --- -Remove the built-in `tailordb erd` commands. They are now provided by the `@tailor-platform/sdk-tailordb-erd-plugin` CLI plugin: install it with `npm install -D @tailor-platform/sdk-tailordb-erd-plugin` and keep running `tailor tailordb erd ` as before — the CLI dispatches to the plugin automatically and suggests the install command when the plugin is missing. The `erdSite` TailorDB setting is unchanged. +Remove the built-in `tailordb erd` commands. They are now provided by the `@tailor-platform/sdk-plugin-tailordb-erd` CLI plugin: install it with `npm install -D @tailor-platform/sdk-plugin-tailordb-erd` and keep running `tailor tailordb erd ` as before — the CLI dispatches to the plugin automatically and suggests the install command when the plugin is missing. The `erdSite` TailorDB setting is unchanged. diff --git a/.github/workflows/changeset-check.yml b/.github/workflows/changeset-check.yml index e07708d9c..faf160238 100644 --- a/.github/workflows/changeset-check.yml +++ b/.github/workflows/changeset-check.yml @@ -41,7 +41,7 @@ jobs: packages: - 'packages/sdk/**' - 'packages/create-sdk/**' - - 'packages/sdk-tailordb-erd-plugin/**' + - 'packages/sdk-plugin-tailordb-erd/**' - 'packages/tailor-proto/**' - name: Fail if no changeset diff --git a/.github/workflows/erd-schema.yml b/.github/workflows/erd-schema.yml index 2c885f092..c6b41cb7d 100644 --- a/.github/workflows/erd-schema.yml +++ b/.github/workflows/erd-schema.yml @@ -5,13 +5,13 @@ on: push: branches: [main, v2] paths: - - packages/sdk-tailordb-erd-plugin/** + - packages/sdk-plugin-tailordb-erd/** - packages/sdk/src/cli/shared/tailordb-namespaces.ts - example/** - .github/workflows/erd-schema.yml pull_request: paths: - - packages/sdk-tailordb-erd-plugin/** + - packages/sdk-plugin-tailordb-erd/** - packages/sdk/src/cli/shared/tailordb-namespaces.ts - example/** - .github/workflows/erd-schema.yml @@ -48,15 +48,15 @@ jobs: # the build below replaces it. - name: Stub ERD plugin bin target run: | - mkdir -p packages/sdk-tailordb-erd-plugin/dist - printf '#!/usr/bin/env node\n' > packages/sdk-tailordb-erd-plugin/dist/index.js + mkdir -p packages/sdk-plugin-tailordb-erd/dist + printf '#!/usr/bin/env node\n' > packages/sdk-plugin-tailordb-erd/dist/index.js - uses: ./.github/actions/install-deps # install-deps only builds the SDK; `tailor tailordb erd` dispatches to # this plugin's bin. - name: Build ERD plugin - run: pnpm --filter @tailor-platform/sdk-tailordb-erd-plugin run build + run: pnpm --filter @tailor-platform/sdk-plugin-tailordb-erd run build - uses: tailor-platform/actions/erd-schema-export@0bd69a0fc9aab3f818b636aa111995441a2c1a03 # v2 branch (unreleased) with: @@ -98,8 +98,8 @@ jobs: # the build below replaces it. - name: Stub ERD plugin bin target run: | - mkdir -p packages/sdk-tailordb-erd-plugin/dist - printf '#!/usr/bin/env node\n' > packages/sdk-tailordb-erd-plugin/dist/index.js + mkdir -p packages/sdk-plugin-tailordb-erd/dist + printf '#!/usr/bin/env node\n' > packages/sdk-plugin-tailordb-erd/dist/index.js # install-deps builds the SDK once, so the `tailor-sdk` CLI bin resolves. - name: Install deps @@ -108,7 +108,7 @@ jobs: # install-deps only builds the SDK; `tailor tailordb erd` dispatches to # this plugin's bin. - name: Build ERD plugin - run: pnpm --filter @tailor-platform/sdk-tailordb-erd-plugin run build + run: pnpm --filter @tailor-platform/sdk-plugin-tailordb-erd run build - uses: tailor-platform/actions/erd-schema-preview@0bd69a0fc9aab3f818b636aa111995441a2c1a03 # v2 branch (unreleased) with: @@ -123,7 +123,7 @@ jobs: preview-artifact-name: ${{ matrix.namespace }}.html always-relevant-paths: | example/tailor.config.ts - packages/sdk-tailordb-erd-plugin/ + packages/sdk-plugin-tailordb-erd/ packages/sdk/src/cli/shared/tailordb-namespaces.ts relevant-path-prefix: example/ github-token: ${{ github.token }} diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml index f00d2c35e..7af845ccd 100644 --- a/.github/workflows/pkg-pr-new.yml +++ b/.github/workflows/pkg-pr-new.yml @@ -70,7 +70,7 @@ jobs: pnpm --config.verify-deps-before-run=false --filter @tailor-platform/create-sdk build - name: Publish to pkg.pr.new - run: pnpm --config.verify-deps-before-run=false exec pkg-pr-new publish --pnpm --compact --commentWithSha --packageManager=pnpm "packages/sdk" "packages/create-sdk" "packages/sdk-tailordb-erd-plugin" # zizmor: ignore[use-trusted-publishing] + run: pnpm --config.verify-deps-before-run=false exec pkg-pr-new publish --pnpm --compact --commentWithSha --packageManager=pnpm "packages/sdk" "packages/create-sdk" "packages/sdk-plugin-tailordb-erd" # zizmor: ignore[use-trusted-publishing] init-smoke: needs: [publish] diff --git a/example/package.json b/example/package.json index 501889116..65b363740 100644 --- a/example/package.json +++ b/example/package.json @@ -34,7 +34,7 @@ "@bufbuild/protobuf": "2.12.1", "@connectrpc/connect": "2.1.2", "@connectrpc/connect-node": "2.1.2", - "@tailor-platform/sdk-tailordb-erd-plugin": "workspace:^", + "@tailor-platform/sdk-plugin-tailordb-erd": "workspace:^", "@types/node": "24.13.3", "@typescript/native-preview": "7.0.0-dev.20260707.2", "eslint-plugin-zod": "4.7.0", diff --git a/packages/sdk-tailordb-erd-plugin/.oxlintrc.json b/packages/sdk-plugin-tailordb-erd/.oxlintrc.json similarity index 100% rename from packages/sdk-tailordb-erd-plugin/.oxlintrc.json rename to packages/sdk-plugin-tailordb-erd/.oxlintrc.json diff --git a/packages/sdk-tailordb-erd-plugin/CHANGELOG.md b/packages/sdk-plugin-tailordb-erd/CHANGELOG.md similarity index 100% rename from packages/sdk-tailordb-erd-plugin/CHANGELOG.md rename to packages/sdk-plugin-tailordb-erd/CHANGELOG.md diff --git a/packages/sdk-tailordb-erd-plugin/README.md b/packages/sdk-plugin-tailordb-erd/README.md similarity index 93% rename from packages/sdk-tailordb-erd-plugin/README.md rename to packages/sdk-plugin-tailordb-erd/README.md index 64f03e47c..a18a728c7 100644 --- a/packages/sdk-tailordb-erd-plugin/README.md +++ b/packages/sdk-plugin-tailordb-erd/README.md @@ -1,4 +1,4 @@ -# @tailor-platform/sdk-tailordb-erd-plugin +# @tailor-platform/sdk-plugin-tailordb-erd Tailor CLI plugin that provides the `tailor tailordb erd` commands: export, diff, serve, and deploy a TailorDB ERD viewer built from your local TailorDB schema. @@ -10,7 +10,7 @@ Tailor CLI plugin that provides the `tailor tailordb erd` commands: export, diff Install it next to `@tailor-platform/sdk` in your project: ```bash -npm install -D @tailor-platform/sdk-tailordb-erd-plugin@next +npm install -D @tailor-platform/sdk-plugin-tailordb-erd@next ``` The Tailor CLI discovers the plugin automatically from `node_modules/.bin` (or your `PATH`). Run `tailor plugin list` to confirm it resolves. diff --git a/packages/sdk-tailordb-erd-plugin/package.json b/packages/sdk-plugin-tailordb-erd/package.json similarity index 91% rename from packages/sdk-tailordb-erd-plugin/package.json rename to packages/sdk-plugin-tailordb-erd/package.json index 8ecf6886a..c639abf79 100644 --- a/packages/sdk-tailordb-erd-plugin/package.json +++ b/packages/sdk-plugin-tailordb-erd/package.json @@ -1,12 +1,12 @@ { - "name": "@tailor-platform/sdk-tailordb-erd-plugin", + "name": "@tailor-platform/sdk-plugin-tailordb-erd", "version": "0.1.0-next.0", "description": "Tailor CLI plugin providing the `tailor tailordb erd` commands (TailorDB ERD viewer)", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/tailor-platform/sdk.git", - "directory": "packages/sdk-tailordb-erd-plugin" + "directory": "packages/sdk-plugin-tailordb-erd" }, "bin": { "tailor-tailordb-erd": "./dist/index.js" diff --git a/packages/sdk-tailordb-erd-plugin/src/deploy.ts b/packages/sdk-plugin-tailordb-erd/src/deploy.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/deploy.ts rename to packages/sdk-plugin-tailordb-erd/src/deploy.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/diff-command.test.ts b/packages/sdk-plugin-tailordb-erd/src/diff-command.test.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/diff-command.test.ts rename to packages/sdk-plugin-tailordb-erd/src/diff-command.test.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/diff-command.ts b/packages/sdk-plugin-tailordb-erd/src/diff-command.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/diff-command.ts rename to packages/sdk-plugin-tailordb-erd/src/diff-command.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/diff.test.ts b/packages/sdk-plugin-tailordb-erd/src/diff.test.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/diff.test.ts rename to packages/sdk-plugin-tailordb-erd/src/diff.test.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/diff.ts b/packages/sdk-plugin-tailordb-erd/src/diff.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/diff.ts rename to packages/sdk-plugin-tailordb-erd/src/diff.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/export.ts b/packages/sdk-plugin-tailordb-erd/src/export.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/export.ts rename to packages/sdk-plugin-tailordb-erd/src/export.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/index.ts b/packages/sdk-plugin-tailordb-erd/src/index.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/index.ts rename to packages/sdk-plugin-tailordb-erd/src/index.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/local-schema.test.ts b/packages/sdk-plugin-tailordb-erd/src/local-schema.test.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/local-schema.test.ts rename to packages/sdk-plugin-tailordb-erd/src/local-schema.test.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/local-schema.ts b/packages/sdk-plugin-tailordb-erd/src/local-schema.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/local-schema.ts rename to packages/sdk-plugin-tailordb-erd/src/local-schema.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/schema.test.ts b/packages/sdk-plugin-tailordb-erd/src/schema.test.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/schema.test.ts rename to packages/sdk-plugin-tailordb-erd/src/schema.test.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/schema.ts b/packages/sdk-plugin-tailordb-erd/src/schema.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/schema.ts rename to packages/sdk-plugin-tailordb-erd/src/schema.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/serve.test.ts b/packages/sdk-plugin-tailordb-erd/src/serve.test.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/serve.test.ts rename to packages/sdk-plugin-tailordb-erd/src/serve.test.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/serve.ts b/packages/sdk-plugin-tailordb-erd/src/serve.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/serve.ts rename to packages/sdk-plugin-tailordb-erd/src/serve.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/shared/args.ts b/packages/sdk-plugin-tailordb-erd/src/shared/args.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/shared/args.ts rename to packages/sdk-plugin-tailordb-erd/src/shared/args.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/shared/command.ts b/packages/sdk-plugin-tailordb-erd/src/shared/command.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/shared/command.ts rename to packages/sdk-plugin-tailordb-erd/src/shared/command.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/shared/logger.ts b/packages/sdk-plugin-tailordb-erd/src/shared/logger.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/shared/logger.ts rename to packages/sdk-plugin-tailordb-erd/src/shared/logger.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/types.ts b/packages/sdk-plugin-tailordb-erd/src/types.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/types.ts rename to packages/sdk-plugin-tailordb-erd/src/types.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/utils.ts b/packages/sdk-plugin-tailordb-erd/src/utils.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/utils.ts rename to packages/sdk-plugin-tailordb-erd/src/utils.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/viewer-assets/app.js b/packages/sdk-plugin-tailordb-erd/src/viewer-assets/app.js similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/viewer-assets/app.js rename to packages/sdk-plugin-tailordb-erd/src/viewer-assets/app.js diff --git a/packages/sdk-tailordb-erd-plugin/src/viewer-assets/index.html b/packages/sdk-plugin-tailordb-erd/src/viewer-assets/index.html similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/viewer-assets/index.html rename to packages/sdk-plugin-tailordb-erd/src/viewer-assets/index.html diff --git a/packages/sdk-tailordb-erd-plugin/src/viewer-assets/serve.json b/packages/sdk-plugin-tailordb-erd/src/viewer-assets/serve.json similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/viewer-assets/serve.json rename to packages/sdk-plugin-tailordb-erd/src/viewer-assets/serve.json diff --git a/packages/sdk-tailordb-erd-plugin/src/viewer-assets/styles.css b/packages/sdk-plugin-tailordb-erd/src/viewer-assets/styles.css similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/viewer-assets/styles.css rename to packages/sdk-plugin-tailordb-erd/src/viewer-assets/styles.css diff --git a/packages/sdk-tailordb-erd-plugin/src/viewer.test.ts b/packages/sdk-plugin-tailordb-erd/src/viewer.test.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/viewer.test.ts rename to packages/sdk-plugin-tailordb-erd/src/viewer.test.ts diff --git a/packages/sdk-tailordb-erd-plugin/src/viewer.ts b/packages/sdk-plugin-tailordb-erd/src/viewer.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/src/viewer.ts rename to packages/sdk-plugin-tailordb-erd/src/viewer.ts diff --git a/packages/sdk-tailordb-erd-plugin/tsconfig.json b/packages/sdk-plugin-tailordb-erd/tsconfig.json similarity index 100% rename from packages/sdk-tailordb-erd-plugin/tsconfig.json rename to packages/sdk-plugin-tailordb-erd/tsconfig.json diff --git a/packages/sdk-tailordb-erd-plugin/tsdown.config.ts b/packages/sdk-plugin-tailordb-erd/tsdown.config.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/tsdown.config.ts rename to packages/sdk-plugin-tailordb-erd/tsdown.config.ts diff --git a/packages/sdk-tailordb-erd-plugin/vitest.config.ts b/packages/sdk-plugin-tailordb-erd/vitest.config.ts similarity index 100% rename from packages/sdk-tailordb-erd-plugin/vitest.config.ts rename to packages/sdk-plugin-tailordb-erd/vitest.config.ts diff --git a/packages/sdk/docs/cli-reference.md b/packages/sdk/docs/cli-reference.md index 64b486b38..20f3945c8 100644 --- a/packages/sdk/docs/cli-reference.md +++ b/packages/sdk/docs/cli-reference.md @@ -121,7 +121,7 @@ tailor hello world --loud This also works under a built-in command group. The command path is joined with hyphens, so a plugin nested under `tailordb` is named `tailor-tailordb-erd`. This is how the -`@tailor-platform/sdk-tailordb-erd-plugin` +`@tailor-platform/sdk-plugin-tailordb-erd` package provides the `tailordb erd` commands: ```bash diff --git a/packages/sdk/docs/cli-reference.template.md b/packages/sdk/docs/cli-reference.template.md index 540ee287e..410dfb83f 100644 --- a/packages/sdk/docs/cli-reference.template.md +++ b/packages/sdk/docs/cli-reference.template.md @@ -114,7 +114,7 @@ tailor hello world --loud This also works under a built-in command group. The command path is joined with hyphens, so a plugin nested under `tailordb` is named `tailor-tailordb-erd`. This is how the -`@tailor-platform/sdk-tailordb-erd-plugin` +`@tailor-platform/sdk-plugin-tailordb-erd` package provides the `tailordb erd` commands: ```bash diff --git a/packages/sdk/docs/cli/tailordb.md b/packages/sdk/docs/cli/tailordb.md index 37b7c12f6..3c5825c05 100644 --- a/packages/sdk/docs/cli/tailordb.md +++ b/packages/sdk/docs/cli/tailordb.md @@ -231,10 +231,10 @@ See [Global Options](../cli-reference.md#global-options) for options available t ### tailordb erd -The `tailordb erd` commands (export, diff, serve, deploy) are provided by the `@tailor-platform/sdk-tailordb-erd-plugin` CLI plugin. Install it next to the SDK and keep running `tailor tailordb erd ` as before: +The `tailordb erd` commands (export, diff, serve, deploy) are provided by the `@tailor-platform/sdk-plugin-tailordb-erd` CLI plugin. Install it next to the SDK and keep running `tailor tailordb erd ` as before: ```bash -npm install -D @tailor-platform/sdk-tailordb-erd-plugin@next +npm install -D @tailor-platform/sdk-plugin-tailordb-erd@next tailor tailordb erd export --namespace myNamespace ``` diff --git a/packages/sdk/docs/cli/tailordb.template.md b/packages/sdk/docs/cli/tailordb.template.md index be5bc0a6a..7ac0798a6 100644 --- a/packages/sdk/docs/cli/tailordb.template.md +++ b/packages/sdk/docs/cli/tailordb.template.md @@ -72,10 +72,10 @@ Note: Migration scripts are automatically executed during `tailor deploy`. See [ ### tailordb erd -The `tailordb erd` commands (export, diff, serve, deploy) are provided by the `@tailor-platform/sdk-tailordb-erd-plugin` CLI plugin. Install it next to the SDK and keep running `tailor tailordb erd ` as before: +The `tailordb erd` commands (export, diff, serve, deploy) are provided by the `@tailor-platform/sdk-plugin-tailordb-erd` CLI plugin. Install it next to the SDK and keep running `tailor tailordb erd ` as before: ```bash -npm install -D @tailor-platform/sdk-tailordb-erd-plugin@next +npm install -D @tailor-platform/sdk-plugin-tailordb-erd@next tailor tailordb erd export --namespace myNamespace ``` diff --git a/packages/sdk/docs/github-actions.md b/packages/sdk/docs/github-actions.md index 69f73bd0f..8b6bb968c 100644 --- a/packages/sdk/docs/github-actions.md +++ b/packages/sdk/docs/github-actions.md @@ -73,11 +73,11 @@ tailor setup -n my-app-stg --erd-preview ``` The generated workflow runs `tailor tailordb erd`, which is provided by the -`@tailor-platform/sdk-tailordb-erd-plugin` CLI plugin — install it as a +`@tailor-platform/sdk-plugin-tailordb-erd` CLI plugin — install it as a dev-dependency in your project: ```bash -npm install -D @tailor-platform/sdk-tailordb-erd-plugin@next +npm install -D @tailor-platform/sdk-plugin-tailordb-erd@next ``` The generated workflow builds one self-contained ERD viewer HTML file for each diff --git a/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts b/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts index 7c9e727b5..d3708847b 100644 --- a/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts +++ b/packages/sdk/src/cli/commands/setup/workflow-lint.test.ts @@ -124,7 +124,7 @@ describe("repository ERD schema workflow", () => { const content = fs.readFileSync(ERD_SCHEMA_WORKFLOW, "utf-8"); expect(content).toContain("example/tailor.config.ts"); - expect(content).toContain("packages/sdk-tailordb-erd-plugin/"); + expect(content).toContain("packages/sdk-plugin-tailordb-erd/"); expect(content).toContain("relevant-path-prefix: example/"); }); diff --git a/packages/sdk/src/cli/commands/tailordb/index.ts b/packages/sdk/src/cli/commands/tailordb/index.ts index 2ec5fc285..333bcabd6 100644 --- a/packages/sdk/src/cli/commands/tailordb/index.ts +++ b/packages/sdk/src/cli/commands/tailordb/index.ts @@ -6,7 +6,7 @@ export const tailordbCommand = defineCommand({ name: "tailordb", description: "Manage TailorDB tables and data.", notes: - "The `tailordb erd` commands are provided by the @tailor-platform/sdk-tailordb-erd-plugin CLI plugin.", + "The `tailordb erd` commands are provided by the @tailor-platform/sdk-plugin-tailordb-erd CLI plugin.", subCommands: { truncate: truncateCommand, migration: migrationCommand, diff --git a/packages/sdk/src/cli/shared/plugin.test.ts b/packages/sdk/src/cli/shared/plugin.test.ts index 3e0159dfb..7b9889e66 100644 --- a/packages/sdk/src/cli/shared/plugin.test.ts +++ b/packages/sdk/src/cli/shared/plugin.test.ts @@ -434,10 +434,10 @@ describe("dispatchPluginWithInstallHint", () => { expect(code).toBe(1); expect(errorSpy).toHaveBeenCalledWith( - `"${CLI} tailordb erd" is provided by the @tailor-platform/sdk-tailordb-erd-plugin CLI plugin, which is not installed.`, + `"${CLI} tailordb erd" is provided by the @tailor-platform/sdk-plugin-tailordb-erd CLI plugin, which is not installed.`, ); expect(infoSpy).toHaveBeenCalledWith( - "Install it with: npm install -D @tailor-platform/sdk-tailordb-erd-plugin", + "Install it with: npm install -D @tailor-platform/sdk-plugin-tailordb-erd", ); }); diff --git a/packages/sdk/src/cli/shared/plugin.ts b/packages/sdk/src/cli/shared/plugin.ts index b6757b81c..1f6672d20 100644 --- a/packages/sdk/src/cli/shared/plugin.ts +++ b/packages/sdk/src/cli/shared/plugin.ts @@ -19,7 +19,7 @@ import { readPackageJson } from "./package-json"; * install command when the plugin executable is not found. */ const KNOWN_PLUGIN_PACKAGES: Record = { - "tailordb-erd": "@tailor-platform/sdk-tailordb-erd-plugin", + "tailordb-erd": "@tailor-platform/sdk-plugin-tailordb-erd", }; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 243709c10..2020bf13e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,9 +75,9 @@ importers: '@connectrpc/connect-node': specifier: 2.1.2 version: 2.1.2(@bufbuild/protobuf@2.12.1)(@connectrpc/connect@2.1.2(@bufbuild/protobuf@2.12.1)) - '@tailor-platform/sdk-tailordb-erd-plugin': + '@tailor-platform/sdk-plugin-tailordb-erd': specifier: workspace:^ - version: link:../packages/sdk-tailordb-erd-plugin + version: link:../packages/sdk-plugin-tailordb-erd '@types/node': specifier: 24.13.3 version: 24.13.3 @@ -649,7 +649,7 @@ importers: specifier: 4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.0.16(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) - packages/sdk-tailordb-erd-plugin: + packages/sdk-plugin-tailordb-erd: dependencies: chalk: specifier: 5.6.2 From ee382c7d5f5c0a14acf47c1dee6f12d8cecad92d Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 17 Jul 2026 12:56:23 +0900 Subject: [PATCH 612/618] chore: add changesets for the plugin package rename The two changesets that originally covered the plugin extraction and the sdk removal were already consumed by the prior Version Packages release (0.1.0-next.0), so pre.json tracks them as applied and editing their text has no effect on versioning. Add fresh changesets covering the rename itself: a minor bump for the renamed plugin package, and a patch bump for the sdk's updated install-hint message. --- .changeset/rename-erd-plugin-to-sdk-plugin-tailordb-erd.md | 5 +++++ .changeset/update-erd-plugin-install-hint-name.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/rename-erd-plugin-to-sdk-plugin-tailordb-erd.md create mode 100644 .changeset/update-erd-plugin-install-hint-name.md diff --git a/.changeset/rename-erd-plugin-to-sdk-plugin-tailordb-erd.md b/.changeset/rename-erd-plugin-to-sdk-plugin-tailordb-erd.md new file mode 100644 index 000000000..d45b90547 --- /dev/null +++ b/.changeset/rename-erd-plugin-to-sdk-plugin-tailordb-erd.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk-plugin-tailordb-erd": minor +--- + +Renamed the package from `@tailor-platform/sdk-tailordb-erd-plugin` to `@tailor-platform/sdk-plugin-tailordb-erd`, following the `eslint-plugin-*`-style naming convention used for CLI plugin packages. Update your dependency to the new name; the `tailor-tailordb-erd` executable and the `tailor tailordb erd` commands are unchanged. diff --git a/.changeset/update-erd-plugin-install-hint-name.md b/.changeset/update-erd-plugin-install-hint-name.md new file mode 100644 index 000000000..822610f79 --- /dev/null +++ b/.changeset/update-erd-plugin-install-hint-name.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": patch +--- + +Update the `tailordb erd` plugin install hint to reference the renamed `@tailor-platform/sdk-plugin-tailordb-erd` package. From dd88fab31fb593582d249be9a850ba7c2699a603 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sat, 18 Jul 2026 09:47:31 +0900 Subject: [PATCH 613/618] ci(release): clean untracked changeset outputs before release-PR checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit changesets/action's github-api commit mode leaves a first-release package's CHANGELOG.md as an untracked file, which the release PR branch tracks — so gh pr checkout aborted with 'untracked working tree files would be overwritten' once eslint-plugin-sdk entered the release. Clean untracked files after the hard reset before switching branches. --- .github/scripts/resolve-pending-codemod-boundaries.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/scripts/resolve-pending-codemod-boundaries.sh b/.github/scripts/resolve-pending-codemod-boundaries.sh index 8a2f9bc4d..c7241fe94 100644 --- a/.github/scripts/resolve-pending-codemod-boundaries.sh +++ b/.github/scripts/resolve-pending-codemod-boundaries.sh @@ -25,9 +25,12 @@ trap 'git reset --hard --quiet "$original_ref"' EXIT # edits (package.json bumps, CHANGELOG.md, consumed .changeset/*.md deletions) # straight to this worktree and pushes them to the release PR branch through the # API, but never commits them locally — leaving this checkout dirty. Without -# this, `gh pr checkout` below fails with "local changes would be overwritten" -# on every run that actually creates or updates a release PR. +# these, `gh pr checkout` below fails with "local changes would be overwritten" +# on every run that actually creates or updates a release PR. `clean` covers +# untracked files those edits create (a first-release package's CHANGELOG.md), +# which the release PR branch tracks and `reset --hard` leaves behind. git reset --hard --quiet +git clean -fdq PR_BRANCH="$(gh pr view "$PR_NUMBER" --json headRefName -q .headRefName)" gh pr checkout "$PR_NUMBER" From 9d8ce53872ac2c013e46ee9100c03246c2a209a2 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sat, 18 Jul 2026 17:46:42 +0900 Subject: [PATCH 614/618] fix(codemod): mark workflow-start-rename boundary as pending until it ships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Workflow/WorkflowJob .trigger() -> .start() rename lands in the next prerelease, not 2.0.0-next.6 — next.6 has no .start() method, so surfacing the migration guidance for next.6 targets would point users at an API that does not exist there. Use the V2_NEXT_PENDING sentinel so the release workflow resolves the boundary to the version the rename actually ships in. --- packages/sdk-codemod/src/registry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 8083aeb43..b558d83d0 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -917,7 +917,7 @@ export const allCodemods: CodemodPackage[] = [ "Rename `Workflow.trigger()` (returned by `createWorkflow()`) and `WorkflowJob.trigger()` (returned by `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary. No codemod ships for this rename: distinguishing a workflow/job `.trigger()` call from an unrelated object's own `.trigger()` method requires resolving the receiver back to a `createWorkflow`/`createWorkflowJob` result across files, which the SDK's own CLI bundler already does for build-time rewriting. Reusing that logic in a standalone script is a nontrivial lift, and — unlike the bundler, which fails loudly when it cannot rewrite a call — a codemod false positive would silently rewrite an unrelated `.trigger()` call with no error. For the call-site volume this rename typically involves, manual review guided by the prompt below is the safer trade-off.", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_6, + prereleaseUntil: V2_NEXT_PENDING, filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], suspiciousPatterns: [".trigger("], examples: [ From f94c9f212ba0989dfa06c006577c73686da5f2dd Mon Sep 17 00:00:00 2001 From: "tailor-platform-pr-trigger[bot]" <247949890+tailor-platform-pr-trigger[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 08:48:03 +0000 Subject: [PATCH 615/618] Version Packages (next) --- .changeset/pre.json | 14 +++++- packages/create-sdk/CHANGELOG.md | 20 ++++++++ packages/create-sdk/package.json | 2 +- packages/eslint-plugin-sdk/CHANGELOG.md | 7 +++ packages/eslint-plugin-sdk/package.json | 2 +- packages/sdk-codemod/CHANGELOG.md | 35 ++++++++++++++ packages/sdk-codemod/package.json | 2 +- packages/sdk-plugin-tailordb-erd/CHANGELOG.md | 11 +++++ packages/sdk-plugin-tailordb-erd/package.json | 2 +- packages/sdk/CHANGELOG.md | 47 +++++++++++++++++++ packages/sdk/package.json | 2 +- 11 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 packages/eslint-plugin-sdk/CHANGELOG.md diff --git a/.changeset/pre.json b/.changeset/pre.json index b9ecfaa00..8b61c1798 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -5,6 +5,7 @@ "add-erd-plugin-package", "apply-deploy-source-strings", "auth-attributes-rename", + "calm-tools-lint", "cli-plugin-schema-api", "cli-plugins", "codemod-llm-review", @@ -17,9 +18,12 @@ "extract-erd-plugin", "field-parse-runtime-layer", "fix-mockfile-open-download-stream", + "fix-multi-config-cwd", + "fix-next5-codemod-boundaries", "fix-rename-bin-source-files", "fix-ts-hook-dotted-basename", "fix-ts-hook-paths-without-baseurl", + "fix-user-file-tsconfig-paths", "fix-v2-prerelease-codemods", "fix-validate-issues-generic-field", "forward-relation-name", @@ -28,6 +32,7 @@ "isolate-concurrent-deployment-bundles", "keyring-default-storage", "kysely-date-timestamp", + "machine-user-optional-attributes", "open-download-review-scope", "parser-schema-strict", "politty-skill-command-cleanup", @@ -41,6 +46,7 @@ "remove-define-generators", "remove-function-test-run-input-wrapper", "remove-generate-watch", + "remove-legacy-bundle-cleanup", "remove-open-download-stream", "remove-runtime-globals-compatibility", "remove-tailor-sdk-skills-shim", @@ -49,9 +55,12 @@ "remove-v2-cli-aliases", "remove-workflow-test-env-fallback", "rename-bin-command", + "rename-erd-plugin-to-sdk-plugin-tailordb-erd", "rename-tailor-cli-env", + "renovate-1785", "require-function-log-content-hash", "require-tailordb-permission-config", + "resolve-pending-codemod-boundaries", "revert-strict-scalar-strings", "runtime-global-source-strings", "runtime-globals-import", @@ -63,11 +72,14 @@ "tailor-principal-type", "tailordb-shared-now-hook", "timestamps-updated-at-create", + "update-erd-plugin-install-hint-name", "user-profile-type-schema", "v2-baseline", "wait-point-rename", "workflow-canonical-names", "workflow-executor-args-contract", - "workflow-trigger-dispatch" + "workflow-start-rename", + "workflow-trigger-dispatch", + "workflow-trigger-rename" ] } diff --git a/packages/create-sdk/CHANGELOG.md b/packages/create-sdk/CHANGELOG.md index 1811e3891..d6f230f0d 100644 --- a/packages/create-sdk/CHANGELOG.md +++ b/packages/create-sdk/CHANGELOG.md @@ -1,5 +1,25 @@ # @tailor-platform/create-sdk +## 2.0.0-next.7 + +### Major Changes + +- [#1782](https://github.com/tailor-platform/sdk/pull/1782) [`c971797`](https://github.com/tailor-platform/sdk/commit/c971797c9bfa035a43771c46f2b1c3bd93f989a9) Thanks [@toiroakr](https://github.com/toiroakr)! - Rename `Workflow.trigger()` (returned by `createWorkflow()`) and `WorkflowJob.trigger()` (returned by `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary: + + ```diff + const inventory = checkInventory.trigger({ orderId: input.orderId }); + +const inventory = checkInventory.start({ orderId: input.orderId }); + + -const workflowRunId = await orderProcessingWorkflow.trigger(args, { invoker: "manager" }); + +const workflowRunId = await orderProcessingWorkflow.start(args, { invoker: "manager" }); + ``` + + `mockWorkflow()`'s `wf.job(definition)` / `wf.workflow(definition)` now return a mock of the `.start` method, and `wf.setTriggerHandler` / `wf.triggeredJobs` are renamed to `wf.setStartHandler` / `wf.startedJobs`. No codemod ships for the `.trigger()` → `.start()` call-site rename itself — see the `v2/workflow-start-rename` migration guide entry for manual migration steps. + +### Minor Changes + +- [#1737](https://github.com/tailor-platform/sdk/pull/1737) [`e349b9e`](https://github.com/tailor-platform/sdk/commit/e349b9e3d9c61f324f21dea92dd08055493a2c6d) Thanks [@dqn](https://github.com/dqn)! - Add lint rules that flag the external /api prefix in HTTP adapter path patterns and permission settings that grant access unconditionally, and enable them in newly scaffolded projects. + ## 2.0.0-next.6 ## 2.0.0-next.5 diff --git a/packages/create-sdk/package.json b/packages/create-sdk/package.json index e81875c3b..66bcdfb08 100644 --- a/packages/create-sdk/package.json +++ b/packages/create-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/create-sdk", - "version": "2.0.0-next.6", + "version": "2.0.0-next.7", "description": "A CLI tool to quickly create a new Tailor Platform SDK project", "license": "MIT", "repository": { diff --git a/packages/eslint-plugin-sdk/CHANGELOG.md b/packages/eslint-plugin-sdk/CHANGELOG.md new file mode 100644 index 000000000..1d7fec3c1 --- /dev/null +++ b/packages/eslint-plugin-sdk/CHANGELOG.md @@ -0,0 +1,7 @@ +# @tailor-platform/eslint-plugin-sdk + +## 0.1.0-next.0 + +### Minor Changes + +- [#1737](https://github.com/tailor-platform/sdk/pull/1737) [`e349b9e`](https://github.com/tailor-platform/sdk/commit/e349b9e3d9c61f324f21dea92dd08055493a2c6d) Thanks [@dqn](https://github.com/dqn)! - Add lint rules that flag the external /api prefix in HTTP adapter path patterns and permission settings that grant access unconditionally, and enable them in newly scaffolded projects. diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index 389bc607b..c88814df3 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/eslint-plugin-sdk", - "version": "0.0.0", + "version": "0.1.0-next.0", "description": "Lint rules for Tailor Platform SDK applications", "license": "MIT", "repository": { diff --git a/packages/sdk-codemod/CHANGELOG.md b/packages/sdk-codemod/CHANGELOG.md index 0901a0135..c17b9189c 100644 --- a/packages/sdk-codemod/CHANGELOG.md +++ b/packages/sdk-codemod/CHANGELOG.md @@ -1,5 +1,40 @@ # @tailor-platform/sdk-codemod +## 0.3.0-next.7 + +### Patch Changes + +- [#1787](https://github.com/tailor-platform/sdk/pull/1787) [`898d0b0`](https://github.com/tailor-platform/sdk/commit/898d0b0f809d15ea883a32a78a247eda4ca7caa7) Thanks [@toiroakr](https://github.com/toiroakr)! - Fix `tailor upgrade` reporting zero codemods across several v2 prerelease boundaries. `v2/db-type-to-table` and `v2/runtime-subpath-namespace` now trigger at `2.0.0-next.4` (where they actually shipped) instead of `2.0.0-next.3`, and `v2/forward-relation-name`, `v2/tailordb-validate-simplify`, and `v2/tailordb-hook-redesign` now trigger at `2.0.0-next.5` instead of `2.0.0-next.4`. + +- [#1787](https://github.com/tailor-platform/sdk/pull/1787) [`6653afd`](https://github.com/tailor-platform/sdk/commit/6653afd5a0be52f8c9a1dc6fa50e445f0f678dc0) Thanks [@toiroakr](https://github.com/toiroakr)! - Add a `V2_NEXT_PENDING` placeholder `prereleaseUntil` for codemods whose exact `2.0.0-next.N` release boundary isn't known yet at implementation time, plus a `pnpm codemod:resolve-pending` step (wired into the release workflow) that resolves it to the real version constant once the release PR bumps `@tailor-platform/sdk`'s version. Prevents codemod boundaries from drifting out of sync with the version they actually ship in, which previously required a manual follow-up fix after each release. + +- [#1782](https://github.com/tailor-platform/sdk/pull/1782) [`c971797`](https://github.com/tailor-platform/sdk/commit/c971797c9bfa035a43771c46f2b1c3bd93f989a9) Thanks [@toiroakr](https://github.com/toiroakr)! - Rename `Workflow.trigger()` (returned by `createWorkflow()`) and `WorkflowJob.trigger()` (returned by `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary: + + ```diff + const inventory = checkInventory.trigger({ orderId: input.orderId }); + +const inventory = checkInventory.start({ orderId: input.orderId }); + + -const workflowRunId = await orderProcessingWorkflow.trigger(args, { invoker: "manager" }); + +const workflowRunId = await orderProcessingWorkflow.start(args, { invoker: "manager" }); + ``` + + `mockWorkflow()`'s `wf.job(definition)` / `wf.workflow(definition)` now return a mock of the `.start` method, and `wf.setTriggerHandler` / `wf.triggeredJobs` are renamed to `wf.setStartHandler` / `wf.startedJobs`. No codemod ships for the `.trigger()` → `.start()` call-site rename itself — see the `v2/workflow-start-rename` migration guide entry for manual migration steps. + +- [#1782](https://github.com/tailor-platform/sdk/pull/1782) [`c971797`](https://github.com/tailor-platform/sdk/commit/c971797c9bfa035a43771c46f2b1c3bd93f989a9) Thanks [@toiroakr](https://github.com/toiroakr)! - Remove the pre-alignment `tailor.workflow` names `triggerWorkflow`, `triggerJobFunction`, and `resumeWorkflow` (and their `TriggerWorkflowOptions` / `TriggerJobFunctionOptions` option types) from `@tailor-platform/sdk/runtime`, the ambient `@tailor-platform/sdk/runtime/globals` types, and the `mockWorkflow()` test facade. Use the canonical names instead: + + ```diff + import { workflow } from "@tailor-platform/sdk/runtime"; + + -await workflow.triggerWorkflow("myWorkflow", { data: "value" }); + +await workflow.startWorkflow("myWorkflow", { data: "value" }); + -workflow.triggerJobFunction("myJob", { data: "value" }); + +workflow.startJobFunction("myJob", { data: "value" }); + -await workflow.resumeWorkflow("execution-id"); + +await workflow.resumeWorkflowExecution("execution-id"); + ``` + + Run the `v2/workflow-trigger-rename` codemod to migrate call sites automatically. + ## 0.3.0-next.6 ### Patch Changes diff --git a/packages/sdk-codemod/package.json b/packages/sdk-codemod/package.json index 1abfbc2ba..b6011c22c 100644 --- a/packages/sdk-codemod/package.json +++ b/packages/sdk-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk-codemod", - "version": "0.3.0-next.6", + "version": "0.3.0-next.7", "description": "Codemod runner for Tailor Platform SDK upgrades", "license": "MIT", "repository": { diff --git a/packages/sdk-plugin-tailordb-erd/CHANGELOG.md b/packages/sdk-plugin-tailordb-erd/CHANGELOG.md index 761e68493..aca2c9a97 100644 --- a/packages/sdk-plugin-tailordb-erd/CHANGELOG.md +++ b/packages/sdk-plugin-tailordb-erd/CHANGELOG.md @@ -1,5 +1,16 @@ # @tailor-platform/sdk-tailordb-erd-plugin +## 0.1.0-next.1 + +### Minor Changes + +- [#1801](https://github.com/tailor-platform/sdk/pull/1801) [`ee382c7`](https://github.com/tailor-platform/sdk/commit/ee382c7d5f5c0a14acf47c1dee6f12d8cecad92d) Thanks [@toiroakr](https://github.com/toiroakr)! - Renamed the package from `@tailor-platform/sdk-tailordb-erd-plugin` to `@tailor-platform/sdk-plugin-tailordb-erd`, following the `eslint-plugin-*`-style naming convention used for CLI plugin packages. Update your dependency to the new name; the `tailor-tailordb-erd` executable and the `tailor tailordb erd` commands are unchanged. + +### Patch Changes + +- Updated dependencies [[`f1cbda5`](https://github.com/tailor-platform/sdk/commit/f1cbda56df96670f18dccf2b7f2473430584f377), [`c870196`](https://github.com/tailor-platform/sdk/commit/c8701961f90d7bdcc887c793c806d4f26cc9b197), [`d07a82a`](https://github.com/tailor-platform/sdk/commit/d07a82aa4ded74c3d84e157b4bed5c37ef0ec239), [`da7d0c4`](https://github.com/tailor-platform/sdk/commit/da7d0c49322deebc9343dee88652152620a7cef9), [`cb97bd4`](https://github.com/tailor-platform/sdk/commit/cb97bd45314c5897818233dc8bc3b84b83bea8a3), [`ee382c7`](https://github.com/tailor-platform/sdk/commit/ee382c7d5f5c0a14acf47c1dee6f12d8cecad92d), [`c971797`](https://github.com/tailor-platform/sdk/commit/c971797c9bfa035a43771c46f2b1c3bd93f989a9), [`c971797`](https://github.com/tailor-platform/sdk/commit/c971797c9bfa035a43771c46f2b1c3bd93f989a9)]: + - @tailor-platform/sdk@2.0.0-next.7 + ## 0.1.0-next.0 ### Minor Changes diff --git a/packages/sdk-plugin-tailordb-erd/package.json b/packages/sdk-plugin-tailordb-erd/package.json index c639abf79..486cfd1a7 100644 --- a/packages/sdk-plugin-tailordb-erd/package.json +++ b/packages/sdk-plugin-tailordb-erd/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk-plugin-tailordb-erd", - "version": "0.1.0-next.0", + "version": "0.1.0-next.1", "description": "Tailor CLI plugin providing the `tailor tailordb erd` commands (TailorDB ERD viewer)", "license": "MIT", "repository": { diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index c672fb6d4..63f9d8d25 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,5 +1,52 @@ # @tailor-platform/sdk +## 2.0.0-next.7 + +### Major Changes + +- [#1782](https://github.com/tailor-platform/sdk/pull/1782) [`c971797`](https://github.com/tailor-platform/sdk/commit/c971797c9bfa035a43771c46f2b1c3bd93f989a9) Thanks [@toiroakr](https://github.com/toiroakr)! - Rename `Workflow.trigger()` (returned by `createWorkflow()`) and `WorkflowJob.trigger()` (returned by `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary: + + ```diff + const inventory = checkInventory.trigger({ orderId: input.orderId }); + +const inventory = checkInventory.start({ orderId: input.orderId }); + + -const workflowRunId = await orderProcessingWorkflow.trigger(args, { invoker: "manager" }); + +const workflowRunId = await orderProcessingWorkflow.start(args, { invoker: "manager" }); + ``` + + `mockWorkflow()`'s `wf.job(definition)` / `wf.workflow(definition)` now return a mock of the `.start` method, and `wf.setTriggerHandler` / `wf.triggeredJobs` are renamed to `wf.setStartHandler` / `wf.startedJobs`. No codemod ships for the `.trigger()` → `.start()` call-site rename itself — see the `v2/workflow-start-rename` migration guide entry for manual migration steps. + +- [#1782](https://github.com/tailor-platform/sdk/pull/1782) [`c971797`](https://github.com/tailor-platform/sdk/commit/c971797c9bfa035a43771c46f2b1c3bd93f989a9) Thanks [@toiroakr](https://github.com/toiroakr)! - Remove the pre-alignment `tailor.workflow` names `triggerWorkflow`, `triggerJobFunction`, and `resumeWorkflow` (and their `TriggerWorkflowOptions` / `TriggerJobFunctionOptions` option types) from `@tailor-platform/sdk/runtime`, the ambient `@tailor-platform/sdk/runtime/globals` types, and the `mockWorkflow()` test facade. Use the canonical names instead: + + ```diff + import { workflow } from "@tailor-platform/sdk/runtime"; + + -await workflow.triggerWorkflow("myWorkflow", { data: "value" }); + +await workflow.startWorkflow("myWorkflow", { data: "value" }); + -workflow.triggerJobFunction("myJob", { data: "value" }); + +workflow.startJobFunction("myJob", { data: "value" }); + -await workflow.resumeWorkflow("execution-id"); + +await workflow.resumeWorkflowExecution("execution-id"); + ``` + + Run the `v2/workflow-trigger-rename` codemod to migrate call sites automatically. + +### Minor Changes + +- [#1800](https://github.com/tailor-platform/sdk/pull/1800) [`d07a82a`](https://github.com/tailor-platform/sdk/commit/d07a82aa4ded74c3d84e157b4bed5c37ef0ec239) Thanks [@toiroakr](https://github.com/toiroakr)! - Machine user attribute keys now mirror the field's optionality: attributes derived from optional user fields (or optional `machineUserAttributes` fields) can be omitted, and `null`/`undefined` values are treated as "attribute not set" instead of being rejected at deploy time. Attributes derived from required fields remain mandatory, and undeclared attribute keys are still rejected. The generated `AttributeMap` type used to read `user.attributes` in resolvers, executors, and workflows now mirrors this same optionality, so an attribute derived from an optional field is typed as possibly absent instead of always present. + +### Patch Changes + +- [#1741](https://github.com/tailor-platform/sdk/pull/1741) [`f1cbda5`](https://github.com/tailor-platform/sdk/commit/f1cbda56df96670f18dccf2b7f2473430584f377) Thanks [@toiroakr](https://github.com/toiroakr)! - Resolve each config's `files` glob patterns and bundler `tsconfig` relative to that config file's own directory instead of the invocation `cwd`, so `--config a/tailor.config.ts,b/tailor.config.ts` no longer lets one app's file glob or path aliases bleed into another. If a `files` pattern matches nothing under the new directory, it falls back to resolving against `cwd` as before, so existing configs whose patterns were written against the invocation directory keep working. + +- [#1767](https://github.com/tailor-platform/sdk/pull/1767) [`c870196`](https://github.com/tailor-platform/sdk/commit/c8701961f90d7bdcc887c793c806d4f26cc9b197) Thanks [@toiroakr](https://github.com/toiroakr)! - Fix tsconfig `paths` alias resolution for dynamically loaded resolver, executor, workflow, HTTP adapter, and TailorDB type files. Previously, an import like `import { foo } from "@/utils"` in one of these files would fail to resolve when the file lived outside the directory tsx was registered from (e.g. in multi-app setups). Each file's `paths` aliases are now resolved as a fallback from its own tsconfig, based on the importing file's own directory. + +- [#1788](https://github.com/tailor-platform/sdk/pull/1788) [`da7d0c4`](https://github.com/tailor-platform/sdk/commit/da7d0c49322deebc9343dee88652152620a7cef9) Thanks [@toiroakr](https://github.com/toiroakr)! - `tailor deploy` no longer automatically deletes on-disk bundle artifacts left by SDK versions predating the in-memory bundling approach; delete those specific stale files manually if any remain from a very old SDK version (do not delete the whole output directory, which also holds deploy state such as secrets and Auth Connection records) + +- [#1785](https://github.com/tailor-platform/sdk/pull/1785) [`cb97bd4`](https://github.com/tailor-platform/sdk/commit/cb97bd45314c5897818233dc8bc3b84b83bea8a3) Thanks [@renovate](https://github.com/apps/renovate)! - fix(deps): update dependency tsx to v4.23.1 + +- [#1801](https://github.com/tailor-platform/sdk/pull/1801) [`ee382c7`](https://github.com/tailor-platform/sdk/commit/ee382c7d5f5c0a14acf47c1dee6f12d8cecad92d) Thanks [@toiroakr](https://github.com/toiroakr)! - Update the `tailordb erd` plugin install hint to reference the renamed `@tailor-platform/sdk-plugin-tailordb-erd` package. + ## 2.0.0-next.6 ### Major Changes diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 3c2d0932f..3488b649a 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk", - "version": "2.0.0-next.6", + "version": "2.0.0-next.7", "description": "Tailor Platform SDK - The SDK to work with Tailor Platform", "license": "MIT", "repository": { From 1acc75bedb1a625bef9c6dcae04d6409f74441bc Mon Sep 17 00:00:00 2001 From: "tailor-platform-pr-trigger[bot]" <247949890+tailor-platform-pr-trigger[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 08:48:11 +0000 Subject: [PATCH 616/618] chore(codemod): resolve pending prerelease boundary --- packages/sdk-codemod/src/registry.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index b558d83d0..d3386cca4 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -99,6 +99,7 @@ const V2_NEXT_2 = "2.0.0-next.2"; const V2_NEXT_4 = "2.0.0-next.4"; const V2_NEXT_5 = "2.0.0-next.5"; const V2_NEXT_6 = "2.0.0-next.6"; +const V2_NEXT_7 = "2.0.0-next.7"; /** * Sentinel `prereleaseUntil` for a codemod whose exact `2.0.0-next.N` release is not * known yet. `pnpm codemod:resolve-pending`, run in CI against the release PR, replaces @@ -917,7 +918,7 @@ export const allCodemods: CodemodPackage[] = [ "Rename `Workflow.trigger()` (returned by `createWorkflow()`) and `WorkflowJob.trigger()` (returned by `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary. No codemod ships for this rename: distinguishing a workflow/job `.trigger()` call from an unrelated object's own `.trigger()` method requires resolving the receiver back to a `createWorkflow`/`createWorkflowJob` result across files, which the SDK's own CLI bundler already does for build-time rewriting. Reusing that logic in a standalone script is a nontrivial lift, and — unlike the bundler, which fails loudly when it cannot rewrite a call — a codemod false positive would silently rewrite an unrelated `.trigger()` call with no error. For the call-site volume this rename typically involves, manual review guided by the prompt below is the safer trade-off.", since: "1.0.0", until: "2.0.0", - prereleaseUntil: V2_NEXT_PENDING, + prereleaseUntil: V2_NEXT_7, filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], suspiciousPatterns: [".trigger("], examples: [ From 61ac76cff992ef30e6115ba96c7d3a676010204f Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sun, 19 Jul 2026 15:51:23 +0900 Subject: [PATCH 617/618] fix(sdk): keep permission operand keys usable when Attributes fields are optional Machine user attribute keys mirroring field optionality made the generated Attributes interface's properties optional, which broke the IdP/TailorDB permission condition helpers two ways: the homomorphic key-extraction types leaked undefined into the user operand key unions (failing assignability to the generated permission types even for _loggedIn-only conditions), and keys derived from optional fields stopped being accepted as operands. Strip the optional modifier and exclude undefined from the value side so optional attribute fields stay valid operand keys with undefined-free unions. --- ...fix-optional-attributes-permission-keys.md | 5 +++ .../configure/services/idp/permission.test.ts | 30 +++++++++++++++ .../src/configure/services/idp/permission.ts | 10 +++-- .../services/tailordb/permission.test.ts | 38 ++++++++++++++++++- .../configure/services/tailordb/permission.ts | 10 +++-- 5 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 .changeset/fix-optional-attributes-permission-keys.md diff --git a/.changeset/fix-optional-attributes-permission-keys.md b/.changeset/fix-optional-attributes-permission-keys.md new file mode 100644 index 000000000..ae1aba50f --- /dev/null +++ b/.changeset/fix-optional-attributes-permission-keys.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": patch +--- + +Fix IdP and TailorDB permission condition types breaking when `Attributes` fields are optional. Since machine user attribute keys started mirroring the source field's optionality, the `user` operand key helpers leaked `undefined` into their key unions — failing typecheck against the generated permission types even for `_loggedIn`-only conditions — and rejected attribute keys derived from optional fields. Optional attribute fields are now valid operand keys and `undefined` no longer appears in the unions. diff --git a/packages/sdk/src/configure/services/idp/permission.test.ts b/packages/sdk/src/configure/services/idp/permission.test.ts index 4aa4260b9..601f1cd83 100644 --- a/packages/sdk/src/configure/services/idp/permission.test.ts +++ b/packages/sdk/src/configure/services/idp/permission.test.ts @@ -47,6 +47,36 @@ describe("IdPPermissionCondition type checks", () => { }); }); +describe("IdPPermissionCondition with optional attribute fields", () => { + type OptionalAttrs = { + role?: string; + permissions?: string[]; + active?: boolean; + flags?: boolean[]; + }; + + test("accepts user operands referencing optional attribute fields", () => { + const _str: IdPPermissionCondition = [{ user: "role" }, "=", "ADMIN"]; + const _bool: IdPPermissionCondition = [{ user: "active" }, "=", true]; + const _strArr: IdPPermissionCondition = [ + "a", + "in", + { user: "permissions" }, + ]; + expectTypeOf(_str).toExtend>(); + expectTypeOf(_bool).toExtend>(); + expectTypeOf(_strArr).toExtend>(); + }); + + test("does not leak undefined into user operand keys", () => { + type UserOperandKeys = Extract< + IdPPermissionCondition[0], + { user: unknown } + >["user"]; + expectTypeOf().not.toExtend(); + }); +}); + describe("IdPPermission type checks", () => { test("accepts valid full permission", () => { const _perm: IdPPermission = { diff --git a/packages/sdk/src/configure/services/idp/permission.ts b/packages/sdk/src/configure/services/idp/permission.ts index 59e77c6ab..6e7b797d4 100644 --- a/packages/sdk/src/configure/services/idp/permission.ts +++ b/packages/sdk/src/configure/services/idp/permission.ts @@ -4,20 +4,22 @@ import type { InferredAttributes } from "#/runtime/types"; type EqualityOperator = "=" | "!="; type ContainsOperator = "in" | "not in"; +// `-?` keeps the indexed access from folding optional fields' `never` into +// `undefined`; `Exclude` lets an optional field's value type still match. type StringFieldKeys = { - [K in keyof User]: User[K] extends string ? K : never; + [K in keyof User]-?: Exclude extends string ? K : never; }[keyof User]; type StringArrayFieldKeys = { - [K in keyof User]: User[K] extends string[] ? K : never; + [K in keyof User]-?: Exclude extends string[] ? K : never; }[keyof User]; type BooleanFieldKeys = { - [K in keyof User]: User[K] extends boolean ? K : never; + [K in keyof User]-?: Exclude extends boolean ? K : never; }[keyof User]; type BooleanArrayFieldKeys = { - [K in keyof User]: User[K] extends boolean[] ? K : never; + [K in keyof User]-?: Exclude extends boolean[] ? K : never; }[keyof User]; type UserStringOperand = { diff --git a/packages/sdk/src/configure/services/tailordb/permission.test.ts b/packages/sdk/src/configure/services/tailordb/permission.test.ts index 0a6713696..11d0fb5cb 100644 --- a/packages/sdk/src/configure/services/tailordb/permission.test.ts +++ b/packages/sdk/src/configure/services/tailordb/permission.test.ts @@ -1,5 +1,5 @@ // oxlint-disable vitest/expect-expect -- Type-only assertions are checked by TypeScript. -import { describe, test } from "vitest"; +import { describe, expectTypeOf, test } from "vitest"; import type { PermissionCondition } from "./permission"; describe("tailordb permission types", () => { @@ -109,3 +109,39 @@ describe("tailordb permission types", () => { }); }); }); + +describe("tailordb permission types with optional user attribute fields", () => { + type OptionalUser = { + role?: string; + permissions?: string[]; + isAdmin?: boolean; + flags?: boolean[]; + }; + + test("user operand accepts keys derived from optional fields", () => { + const _str = [{ user: "role" }, "=", "ADMIN"] satisfies PermissionCondition< + "record", + OptionalUser + >; + const _bool = [{ user: "isAdmin" }, "=", true] satisfies PermissionCondition< + "record", + OptionalUser + >; + const _strArr = ["a", "in", { user: "permissions" }] satisfies PermissionCondition< + "record", + OptionalUser + >; + const _boolArr = [true, "in", { user: "flags" }] satisfies PermissionCondition< + "record", + OptionalUser + >; + }); + + test("does not leak undefined into user operand keys", () => { + type UserOperandKeys = Extract< + PermissionCondition<"record", OptionalUser>[0], + { user: unknown } + >["user"]; + expectTypeOf().not.toExtend(); + }); +}); diff --git a/packages/sdk/src/configure/services/tailordb/permission.ts b/packages/sdk/src/configure/services/tailordb/permission.ts index e03847db1..3e0273f02 100644 --- a/packages/sdk/src/configure/services/tailordb/permission.ts +++ b/packages/sdk/src/configure/services/tailordb/permission.ts @@ -72,20 +72,22 @@ type ContainsOperator = "in" | "not in"; type HasAnyOperator = "hasAny" | "not hasAny"; // Helper types for User field extraction +// `-?` keeps the indexed access from folding optional fields' `never` into +// `undefined`; `Exclude` lets an optional field's value type still match. type StringFieldKeys = { - [K in keyof User]: User[K] extends string ? K : never; + [K in keyof User]-?: Exclude extends string ? K : never; }[keyof User]; type StringArrayFieldKeys = { - [K in keyof User]: User[K] extends string[] ? K : never; + [K in keyof User]-?: Exclude extends string[] ? K : never; }[keyof User]; type BooleanFieldKeys = { - [K in keyof User]: User[K] extends boolean ? K : never; + [K in keyof User]-?: Exclude extends boolean ? K : never; }[keyof User]; type BooleanArrayFieldKeys = { - [K in keyof User]: User[K] extends boolean[] ? K : never; + [K in keyof User]-?: Exclude extends boolean[] ? K : never; }[keyof User]; type UserStringOperand = { From 3a6041c30dcf2368043d4005a22eeedd2bb143c5 Mon Sep 17 00:00:00 2001 From: "tailor-platform-pr-trigger[bot]" <247949890+tailor-platform-pr-trigger[bot]@users.noreply.github.com> Date: Sun, 19 Jul 2026 06:53:20 +0000 Subject: [PATCH 618/618] Version Packages (next) --- .changeset/pre.json | 1 + packages/create-sdk/CHANGELOG.md | 2 ++ packages/create-sdk/package.json | 2 +- packages/sdk/CHANGELOG.md | 6 ++++++ packages/sdk/package.json | 2 +- 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 8b61c1798..7166aaa78 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -20,6 +20,7 @@ "fix-mockfile-open-download-stream", "fix-multi-config-cwd", "fix-next5-codemod-boundaries", + "fix-optional-attributes-permission-keys", "fix-rename-bin-source-files", "fix-ts-hook-dotted-basename", "fix-ts-hook-paths-without-baseurl", diff --git a/packages/create-sdk/CHANGELOG.md b/packages/create-sdk/CHANGELOG.md index d6f230f0d..aedf22b66 100644 --- a/packages/create-sdk/CHANGELOG.md +++ b/packages/create-sdk/CHANGELOG.md @@ -1,5 +1,7 @@ # @tailor-platform/create-sdk +## 2.0.0-next.8 + ## 2.0.0-next.7 ### Major Changes diff --git a/packages/create-sdk/package.json b/packages/create-sdk/package.json index 66bcdfb08..a9c04b10a 100644 --- a/packages/create-sdk/package.json +++ b/packages/create-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/create-sdk", - "version": "2.0.0-next.7", + "version": "2.0.0-next.8", "description": "A CLI tool to quickly create a new Tailor Platform SDK project", "license": "MIT", "repository": { diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 63f9d8d25..6a1eea007 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,5 +1,11 @@ # @tailor-platform/sdk +## 2.0.0-next.8 + +### Patch Changes + +- [#1808](https://github.com/tailor-platform/sdk/pull/1808) [`61ac76c`](https://github.com/tailor-platform/sdk/commit/61ac76cff992ef30e6115ba96c7d3a676010204f) Thanks [@toiroakr](https://github.com/toiroakr)! - Fix IdP and TailorDB permission condition types breaking when `Attributes` fields are optional. Since machine user attribute keys started mirroring the source field's optionality, the `user` operand key helpers leaked `undefined` into their key unions — failing typecheck against the generated permission types even for `_loggedIn`-only conditions — and rejected attribute keys derived from optional fields. Optional attribute fields are now valid operand keys and `undefined` no longer appears in the unions. + ## 2.0.0-next.7 ### Major Changes diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 3488b649a..a415af0b4 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk", - "version": "2.0.0-next.7", + "version": "2.0.0-next.8", "description": "Tailor Platform SDK - The SDK to work with Tailor Platform", "license": "MIT", "repository": {

>>(field: P, message: string) => void, ) => void; type TypeCreateHookFn< From 66b7c7de29176c1ac924d067ccfd93ca50196dc7 Mon Sep 17 00:00:00 2001 From: "tailor-platform-pr-trigger[bot]" <247949890+tailor-platform-pr-trigger[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 07:51:22 +0000 Subject: [PATCH 514/618] Version Packages (next) --- .changeset/pre.json | 8 +++++++ packages/create-sdk/CHANGELOG.md | 25 ++++++++++++++++++++ packages/create-sdk/package.json | 2 +- packages/sdk-codemod/CHANGELOG.md | 23 ++++++++++++++++++ packages/sdk-codemod/package.json | 2 +- packages/sdk/CHANGELOG.md | 39 +++++++++++++++++++++++++++++++ packages/sdk/package.json | 2 +- 7 files changed, 98 insertions(+), 3 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 64c265ec6..3b4d4db04 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -4,18 +4,24 @@ "changesets": [ "apply-deploy-source-strings", "auth-attributes-rename", + "clarify-setup-delete-warning", "cli-plugins", "codemod-llm-review", "codemod-migration-docs", "codemod-residual-matching", "codemod-runner-metadata", + "db-table-builder", "execute-script-json-arg", "execute-script-review-scope", + "fix-machine-user-override-env-source", "fix-mockfile-open-download-stream", "fix-rename-bin-source-files", + "fix-ts-hook-dotted-basename", + "fix-ts-hook-paths-without-baseurl", "fix-v2-prerelease-codemods", "invoker-option-rename", "keyring-default-storage", + "kysely-date-timestamp", "open-download-review-scope", "parser-schema-strict", "politty-skill-management", @@ -41,6 +47,8 @@ "runtime-global-source-strings", "runtime-globals-import", "runtime-idp-wrapper", + "runtime-subpath-namespace", + "setup-coordinate-multi-config", "share-codemod-runtime-imports", "store-cli-users-by-subject", "tailor-output-ignore-dir", diff --git a/packages/create-sdk/CHANGELOG.md b/packages/create-sdk/CHANGELOG.md index 855bbe70b..6b41eab62 100644 --- a/packages/create-sdk/CHANGELOG.md +++ b/packages/create-sdk/CHANGELOG.md @@ -1,5 +1,30 @@ # @tailor-platform/create-sdk +## 2.0.0-next.4 + +### Major Changes + +- [#1693](https://github.com/tailor-platform/sdk/pull/1693) [`4751214`](https://github.com/tailor-platform/sdk/commit/4751214c0923e094a844f9ce322279a47e871075) Thanks [@dqn](https://github.com/dqn)! - Rename the TailorDB schema builder from `db.type()` to `db.table()`. + + Update TailorDB definitions: + + ```diff + import { db } from "@tailor-platform/sdk"; + + -export const user = db.type("User", { + +export const user = db.table("User", { + name: db.string(), + }); + ``` + +### Patch Changes + +- [#1704](https://github.com/tailor-platform/sdk/pull/1704) [`9c81d9c`](https://github.com/tailor-platform/sdk/commit/9c81d9c18b1d29b3e9307ea17fe54c8ce55f4dda) Thanks [@dqn](https://github.com/dqn)! - Remove flat value and default exports from `@tailor-platform/sdk/runtime/*` subpath modules. Import each subpath through its self-named namespace export instead, for example `import { iconv } from "@tailor-platform/sdk/runtime/iconv"`. + + The aggregate `@tailor-platform/sdk/runtime` entry remains named-only, and its deprecated `file.deleteFile` alias is removed in favor of `file.delete`. The v2 codemod rewrites straightforward namespace-star subpath imports, flat named value imports, and aggregate `file.deleteFile` calls to the new namespace-object style. + + `TailorContextAPI` and `TailorWorkflowAPI` now describe the SDK wrapper objects. Code that types the platform-provided `globalThis.tailor.context` or `globalThis.tailor.workflow` objects directly must use `PlatformContextAPI` or `PlatformWorkflowAPI` instead. + ## 2.0.0-next.3 ### Major Changes diff --git a/packages/create-sdk/package.json b/packages/create-sdk/package.json index e4ae09dea..e2da618ab 100644 --- a/packages/create-sdk/package.json +++ b/packages/create-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/create-sdk", - "version": "2.0.0-next.3", + "version": "2.0.0-next.4", "description": "A CLI tool to quickly create a new Tailor Platform SDK project", "license": "MIT", "repository": { diff --git a/packages/sdk-codemod/CHANGELOG.md b/packages/sdk-codemod/CHANGELOG.md index 20ab87773..af508c1b9 100644 --- a/packages/sdk-codemod/CHANGELOG.md +++ b/packages/sdk-codemod/CHANGELOG.md @@ -1,5 +1,28 @@ # @tailor-platform/sdk-codemod +## 0.3.0-next.4 + +### Patch Changes + +- [#1693](https://github.com/tailor-platform/sdk/pull/1693) [`4751214`](https://github.com/tailor-platform/sdk/commit/4751214c0923e094a844f9ce322279a47e871075) Thanks [@dqn](https://github.com/dqn)! - Rename the TailorDB schema builder from `db.type()` to `db.table()`. + + Update TailorDB definitions: + + ```diff + import { db } from "@tailor-platform/sdk"; + + -export const user = db.type("User", { + +export const user = db.table("User", { + name: db.string(), + }); + ``` + +- [#1704](https://github.com/tailor-platform/sdk/pull/1704) [`9c81d9c`](https://github.com/tailor-platform/sdk/commit/9c81d9c18b1d29b3e9307ea17fe54c8ce55f4dda) Thanks [@dqn](https://github.com/dqn)! - Remove flat value and default exports from `@tailor-platform/sdk/runtime/*` subpath modules. Import each subpath through its self-named namespace export instead, for example `import { iconv } from "@tailor-platform/sdk/runtime/iconv"`. + + The aggregate `@tailor-platform/sdk/runtime` entry remains named-only, and its deprecated `file.deleteFile` alias is removed in favor of `file.delete`. The v2 codemod rewrites straightforward namespace-star subpath imports, flat named value imports, and aggregate `file.deleteFile` calls to the new namespace-object style. + + `TailorContextAPI` and `TailorWorkflowAPI` now describe the SDK wrapper objects. Code that types the platform-provided `globalThis.tailor.context` or `globalThis.tailor.workflow` objects directly must use `PlatformContextAPI` or `PlatformWorkflowAPI` instead. + ## 0.3.0-next.3 ### Patch Changes diff --git a/packages/sdk-codemod/package.json b/packages/sdk-codemod/package.json index 6fe095d9b..de5b1a511 100644 --- a/packages/sdk-codemod/package.json +++ b/packages/sdk-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk-codemod", - "version": "0.3.0-next.3", + "version": "0.3.0-next.4", "description": "Codemod runner for Tailor Platform SDK upgrades", "license": "MIT", "repository": { diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index d1800e064..5abacd975 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,5 +1,44 @@ # @tailor-platform/sdk +## 2.0.0-next.4 + +### Major Changes + +- [#1693](https://github.com/tailor-platform/sdk/pull/1693) [`4751214`](https://github.com/tailor-platform/sdk/commit/4751214c0923e094a844f9ce322279a47e871075) Thanks [@dqn](https://github.com/dqn)! - Rename the TailorDB schema builder from `db.type()` to `db.table()`. + + Update TailorDB definitions: + + ```diff + import { db } from "@tailor-platform/sdk"; + + -export const user = db.type("User", { + +export const user = db.table("User", { + name: db.string(), + }); + ``` + +- [#1704](https://github.com/tailor-platform/sdk/pull/1704) [`9c81d9c`](https://github.com/tailor-platform/sdk/commit/9c81d9c18b1d29b3e9307ea17fe54c8ce55f4dda) Thanks [@dqn](https://github.com/dqn)! - Remove flat value and default exports from `@tailor-platform/sdk/runtime/*` subpath modules. Import each subpath through its self-named namespace export instead, for example `import { iconv } from "@tailor-platform/sdk/runtime/iconv"`. + + The aggregate `@tailor-platform/sdk/runtime` entry remains named-only, and its deprecated `file.deleteFile` alias is removed in favor of `file.delete`. The v2 codemod rewrites straightforward namespace-star subpath imports, flat named value imports, and aggregate `file.deleteFile` calls to the new namespace-object style. + + `TailorContextAPI` and `TailorWorkflowAPI` now describe the SDK wrapper objects. Code that types the platform-provided `globalThis.tailor.context` or `globalThis.tailor.workflow` objects directly must use `PlatformContextAPI` or `PlatformWorkflowAPI` instead. + +### Minor Changes + +- [#1699](https://github.com/tailor-platform/sdk/pull/1699) [`f6a8d07`](https://github.com/tailor-platform/sdk/commit/f6a8d0779f94c2de5502dfdc68348e4a41ceee47) Thanks [@dqn](https://github.com/dqn)! - Allow `setup coordinate --action` values to group multiple generated actions into one multi-config deploy by separating action names with commas. + +### Patch Changes + +- [#1702](https://github.com/tailor-platform/sdk/pull/1702) [`03143f5`](https://github.com/tailor-platform/sdk/commit/03143f5213d9fa9d3c7697de78f2376994716679) Thanks [@dqn](https://github.com/dqn)! - Clarify the setup delete warning for coordinator action references so grouped `--action` values tell users to remove the action name from the relevant value. + +- [#1691](https://github.com/tailor-platform/sdk/pull/1691) [`2f3dbab`](https://github.com/tailor-platform/sdk/commit/2f3dbab7ed3cf40fa174a82053d70c23c356204d) Thanks [@dqn](https://github.com/dqn)! - Clarify when profile machine user override errors are caused by `TAILOR_PLATFORM_MACHINE_USER_NAME`. + +- [#1700](https://github.com/tailor-platform/sdk/pull/1700) [`0063115`](https://github.com/tailor-platform/sdk/commit/0063115f567ba7e73c0b679a392d5983869e8ac4) Thanks [@toiroakr](https://github.com/toiroakr)! - Fix `tailor` CLI failing with `ERR_MODULE_NOT_FOUND` when resolving extensionless relative imports of files whose basename contains a dot (e.g. `./permissions.generated`). + +- [#1705](https://github.com/tailor-platform/sdk/pull/1705) [`958d571`](https://github.com/tailor-platform/sdk/commit/958d571555a5c66e2e3b9beb3d2247f63cbb8f2c) Thanks [@toiroakr](https://github.com/toiroakr)! - Fix `tailor` CLI failing with `ERR_MODULE_NOT_FOUND` when resolving `tsconfig.json` path aliases (`compilerOptions.paths`) in projects that omit `baseUrl`, which is the standard style since TypeScript 5.0. Also fix path alias resolution to match TypeScript's `extends` behavior: a child config's `baseUrl` is now correctly inherited from an extended config, and a child config's own `paths` now replaces (rather than merges with) inherited `paths`. + +- [#1703](https://github.com/tailor-platform/sdk/pull/1703) [`4681778`](https://github.com/tailor-platform/sdk/commit/46817786354665cb97d0de63b78fa83e64096e03) Thanks [@toiroakr](https://github.com/toiroakr)! - Fix generated Kysely types for `db.date()` fields to use `Timestamp` instead of `string`, matching `db.datetime()` and allowing `insertInto`/`updateTable` calls to accept a `Date` value. + ## 2.0.0-next.3 ### Major Changes diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 847a9617c..ab243ea85 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tailor-platform/sdk", - "version": "2.0.0-next.3", + "version": "2.0.0-next.4", "description": "Tailor Platform SDK - The SDK to work with Tailor Platform", "license": "MIT", "repository": { From c7d22a1b632781a210e378c5c96066a42bc1b10d Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 17:07:19 +0900 Subject: [PATCH 515/618] chore(example): regenerate migration files for hook type changes --- example/migrations/0006/diff.json | 516 ++++++++++++++++++ example/migrations/analyticsdb/0003/diff.json | 68 +++ 2 files changed, 584 insertions(+) create mode 100644 example/migrations/0006/diff.json create mode 100644 example/migrations/analyticsdb/0003/diff.json diff --git a/example/migrations/0006/diff.json b/example/migrations/0006/diff.json new file mode 100644 index 000000000..647ca11c3 --- /dev/null +++ b/example/migrations/0006/diff.json @@ -0,0 +1,516 @@ +{ + "version": 1, + "namespace": "tailordb", + "createdAt": "2026-07-10T08:03:31.987Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": false +} diff --git a/example/migrations/analyticsdb/0003/diff.json b/example/migrations/analyticsdb/0003/diff.json new file mode 100644 index 000000000..df575d8ab --- /dev/null +++ b/example/migrations/analyticsdb/0003/diff.json @@ -0,0 +1,68 @@ +{ + "version": 1, + "namespace": "analyticsdb", + "createdAt": "2026-07-10T08:03:32.003Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Event", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Event", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ value, now }) => value ?? now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ value: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": false +} From d1f8d7a7e4264d6728dd800123deea834c1fd123 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 21:55:37 +0900 Subject: [PATCH 516/618] fix(tailordb): align test helper type hook input with platform runtime --- packages/sdk/src/utils/test/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index 407303f9c..5e09e2ca4 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -54,6 +54,10 @@ export function createTailorDBHook>(type: T) { } else if (obj) { hooked[key] = obj[key]; } + if (hooked[key] === undefined && field.metadata.default !== undefined) { + hooked[key] = + field.metadata.default === "now" ? now.toISOString() : field.metadata.default; + } return hooked; }, {} as Record, @@ -61,9 +65,10 @@ export function createTailorDBHook>(type: T) { // oxlint-disable-next-line typescript/no-unnecessary-condition -- metadata absent in recursive nested calls if (type.metadata?.typeHook?.create) { + const { id: _id, ...typeHookInput } = hooked; // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type const overrides = (type.metadata.typeHook.create as Function)({ - input: data, + input: typeHookInput, invoker: null, now, }); From 8d2451b7042f73f8f1f5fb8322cd3018bb750971 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Fri, 10 Jul 2026 22:07:12 +0900 Subject: [PATCH 517/618] fix(tailordb): apply defaults on null values to match platform nullish coalescing --- packages/sdk/src/utils/test/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index 5e09e2ca4..97ac97bf6 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -54,7 +54,7 @@ export function createTailorDBHook>(type: T) { } else if (obj) { hooked[key] = obj[key]; } - if (hooked[key] === undefined && field.metadata.default !== undefined) { + if (hooked[key] == null && field.metadata.default !== undefined) { hooked[key] = field.metadata.default === "now" ? now.toISOString() : field.metadata.default; } From 2ae5854afa80757b93cefe630b32f78c073ef446 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sat, 11 Jul 2026 00:05:16 +0900 Subject: [PATCH 518/618] fix(tailordb): address review feedback for hook args and optionalOnCreate --- .changeset/tailordb-shared-now-hook.md | 2 +- .../migrate/snapshot-manifest.test.ts | 10 ++++---- .../tailordb/migrate/snapshot-manifest.ts | 4 ---- .../tailordb/hooks-validate-bundler.ts | 24 ++++++++++--------- .../services/tailordb/schema.test.ts | 12 ++++++---- .../src/configure/services/tailordb/schema.ts | 5 +++- .../sdk/src/parser/service/tailordb/field.ts | 4 +++- .../service/tailordb/type-script.test.ts | 16 ++++--------- .../parser/service/tailordb/type-script.ts | 4 +++- 9 files changed, 41 insertions(+), 40 deletions(-) diff --git a/.changeset/tailordb-shared-now-hook.md b/.changeset/tailordb-shared-now-hook.md index 86a2ce574..d6a6fde37 100644 --- a/.changeset/tailordb-shared-now-hook.md +++ b/.changeset/tailordb-shared-now-hook.md @@ -6,7 +6,7 @@ Redesign TailorDB hooks and validators with several breaking changes (pre-release): - Add shared `now` timestamp to all hooks — multiple fields stamped with the same `Date` -- Field-level hooks: `{ value, data, invoker }` → create `{ value, invoker, now }` / update `{ value, oldValue, invoker, now }` (`data` removed, `oldValue` added for update only) +- Field-level hooks: `{ value, data, invoker }` → create `{ input, invoker, now }` / update `{ input, oldValue, invoker, now }` (`data` removed, `oldValue` added for update only) - Type-level hooks: per-field mapping (`Hooks`) → single `{ create, update }` object (`TypeHook`) returning partial field overrides - Type-level create hooks no longer receive `oldRecord`; update hooks receive non-nullable `oldRecord` - Field-level validators: return type changed from `boolean` to `string | void` (return error message or void to pass); `[fn, message]` tuple form removed diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts index 6a42c3ea4..f06cce271 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts @@ -318,9 +318,7 @@ describe("snapshot-manifest", () => { // timestamp once and dispatches each field's hook. const createHook = manifest.schema?.typeHook?.create?.expr ?? ""; expect(createHook).toContain("const _now = new Date()"); - expect(createHook).toContain( - '"updatedAt": ((_value, _oldValue) => (now()))(_input["updatedAt"], _oldRecord?.["updatedAt"] ?? null)', - ); + expect(createHook).toContain('"updatedAt": ((_value) => (now()))(_input["updatedAt"])'); expect(manifest.schema?.typeHook?.update?.expr).toContain('_input["updatedAt"]'); expect(manifest.schema?.typeValidate).toBeUndefined(); }); @@ -403,7 +401,7 @@ describe("snapshot-manifest", () => { expect(manifest.schema?.typeValidate?.update?.expr).toBe(validateExpr); }); - test("marks required fields optionalOnCreate when user type-level create hook exists", () => { + test("type-level create hook does not make required fields optionalOnCreate", () => { const snapshotType = createTestSnapshotType("Customer", { fields: { id: { type: "uuid", required: true }, @@ -418,8 +416,8 @@ describe("snapshot-manifest", () => { }); const manifest = generateTailorDBTypeManifestFromSnapshot(snapshotType); - expect(manifest.schema?.fields?.name?.optionalOnCreate).toBe(true); - expect(manifest.schema?.fields?.fullAddress?.optionalOnCreate).toBe(true); + expect(manifest.schema?.fields?.name?.optionalOnCreate).toBeUndefined(); + expect(manifest.schema?.fields?.fullAddress?.optionalOnCreate).toBeUndefined(); expect(manifest.schema?.fields?.phone?.optionalOnCreate).toBeUndefined(); }); diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts index 30fdc3cc4..637922b4b 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts @@ -112,14 +112,10 @@ export function generateTailorDBTypeManifestFromSnapshot( } // Build fields - const hasUserTypeCreateHook = snapshotType.typeHookExpr?.create !== undefined; const fields: Record> = {}; for (const [fieldName, fieldConfig] of Object.entries(snapshotType.fields)) { if (fieldName === "id") continue; const fieldProto = convertFieldConfigToProto(fieldConfig); - if (hasUserTypeCreateHook && fieldConfig.required && !fieldProto.optionalOnCreate) { - fieldProto.optionalOnCreate = true; - } fields[fieldName] = fieldProto; } diff --git a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts index 5a468f4b9..5f55fd822 100644 --- a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts +++ b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts @@ -24,7 +24,7 @@ type ScriptFunction = (...args: unknown[]) => unknown; type ScriptTarget = { fn: ScriptFunction; - kind: "hooks" | "validate" | "typeHook" | "typeValidate"; + kind: "hooks.create" | "hooks.update" | "validate" | "typeHook" | "typeValidate"; }; /** Binding found in the source file: either an import or a top-level declaration */ @@ -97,11 +97,11 @@ function collectScriptTargets(type: TailorDBTypeSchemaOutput): ScriptTarget[] { const createHook = toScriptFunction(metadata.hooks?.create); if (createHook) { - targets.push({ fn: createHook, kind: "hooks" }); + targets.push({ fn: createHook, kind: "hooks.create" }); } const updateHook = toScriptFunction(metadata.hooks?.update); if (updateHook) { - targets.push({ fn: updateHook, kind: "hooks" }); + targets.push({ fn: updateHook, kind: "hooks.update" }); } for (const validateInput of metadata.validate ?? []) { @@ -444,7 +444,7 @@ export function buildMinimalEntryFromResolved( async function bundleScriptTarget(args: { fn: ScriptFunction; - kind: "hooks" | "validate" | "typeHook" | "typeValidate"; + kind: "hooks.create" | "hooks.update" | "validate" | "typeHook" | "typeValidate"; sourceFilePath: string; sourceBindings: Map; tempDir: string; @@ -455,13 +455,15 @@ async function bundleScriptTarget(args: { const context = `${kind} in ${sourceFilePath}`; const fnSource = stringifyFunction(fn); const argsObject = - kind === "hooks" - ? `{ input: _value, oldValue: _oldValue, invoker: ${tailorPrincipalMap}, now: _now }` - : kind === "validate" - ? `{ value: _value }` - : kind === "typeHook" - ? `{ input: _input, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap}, now: _now }` - : `{ newRecord: _newRecord, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap} }, __issues`; + kind === "hooks.create" + ? `{ input: _value, invoker: ${tailorPrincipalMap}, now: _now }` + : kind === "hooks.update" + ? `{ input: _value, oldValue: _oldValue, invoker: ${tailorPrincipalMap}, now: _now }` + : kind === "validate" + ? `{ value: _value }` + : kind === "typeHook" + ? `{ input: _input, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap}, now: _now }` + : `{ newRecord: _newRecord, oldRecord: _oldRecord, invoker: ${tailorPrincipalMap} }, __issues`; const inlineExpr = assertParsableExpression(`(${fnSource})(${argsObject})`, context); // Check if the function has free variables that need bundling diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index fc396e7a0..355f8c150 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -597,6 +597,14 @@ describe("TailorDBField hooks modifier tests", () => { }, }); }); + + test("create hook allows nullable return when default is set", () => { + db.string() + .default("fallback") + .hooks({ + create: ({ input }) => input?.trim(), + }); + }); }); describe("TailorDBField validate modifier tests", () => { @@ -762,7 +770,6 @@ describe("TailorDBType withTimestamps option tests", () => { const now = new Date("2025-06-01T00:00:00Z"); const result = createHook!({ input: specified, - oldValue: null, invoker: timestampHookInvoker, now, }); @@ -777,7 +784,6 @@ describe("TailorDBType withTimestamps option tests", () => { const now = new Date("2025-06-01T12:00:00Z"); const result = createHook!({ input: null, - oldValue: null, invoker: timestampHookInvoker, now, }); @@ -793,7 +799,6 @@ describe("TailorDBType withTimestamps option tests", () => { const now = new Date("2025-06-01T12:00:00Z"); const result = createHook!({ input: specified, - oldValue: null, invoker: timestampHookInvoker, now, }); @@ -808,7 +813,6 @@ describe("TailorDBType withTimestamps option tests", () => { const now = new Date("2025-06-01T12:00:00Z"); const result = createHook!({ input: null, - oldValue: null, invoker: timestampHookInvoker, now, }); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 5414b67ba..ba74bd47a 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -153,7 +153,10 @@ type DBFieldVectorFn = () => Tai Output >; type DBFieldHooksFn = < - const H extends Hook, + const H extends Hook< + Output, + Defined extends { default: true } ? Output | null | undefined : Output + >, >( hooks: H, ) => TailorDBField, Output>; diff --git a/packages/sdk/src/parser/service/tailordb/field.ts b/packages/sdk/src/parser/service/tailordb/field.ts index c62617024..e7b591729 100644 --- a/packages/sdk/src/parser/service/tailordb/field.ts +++ b/packages/sdk/src/parser/service/tailordb/field.ts @@ -201,7 +201,9 @@ const convertToScriptExpr = ( const argsObject = kind === "validate" ? `{ value: _value }` - : `{ input: _value, oldValue: _oldValue, invoker: ${tailorPrincipalMap}, now: _now }`; + : kind === "hooks.create" + ? `{ input: _value, invoker: ${tailorPrincipalMap}, now: _now }` + : `{ input: _value, oldValue: _oldValue, invoker: ${tailorPrincipalMap}, now: _now }`; return assertParsableExpression( `(${normalized})(${argsObject})`, formatScriptContext(kind, context), diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index e1331377f..c9680cbc2 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -28,12 +28,8 @@ describe("buildTypeScripts", () => { // A single `new Date()` is bound once and dispatched to every field. const createExpr = typeHook?.create?.expr ?? ""; expect(createExpr.match(/new Date\(\)/g)).toHaveLength(1); - expect(createExpr).toContain( - '"createdAt": ((_value, _oldValue) => (_now))(_input["createdAt"], _oldRecord?.["createdAt"] ?? null)', - ); - expect(createExpr).toContain( - '"updatedAt": ((_value, _oldValue) => (_now))(_input["updatedAt"], _oldRecord?.["updatedAt"] ?? null)', - ); + expect(createExpr).toContain('"createdAt": ((_value) => (_now))(_input["createdAt"])'); + expect(createExpr).toContain('"updatedAt": ((_value) => (_now))(_input["updatedAt"])'); // createdAt has no update hook, so the update script only touches updatedAt. const updateExpr = typeHook?.update?.expr ?? ""; @@ -60,13 +56,13 @@ describe("buildTypeScripts", () => { const createExpr = buildTypeScripts(fields).typeHook?.create?.expr ?? ""; expect(createExpr).toContain('"profile": Object.assign({}, _input["profile"], {'); expect(createExpr).toContain( - '(_input["profile"] || {})["displayName"], _oldRecord?.["profile"]?.["displayName"] ?? null)', + '"displayName": ((_value) => (_value.trim()))((_input["profile"] || {})["displayName"])', ); expect(createExpr).toContain( '"contact": Object.assign({}, (_input["profile"] || {})["contact"], {', ); expect(createExpr).toContain( - '((_input["profile"] || {})["contact"] || {})["email"], _oldRecord?.["profile"]?.["contact"]?.["email"] ?? null)', + '"email": ((_value) => (_value.toLowerCase()))(((_input["profile"] || {})["contact"] || {})["email"])', ); }); @@ -87,9 +83,7 @@ describe("buildTypeScripts", () => { const createExpr = typeHook?.create?.expr ?? ""; // hook + default: hookResult ?? defaultValue - expect(createExpr).toContain( - '"status": ((_value, _oldValue) => (_value))(_input["status"], _oldRecord?.["status"] ?? null) ?? "active"', - ); + expect(createExpr).toContain('"status": ((_value) => (_value))(_input["status"]) ?? "active"'); // default only: input ?? defaultValue expect(createExpr).toContain('"name": _input["name"] ?? "unnamed"'); diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index 8254d07aa..25ac7634e 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -80,8 +80,10 @@ function buildHookObject( if (hook && hasDefault) { parts.push( - `${key(name)}: ((_value, _oldValue) => (${hook.expr}))(${access}, ${oldAccess} ?? null) ?? ${serializeDefault(config.default, config.type)}`, + `${key(name)}: ((_value) => (${hook.expr}))(${access}) ?? ${serializeDefault(config.default, config.type)}`, ); + } else if (hook && operation === "create") { + parts.push(`${key(name)}: ((_value) => (${hook.expr}))(${access})`); } else if (hook) { parts.push( `${key(name)}: ((_value, _oldValue) => (${hook.expr}))(${access}, ${oldAccess} ?? null)`, From 1c81db8b28a46362abf04dcb55f19349de39c635 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sat, 11 Jul 2026 00:23:51 +0900 Subject: [PATCH 519/618] fix(tailordb): fix test helper default and type hook input alignment - Only treat "now" as timestamp for datetime/date/time fields (not string) - Pass raw input to type-level create hook instead of post-field-hook data --- packages/sdk/src/utils/test/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index 97ac97bf6..591c24efa 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -55,8 +55,12 @@ export function createTailorDBHook>(type: T) { hooked[key] = obj[key]; } if (hooked[key] == null && field.metadata.default !== undefined) { + const isTimeType = + field.type === "datetime" || field.type === "date" || field.type === "time"; hooked[key] = - field.metadata.default === "now" ? now.toISOString() : field.metadata.default; + field.metadata.default === "now" && isTimeType + ? now.toISOString() + : field.metadata.default; } return hooked; }, @@ -65,7 +69,7 @@ export function createTailorDBHook>(type: T) { // oxlint-disable-next-line typescript/no-unnecessary-condition -- metadata absent in recursive nested calls if (type.metadata?.typeHook?.create) { - const { id: _id, ...typeHookInput } = hooked; + const { id: _id, ...typeHookInput } = obj ?? {}; // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type const overrides = (type.metadata.typeHook.create as Function)({ input: typeHookInput, From 09f5691ef5a76761812f039d125d33eb3211994a Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 10 Jul 2026 18:30:22 +0900 Subject: [PATCH 520/618] fix(executor): align workflow argument contracts --- .changeset/workflow-executor-args-contract.md | 5 + packages/sdk/docs/services/executor.md | 5 + .../src/cli/commands/deploy/executor.test.ts | 23 ++++ .../sdk/src/cli/commands/deploy/executor.ts | 11 +- .../services/executor/executor.test.ts | 54 +++++++++ .../configure/services/executor/executor.ts | 15 +-- .../configure/services/executor/operation.ts | 10 +- .../parser/service/executor/schema.test.ts | 60 +++++++++- .../sdk/src/parser/service/executor/schema.ts | 82 ++++++++----- .../sdk/src/parser/service/executor/types.ts | 11 ++ packages/sdk/src/types/executor.generated.ts | 109 +++++++----------- 11 files changed, 271 insertions(+), 114 deletions(-) create mode 100644 .changeset/workflow-executor-args-contract.md create mode 100644 packages/sdk/src/parser/service/executor/types.ts diff --git a/.changeset/workflow-executor-args-contract.md b/.changeset/workflow-executor-args-contract.md new file mode 100644 index 000000000..0a13c1ac4 --- /dev/null +++ b/.changeset/workflow-executor-args-contract.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": patch +--- + +Fix workflow executor `args` to be required when the target workflow input is required, and accept primitive and array static JSON inputs during configuration parsing. diff --git a/packages/sdk/docs/services/executor.md b/packages/sdk/docs/services/executor.md index 954b588c9..784e08e10 100644 --- a/packages/sdk/docs/services/executor.md +++ b/packages/sdk/docs/services/executor.md @@ -329,6 +329,11 @@ createExecutor({ }); ``` +`args` must match the workflow's main job input. It is required when that input is required +and can be omitted when the workflow has no input. Static arguments can be JSON-compatible +primitives, arrays, or plain objects; top-level `null` is not supported. An argument callback +must return the same input type. + ### Authentication for Operations GraphQL and Workflow operations can specify an `invoker` to execute with machine user credentials. Pass the machine user name as a plain string — it is type-narrowed to the names defined in your auth config: diff --git a/packages/sdk/src/cli/commands/deploy/executor.test.ts b/packages/sdk/src/cli/commands/deploy/executor.test.ts index 474f7f3ca..06a08c066 100644 --- a/packages/sdk/src/cli/commands/deploy/executor.test.ts +++ b/packages/sdk/src/cli/commands/deploy/executor.test.ts @@ -371,6 +371,29 @@ describe("planExecutor", () => { expect(result.changeSet.updates).toHaveLength(0); expect(result.changeSet.deletes).toHaveLength(0); }); + + test.each([ + ["false", false, "false"], + ["zero", 0, "0"], + ["empty string", "", '""'], + ])("preserves %s workflow args", async (_description, args, expectedExpression) => { + const executor = createMockExecutor("workflow-executor"); + executor.operation = { + kind: "workflow", + workflowName: "test-workflow", + args, + }; + + const result = await planExecutor( + buildPlanContext(createMockApplication([executor]), { client: createMockClient([]) }), + ); + const targetConfig = result.changeSet.creates[0]?.request.executor?.targetConfig?.config; + if (targetConfig?.case !== "workflow") { + throw new Error("Expected workflow target config"); + } + + expect(targetConfig.value.variables?.expr).toBe(expectedExpression); + }); }); describe("update scenarios", () => { diff --git a/packages/sdk/src/cli/commands/deploy/executor.ts b/packages/sdk/src/cli/commands/deploy/executor.ts index 79603056f..fc531538f 100644 --- a/packages/sdk/src/cli/commands/deploy/executor.ts +++ b/packages/sdk/src/cli/commands/deploy/executor.ts @@ -703,11 +703,12 @@ function protoExecutor( case: "workflow", value: { workflowName: target.workflowName, - variables: target.args - ? typeof target.args === "function" - ? { expr: `(${stringifyFunction(target.args)})(${argsExpr})` } - : { expr: JSON.stringify(target.args) } - : undefined, + variables: + target.args !== undefined + ? typeof target.args === "function" + ? { expr: `(${stringifyFunction(target.args)})(${argsExpr})` } + : { expr: JSON.stringify(target.args) } + : undefined, invoker: normalizeInvoker(target.invoker, authNamespace, invokerContext), }, }, diff --git a/packages/sdk/src/configure/services/executor/executor.test.ts b/packages/sdk/src/configure/services/executor/executor.test.ts index 8778d760a..75ff1a28e 100644 --- a/packages/sdk/src/configure/services/executor/executor.test.ts +++ b/packages/sdk/src/configure/services/executor/executor.test.ts @@ -1197,6 +1197,60 @@ describe("workflowTarget", () => { expect(executor.operation.workflow.name).toBe("test-workflow"); }); + test("requires args for workflow with required input", () => { + createExecutor({ + name: "test", + trigger: scheduleTrigger({ cron: "0 12 * * *" }), + // @ts-expect-error - args is required by the workflow's main job input + operation: { + kind: "workflow", + workflow: testWorkflow, + }, + }); + }); + + test("accepts primitive static args", () => { + const primitiveJob = createWorkflowJob({ + name: "primitive-input-job", + body: (input: string) => input, + }); + const primitiveWorkflow = createWorkflow({ + name: "primitive-input-workflow", + mainJob: primitiveJob, + }); + + createExecutor({ + name: "test", + trigger: scheduleTrigger({ cron: "0 12 * * *" }), + operation: { + kind: "workflow", + workflow: primitiveWorkflow, + args: "hello", + }, + }); + }); + + test("accepts array static args", () => { + const arrayJob = createWorkflowJob({ + name: "array-input-job", + body: (input: string[]) => input.length, + }); + const arrayWorkflow = createWorkflow({ + name: "array-input-workflow", + mainJob: arrayJob, + }); + + createExecutor({ + name: "test", + trigger: scheduleTrigger({ cron: "0 12 * * *" }), + operation: { + kind: "workflow", + workflow: arrayWorkflow, + args: ["hello"], + }, + }); + }); + test("args can be a function", () => { createExecutor({ name: "test", diff --git a/packages/sdk/src/configure/services/executor/executor.ts b/packages/sdk/src/configure/services/executor/executor.ts index 42ec21b27..c9bb62a97 100644 --- a/packages/sdk/src/configure/services/executor/executor.ts +++ b/packages/sdk/src/configure/services/executor/executor.ts @@ -1,15 +1,9 @@ import { brandValue } from "#/utils/brand"; import type { Workflow } from "#/configure/services/workflow/workflow"; -import type { MachineUserName } from "#/configure/types/machine-user"; import type { ExecutorInput } from "#/types/executor.generated"; -import type { Operation } from "./operation"; +import type { Operation, WorkflowOperation } from "./operation"; import type { Trigger } from "./trigger"; -/** - * Extract mainJob's Input type from Workflow. - */ -type WorkflowInput = Parameters[0]; - type TriggerArgs> = T extends { __args: infer Args } ? Args : never; type ExecutorBase> = Omit & { @@ -26,12 +20,7 @@ type Executor, O> = O extends { workflow: infer W extends Workflow; } ? ExecutorBase & { - operation: { - kind: "workflow"; - workflow: W; - args?: WorkflowInput | ((args: TriggerArgs) => WorkflowInput); - invoker?: MachineUserName; - }; + operation: WorkflowOperation, W>; } : ExecutorBase & { operation: O; diff --git a/packages/sdk/src/configure/services/executor/operation.ts b/packages/sdk/src/configure/services/executor/operation.ts index 6b92d008b..0473bb51b 100644 --- a/packages/sdk/src/configure/services/executor/operation.ts +++ b/packages/sdk/src/configure/services/executor/operation.ts @@ -287,15 +287,21 @@ export type WebhookOperation = Omit< */ type WorkflowInput = Parameters[0]; +type WorkflowArgs = WorkflowInput | ((args: Args) => WorkflowInput); + +type WorkflowArgsProperty = + undefined extends WorkflowInput + ? { args?: WorkflowArgs } + : { args: WorkflowArgs }; + /** Workflow-triggering executor operation. Triggers a workflow in response to an event. */ export type WorkflowOperation = Omit< ParserWorkflowOperation, "workflowName" | "args" | "invoker" > & { workflow: W; - args?: WorkflowInput | ((args: Args) => WorkflowInput); invoker?: MachineUserName; -}; +} & WorkflowArgsProperty; export type Operation = | FunctionOperation diff --git a/packages/sdk/src/parser/service/executor/schema.test.ts b/packages/sdk/src/parser/service/executor/schema.test.ts index ef07516e0..cd982b69f 100644 --- a/packages/sdk/src/parser/service/executor/schema.test.ts +++ b/packages/sdk/src/parser/service/executor/schema.test.ts @@ -1,10 +1,11 @@ -import { describe, expect, test } from "vitest"; +import { describe, expect, expectTypeOf, test } from "vitest"; import { ExecutorSchema, FunctionOperationSchema, GqlOperationSchema, WorkflowOperationSchema, } from "./schema"; +import type { Executor, WorkflowOperationArgs } from "#/types/executor.generated"; function expectParseSuccess( result: { success: true; data: T } | { success: false; error: unknown }, @@ -117,6 +118,63 @@ describe("WorkflowOperationSchema", () => { }), ); }); + + test.each([ + ["string", "hello"], + ["empty string", ""], + ["number", 42], + ["zero", 0], + ["boolean", true], + ["false", false], + ["array", ["hello", 42, false, null]], + ])("accepts %s static args", (_description, args) => { + const result = WorkflowOperationSchema.safeParse({ + kind: "workflow", + workflowName: "my-workflow", + args, + }); + + const data = expectParseSuccess(result); + expect(data.args).toEqual(args); + }); + + test.each([ + ["top-level null", null], + ["Date", new Date("2026-01-01T00:00:00.000Z")], + ["Map", new Map([["key", "value"]])], + ["nested Date", { nested: new Date("2026-01-01T00:00:00.000Z") }], + ["nested Map", [new Map([["key", "value"]])]], + ])("rejects unsupported %s static args", (_description, args) => { + const result = WorkflowOperationSchema.safeParse({ + kind: "workflow", + workflowName: "my-workflow", + args, + }); + + expect(result.success).toBe(false); + }); + + test("accepts a dynamic args function", () => { + const args = () => ({ orderId: "123" }); + const result = WorkflowOperationSchema.safeParse({ + kind: "workflow", + workflowName: "my-workflow", + args, + }); + + const data = expectParseSuccess(result); + expect(data.args).toBe(args); + }); + + test("keeps generated Executor workflow args aligned with the generated contract", () => { + type ExecutorWorkflowArgs = Extract["args"]; + + expectTypeOf().toEqualTypeOf(); + + // @ts-expect-error Date is neither a JSON-compatible input nor an args callback. + const invalidArgs: ExecutorWorkflowArgs = new Date(); + void invalidArgs; + }); }); describe("ExecutorSchema", () => { diff --git a/packages/sdk/src/parser/service/executor/schema.ts b/packages/sdk/src/parser/service/executor/schema.ts index cad70a6a6..0a84fb76d 100644 --- a/packages/sdk/src/parser/service/executor/schema.ts +++ b/packages/sdk/src/parser/service/executor/schema.ts @@ -1,6 +1,8 @@ import { z } from "zod"; import { AuthInvokerSchema } from "../auth"; import { functionSchema } from "../common"; +import type { JsonValue } from "#/types/helpers"; +import type { WorkflowOperation as WorkflowOperationValue, WorkflowOperationArgs } from "./types"; export const TailorDBTriggerSchema = z.strictObject({ kind: z.literal("tailordb").describe("TailorDB record event trigger"), @@ -111,33 +113,61 @@ export const WebhookOperationSchema = z.strictObject({ .describe("HTTP headers for the webhook request"), }); -export const WorkflowOperationSchema = z.preprocess( - (val) => { - if ( - val == null || - typeof val !== "object" || - !("workflow" in val) || - typeof val.workflow !== "object" || - val.workflow === null - ) { - return val; - } - - const { workflow, ...rest } = val as { workflow: { name: string } }; - return { ...rest, workflowName: workflow.name }; - }, - z.strictObject({ - kind: z.literal("workflow"), - workflowName: z.string().describe("Name of the workflow to execute"), - args: z - .union([z.record(z.string(), z.unknown()), functionSchema]) - .optional() - .describe("Arguments to pass to the workflow"), - invoker: AuthInvokerSchema.optional().describe("Invoker for the workflow execution"), - }), +export const JsonValueSchema: z.ZodType = z.lazy(() => + z.union([ + z.string(), + z.number(), + z.boolean(), + z.null(), + z.array(JsonValueSchema), + z.record(z.string(), JsonValueSchema), + ]), ); -const OperationSchema = z.union([ +export const WorkflowInputSchema: z.ZodType> = z.union([ + z.string(), + z.number(), + z.boolean(), + z.array(JsonValueSchema), + z.record(z.string(), JsonValueSchema), +]); + +export const WorkflowOperationArgsSchema: z.ZodType = + z + .custom( + (value) => typeof value === "function" || WorkflowInputSchema.safeParse(value).success, + ) + .describe("Arguments to pass to the workflow"); + +const workflowOperationShape = { + kind: z.literal("workflow"), + args: WorkflowOperationArgsSchema.optional(), + invoker: AuthInvokerSchema.optional().describe("Invoker for the workflow execution"), +}; + +const WorkflowOperationByNameSchema = z.strictObject({ + ...workflowOperationShape, + workflowName: z.string().describe("Name of the workflow to execute"), +}); + +const WorkflowOperationByReferenceSchema = z + .strictObject({ + ...workflowOperationShape, + workflow: z.object({ name: z.string() }).passthrough(), + }) + .transform( + ({ workflow, ...operation }): WorkflowOperationValue => ({ + ...operation, + workflowName: workflow.name, + }), + ); + +export const WorkflowOperationSchema: z.ZodType = z.union([ + WorkflowOperationByNameSchema, + WorkflowOperationByReferenceSchema, +]); + +export const OperationSchema = z.union([ FunctionOperationSchema, GqlOperationSchema, WebhookOperationSchema, @@ -149,5 +179,5 @@ export const ExecutorSchema = z.strictObject({ description: z.string().optional().describe("Executor description"), disabled: z.boolean().optional().default(false).describe("Whether the executor is disabled"), trigger: TriggerSchema.describe("Event trigger configuration"), - operation: OperationSchema.describe("Operation to execute when triggered"), + operation: OperationSchema, }); diff --git a/packages/sdk/src/parser/service/executor/types.ts b/packages/sdk/src/parser/service/executor/types.ts new file mode 100644 index 000000000..593d31a2c --- /dev/null +++ b/packages/sdk/src/parser/service/executor/types.ts @@ -0,0 +1,11 @@ +import type { JsonValue } from "#/types/helpers"; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +export type WorkflowOperationArgs = Exclude | Function; + +export type WorkflowOperation = { + kind: "workflow"; + workflowName: string; + args?: WorkflowOperationArgs | undefined; + invoker?: string | { namespace: string; machineUserName: string } | undefined; +}; diff --git a/packages/sdk/src/types/executor.generated.ts b/packages/sdk/src/types/executor.generated.ts index 61b872588..81c52dbaa 100644 --- a/packages/sdk/src/types/executor.generated.ts +++ b/packages/sdk/src/types/executor.generated.ts @@ -168,17 +168,41 @@ export type WebhookOperation = { }; export type WebhookOperationInput = WebhookOperation; -export type WorkflowOperationInput = unknown; +export type JsonValueInput = + | string + | number + | boolean + | JsonValueInput[] + | { [key: string]: JsonValueInput } + | null; + +export type JsonValue = + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue } + | null; + +export type WorkflowInput = string | number | boolean | JsonValue[] | { [key: string]: JsonValue }; +export type WorkflowInputInput = WorkflowInput; + +/** + * Arguments to pass to the workflow + */ +export type WorkflowOperationArgs = + | string + | number + | boolean + | Function + | JsonValue[] + | { [key: string]: JsonValue }; +export type WorkflowOperationArgsInput = WorkflowOperationArgs; export type WorkflowOperation = { kind: "workflow"; workflowName: string; - args?: - | Function - | { - [x: string]: unknown; - } - | undefined; + args?: WorkflowOperationArgs | undefined; invoker?: | string | { @@ -187,6 +211,15 @@ export type WorkflowOperation = { } | undefined; }; +export type WorkflowOperationInput = WorkflowOperation; + +export type OperationInput = + | FunctionOperation + | GqlOperationInput + | WebhookOperation + | WorkflowOperation; + +export type Operation = FunctionOperation | GqlOperation | WebhookOperation | WorkflowOperation; export type ExecutorInput = { /** Executor name */ @@ -235,8 +268,7 @@ export type ExecutorInput = { | "auth.access_token.revoked" )[]; }; - /** Operation to execute when triggered */ - operation: unknown; + operation: OperationInput; /** Executor description */ description?: string | undefined; /** Whether the executor is disabled */ @@ -292,64 +324,7 @@ export type Executor = { | "auth.access_token.revoked" )[]; }; - /** Operation to execute when triggered */ - operation: - | { - kind: "workflow"; - workflowName: string; - args?: - | Function - | { - [x: string]: unknown; - } - | undefined; - invoker?: - | string - | { - namespace: string; - machineUserName: string; - } - | undefined; - } - | { - kind: "function" | "jobFunction"; - body: Function; - invoker?: - | string - | { - namespace: string; - machineUserName: string; - } - | undefined; - } - | { - kind: "graphql"; - query: string; - appName?: string | undefined; - variables?: Function | undefined; - invoker?: - | string - | { - namespace: string; - machineUserName: string; - } - | undefined; - } - | { - kind: "webhook"; - url: Function; - requestBody?: Function | undefined; - headers?: - | { - [x: string]: - | string - | { - vault: string; - key: string; - }; - } - | undefined; - }; + operation: Operation; /** Executor description */ description?: string | undefined; }; From 18c3014ba3cb887b0ba9e5b5b96c449a3c8dbaba Mon Sep 17 00:00:00 2001 From: dqn Date: Fri, 10 Jul 2026 18:50:02 +0900 Subject: [PATCH 521/618] fix(executor): preserve workflow operation normalization --- .../parser/service/executor/schema.test.ts | 46 +++++++++++++++++++ .../sdk/src/parser/service/executor/schema.ts | 31 +++++++++---- .../sdk/src/parser/service/executor/types.ts | 5 +- packages/sdk/src/types/executor.generated.ts | 16 ++----- 4 files changed, 75 insertions(+), 23 deletions(-) diff --git a/packages/sdk/src/parser/service/executor/schema.test.ts b/packages/sdk/src/parser/service/executor/schema.test.ts index cd982b69f..e1e291b33 100644 --- a/packages/sdk/src/parser/service/executor/schema.test.ts +++ b/packages/sdk/src/parser/service/executor/schema.test.ts @@ -96,6 +96,29 @@ describe("WorkflowOperationSchema", () => { expect(data).not.toHaveProperty("workflow"); }); + test("prefers workflow object name when workflowName is also present", () => { + const result = WorkflowOperationSchema.safeParse({ + kind: "workflow", + workflowName: "stale-workflow", + workflow: { name: "current-workflow" }, + args: { id: "123" }, + }); + + const data = expectParseSuccess(result); + expect(data.workflowName).toBe("current-workflow"); + }); + + test("rejects a malformed workflow object even when workflowName is present", () => { + const result = WorkflowOperationSchema.safeParse({ + kind: "workflow", + workflowName: "stale-workflow", + workflow: {}, + args: { id: "123" }, + }); + + expect(result.success).toBe(false); + }); + test("accepts workflowName directly", () => { const result = WorkflowOperationSchema.safeParse({ kind: "workflow", @@ -142,6 +165,7 @@ describe("WorkflowOperationSchema", () => { ["top-level null", null], ["Date", new Date("2026-01-01T00:00:00.000Z")], ["Map", new Map([["key", "value"]])], + ["nested undefined", { nested: undefined }], ["nested Date", { nested: new Date("2026-01-01T00:00:00.000Z") }], ["nested Map", [new Map([["key", "value"]])]], ])("rejects unsupported %s static args", (_description, args) => { @@ -166,6 +190,28 @@ describe("WorkflowOperationSchema", () => { expect(data.args).toBe(args); }); + test("returns the validated copy of static args", () => { + let reads = 0; + const args = { + get value() { + reads += 1; + return "stable"; + }, + }; + const result = WorkflowOperationSchema.safeParse({ + kind: "workflow", + workflowName: "my-workflow", + args, + }); + + const data = expectParseSuccess(result); + expect(data.args).not.toBe(args); + expect(data.args).toEqual({ value: "stable" }); + const readsAfterParse = reads; + expect(JSON.stringify(data.args)).toBe('{"value":"stable"}'); + expect(reads).toBe(readsAfterParse); + }); + test("keeps generated Executor workflow args aligned with the generated contract", () => { type ExecutorWorkflowArgs = Extract["args"]; diff --git a/packages/sdk/src/parser/service/executor/schema.ts b/packages/sdk/src/parser/service/executor/schema.ts index 0a84fb76d..c9f9863f8 100644 --- a/packages/sdk/src/parser/service/executor/schema.ts +++ b/packages/sdk/src/parser/service/executor/schema.ts @@ -2,7 +2,10 @@ import { z } from "zod"; import { AuthInvokerSchema } from "../auth"; import { functionSchema } from "../common"; import type { JsonValue } from "#/types/helpers"; -import type { WorkflowOperation as WorkflowOperationValue, WorkflowOperationArgs } from "./types"; +import type { + WorkflowOperation as WorkflowOperationValue, + WorkflowOperationFunction, +} from "./types"; export const TailorDBTriggerSchema = z.strictObject({ kind: z.literal("tailordb").describe("TailorDB record event trigger"), @@ -113,7 +116,7 @@ export const WebhookOperationSchema = z.strictObject({ .describe("HTTP headers for the webhook request"), }); -export const JsonValueSchema: z.ZodType = z.lazy(() => +export const JsonValueSchema: z.ZodType = z.lazy(() => z.union([ z.string(), z.number(), @@ -124,7 +127,10 @@ export const JsonValueSchema: z.ZodType = z.lazy(() => ]), ); -export const WorkflowInputSchema: z.ZodType> = z.union([ +export const WorkflowInputSchema: z.ZodType< + Exclude, + Exclude +> = z.union([ z.string(), z.number(), z.boolean(), @@ -132,12 +138,15 @@ export const WorkflowInputSchema: z.ZodType> = z.union( z.record(z.string(), JsonValueSchema), ]); -export const WorkflowOperationArgsSchema: z.ZodType = - z - .custom( - (value) => typeof value === "function" || WorkflowInputSchema.safeParse(value).success, - ) - .describe("Arguments to pass to the workflow"); +export const WorkflowOperationFunctionSchema: z.ZodType< + WorkflowOperationFunction, + WorkflowOperationFunction +> = functionSchema; + +export const WorkflowOperationArgsSchema = z.union([ + WorkflowInputSchema, + WorkflowOperationFunctionSchema, +]); const workflowOperationShape = { kind: z.literal("workflow"), @@ -148,12 +157,14 @@ const workflowOperationShape = { const WorkflowOperationByNameSchema = z.strictObject({ ...workflowOperationShape, workflowName: z.string().describe("Name of the workflow to execute"), + workflow: z.never().optional(), }); const WorkflowOperationByReferenceSchema = z .strictObject({ ...workflowOperationShape, workflow: z.object({ name: z.string() }).passthrough(), + workflowName: z.string().optional(), }) .transform( ({ workflow, ...operation }): WorkflowOperationValue => ({ @@ -163,8 +174,8 @@ const WorkflowOperationByReferenceSchema = z ); export const WorkflowOperationSchema: z.ZodType = z.union([ - WorkflowOperationByNameSchema, WorkflowOperationByReferenceSchema, + WorkflowOperationByNameSchema, ]); export const OperationSchema = z.union([ diff --git a/packages/sdk/src/parser/service/executor/types.ts b/packages/sdk/src/parser/service/executor/types.ts index 593d31a2c..3feb8d8ff 100644 --- a/packages/sdk/src/parser/service/executor/types.ts +++ b/packages/sdk/src/parser/service/executor/types.ts @@ -1,11 +1,12 @@ import type { JsonValue } from "#/types/helpers"; +// Workflow argument callbacks can use any callable signature. // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -export type WorkflowOperationArgs = Exclude | Function; +export type WorkflowOperationFunction = Function; export type WorkflowOperation = { kind: "workflow"; workflowName: string; - args?: WorkflowOperationArgs | undefined; + args?: Exclude | WorkflowOperationFunction | undefined; invoker?: string | { namespace: string; machineUserName: string } | undefined; }; diff --git a/packages/sdk/src/types/executor.generated.ts b/packages/sdk/src/types/executor.generated.ts index 81c52dbaa..7db66f758 100644 --- a/packages/sdk/src/types/executor.generated.ts +++ b/packages/sdk/src/types/executor.generated.ts @@ -187,22 +187,16 @@ export type JsonValue = export type WorkflowInput = string | number | boolean | JsonValue[] | { [key: string]: JsonValue }; export type WorkflowInputInput = WorkflowInput; -/** - * Arguments to pass to the workflow - */ -export type WorkflowOperationArgs = - | string - | number - | boolean - | Function - | JsonValue[] - | { [key: string]: JsonValue }; +export type WorkflowOperationFunction = Function; +export type WorkflowOperationFunctionInput = WorkflowOperationFunction; + +export type WorkflowOperationArgs = WorkflowInput | WorkflowOperationFunction; export type WorkflowOperationArgsInput = WorkflowOperationArgs; export type WorkflowOperation = { kind: "workflow"; workflowName: string; - args?: WorkflowOperationArgs | undefined; + args?: Exclude | WorkflowOperationFunction | undefined; invoker?: | string | { From e5d7906b981ec6a824813f7d2958266d8a7cb714 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 11 Jul 2026 00:25:51 +0900 Subject: [PATCH 522/618] fix(executor): derive workflow operation types from schemas --- .changeset/workflow-executor-args-contract.md | 4 +- packages/sdk/package.json | 2 +- packages/sdk/src/parser/service/common.ts | 4 +- .../parser/service/executor/schema.test.ts | 52 +- .../sdk/src/parser/service/executor/schema.ts | 34 +- .../sdk/src/parser/service/executor/types.ts | 12 - packages/sdk/src/types/auth.generated.ts | 55 +- packages/sdk/src/types/executor.generated.ts | 177 +- packages/sdk/src/types/idp.generated.ts | 2592 +---------------- packages/sdk/src/types/resolver.generated.ts | 8 +- packages/sdk/src/types/tailordb.generated.ts | 18 +- packages/sdk/src/types/workflow.generated.ts | 44 +- pnpm-lock.yaml | 12 +- pnpm-workspace.yaml | 1 + 14 files changed, 229 insertions(+), 2786 deletions(-) delete mode 100644 packages/sdk/src/parser/service/executor/types.ts diff --git a/.changeset/workflow-executor-args-contract.md b/.changeset/workflow-executor-args-contract.md index 0a13c1ac4..804d9e924 100644 --- a/.changeset/workflow-executor-args-contract.md +++ b/.changeset/workflow-executor-args-contract.md @@ -1,5 +1,5 @@ --- -"@tailor-platform/sdk": patch +"@tailor-platform/sdk": major --- -Fix workflow executor `args` to be required when the target workflow input is required, and accept primitive and array static JSON inputs during configuration parsing. +Require workflow executor `args` when the target workflow input is required, and accept primitive and array static JSON inputs during configuration parsing. Existing workflow executor configurations that omit required input must add `args`. diff --git a/packages/sdk/package.json b/packages/sdk/package.json index ab243ea85..ce9ae34c5 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -236,7 +236,7 @@ "tsdown": "0.22.3", "typescript": "6.0.3", "vitest": "4.1.9", - "zinfer": "0.2.3" + "zinfer": "0.2.4" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", diff --git a/packages/sdk/src/parser/service/common.ts b/packages/sdk/src/parser/service/common.ts index 1012c14c5..405b628d9 100644 --- a/packages/sdk/src/parser/service/common.ts +++ b/packages/sdk/src/parser/service/common.ts @@ -2,4 +2,6 @@ import { z } from "zod"; // Use `z.custom` instead of `z.function`, since `z.function` changes `toString` representation. // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -export const functionSchema = z.custom((val) => typeof val === "function"); +export const functionSchema: z.ZodType = z.custom( + (val) => typeof val === "function", +); diff --git a/packages/sdk/src/parser/service/executor/schema.test.ts b/packages/sdk/src/parser/service/executor/schema.test.ts index e1e291b33..1920d2ff8 100644 --- a/packages/sdk/src/parser/service/executor/schema.test.ts +++ b/packages/sdk/src/parser/service/executor/schema.test.ts @@ -5,7 +5,7 @@ import { GqlOperationSchema, WorkflowOperationSchema, } from "./schema"; -import type { Executor, WorkflowOperationArgs } from "#/types/executor.generated"; +import type { Executor, ExecutorInput, WorkflowOperationArgs } from "#/types/executor.generated"; function expectParseSuccess( result: { success: true; data: T } | { success: false; error: unknown }, @@ -133,13 +133,28 @@ describe("WorkflowOperationSchema", () => { test("rejects unknown options", () => { expect.hasAssertions(); - expectUnknownKeyRejected( + const error = expectParseFailure( WorkflowOperationSchema.safeParse({ kind: "workflow", workflowName: "my-workflow", unknownOption: true, }), ); + + expect(error.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "invalid_union", + errors: expect.arrayContaining([ + expect.arrayContaining([ + expect.objectContaining({ + code: "unrecognized_keys", + }), + ]), + ]), + }), + ]), + ); }); test.each([ @@ -221,6 +236,39 @@ describe("WorkflowOperationSchema", () => { const invalidArgs: ExecutorWorkflowArgs = new Date(); void invalidArgs; }); + + test("keeps generated workflow operation input aligned with reference parsing", () => { + type WorkflowReferenceInput = Extract< + ExecutorInput["operation"], + { kind: "workflow"; workflow: unknown } + >; + + expectTypeOf().not.toBeAny(); + expectTypeOf().not.toBeUnknown(); + expectTypeOf().not.toBeNever(); + + const executor: ExecutorInput = { + name: "test-executor", + trigger: { kind: "schedule", cron: "0 12 * * *" }, + operation: { + kind: "workflow", + workflow: { name: "my-workflow" }, + args: { orderId: "123" }, + }, + }; + + const invalidExecutor: ExecutorInput = { + ...executor, + operation: { + kind: "workflow", + // @ts-expect-error Workflow references require a name. + workflow: {}, + }, + }; + void invalidExecutor; + + expect(executor.operation.kind).toBe("workflow"); + }); }); describe("ExecutorSchema", () => { diff --git a/packages/sdk/src/parser/service/executor/schema.ts b/packages/sdk/src/parser/service/executor/schema.ts index c9f9863f8..3c56dc80b 100644 --- a/packages/sdk/src/parser/service/executor/schema.ts +++ b/packages/sdk/src/parser/service/executor/schema.ts @@ -2,10 +2,6 @@ import { z } from "zod"; import { AuthInvokerSchema } from "../auth"; import { functionSchema } from "../common"; import type { JsonValue } from "#/types/helpers"; -import type { - WorkflowOperation as WorkflowOperationValue, - WorkflowOperationFunction, -} from "./types"; export const TailorDBTriggerSchema = z.strictObject({ kind: z.literal("tailordb").describe("TailorDB record event trigger"), @@ -138,15 +134,9 @@ export const WorkflowInputSchema: z.ZodType< z.record(z.string(), JsonValueSchema), ]); -export const WorkflowOperationFunctionSchema: z.ZodType< - WorkflowOperationFunction, - WorkflowOperationFunction -> = functionSchema; - -export const WorkflowOperationArgsSchema = z.union([ - WorkflowInputSchema, - WorkflowOperationFunctionSchema, -]); +export const WorkflowOperationArgsSchema = z + .union([WorkflowInputSchema, functionSchema]) + .describe("Arguments to pass to the workflow"); const workflowOperationShape = { kind: z.literal("workflow"), @@ -163,17 +153,15 @@ const WorkflowOperationByNameSchema = z.strictObject({ const WorkflowOperationByReferenceSchema = z .strictObject({ ...workflowOperationShape, - workflow: z.object({ name: z.string() }).passthrough(), + workflow: z.looseObject({ name: z.string() }), workflowName: z.string().optional(), }) - .transform( - ({ workflow, ...operation }): WorkflowOperationValue => ({ - ...operation, - workflowName: workflow.name, - }), - ); - -export const WorkflowOperationSchema: z.ZodType = z.union([ + .transform(({ workflow, ...operation }) => ({ + ...operation, + workflowName: workflow.name, + })); + +export const WorkflowOperationSchema = z.union([ WorkflowOperationByReferenceSchema, WorkflowOperationByNameSchema, ]); @@ -190,5 +178,5 @@ export const ExecutorSchema = z.strictObject({ description: z.string().optional().describe("Executor description"), disabled: z.boolean().optional().default(false).describe("Whether the executor is disabled"), trigger: TriggerSchema.describe("Event trigger configuration"), - operation: OperationSchema, + operation: OperationSchema.describe("Operation to execute when triggered"), }); diff --git a/packages/sdk/src/parser/service/executor/types.ts b/packages/sdk/src/parser/service/executor/types.ts deleted file mode 100644 index 3feb8d8ff..000000000 --- a/packages/sdk/src/parser/service/executor/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { JsonValue } from "#/types/helpers"; - -// Workflow argument callbacks can use any callable signature. -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -export type WorkflowOperationFunction = Function; - -export type WorkflowOperation = { - kind: "workflow"; - workflowName: string; - args?: Exclude | WorkflowOperationFunction | undefined; - invoker?: string | { namespace: string; machineUserName: string } | undefined; -}; diff --git a/packages/sdk/src/types/auth.generated.ts b/packages/sdk/src/types/auth.generated.ts index efe5d57e8..c08194c75 100644 --- a/packages/sdk/src/types/auth.generated.ts +++ b/packages/sdk/src/types/auth.generated.ts @@ -89,6 +89,12 @@ export type IdProviderInput = OIDC | SAMLInput | IDToken | BuiltinIdP; export type IdProvider = OIDC | SAML | IDToken | BuiltinIdP; +/** + * OAuth2 grant type + */ +export type OAuth2ClientGrantType = "authorization_code" | "refresh_token"; +export type OAuth2ClientGrantTypeInput = OAuth2ClientGrantType; + export type OAuth2ClientInput = { /** Allowed redirect URIs */ redirectURIs: ( @@ -156,9 +162,15 @@ export type SCIMAuthorization = { }; export type SCIMAuthorizationInput = SCIMAuthorization; +/** + * SCIM attribute data type + */ +export type SCIMAttributeType = "string" | "number" | "boolean" | "datetime" | "complex"; +export type SCIMAttributeTypeInput = SCIMAttributeType; + export type SCIMAttributeInput = { /** Attribute data type */ - type: "string" | "number" | "boolean" | "datetime" | "complex"; + type: SCIMAttributeType; /** Attribute name */ name: string; /** Attribute description */ @@ -178,7 +190,7 @@ export type SCIMAttributeInput = { export type SCIMAttribute = { /** Attribute data type */ - type: "string" | "number" | "boolean" | "datetime" | "complex"; + type: SCIMAttributeType; /** Attribute name */ name: string; /** Attribute description */ @@ -229,10 +241,7 @@ export type SCIMResource = { }[]; }; /** Attribute mapping configuration */ - attributeMapping: { - tailorDBField: string; - scimPath: string; - }[]; + attributeMapping: SCIMAttributeMapping[]; }; export type SCIMResourceInput = SCIMResource; @@ -240,39 +249,9 @@ export type SCIMConfig = { /** Machine user name for SCIM operations */ machineUserName: string; /** SCIM authorization configuration */ - authorization: { - type: "oauth2" | "bearer"; - bearerSecret?: - | { - vaultName: string; - secretKey: string; - } - | undefined; - }; + authorization: SCIMAuthorization; /** SCIM resource definitions */ - resources: { - name: string; - tailorDBNamespace: string; - tailorDBType: string; - coreSchema: { - name: string; - attributes: { - type: "string" | "number" | "boolean" | "datetime" | "complex"; - name: string; - description?: string | undefined; - mutability?: "readOnly" | "readWrite" | "writeOnly" | undefined; - required?: boolean | undefined; - multiValued?: boolean | undefined; - uniqueness?: "none" | "server" | "global" | undefined; - canonicalValues?: string[] | null | undefined; - subAttributes?: any[] | null | undefined; - }[]; - }; - attributeMapping: { - tailorDBField: string; - scimPath: string; - }[]; - }[]; + resources: SCIMResource[]; }; export type SCIMConfigInput = SCIMConfig; diff --git a/packages/sdk/src/types/executor.generated.ts b/packages/sdk/src/types/executor.generated.ts index 7db66f758..9214d7674 100644 --- a/packages/sdk/src/types/executor.generated.ts +++ b/packages/sdk/src/types/executor.generated.ts @@ -52,12 +52,7 @@ export type IncomingWebhookTriggerResponseInput = IncomingWebhookTriggerResponse export type IncomingWebhookTrigger = { kind: "incomingWebhook"; /** Response configuration */ - response?: - | { - body?: Function | undefined; - statusCode?: number | undefined; - } - | undefined; + response?: IncomingWebhookTriggerResponse; }; export type IncomingWebhookTriggerInput = IncomingWebhookTrigger; @@ -187,31 +182,75 @@ export type JsonValue = export type WorkflowInput = string | number | boolean | JsonValue[] | { [key: string]: JsonValue }; export type WorkflowInputInput = WorkflowInput; -export type WorkflowOperationFunction = Function; -export type WorkflowOperationFunctionInput = WorkflowOperationFunction; - -export type WorkflowOperationArgs = WorkflowInput | WorkflowOperationFunction; +/** + * Arguments to pass to the workflow + */ +export type WorkflowOperationArgs = WorkflowInput | Function; export type WorkflowOperationArgsInput = WorkflowOperationArgs; -export type WorkflowOperation = { - kind: "workflow"; - workflowName: string; - args?: Exclude | WorkflowOperationFunction | undefined; - invoker?: - | string - | { - namespace: string; - machineUserName: string; - } - | undefined; -}; -export type WorkflowOperationInput = WorkflowOperation; +export type WorkflowOperationInput = + | { + workflow: { + [x: string]: unknown; + name: string; + }; + kind: "workflow"; + workflowName?: string | undefined; + args?: WorkflowOperationArgs; + invoker?: + | string + | { + namespace: string; + machineUserName: string; + } + | undefined; + } + | { + workflowName: string; + kind: "workflow"; + workflow?: undefined; + args?: WorkflowOperationArgs; + invoker?: + | string + | { + namespace: string; + machineUserName: string; + } + | undefined; + }; + +export type WorkflowOperation = + | { + workflowName: string; + kind: "workflow"; + args?: WorkflowOperationArgs; + invoker?: + | string + | { + namespace: string; + machineUserName: string; + } + | undefined; + } + | { + workflowName: string; + kind: "workflow"; + workflow?: undefined; + args?: WorkflowOperationArgs; + invoker?: + | string + | { + namespace: string; + machineUserName: string; + } + | undefined; + }; export type OperationInput = | FunctionOperation | GqlOperationInput | WebhookOperation - | WorkflowOperation; + | WorkflowOperationInput; export type Operation = FunctionOperation | GqlOperation | WebhookOperation | WorkflowOperation; @@ -219,49 +258,8 @@ export type ExecutorInput = { /** Executor name */ name: string; /** Event trigger configuration */ - trigger: - | { - kind: "tailordb"; - events: ( - | "tailordb.type_record.created" - | "tailordb.type_record.updated" - | "tailordb.type_record.deleted" - )[]; - typeName: string; - condition?: Function | undefined; - } - | { - kind: "resolverExecuted"; - resolverName: string; - condition?: Function | undefined; - } - | { - kind: "schedule"; - cron: string; - timezone?: string | undefined; - } - | { - kind: "incomingWebhook"; - response?: - | { - body?: Function | undefined; - statusCode?: number | undefined; - } - | undefined; - } - | { - kind: "idpUser"; - events: ("idp.user.created" | "idp.user.updated" | "idp.user.deleted")[]; - idp?: string | undefined; - } - | { - kind: "authAccessToken"; - events: ( - | "auth.access_token.issued" - | "auth.access_token.refreshed" - | "auth.access_token.revoked" - )[]; - }; + trigger: TriggerInput; + /** Operation to execute when triggered */ operation: OperationInput; /** Executor description */ description?: string | undefined; @@ -275,49 +273,8 @@ export type Executor = { /** Whether the executor is disabled */ disabled: boolean; /** Event trigger configuration */ - trigger: - | { - kind: "tailordb"; - events: ( - | "tailordb.type_record.created" - | "tailordb.type_record.updated" - | "tailordb.type_record.deleted" - )[]; - typeName: string; - condition?: Function | undefined; - } - | { - kind: "resolverExecuted"; - resolverName: string; - condition?: Function | undefined; - } - | { - kind: "schedule"; - cron: string; - timezone: string; - } - | { - kind: "incomingWebhook"; - response?: - | { - body?: Function | undefined; - statusCode?: number | undefined; - } - | undefined; - } - | { - kind: "idpUser"; - events: ("idp.user.created" | "idp.user.updated" | "idp.user.deleted")[]; - idp?: string | undefined; - } - | { - kind: "authAccessToken"; - events: ( - | "auth.access_token.issued" - | "auth.access_token.refreshed" - | "auth.access_token.revoked" - )[]; - }; + trigger: Trigger; + /** Operation to execute when triggered */ operation: Operation; /** Executor description */ description?: string | undefined; diff --git a/packages/sdk/src/types/idp.generated.ts b/packages/sdk/src/types/idp.generated.ts index 5eeb9f70c..32225103d 100644 --- a/packages/sdk/src/types/idp.generated.ts +++ b/packages/sdk/src/types/idp.generated.ts @@ -1378,2559 +1378,53 @@ export type IdPInput = { } | undefined; /** Namespace-level email configuration defaults */ - emailConfig?: + emailConfig?: IdPEmailConfig; + /** Per-operation permission policies for IdP users */ + permission?: IdPPermission; +}; + +export type IdP = { + /** IdP service name */ + name: string; + /** OAuth2 client names that can use this IdP */ + clients: string[]; + /** Authorization mode for IdP API access */ + authorization?: + | "insecure" + | "loggedIn" | { - fromName?: string | undefined; - passwordResetSubject?: string | undefined; + cel: string; } | undefined; - /** Per-operation permission policies for IdP users */ - permission?: + /** UI language for IdP pages */ + lang?: "en" | "ja" | undefined; + /** User authentication policy configuration */ + userAuthPolicy?: | { - create: readonly ( - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - boolean, - ] - | readonly ( - | boolean - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - )[] - | { - conditions: - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly (readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ])[]; - description?: string | undefined; - permit?: boolean | undefined; - } - )[]; - read: readonly ( - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - boolean, - ] - | readonly ( - | boolean - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - )[] - | { - conditions: - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly (readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ])[]; - description?: string | undefined; - permit?: boolean | undefined; - } - )[]; - update: readonly ( - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - boolean, - ] - | readonly ( - | boolean - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - )[] - | { - conditions: - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly (readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ])[]; - description?: string | undefined; - permit?: boolean | undefined; - } - )[]; - delete: readonly ( - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - boolean, - ] - | readonly ( - | boolean - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - )[] - | { - conditions: - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly (readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ])[]; - description?: string | undefined; - permit?: boolean | undefined; - } - )[]; - sendPasswordResetEmail?: - | readonly ( - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - boolean, - ] - | readonly ( - | boolean - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - )[] - | { - conditions: - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly (readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ])[]; - description?: string | undefined; - permit?: boolean | undefined; - } - )[] - | undefined; - unenrollMfa?: - | readonly ( - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - boolean, - ] - | readonly ( - | boolean - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - )[] - | { - conditions: - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly (readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ])[]; - description?: string | undefined; - permit?: boolean | undefined; - } - )[] - | undefined; - } - | undefined; -}; - -export type IdP = { - /** IdP service name */ - name: string; - /** OAuth2 client names that can use this IdP */ - clients: string[]; - /** Authorization mode for IdP API access */ - authorization?: - | "insecure" - | "loggedIn" - | { - cel: string; - } - | undefined; - /** UI language for IdP pages */ - lang?: "en" | "ja" | undefined; - /** User authentication policy configuration */ - userAuthPolicy?: - | { - useNonEmailIdentifier?: boolean | undefined; - allowSelfPasswordReset?: boolean | undefined; - passwordRequireUppercase?: boolean | undefined; - passwordRequireLowercase?: boolean | undefined; - passwordRequireNonAlphanumeric?: boolean | undefined; - passwordRequireNumeric?: boolean | undefined; - passwordMinLength?: number | undefined; - passwordMaxLength?: number | undefined; - allowedEmailDomains?: string[] | undefined; - allowGoogleOauth?: boolean | undefined; - allowMicrosoftOauth?: boolean | undefined; - disablePasswordAuth?: boolean | undefined; - enableMfa?: boolean | undefined; - requireMfa?: boolean | undefined; - allowedReturnOrigins?: string[] | undefined; - mfaIssuer?: string | undefined; - } - | undefined; - /** Enable publishing user lifecycle events */ - publishUserEvents?: boolean | undefined; - /** Configure which GraphQL operations are enabled */ - gqlOperations?: - | { - create?: boolean | undefined; - update?: boolean | undefined; - delete?: boolean | undefined; - read?: boolean | undefined; - sendPasswordResetEmail?: boolean | undefined; - requestMfaSettingsUrl?: boolean | undefined; - unenrollMfa?: boolean | undefined; - } - | undefined; - /** Namespace-level email configuration defaults */ - emailConfig?: - | { - fromName?: string | undefined; - passwordResetSubject?: string | undefined; - } - | undefined; - /** Per-operation permission policies for IdP users */ - permission?: - | { - create: readonly ( - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - boolean, - ] - | readonly ( - | boolean - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - )[] - | { - conditions: - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly (readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ])[]; - description?: string | undefined; - permit?: boolean | undefined; - } - )[]; - read: readonly ( - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - boolean, - ] - | readonly ( - | boolean - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - )[] - | { - conditions: - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly (readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ])[]; - description?: string | undefined; - permit?: boolean | undefined; - } - )[]; - update: readonly ( - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - boolean, - ] - | readonly ( - | boolean - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - )[] - | { - conditions: - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly (readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ])[]; - description?: string | undefined; - permit?: boolean | undefined; - } - )[]; - delete: readonly ( - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - boolean, - ] - | readonly ( - | boolean - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - )[] - | { - conditions: - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly (readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ])[]; - description?: string | undefined; - permit?: boolean | undefined; - } - )[]; - sendPasswordResetEmail?: - | readonly ( - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - boolean, - ] - | readonly ( - | boolean - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - )[] - | { - conditions: - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly (readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ])[]; - description?: string | undefined; - permit?: boolean | undefined; - } - )[] - | undefined; - unenrollMfa?: - | readonly ( - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - boolean, - ] - | readonly ( - | boolean - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - )[] - | { - conditions: - | readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ] - | readonly (readonly [ - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - "in" | "=" | "!=" | "not in", - ( - | string - | boolean - | readonly string[] - | readonly boolean[] - | { - user: string; - } - | { - idpUser: "name" | "id" | "disabled"; - } - | { - oldIdpUser: "name" | "id" | "disabled"; - } - | { - newIdpUser: "name" | "id" | "disabled"; - } - ), - ])[]; - description?: string | undefined; - permit?: boolean | undefined; - } - )[] - | undefined; + useNonEmailIdentifier?: boolean | undefined; + allowSelfPasswordReset?: boolean | undefined; + passwordRequireUppercase?: boolean | undefined; + passwordRequireLowercase?: boolean | undefined; + passwordRequireNonAlphanumeric?: boolean | undefined; + passwordRequireNumeric?: boolean | undefined; + passwordMinLength?: number | undefined; + passwordMaxLength?: number | undefined; + allowedEmailDomains?: string[] | undefined; + allowGoogleOauth?: boolean | undefined; + allowMicrosoftOauth?: boolean | undefined; + disablePasswordAuth?: boolean | undefined; + enableMfa?: boolean | undefined; + requireMfa?: boolean | undefined; + allowedReturnOrigins?: string[] | undefined; + mfaIssuer?: string | undefined; } | undefined; + /** Enable publishing user lifecycle events */ + publishUserEvents?: boolean | undefined; + /** Configure which GraphQL operations are enabled */ + gqlOperations?: IdPGqlOperations; + /** Namespace-level email configuration defaults */ + emailConfig?: IdPEmailConfig; + /** Per-operation permission policies for IdP users */ + permission?: IdPPermission; }; diff --git a/packages/sdk/src/types/resolver.generated.ts b/packages/sdk/src/types/resolver.generated.ts index f867c5d10..deeb016d6 100644 --- a/packages/sdk/src/types/resolver.generated.ts +++ b/packages/sdk/src/types/resolver.generated.ts @@ -1,8 +1,14 @@ // Generated by zinfer - Do not edit manually +/** + * GraphQL operation type + */ +export type QueryType = "query" | "mutation"; +export type QueryTypeInput = QueryType; + export type Resolver = { /** GraphQL operation type (query or mutation) */ - operation: "query" | "mutation"; + operation: QueryType; /** Resolver name */ name: string; /** Resolver implementation function */ diff --git a/packages/sdk/src/types/tailordb.generated.ts b/packages/sdk/src/types/tailordb.generated.ts index 40984ceb8..d1ff55fe4 100644 --- a/packages/sdk/src/types/tailordb.generated.ts +++ b/packages/sdk/src/types/tailordb.generated.ts @@ -120,14 +120,7 @@ export type TailorDBTypeParsedSettings = { /** Enable bulk upsert mutation for this type */ bulkUpsert?: boolean | undefined; /** Configure GraphQL operations for this type. Use "query" for read-only mode, or an object for granular control. */ - gqlOperations?: - | { - create?: boolean | undefined; - update?: boolean | undefined; - delete?: boolean | undefined; - read?: boolean | undefined; - } - | undefined; + gqlOperations?: GqlOperations; /** * Enable publishing events for this type. * When enabled, record creation/update/deletion events are published. @@ -1136,12 +1129,5 @@ export type TailorDBServiceConfig = { } | undefined; /** Default GraphQL operations for all types in this service */ - gqlOperations?: - | { - create?: boolean | undefined; - update?: boolean | undefined; - delete?: boolean | undefined; - read?: boolean | undefined; - } - | undefined; + gqlOperations?: GqlOperations; }; diff --git a/packages/sdk/src/types/workflow.generated.ts b/packages/sdk/src/types/workflow.generated.ts index a2b629500..cfef29b04 100644 --- a/packages/sdk/src/types/workflow.generated.ts +++ b/packages/sdk/src/types/workflow.generated.ts @@ -28,17 +28,25 @@ export type ConcurrencyPolicy = { }; export type ConcurrencyPolicyInput = ConcurrencyPolicy; +/** + * Workspace-unique execution policy name embedded in the resource TRN + */ +export type ExecutionPolicyName = string; +export type ExecutionPolicyNameInput = ExecutionPolicyName; + +/** + * Execution policy key passed to triggerJobFunction's executionPolicyKey option + */ +export type ExecutionPolicyKey = string; +export type ExecutionPolicyKeyInput = ExecutionPolicyKey; + export type WorkflowJobFunctionExecutionPolicy = { /** Workspace-unique execution policy name embedded in the resource TRN */ - name: string; + name: ExecutionPolicyName; /** Execution policy key passed to triggerJobFunction's executionPolicyKey option */ - key: string; + key: ExecutionPolicyKey; /** Optional per-key concurrency cap for job function dispatches matching this policy */ - concurrencyPolicy?: - | { - maxConcurrentExecutions: number; - } - | undefined; + concurrencyPolicy?: ConcurrencyPolicy; }; export type WorkflowJobFunctionExecutionPolicyInput = WorkflowJobFunctionExecutionPolicy; @@ -46,26 +54,10 @@ export type Workflow = { /** Workflow name */ name: string; /** Main job that starts the workflow */ - mainJob: { - /** Workflow name */ - name: string; - trigger: Function; - body: Function; - }; + mainJob: WorkflowJob; /** Retry policy for the workflow */ - retryPolicy?: - | { - maxRetries: number; - initialBackoff: `${number}ms` | `${number}s` | `${number}m`; - maxBackoff: `${number}ms` | `${number}s` | `${number}m`; - backoffMultiplier: number; - } - | undefined; + retryPolicy?: RetryPolicy; /** Concurrency policy for the workflow */ - concurrencyPolicy?: - | { - maxConcurrentExecutions: number; - } - | undefined; + concurrencyPolicy?: ConcurrencyPolicy; }; export type WorkflowInput = Workflow; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e9027479..f2284cc71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: esbuild@>=0.17.0 <0.28.1: 0.28.1 + zinfer: https://pkg.pr.new/zinfer@054a3a5?v=054a3a5 importers: @@ -588,8 +589,8 @@ importers: specifier: 4.1.9 version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.5)(yaml@2.9.0)) zinfer: - specifier: 0.2.3 - version: 0.2.3(typescript@6.0.3)(zod@4.4.3) + specifier: https://pkg.pr.new/zinfer@054a3a5?v=054a3a5 + version: https://pkg.pr.new/zinfer@054a3a5?v=054a3a5(typescript@6.0.3)(zod@4.4.3) packages/sdk-codemod: dependencies: @@ -4152,8 +4153,9 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} - zinfer@0.2.3: - resolution: {integrity: sha512-BcAk5ZPmzUTpZKFZ52noVwEbdNVpLIIkNu/LdAXBXZRGWsO+9Ok/T9xSKlmDgyAPZcJcNOm7tuEqj/IfGcEFBQ==} + zinfer@https://pkg.pr.new/zinfer@054a3a5?v=054a3a5: + resolution: {integrity: sha512-wFiU6TH9pytTFxG2QkX1ktyWeq2xAQFneK/H0kaaJNQ8/D8o7FgosF6OVrbbjdcXYfR0JmE0Eh6RGzJSMm+UoA==, tarball: https://pkg.pr.new/zinfer@054a3a5?v=054a3a5} + version: 0.2.4 hasBin: true peerDependencies: typescript: '>=5.0.0' @@ -7151,7 +7153,7 @@ snapshots: yoctocolors@2.1.2: {} - zinfer@0.2.3(typescript@6.0.3)(zod@4.4.3): + zinfer@https://pkg.pr.new/zinfer@054a3a5?v=054a3a5(typescript@6.0.3)(zod@4.4.3): dependencies: commander: 15.0.0 glob: 13.0.6 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 37eed56a4..ce21b4f6b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -39,6 +39,7 @@ allowBuilds: overrides: esbuild@>=0.17.0 <0.28.1: 0.28.1 + zinfer: "https://pkg.pr.new/zinfer@054a3a5?v=054a3a5" peerDependencyRules: allowedVersions: From 2c0b346bb8dd3205fc1395c5157a335eed59520a Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sat, 11 Jul 2026 13:03:21 +0900 Subject: [PATCH 523/618] fix: add missing codemod suspicious patterns and fix nested Generated Kysely resolution --- packages/sdk-codemod/src/registry.ts | 4 ++-- .../builtin/kysely-type/type-processor.test.ts | 14 ++++++++++++++ .../plugin/builtin/kysely-type/type-processor.ts | 5 ++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index a15e4ba95..e63dee175 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -853,7 +853,7 @@ export const allCodemods: CodemodPackage[] = [ since: "1.0.0", until: "2.0.0", prereleaseUntil: V2_NEXT_4, - suspiciousPatterns: ["ValidateConfig", "Validators<", "ValidatorsBase"], + suspiciousPatterns: ["ValidateConfig", "Validators<", "ValidatorsBase", ".validate("], examples: [ { caption: @@ -904,7 +904,7 @@ export const allCodemods: CodemodPackage[] = [ since: "1.0.0", until: "2.0.0", prereleaseUntil: V2_NEXT_4, - suspiciousPatterns: ["Hooks<", "HookFn<", "Hook<"], + suspiciousPatterns: ["Hooks<", "HookFn<", "Hook<", ".hooks("], examples: [ { caption: diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts index 658cc0324..9863ddd88 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts @@ -252,6 +252,20 @@ describe("Kysely TypeProcessor", () => { expect(typeDef).not.toContain("ArrayColumnType"); }); + test("should use ObjectColumnType for nested objects with defaulted fields", async () => { + const typeDef = await getTypeDef( + db.table("Config", { + settings: db.object({ + name: db.string(), + status: db.string().default("active"), + }), + }), + ); + + expect(typeDef).toContain("ObjectColumnType<"); + expect(typeDef).toContain("Generated"); + }); + test("should handle optional nested objects", async () => { const typeDef = await getTypeDef( db.table("User", { diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts index feec20f8a..00f0ebc42 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts @@ -63,7 +63,10 @@ function getNestedType(fieldConfig: OperatorFieldConfig): FieldTypeResult { const obj = `{\n ${fieldTypes.join(";\n ")}${fieldTypes.length > 0 ? ";" : ""}\n}`; const hasOptionalFields = Object.values(fields).some((config) => config.required !== true); - if (aggregatedUtilityTypes.Timestamp || hasOptionalFields) { + const hasGeneratedFields = Object.values(fields).some( + (config) => config.hooks?.create || config.default !== undefined, + ); + if (aggregatedUtilityTypes.Timestamp || hasOptionalFields || hasGeneratedFields) { return { type: `ObjectColumnType<${obj}>`, usedUtilityTypes: aggregatedUtilityTypes }; } return { type: obj, usedUtilityTypes: aggregatedUtilityTypes }; From 4d833562c871ab776af9959c5c6fc26e44633fcd Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sat, 11 Jul 2026 19:36:01 +0900 Subject: [PATCH 524/618] chore: use oxlint-disable, document hook priority, fix docs example and changeset wording --- .changeset/tailordb-shared-now-hook.md | 2 +- packages/sdk/docs/services/tailordb.md | 4 ++-- packages/sdk/src/configure/services/tailordb/schema.ts | 4 ++-- packages/sdk/src/configure/services/tailordb/types.ts | 4 ++-- packages/sdk/src/parser/service/tailordb/field.ts | 6 +++--- packages/sdk/src/utils/test/index.ts | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.changeset/tailordb-shared-now-hook.md b/.changeset/tailordb-shared-now-hook.md index d6a6fde37..7911c49eb 100644 --- a/.changeset/tailordb-shared-now-hook.md +++ b/.changeset/tailordb-shared-now-hook.md @@ -3,7 +3,7 @@ "@tailor-platform/sdk-codemod": patch --- -Redesign TailorDB hooks and validators with several breaking changes (pre-release): +Redesign TailorDB hooks and validators with several breaking changes: - Add shared `now` timestamp to all hooks — multiple fields stamped with the same `Date` - Field-level hooks: `{ value, data, invoker }` → create `{ input, invoker, now }` / update `{ input, oldValue, invoker, now }` (`data` removed, `oldValue` added for update only) diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index 4cffdded6..dd7d16c67 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -306,7 +306,7 @@ Field-level hooks operate on a single field and cannot access other fields. Use #### Type-level Hooks -Set hooks across multiple fields using `db.table().hooks()`. The hook returns an object with the fields to override. +Set hooks across multiple fields using `db.table().hooks()`. The hook returns an object with the fields to override. When both field-level and type-level hooks exist for the same field, type-level hooks take priority. Create hooks receive: @@ -330,7 +330,7 @@ export const customer = db fullName: `${input.firstName} ${input.lastName}`, }), update: ({ input, oldRecord }) => ({ - fullName: `${input.firstName} ${input.lastName}`, + fullName: `${input.firstName ?? oldRecord.firstName} ${input.lastName ?? oldRecord.lastName}`, }), }); ``` diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index ba74bd47a..784879cd5 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -835,9 +835,9 @@ function createTailorDBType< const _permissions: RawPermissions = {}; let _files: Record = {}; const _plugins: PluginAttachment[] = []; - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + // oxlint-disable-next-line typescript/no-unsafe-function-type let _typeHook: { create?: Function; update?: Function } | undefined; - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + // oxlint-disable-next-line typescript/no-unsafe-function-type let _typeValidate: Function | undefined; if (options.pluralForm) { diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index 361ddc26c..bbc71bbc8 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -89,9 +89,9 @@ export interface TailorDBTypeMetadata { unique?: boolean; } >; - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + // oxlint-disable-next-line typescript/no-unsafe-function-type typeHook?: { create?: Function; update?: Function }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + // oxlint-disable-next-line typescript/no-unsafe-function-type typeValidate?: Function; } diff --git a/packages/sdk/src/parser/service/tailordb/field.ts b/packages/sdk/src/parser/service/tailordb/field.ts index e7b591729..5bf0aa810 100644 --- a/packages/sdk/src/parser/service/tailordb/field.ts +++ b/packages/sdk/src/parser/service/tailordb/field.ts @@ -142,7 +142,7 @@ const firstObjectProperty = (wrapped: string) => { * @param fn - Function to stringify * @returns Stringified function source */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +// oxlint-disable-next-line typescript/no-unsafe-function-type export const stringifyFunction = (fn: Function): string => { const src = fn.toString().trim(); // `src` is already a valid function/arrow expression (e.g. `function () {}`, @@ -210,7 +210,7 @@ const convertToScriptExpr = ( ); }; -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +// oxlint-disable-next-line typescript/no-unsafe-function-type export const convertTypeHookToExpr = (fn: Function): string => { const precompiledExpr = getPrecompiledScriptExpr(fn as (...args: never[]) => unknown); if (precompiledExpr) { @@ -223,7 +223,7 @@ export const convertTypeHookToExpr = (fn: Function): string => { ); }; -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +// oxlint-disable-next-line typescript/no-unsafe-function-type export const convertTypeValidateToExpr = (fn: Function): string => { const precompiledExpr = getPrecompiledScriptExpr(fn as (...args: never[]) => unknown); if (precompiledExpr) { diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index 591c24efa..fa37a387c 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -70,7 +70,7 @@ export function createTailorDBHook>(type: T) { // oxlint-disable-next-line typescript/no-unnecessary-condition -- metadata absent in recursive nested calls if (type.metadata?.typeHook?.create) { const { id: _id, ...typeHookInput } = obj ?? {}; - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + // oxlint-disable-next-line typescript/no-unsafe-function-type const overrides = (type.metadata.typeHook.create as Function)({ input: typeHookInput, invoker: null, From 145751828b30644379ed8929a5cc64a4e0f57914 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sat, 11 Jul 2026 21:36:36 +0900 Subject: [PATCH 525/618] feat(tailordb): support nested object arrays in type-level defaults and validates --- .../services/tailordb/schema.test.ts | 43 ++++++++++++++ .../src/configure/services/tailordb/schema.ts | 13 ++++- .../src/configure/services/tailordb/types.ts | 39 +++++++++++-- .../service/tailordb/type-script.test.ts | 57 +++++++++++++++++++ .../parser/service/tailordb/type-script.ts | 40 +++++++++++-- packages/sdk/src/utils/test/index.test.ts | 2 + 6 files changed, 183 insertions(+), 11 deletions(-) diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 355f8c150..0e44f6887 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -1187,6 +1187,32 @@ describe("TailorDBType type-level validate (function form) tests", () => { }); }); + test("issues function accepts indexed paths for nested array fields", () => { + db.table("Test", { + items: db.object( + { + name: db.string(), + qty: db.int(), + }, + { array: true }, + ), + }).validate(({ newRecord }, issues) => { + issues("items", "ok"); + newRecord.items?.forEach((item, i) => { + if (!item.name) { + issues(`items[${i}].name`, "required"); + } + if (item.qty < 0) { + issues(`items[${i}].qty`, "must be positive"); + } + }); + // @ts-expect-error TS2345 — "items.name" (without index) is not valid for array fields + issues("items.name", "bad"); + // @ts-expect-error TS2345 — "items.length" should not be a valid path + issues("items.length", "bad"); + }); + }); + test("type-level validate function stores in metadata", () => { const type = db .table("Test", { @@ -1238,6 +1264,23 @@ describe("db.object tests", () => { }); }); + test("hooks on inner fields of db.object causes type error", () => { + db.object({ + name: db.string(), + // @ts-expect-error hooks on nested inner fields are not allowed + stamped: db.string().hooks({ + create: ({ input }) => input ?? "default", + }), + }); + }); + + test("default and validate on inner fields of db.object are allowed", () => { + db.object({ + name: db.string().default("unnamed"), + status: db.string().validate(({ value }) => (value.length > 0 ? undefined : "required")), + }); + }); + test("correctly infers object type with optional fields", () => { const _objectType = db.table("Test", { user: db.object({ diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 784879cd5..a1c350d6c 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -33,7 +33,14 @@ import type { InferredAttributes } from "#/runtime/types"; import type { output, InferFieldsOutput, TypeLevelError } from "#/types/helpers"; import type { RawPermissions } from "#/types/tailordb.generated"; import type { TailorTypeGqlPermission, TailorTypePermission } from "./permission"; -import type { Hook, TypeHook, ExcludeNestedDBFields, TypeFeatures, TypeValidateFn } from "./types"; +import type { + Hook, + TypeHook, + ExcludeNestedDBFields, + ExcludeHookedDBFields, + TypeFeatures, + TypeValidateFn, +} from "./types"; import type { StandardSchemaV1 } from "@standard-schema/spec"; // Erased DB fields stay assignable across builder method-state changes. @@ -802,7 +809,9 @@ function _enum( * @example db.object({ name: db.string() }, { optional: true }) */ function object< - const F extends Record & ExcludeNestedDBFields, + const F extends Record & + ExcludeNestedDBFields & + ExcludeHookedDBFields, const Opt extends FieldOptions, >(fields: F, options?: Opt) { return createField("nested", options, fields) as unknown as TailorDBField< diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index bbc71bbc8..c7bfe0233 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -6,6 +6,7 @@ import type { DefinedFieldMetadata, FieldMetadata, TailorField, + TailorFieldType, } from "#/configure/types/field.types"; import type { InferredAttributes, TailorPrincipal } from "#/runtime/types"; import type { InferFieldsOutput, output, Prettify } from "#/types/helpers"; @@ -171,13 +172,25 @@ export type Hook = { update?: UpdateHookFn; }; +type DotJoin = A extends "" ? B : `${A}.${B}`; + type DottedPaths = string extends keyof T ? string - : T extends Record - ? { - [K in keyof T & string]: `${Prefix}${K}` | DottedPaths, `${Prefix}${K}.`>; - }[keyof T & string] - : never; + : T extends readonly (infer E)[] + ? E extends Record + ? { + [K in keyof E & string]: + | `${Prefix}[${number}].${K}` + | DottedPaths, `${Prefix}[${number}].${K}`>; + }[keyof E & string] + : never + : T extends Record + ? { + [K in keyof T & string]: + | DotJoin + | DottedPaths, DotJoin>; + }[keyof T & string] + : never; export type TypeValidateFn< F extends Record, @@ -223,6 +236,22 @@ export type ExcludeNestedDBFields> = : T[K]; }; +// oxlint-disable no-explicit-any -- conditional type matching requires `any` for the output param +export type ExcludeHookedDBFields> = { + [K in keyof T]: T[K] extends TailorDBField< + { type: TailorFieldType; array: boolean; hooks: { create: true; update: boolean } }, + any + > + ? never + : T[K] extends TailorDBField< + { type: TailorFieldType; array: boolean; hooks: { create: boolean; update: true } }, + any + > + ? never + : T[K]; +}; +// oxlint-enable no-explicit-any + // --- Type features --- export interface TypeFeatures { diff --git a/packages/sdk/src/parser/service/tailordb/type-script.test.ts b/packages/sdk/src/parser/service/tailordb/type-script.test.ts index c9680cbc2..cfb3aac0b 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.test.ts @@ -162,6 +162,63 @@ describe("buildTypeScripts", () => { expect(expr).toContain(typeValidateExpr); }); + test("applies defaults per element in nested array fields", () => { + const fields: Record = { + items: { + type: "nested", + array: true, + fields: { + status: { type: "string", default: "pending" }, + count: { type: "integer", default: 0 }, + }, + }, + }; + + const createExpr = buildTypeScripts(fields).typeHook?.create?.expr ?? ""; + expect(createExpr).toContain( + '"items": (_input["items"] || []).map((__el) => Object.assign({}, __el, {', + ); + expect(createExpr).toContain('"status": __el["status"] ?? "pending"'); + expect(createExpr).toContain('"count": __el["count"] ?? 0'); + + expect(buildTypeScripts(fields).typeHook?.update).toBeUndefined(); + }); + + test("validates per element in nested array fields with indexed error paths", () => { + const fields: Record = { + items: { + type: "nested", + array: true, + fields: { + name: { + type: "string", + validate: [ + { script: { expr: '_value.length > 0 ? undefined : "required"' }, errorMessage: "" }, + ], + }, + }, + }, + }; + + const expr = buildTypeScripts(fields).typeValidate?.create?.expr ?? ""; + expect(expr).toContain('(_newRecord["items"] || []).forEach((__el, __idx) => {'); + expect(expr).toContain('const _value = __el["name"]'); + expect(expr).toContain('"items[" + __idx + "].name"'); + }); + + test("skips nested array with no defaults or validators", () => { + const fields: Record = { + items: { + type: "nested", + array: true, + fields: { + name: { type: "string" }, + }, + }, + }; + expect(buildTypeScripts(fields)).toEqual({}); + }); + test("no typeValidate output when no field validators and no type-level validate", () => { expect(buildTypeScripts({})).toEqual({}); expect(buildTypeScripts({}, undefined)).toEqual({}); diff --git a/packages/sdk/src/parser/service/tailordb/type-script.ts b/packages/sdk/src/parser/service/tailordb/type-script.ts index 25ac7634e..119b7f418 100644 --- a/packages/sdk/src/parser/service/tailordb/type-script.ts +++ b/packages/sdk/src/parser/service/tailordb/type-script.ts @@ -19,6 +19,7 @@ interface ScriptRef { */ export interface ScriptFieldConfig { type: string; + array?: boolean; hooks?: { create?: ScriptRef; update?: ScriptRef; @@ -68,9 +69,18 @@ function buildHookObject( const access = `${accessExpr}[${key(name)}]`; const oldAccess = `${oldAccessExpr}?.[${key(name)}]`; if (isNestedType(config) && config.fields) { - const inner = buildHookObject(config.fields, `(${access} || {})`, oldAccess, operation); - if (inner !== null) { - parts.push(`${key(name)}: Object.assign({}, ${access}, ${inner})`); + if (config.array) { + const inner = buildHookObject(config.fields, "__el", "__oldEl", operation); + if (inner !== null) { + parts.push( + `${key(name)}: (${access} || []).map((__el) => Object.assign({}, __el, ${inner}))`, + ); + } + } else { + const inner = buildHookObject(config.fields, `(${access} || {})`, oldAccess, operation); + if (inner !== null) { + parts.push(`${key(name)}: Object.assign({}, ${access}, ${inner})`); + } } continue; } @@ -129,7 +139,29 @@ function buildValidateStatements( } if (isNestedType(config) && config.fields) { - statements.push(...buildValidateStatements(config.fields, `(${access} || {})`, fieldPath)); + if (config.array) { + const innerParts: string[] = []; + for (const [innerName, innerConfig] of Object.entries(config.fields)) { + const innerValidators = (innerConfig.validate ?? []).filter((v) => v.script?.expr); + if (innerValidators.length > 0) { + const errorKeyExpr = `${JSON.stringify(fieldPath + "[")} + __idx + ${JSON.stringify("]." + innerName)}`; + const checks = innerValidators + .map( + (v) => + `{ const __r = (${v.script?.expr}); if (typeof __r === "string") { __errs[${errorKeyExpr}] = __r; } }`, + ) + .join(" "); + innerParts.push(`{ const _value = __el[${key(innerName)}]; ${checks} }`); + } + } + if (innerParts.length > 0) { + statements.push( + `(${access} || []).forEach((__el, __idx) => { ${innerParts.join(" ")} })`, + ); + } + } else { + statements.push(...buildValidateStatements(config.fields, `(${access} || {})`, fieldPath)); + } } } diff --git a/packages/sdk/src/utils/test/index.test.ts b/packages/sdk/src/utils/test/index.test.ts index 897a1ac2e..3b94d672c 100644 --- a/packages/sdk/src/utils/test/index.test.ts +++ b/packages/sdk/src/utils/test/index.test.ts @@ -80,6 +80,7 @@ describe("createTailorDBHook", () => { test("invokes nested sub-field hooks", () => { const type = db.table("Test", { user: db.object({ + // @ts-expect-error hooks on nested inner fields are now type-blocked name: db.string().hooks({ create: ({ input }) => `hooked:${input as string}`, }), @@ -121,6 +122,7 @@ describe("createTailorDBHook", () => { const type = db.table("Test", { lines: db.object( { + // @ts-expect-error hooks on nested inner fields are now type-blocked stamp: db.string().hooks({ create: ({ input }) => { calls.push(input); From eb019c692f34354a4d68d2d277a19252a0e26ccc Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sat, 11 Jul 2026 23:15:47 +0900 Subject: [PATCH 526/618] feat(example): add ProductBundle with nested array hooks, defaults, validates, and E2E tests --- example/e2e/tailordb.test.ts | 105 +++ example/generated/tailordb.ts | 14 + example/migrations/0007/diff.json | 696 ++++++++++++++++++ example/migrations/analyticsdb/0004/diff.json | 68 ++ example/seed/data/ProductBundle.jsonl | 0 example/seed/data/ProductBundle.schema.ts | 15 + example/seed/exec.mjs | 2 + example/tailordb/productBundle.ts | 35 + 8 files changed, 935 insertions(+) create mode 100644 example/migrations/0007/diff.json create mode 100644 example/migrations/analyticsdb/0004/diff.json create mode 100644 example/seed/data/ProductBundle.jsonl create mode 100644 example/seed/data/ProductBundle.schema.ts create mode 100644 example/tailordb/productBundle.ts diff --git a/example/e2e/tailordb.test.ts b/example/e2e/tailordb.test.ts index c4f1c1d56..5594258f9 100644 --- a/example/e2e/tailordb.test.ts +++ b/example/e2e/tailordb.test.ts @@ -613,6 +613,111 @@ describe("dataplane", () => { }); }); + describe("productBundle", async () => { + test("type-level hook computes label and inner default applies", async () => { + const query = gql` + mutation { + createProductBundle( + input: { + name: "Summer Sale" + items: [ + { productName: "Widget", unitPrice: 10.0 } + { productName: "Gadget", qty: 2, unitPrice: 25.0 } + ] + } + ) { + id + name + label + items { + productName + qty + unitPrice + } + } + } + `; + const result = await graphQLClient.rawRequest(query); + expect(result.errors).toBeUndefined(); + expect(result.data).toEqual({ + createProductBundle: { + id: expect.any(String), + name: "Summer Sale", + label: "Summer Sale Bundle", + items: [ + { productName: "Widget", qty: 1, unitPrice: 10.0 }, + { productName: "Gadget", qty: 2, unitPrice: 25.0 }, + ], + }, + }); + }); + + test("type-level hook recomputes label on update", async () => { + const create = gql` + mutation { + createProductBundle( + input: { name: "Original", items: [{ productName: "Item", unitPrice: 5.0 }] } + ) { + id + label + } + } + `; + interface Data { + createProductBundle: { id: string; label: string }; + } + const createResult = await graphQLClient.rawRequest(create); + expect(createResult.errors).toBeUndefined(); + expect(createResult.data.createProductBundle.label).toBe("Original Bundle"); + const id = createResult.data.createProductBundle.id; + + const update = gql` + mutation { + updateProductBundle(id: "${id}", input: { name: "Renamed" }) { + id + label + } + } + `; + const updateResult = await graphQLClient.rawRequest(update); + expect(updateResult.errors).toBeUndefined(); + expect(updateResult.data).toEqual({ + updateProductBundle: { + id, + label: "Renamed Bundle", + }, + }); + }); + + test("inner field validate rejects invalid qty", async () => { + const query = gql` + mutation { + createProductBundle( + input: { name: "Bad qty", items: [{ productName: "Widget", qty: 0, unitPrice: 10.0 }] } + ) { + id + } + } + `; + const result = await graphQLClient.rawRequest(query); + expect(result.errors).toBeDefined(); + expect(result.errors![0].message).toMatch("qty must be positive"); + }); + + test("type-level validate rejects empty items", async () => { + const query = gql` + mutation { + createProductBundle(input: { name: "Empty", items: [] }) { + id + } + } + `; + const result = await graphQLClient.rawRequest(query); + expect(result.errors).toBeDefined(); + expect(result.errors![0].message).toMatch("At least one item is required"); + }); + }); + describe("file", async () => { test("file type field returns", async () => { const query = gql` diff --git a/example/generated/tailordb.ts b/example/generated/tailordb.ts index 3fd7aeeb6..1381b67d2 100644 --- a/example/generated/tailordb.ts +++ b/example/generated/tailordb.ts @@ -3,6 +3,7 @@ import { type Generated, type Timestamp, type ObjectColumnType, + type ArrayColumnType, type Serial, type NamespaceDB, type NamespaceInsertable, @@ -60,6 +61,19 @@ export interface Namespace { updatedAt: Generated; } + ProductBundle: { + id: Generated; + name: string; + label: string | null; + items: ArrayColumnType; + unitPrice: number; + }>>; + createdAt: Generated; + updatedAt: Generated; + } + PurchaseOrder: { id: Generated; supplierID: string; diff --git a/example/migrations/0007/diff.json b/example/migrations/0007/diff.json new file mode 100644 index 000000000..2794ce649 --- /dev/null +++ b/example/migrations/0007/diff.json @@ -0,0 +1,696 @@ +{ + "version": 1, + "namespace": "tailordb", + "createdAt": "2026-07-11T14:10:41.961Z", + "changes": [ + { + "kind": "type_added", + "typeName": "ProductBundle", + "after": { + "name": "ProductBundle", + "pluralForm": "ProductBundles", + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "label": { + "type": "string", + "required": false + }, + "items": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "productName": { + "type": "string", + "required": true + }, + "qty": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({ value })=>value <= 0 ? \"qty must be positive\" : undefined)({ value: _value })" + }, + "errorMessage": "" + } + ], + "default": 1 + }, + "unitPrice": { + "type": "float", + "required": true + } + } + }, + "createdAt": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "updatedAt": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + "description": "Product bundle with nested array hooks", + "settings": {}, + "typeHookExpr": { + "create": "(({ input })=>({\n label: `${input.name} Bundle`\n }))({ input: _input, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })", + "update": "(({ input })=>({\n label: `${input.name} Bundle`\n }))({ input: _input, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "typeValidateExpr": "(({ newRecord }, issues)=>{\n if (newRecord.items && newRecord.items.length === 0) {\n issues(\"items\", \"At least one item is required\");\n }\n})({ newRecord: _newRecord, oldRecord: _oldRecord, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user) }, __issues)", + "permissions": { + "record": { + "create": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "read": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + }, + { + "conditions": [ + [ + { + "user": "_loggedIn" + }, + "eq", + true + ] + ], + "permit": "allow" + } + ], + "update": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "delete": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ] + }, + "gql": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "actions": ["create", "read", "update", "delete", "aggregate", "bulkUpsert"], + "permit": "allow" + }, + { + "conditions": [ + [ + { + "user": "_loggedIn" + }, + "eq", + true + ] + ], + "actions": ["read"], + "permit": "allow" + } + ] + } + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": false +} diff --git a/example/migrations/analyticsdb/0004/diff.json b/example/migrations/analyticsdb/0004/diff.json new file mode 100644 index 000000000..488e28a00 --- /dev/null +++ b/example/migrations/analyticsdb/0004/diff.json @@ -0,0 +1,68 @@ +{ + "version": 1, + "namespace": "analyticsdb", + "createdAt": "2026-07-11T14:10:41.983Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Event", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Event", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record update timestamp", + "hooks": { + "create": { + "expr": "(({ input, now }) => input ?? now)({ input: _value, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + }, + "update": { + "expr": "(({ now }) => now)({ input: _value, oldValue: _oldValue, invoker: (($raw) => {\n if (!$raw) {\n return null;\n }\n const type = $raw?.type === \"USER_TYPE_USER\"\n ? \"user\"\n : $raw?.type === \"USER_TYPE_MACHINE_USER\"\n ? \"machine_user\"\n : $raw?.type;\n const id = $raw.id;\n if (!type || type === \"USER_TYPE_UNSPECIFIED\" || id === \"00000000-0000-0000-0000-000000000000\") {\n return null;\n }\n return {\n id,\n type,\n workspaceId: $raw.workspace_id ?? $raw.workspaceId,\n attributes: $raw.attribute_map ?? $raw.attributeMap ?? {},\n attributeList: $raw.attributes ?? [],\n };\n})(user), now: _now })" + } + } + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": false +} diff --git a/example/seed/data/ProductBundle.jsonl b/example/seed/data/ProductBundle.jsonl new file mode 100644 index 000000000..e69de29bb diff --git a/example/seed/data/ProductBundle.schema.ts b/example/seed/data/ProductBundle.schema.ts new file mode 100644 index 000000000..ec68e9c63 --- /dev/null +++ b/example/seed/data/ProductBundle.schema.ts @@ -0,0 +1,15 @@ +import { t } from "@tailor-platform/sdk"; +import { defineSchema } from "@tailor-platform/sdk/seed"; +import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/test"; +import { productBundle } from "../../tailordb/productBundle"; + +const schemaType = t.object({ + ...productBundle.pickFields(["id","createdAt","updatedAt"], { optional: true }), + ...productBundle.omitFields(["id","createdAt","updatedAt"]), +}); + +const hook = createTailorDBHook(productBundle); + +export const schema = defineSchema( + createStandardSchema(schemaType, hook), +); diff --git a/example/seed/exec.mjs b/example/seed/exec.mjs index 534e5424f..0e065fa92 100644 --- a/example/seed/exec.mjs +++ b/example/seed/exec.mjs @@ -149,6 +149,7 @@ const namespaceEntities = { "Customer", "Invoice", "NestedProfile", + "ProductBundle", "PurchaseOrder", "SalesOrder", "SalesOrderCreated", @@ -167,6 +168,7 @@ const namespaceDeps = { "Customer": [], "Invoice": ["SalesOrder"], "NestedProfile": [], + "ProductBundle": [], "PurchaseOrder": ["Supplier"], "SalesOrder": ["Customer", "User"], "SalesOrderCreated": [], diff --git a/example/tailordb/productBundle.ts b/example/tailordb/productBundle.ts new file mode 100644 index 000000000..bd6567528 --- /dev/null +++ b/example/tailordb/productBundle.ts @@ -0,0 +1,35 @@ +import { db } from "@tailor-platform/sdk"; +import { defaultGqlPermission, defaultPermission } from "./permissions"; + +export const productBundle = db + .table("ProductBundle", "Product bundle with nested array hooks", { + name: db.string(), + label: db.string({ optional: true }), + items: db.object( + { + productName: db.string(), + qty: db + .int() + .default(1) + .validate(({ value }) => (value <= 0 ? "qty must be positive" : undefined)), + unitPrice: db.float(), + }, + { array: true }, + ), + ...db.fields.timestamps(), + }) + .hooks({ + create: ({ input }) => ({ + label: `${input.name} Bundle`, + }), + update: ({ input }) => ({ + label: `${input.name} Bundle`, + }), + }) + .validate(({ newRecord }, issues) => { + if (newRecord.items && newRecord.items.length === 0) { + issues("items", "At least one item is required"); + } + }) + .permission(defaultPermission) + .gqlPermission(defaultGqlPermission); From 245e0ab9c8e4410618dcd8fb8962b36cbaa7ebbc Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Sun, 12 Jul 2026 07:17:54 +0900 Subject: [PATCH 527/618] fix(tailordb): make type-level hook input types reflect field-level results --- example/tailordb/customer.ts | 4 +- example/tailordb/productBundle.ts | 4 +- .../services/tailordb/schema.test.ts | 69 +++++++++++-------- .../src/configure/services/tailordb/types.ts | 8 +-- .../parser/service/tailordb/type-script.ts | 4 +- 5 files changed, 52 insertions(+), 37 deletions(-) diff --git a/example/tailordb/customer.ts b/example/tailordb/customer.ts index 23568df2f..81b8ea3b5 100644 --- a/example/tailordb/customer.ts +++ b/example/tailordb/customer.ts @@ -27,8 +27,8 @@ export const customer = db create: ({ input }) => ({ fullAddress: `${input.postalCode} ${input.address} ${input.city}`, }), - update: ({ input }) => ({ - fullAddress: `${input.postalCode} ${input.address} ${input.city}`, + update: ({ input, oldRecord }) => ({ + fullAddress: `${input.postalCode ?? oldRecord.postalCode} ${input.address ?? oldRecord.address} ${input.city ?? oldRecord.city}`, }), }) .validate(({ newRecord }, issues) => { diff --git a/example/tailordb/productBundle.ts b/example/tailordb/productBundle.ts index bd6567528..384d04270 100644 --- a/example/tailordb/productBundle.ts +++ b/example/tailordb/productBundle.ts @@ -22,8 +22,8 @@ export const productBundle = db create: ({ input }) => ({ label: `${input.name} Bundle`, }), - update: ({ input }) => ({ - label: `${input.name} Bundle`, + update: ({ input, oldRecord }) => ({ + label: `${input.name ?? oldRecord.name} Bundle`, }), }) .validate(({ newRecord }, issues) => { diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index 0e44f6887..939816cf5 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -1115,11 +1115,11 @@ describe("TailorDBType hooks modifier tests", () => { }); }); - test("type create hook input args receive correct types (no oldRecord)", () => { + test("type create hook input: required fields are non-nullable, optional fields are nullable", () => { db.table("Test", { name: db.string(), age: db.int({ optional: true }) }).hooks({ create: ({ input, invoker, now }) => { - expectTypeOf(input.name).toEqualTypeOf(); - expectTypeOf(input.age).toEqualTypeOf(); + expectTypeOf(input.name).toEqualTypeOf(); + expectTypeOf(input.age).toEqualTypeOf(); expectTypeOf(invoker).toBeNullable(); expectTypeOf(now).toEqualTypeOf(); return {}; @@ -1127,12 +1127,26 @@ describe("TailorDBType hooks modifier tests", () => { }); }); - test("type update hook input args include oldRecord", () => { + test("type create hook input: field with .default() is also non-nullable", () => { + db.table("Test", { + name: db.string(), + status: db.string().default("active"), + }).hooks({ + create: ({ input }) => { + expectTypeOf(input.name).toEqualTypeOf(); + expectTypeOf(input.status).toEqualTypeOf(); + return {}; + }, + }); + }); + + test("type update hook: input is partial (nullable), oldRecord is full record", () => { db.table("Test", { name: db.string(), age: db.int({ optional: true }) }).hooks({ update: ({ input, oldRecord, invoker, now }) => { expectTypeOf(input.name).toEqualTypeOf(); - expectTypeOf(oldRecord.name).toEqualTypeOf(); - expectTypeOf(oldRecord.age).toEqualTypeOf(); + expectTypeOf(input.age).toEqualTypeOf(); + expectTypeOf(oldRecord.name).toEqualTypeOf(); + expectTypeOf(oldRecord.age).toEqualTypeOf(); expectTypeOf(invoker).toBeNullable(); expectTypeOf(now).toEqualTypeOf(); return {}; @@ -1142,22 +1156,23 @@ describe("TailorDBType hooks modifier tests", () => { }); describe("TailorDBType type-level validate (function form) tests", () => { - test("accepts type-level validate function", () => { - const _type = db - .table("Test", { - name: db.string(), - email: db.string(), - }) - .validate(({ newRecord }, issues) => { - if (!newRecord.name) issues("name", "Name is required"); - if (!newRecord.email) issues("email", "Email is required"); - }); - - expectTypeOf>().toEqualTypeOf<{ - id: string; - name: string; - email: string; - }>(); + test("newRecord has non-nullable required fields and nullable optional fields", () => { + db.table("Test", { + name: db.string(), + email: db.string(), + phone: db.string({ optional: true }), + }).validate(({ newRecord, oldRecord }, issues) => { + expectTypeOf(newRecord.name).toEqualTypeOf(); + expectTypeOf(newRecord.email).toEqualTypeOf(); + expectTypeOf(newRecord.phone).toEqualTypeOf(); + expectTypeOf(oldRecord).toEqualTypeOf | null>(); + if (newRecord.name.length > 100) issues("name", "Name too long"); + }); }); test("issues function only accepts valid field paths", () => { @@ -1198,7 +1213,7 @@ describe("TailorDBType type-level validate (function form) tests", () => { ), }).validate(({ newRecord }, issues) => { issues("items", "ok"); - newRecord.items?.forEach((item, i) => { + newRecord.items.forEach((item, i) => { if (!item.name) { issues(`items[${i}].name`, "required"); } @@ -1223,15 +1238,15 @@ describe("TailorDBType type-level validate (function form) tests", () => { expect(type.metadata.typeValidate).toBeDefined(); }); - test("type-level validate receives newRecord and oldRecord", () => { + test("type-level validate receives newRecord and oldRecord with correct types", () => { db.table("Test", { name: db.string(), age: db.int({ optional: true }), }).validate(({ newRecord, oldRecord }) => { - expectTypeOf(newRecord.name).toEqualTypeOf(); - expectTypeOf(newRecord.age).toEqualTypeOf(); + expectTypeOf(newRecord.name).toEqualTypeOf(); + expectTypeOf(newRecord.age).toEqualTypeOf(); if (oldRecord) { - expectTypeOf(oldRecord.name).toEqualTypeOf(); + expectTypeOf(oldRecord.name).toEqualTypeOf(); } }); }); diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index c7bfe0233..564739d6b 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -197,8 +197,8 @@ export type TypeValidateFn< TData = { [K in keyof F]: output }, > = ( args: { - newRecord: HookArgs; - oldRecord: HookArgs | null; + newRecord: Readonly; + oldRecord: Readonly | null; invoker: TailorPrincipal | null; }, issues: